Repository: mie-lab/trackintel

Issue:
generate_trips fails for linestrings with same start and end point
`generate_trips` calls the boundary property of a linestring [here](https://github.com/mie-lab/trackintel/blob/master/trackintel/preprocessing/triplegs.py#L248) and [here](https://github.com/mie-lab/trackintel/blob/master/trackintel/preprocessing/triplegs.py#L254). Linestrings with the same start and end point are considered valid by shapely but do not have a boundary. E.g.

```python
print(LineString([(0, 0), (1, 1), (0, 0)]).boundary)
>>>MULTIPOINT EMPTY
```

Here is a test to reproduce the error:
```python
def test_generate_trips_modified(self):
    """Test if we can generate the example trips based on example data."""
    # load pregenerated trips
    trips_loaded = ti.read_trips_csv(os.path.join("tests", "data", "geolife_long", "trips.csv"), index_col="id")

    # create trips from geolife (based on positionfixes)
    pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
    pfs, stps = pfs.as_positionfixes.generate_staypoints(
        method="sliding", dist_threshold=25, time_threshold=5, gap_threshold=1e6
    )
    stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
    pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)

    # generate trips and a joint staypoint/triplegs dataframe
    from shapely.geometry import LineString
    tpls.loc[0, 'geom'] = LineString([(0, 0), (1, 1), (0, 0)]) # modify first geometry
    stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
    trips = trips[
        ["user_id", "started_at", "finished_at", "origin_staypoint_id", "destination_staypoint_id", "geom"]
    ]
    # test if generated trips are equal
    assert_geodataframe_equal(trips_loaded, trips)
```

A fix could be to use 
```python
first = Point(line.coords[0])
last = Point(line.coords[-1])
```
instead of the boundary argument

Discussion:

Patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index 460edf2..63e2b78 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,19 +1,23 @@
-# Main Authors and Maintainers
+# Maintainers
 
 Dominik Bucher: <dobucher@ethz.ch>, [@dominikbucher (Github)](https://github.com/dominikbucher), [@dominikbucher (Twitter)](https://twitter.com/DominikBucher).
 
 Henry Martin: [@henrymartin1 (Github)](https://github.com/henrymartin1).
 
-René Buffat: [@rbuffat (Github)](https://github.com/rbuffat).
-
 Ye Hong: [@hong2223 (Github)](https://github.com/hong2223).
 
 Sven Ruf: [@svenruf (Github)](https://github.com/svenruf).
 
 Christof Leutenegger: [@bifbof (Github)](https://github.com/bifbof).
 
-Nishant: [@abcnishant007 (Github)](https://github.com/abcnishant007).
+Nishant Kumar: [@abcnishant007 (Github)](https://github.com/abcnishant007).
+
+Nina Wiedemann: [@NinaWie (Github)](https://github.com/NinaWie)
 
-# Previous Authors, Advisors, Visionaries
+# Advisors and Visionaries
 
 David Jonietz: [@DavidJonietz (Github)](https://github.com/DavidJonietz).
+
+René Buffat: [@rbuffat (Github)](https://github.com/rbuffat).
+
+Jiří Kunčar: [@jirikuncar (Github)](https://github.com/jirikuncar)
diff --git a/ROADMAP.md b/ROADMAP.md
index 875c4fd..4376a0f 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,89 +2,47 @@
 
 This document explains the functionality planned for various releases.
 
-Ideas that are planned:
-## v1.1.0
-
-
-* More method to predict activity without context data.
-* Next place prediction (e.g., markov-model)
-
-## v1.2.0
-* Map match *triplegs* (based on transport mode identification). Solutions could be *LeuvenMapMatching* or based on *osrm*.
-* Add context data to movement trajectories (using spatio-temporal map algebra).
-* Implement *tours* (starting and ending at a persons home location) and *customermovements* (consecutive triplegs with using transport provided by a single provider (e.g., a local bus company)).
-
-## v2.0.0
-Ideas that are on the list:
+## v2.0
+* :heavy_check_mark: Parallelization.
+* More method to predict activity without context data. [#213](https://github.com/mie-lab/trackintel/issues/213)
+* Add context data to movement trajectories (using spatio-temporal map algebra). [#63](https://github.com/mie-lab/trackintel/issues/63) 
+* Implement *tours* (starting and ending at a persons home location). [#287](https://github.com/mie-lab/trackintel/pull/287)
+* Speed calculation for triplegs/positionfixes. [#191](https://github.com/mie-lab/trackintel/issues/191)
+* CodePeerReview: Next milestone after 1.0. [#86](https://github.com/mie-lab/trackintel/issues/86) 
+* Include the calculation of common mobility indicators (e.g., radius of gyration)
 
+## Ideas that are on the list
 I/O:
-* Read *customermovements*, *tours* from files.
-* Read *customermovements*, *tours* from PostGIS.
-* Write *customermovements*, *tours* to files.
-* Write *customermovements*, *tours* to PostGIS.
+* Read *tours* from files.
+* Read *tours* from PostGIS.
+* Write *tours* to files.
+* Write *tours* to PostGIS.
 * Include data input/output methods for more common datasets
 
-
 Preprocessing:
 * Short walks to/from cars/buses/etc.: These are often not recognized by the tracking applications.
 * Imputation of trivial gaps in tracking data (*triplegs* and *staypoints*).
 * Simple outlier filtering for *triplegs*
+* Create a solution to match trajectories from different tracking sources. [#49](https://github.com/mie-lab/trackintel/issues/49)
 
 Augment tracking data:
 * Better transport mode prediction based on movement data (speed, associated features from accelerometer data, etc.) as well as by aligning them with context data such as from OpenStreetMap or GTFS departure schedules. 
 * Better activity prediction. This functionality is based on properties of the movement data and on context data such as points of interests.
 * Provide holistic inference of transport modes (take into account the fact that if someone uses the car to reach a certain location, the person is likely to leave by car as well).
+* Map match *triplegs* (based on transport mode identification). Solutions could be *LeuvenMapMatching* or based on *osrm*.
 
 Analysis
 * Mobility behaviour (and its changes): automatic detection of uncharacteristic changes in mobility patterns.
 * User profiling and clustering.
-* Extract *customermovements* from *triplegs* and *trips*.
 * Anomaly detection (based on properties of movement data as well as contextual factors).
 * Clustering of triplegs using similarity metrics
-* Include the calculation of common mobility indicators (e.g., radius of gyration)
+* Next place prediction (e.g., markov-model)
 
 Visualization 
-* Visualize *customermovements*, *tours* geographically.
+* Visualize *tours* geographically.
 * Visualize *trips* and *locations* geographically, e.g., by coloring *triplegs* differently or by making a buffer around all *staypoints* that are part of a *locations*.
 
 Various
-* Parallelization
-
-
-
-## v1.0.0
-
-I/O and model:
-* :heavy_check_mark: Read *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* from CSV files.
-* :heavy_check_mark: Read *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* from PostGIS.
-* :heavy_check_mark: Write *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* to CSV files.
-* :heavy_check_mark: Write *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* to PostGIS.
-* :heavy_check_mark: Augment model of *triplegs* and *staypoints* in such a way that they can hold references to *trips* and *locations*.
-* :heavy_check_mark: Support flexible geometry columns [Issue15](https://github.com/mie-lab/trackintel/issues/15)
-
-
-Preprocessing
-* :heavy_check_mark: Extract *triplegs* and *staypoints* from *positionfixes*.
-* :heavy_check_mark: Functions for smoothening tripleg data (e.g., Douglas-Peucker).
-* :heavy_check_mark: Enhance tripleg extraction to be _gap aware_ [issue27](https://github.com/mie-lab/trackintel/issues/27)
-* :heavy_check_mark: Extract *trips* from *triplegs* and *staypoints*
-* :heavy_check_mark: Extract *locations* from *staypoints*.
-* :heavy_check_mark: Filter data by geographic location (e.g., all data within Switzerland)
-
-Augment tracking data:
-* :heavy_check_mark: Simple transport mode prediction. This functionality is based on properties of the movement data only.
-* :heavy_check_mark: Simple activity inference. This functionality is based on properties of the movement data only and should include {Home, Work, Other}.
-
-Analysis:
-* :heavy_check_mark: Create transition graphs from *locations*.
-* :heavy_check_mark: Provide and visualize a range of (transition) graph measures.
-* :heavy_check_mark: Data quality assessments: statistical measures of temporal tracking frequency.
+* RAM profiling of trackintel. [#183](https://github.com/mie-lab/trackintel/issues/183)
 
-Visualization:
-* :heavy_check_mark: Visualize *positionfixes*, *triplegs*, *staypoints* geographically.
 
-Various:
-* :heavy_check_mark: Tutorial for trackintel
-* :heavy_check_mark: Link a guide to the readme how to contribute to the documentation
-* :heavy_check_mark: The documentation should be complete (all functions and methods are mentioned)
-* :heavy_check_mark: The documentation should not be focused on PostGIS 
\ No newline at end of file
diff --git a/docs/environment.yml b/docs/environment.yml
index 4750c75..1392606 100644
--- a/docs/environment.yml
+++ b/docs/environment.yml
@@ -3,19 +3,19 @@ channels:
   - conda-forge
   - defaults
 dependencies:
-- python=3.8
-- numpy
-- matplotlib
-- pandas
+# - python=3.8
 - geopandas
-- scikit-learn
-- networkx
-- pint
-- pip
-- geoalchemy2
-- osmnx
-- psycopg2
-- sphinx
-- sphinx_rtd_theme
-- tqdm
-- similaritymeasures
\ No newline at end of file
+- pip:
+  - numpy
+  - matplotlib
+  - pandas
+  - scikit-learn
+  - networkx
+  - pint
+  - geoalchemy2
+  - osmnx
+  - psycopg2
+  - sphinx
+  - sphinx_rtd_theme
+  - tqdm
+  - similaritymeasures
diff --git a/trackintel/io/postgis.py b/trackintel/io/postgis.py
index b0cba39..2a9c0d2 100644
--- a/trackintel/io/postgis.py
+++ b/trackintel/io/postgis.py
@@ -3,9 +3,9 @@ from inspect import signature
 
 import geopandas as gpd
 from geopandas.io.sql import _get_srid_from_crs
-from shapely.wkb import dumps
+from shapely import wkb
 import pandas as pd
-from geoalchemy2 import Geometry, WKTElement
+from geoalchemy2 import Geometry
 from sqlalchemy import create_engine
 
 import trackintel as ti
@@ -40,7 +40,18 @@ def _handle_con_string(func):
 
 
 @_handle_con_string
-def read_positionfixes_postgis(sql, con, geom_col="geom", **kwargs):
+def read_positionfixes_postgis(
+    sql,
+    con,
+    geom_col="geom",
+    crs=None,
+    index_col=None,
+    coerce_float=True,
+    parse_dates=None,
+    params=None,
+    chunksize=None,
+    **kwargs
+):
     """Reads positionfixes from a PostGIS database.
 
     Parameters
@@ -54,8 +65,37 @@ def read_positionfixes_postgis(sql, con, geom_col="geom", **kwargs):
     geom_col : str, default 'geom'
         The geometry column of the table.
 
+    crs : optional
+        Coordinate reference system to use for the returned GeoDataFrame
+
+    index_col : string or list of strings, optional, default: None
+        Column(s) to set as index(MultiIndex)
+
+    coerce_float : boolean, default True
+        Attempt to convert values of non-string, non-numeric objects (like
+        decimal.Decimal) to floating point, useful for SQL result sets
+
+    parse_dates : list or dict, default None
+        - List of column names to parse as dates.
+        - Dict of ``{column_name: format string}`` where format string is
+            strftime compatible in case of parsing string times, or is one of
+            (D, s, ns, ms, us) in case of parsing integer timestamps.
+        - Dict of ``{column_name: arg dict}``, where the arg dict
+            corresponds to the keyword arguments of
+            :func:`pandas.to_datetime`. Especially useful with databases
+            without native Datetime support, such as SQLite.
+
+    params : list, tuple or dict, optional, default None
+        List of parameters to pass to execute method.
+
+    chunksize : int, default None
+        If specified, return an iterator where chunksize is the number
+        of rows to include in each chunk.
+
     **kwargs
-        Further keyword arguments as available in GeoPanda's GeoDataFrame.from_postgis().
+        Further keyword arguments as available in trackintels trackintel.io.read_positionfixes_gpd().
+        Especially useful to rename column names from the SQL table to trackintel conform column names.
+        See second example how to use it in code.
 
     Returns
     -------
@@ -64,10 +104,22 @@ def read_positionfixes_postgis(sql, con, geom_col="geom", **kwargs):
 
     Examples
     --------
-    >>> pfs = ti.io.read_postifionfixes_postgis("SELECT * FROM postionfixes", con, geom_col="geom")
+    >>> pfs = ti.io.read_positionfixes_postgis("SELECT * FROM positionfixes", con, geom_col="geom")
+    >>> pfs = ti.io.read_positionfixes_postgis("SELECT * FROM positionfixes", con, geom_col="geom",
+    ...                                        index_col="id", user_id="USER", tracked_at="time")
     """
-    pfs = gpd.GeoDataFrame.from_postgis(sql, con, geom_col, **kwargs)
-    return ti.io.read_positionfixes_gpd(pfs, geom_col=geom_col)
+    pfs = gpd.GeoDataFrame.from_postgis(
+        sql,
+        con,
+        geom_col=geom_col,
+        crs=crs,
+        index_col=index_col,
+        coerce_float=coerce_float,
+        parse_dates=parse_dates,
+        params=params,
+        chunksize=chunksize,
+    )
+    return ti.io.read_positionfixes_gpd(pfs, **kwargs)
 
 
 @_handle_con_string
@@ -87,7 +139,18 @@ def write_positionfixes_postgis(
 
 
 @_handle_con_string
-def read_triplegs_postgis(sql, con, geom_col="geom", **kwargs):
+def read_triplegs_postgis(
+    sql,
+    con,
+    geom_col="geom",
+    crs=None,
+    index_col=None,
+    coerce_float=True,
+    parse_dates=None,
+    params=None,
+    chunksize=None,
+    **kwargs
+):
     """Reads triplegs from a PostGIS database.
 
     Parameters
@@ -101,8 +164,37 @@ def read_triplegs_postgis(sql, con, geom_col="geom", **kwargs):
     geom_col : str, default 'geom'
         The geometry column of the table.
 
+    crs : optional
+        Coordinate reference system to use for the returned GeoDataFrame
+
+    index_col : string or list of strings, optional, default: None
+        Column(s) to set as index(MultiIndex)
+
+    coerce_float : boolean, default True
+        Attempt to convert values of non-string, non-numeric objects (like
+        decimal.Decimal) to floating point, useful for SQL result sets
+
+    parse_dates : list or dict, default None
+        - List of column names to parse as dates.
+        - Dict of ``{column_name: format string}`` where format string is
+            strftime compatible in case of parsing string times, or is one of
+            (D, s, ns, ms, us) in case of parsing integer timestamps.
+        - Dict of ``{column_name: arg dict}``, where the arg dict
+            corresponds to the keyword arguments of
+            :func:`pandas.to_datetime`. Especially useful with databases
+            without native Datetime support, such as SQLite.
+
+    params : list, tuple or dict, optional, default None
+        List of parameters to pass to execute method.
+
+    chunksize : int, default None
+        If specified, return an iterator where chunksize is the number
+        of rows to include in each chunk.
+
     **kwargs
-        Further keyword arguments as available in GeoPanda's GeoDataFrame.from_postgis().
+        Further keyword arguments as available in trackintels trackintel.io.read_triplegs_gpd().
+        Especially useful to rename column names from the SQL table to trackintel conform column names.
+        See second example how to use it in code.
 
     Returns
     -------
@@ -112,9 +204,21 @@ def read_triplegs_postgis(sql, con, geom_col="geom", **kwargs):
     Examples
     --------
     >>> tpls = ti.io.read_triplegs_postgis("SELECT * FROM triplegs", con, geom_col="geom")
+    >>> tpls = ti.io.read_triplegs_postgis("SELECT * FROM triplegs", con, geom_col="geom", index_col="id",
+    ...                                    started_at="start_time", finished_at="end_time", user_id="USER")
     """
-    tpls = gpd.GeoDataFrame.from_postgis(sql, con, geom_col=geom_col, index_col="id", **kwargs)
-    return ti.io.read_triplegs_gpd(tpls, geom_col=geom_col)
+    tpls = gpd.GeoDataFrame.from_postgis(
+        sql,
+        con,
+        geom_col=geom_col,
+        crs=crs,
+        index_col=index_col,
+        coerce_float=coerce_float,
+        parse_dates=parse_dates,
+        params=params,
+        chunksize=chunksize,
+    )
+    return ti.io.read_triplegs_gpd(tpls, **kwargs)
 
 
 @_handle_con_string
@@ -134,7 +238,18 @@ def write_triplegs_postgis(
 
 
 @_handle_con_string
-def read_staypoints_postgis(sql, con, geom_col="geom", **kwargs):
+def read_staypoints_postgis(
+    sql,
+    con,
+    geom_col="geom",
+    crs=None,
+    index_col=None,
+    coerce_float=True,
+    parse_dates=None,
+    params=None,
+    chunksize=None,
+    **kwargs
+):
     """Read staypoints from a PostGIS database.
 
     Parameters
@@ -148,8 +263,37 @@ def read_staypoints_postgis(sql, con, geom_col="geom", **kwargs):
     geom_col : str, default 'geom'
         The geometry column of the table.
 
+    crs : optional
+        Coordinate reference system to use for the returned GeoDataFrame
+
+    index_col : string or list of strings, optional, default: None
+        Column(s) to set as index(MultiIndex)
+
+    coerce_float : boolean, default True
+        Attempt to convert values of non-string, non-numeric objects (like
+        decimal.Decimal) to floating point, useful for SQL result sets
+
+    parse_dates : list or dict, default None
+        - List of column names to parse as dates.
+        - Dict of ``{column_name: format string}`` where format string is
+            strftime compatible in case of parsing string times, or is one of
+            (D, s, ns, ms, us) in case of parsing integer timestamps.
+        - Dict of ``{column_name: arg dict}``, where the arg dict
+            corresponds to the keyword arguments of
+            :func:`pandas.to_datetime`. Especially useful with databases
+            without native Datetime support, such as SQLite.
+
+    params : list, tuple or dict, optional, default None
+        List of parameters to pass to execute method.
+
+    chunksize : int, default None
+        If specified, return an iterator where chunksize is the number
+        of rows to include in each chunk.
+
     **kwargs
-        Further keyword arguments as available in GeoPanda's GeoDataFrame.from_postgis().
+        Further keyword arguments as available in trackintels trackintel.io.read_staypoints_gpd().
+        Especially useful to rename column names from the SQL table to trackintel conform column names.
+        See second example how to use it in code.
 
 
     Returns
@@ -160,11 +304,22 @@ def read_staypoints_postgis(sql, con, geom_col="geom", **kwargs):
     Examples
     --------
     >>> spts = ti.io.read_staypoints_postgis("SELECT * FROM staypoints", con, geom_col="geom")
-
+    >>> spts = ti.io.read_staypoints_postgis("SELECT * FROM staypoints", con, geom_col="geom", index_col="id",
+    ...                                      started_at="start_time", finished_at="end_time", user_id="USER")
     """
-    spts = gpd.GeoDataFrame.from_postgis(sql, con, geom_col=geom_col, index_col="id", **kwargs)
+    spts = gpd.GeoDataFrame.from_postgis(
+        sql,
+        con,
+        geom_col=geom_col,
+        crs=crs,
+        index_col=index_col,
+        coerce_float=coerce_float,
+        parse_dates=parse_dates,
+        params=params,
+        chunksize=chunksize,
+    )
 
-    return ti.io.read_staypoints_gpd(spts, geom_col=geom_col)
+    return ti.io.read_staypoints_gpd(spts, **kwargs)
 
 
 @_handle_con_string
@@ -184,7 +339,18 @@ def write_staypoints_postgis(
 
 
 @_handle_con_string
-def read_locations_postgis(sql, con, geom_col="geom", **kwargs):
+def read_locations_postgis(
+    sql,
+    con,
+    center="center",
+    crs=None,
+    index_col=None,
+    coerce_float=True,
+    parse_dates=None,
+    params=None,
+    chunksize=None,
+    **kwargs
+):
     """Reads locations from a PostGIS database.
 
     Parameters
@@ -195,14 +361,40 @@ def read_locations_postgis(sql, con, geom_col="geom", **kwargs):
     con : str, sqlalchemy.engine.Connection or sqlalchemy.engine.Engine
         Connection string or active connection to PostGIS database.
 
-    geom_col : str, default 'geom'
+    center : str, default 'center'
         The geometry column of the table. For the center of the location.
 
-    *args
-        Further arguments as available in GeoPanda's GeoDataFrame.from_postgis().
+    crs : optional
+        Coordinate reference system to use for the returned GeoDataFrame
+
+    index_col : string or list of strings, optional, default: None
+        Column(s) to set as index(MultiIndex)
+
+    coerce_float : boolean, default True
+        Attempt to convert values of non-string, non-numeric objects (like
+        decimal.Decimal) to floating point, useful for SQL result sets
+
+    parse_dates : list or dict, default None
+        - List of column names to parse as dates.
+        - Dict of ``{column_name: format string}`` where format string is
+            strftime compatible in case of parsing string times, or is one of
+            (D, s, ns, ms, us) in case of parsing integer timestamps.
+        - Dict of ``{column_name: arg dict}``, where the arg dict
+            corresponds to the keyword arguments of
+            :func:`pandas.to_datetime`. Especially useful with databases
+            without native Datetime support, such as SQLite.
+
+    params : list, tuple or dict, optional, default None
+        List of parameters to pass to execute method.
+
+    chunksize : int, default None
+        If specified, return an iterator where chunksize is the number
+        of rows to include in each chunk.
 
     **kwargs
-        Further keyword arguments as available in GeoPanda's GeoDataFrame.from_postgis().
+        Further keyword arguments as available in trackintels trackintel.io.read_locations_gpd().
+        Especially useful to rename column names from the SQL table to trackintel conform column names.
+        See second example how to use it in code.
 
     Returns
     -------
@@ -211,11 +403,24 @@ def read_locations_postgis(sql, con, geom_col="geom", **kwargs):
 
     Examples
     --------
-    >>> locs = ti.io.read_locations_postgis("SELECT * FROM locations", con, geom_col="geom")
+    >>> locs = ti.io.read_locations_postgis("SELECT * FROM locations", con, center="center")
+    >>> locs = ti.io.read_locations_postgis("SELECT * FROM locations", con, center="geom", index_col="id",
+    ...                                     user_id="USER", extent="extent")
+    )
     """
-    locs = gpd.GeoDataFrame.from_postgis(sql, con, geom_col=geom_col, index_col="id", **kwargs)
+    locs = gpd.GeoDataFrame.from_postgis(
+        sql,
+        con,
+        geom_col=center,
+        crs=crs,
+        index_col=index_col,
+        coerce_float=coerce_float,
+        parse_dates=parse_dates,
+        params=params,
+        chunksize=chunksize,
+    )
 
-    return ti.io.read_locations_gpd(locs, center=geom_col)
+    return ti.io.read_locations_gpd(locs, center=center, **kwargs)
 
 
 @_handle_con_string
@@ -234,7 +439,7 @@ def write_locations_postgis(
         else:
             dtype["extent"] = extent_schema
         locations = locations.copy()
-        locations["extent"] = locations["extent"].apply(lambda x: dumps(x, srid=srid, hex=True))
+        locations["extent"] = locations["extent"].apply(lambda x: wkb.dumps(x, srid=srid, hex=True))
 
     locations.to_postgis(
         name,
@@ -249,7 +454,18 @@ def write_locations_postgis(
 
 
 @_handle_con_string
-def read_trips_postgis(sql, con, **kwargs):
+def read_trips_postgis(
+    sql,
+    con,
+    geom_col=None,
+    crs=None,
+    index_col=None,
+    coerce_float=True,
+    parse_dates=None,
+    params=None,
+    chunksize=None,
+    **kwargs
+):
     """Read trips from a PostGIS database.
 
     Parameters
@@ -260,39 +476,107 @@ def read_trips_postgis(sql, con, **kwargs):
     con : str, sqlalchemy.engine.Connection or sqlalchemy.engine.Engine
         Connection string or active connection to PostGIS database.
 
+    geom_col : str, optional
+        The geometry column of the table (if exists). Start and endpoint of the trip.
+
+    crs : optional
+        Coordinate reference system if table has geometry.
+
+    index_col : string or list of strings, optional, default: None
+        Column(s) to set as index(MultiIndex)
+
+    coerce_float : boolean, default True
+        Attempt to convert values of non-string, non-numeric objects (like
+        decimal.Decimal) to floating point, useful for SQL result sets
+
+    parse_dates : list or dict, default None
+        - List of column names to parse as dates.
+        - Dict of ``{column_name: format string}`` where format string is
+            strftime compatible in case of parsing string times, or is one of
+            (D, s, ns, ms, us) in case of parsing integer timestamps.
+        - Dict of ``{column_name: arg dict}``, where the arg dict
+            corresponds to the keyword arguments of
+            :func:`pandas.to_datetime`. Especially useful with databases
+            without native Datetime support, such as SQLite.
+
+    params : list, tuple or dict, optional, default None
+        List of parameters to pass to execute method.
+
+    chunksize : int, default None
+        If specified, return an iterator where chunksize is the number
+        of rows to include in each chunk.
+
     **kwargs
-        Further keyword arguments as available in GeoPanda's GeoDataFrame.from_postgis().
+        Further keyword arguments as available in trackintels trackintel.io.read_trips_gpd().
+        Especially useful to rename column names from the SQL table to trackintel conform column names.
+        See second example how to use it in code.
 
 
     Returns
     -------
-    DataFrame
-        A DataFrame containing the trips.
+    GeoDataFrame
+        A GeoDataFrame containing the trips.
 
     Examples
     --------
-    >>> trips = ti.io.read_trips_postgis("SELECT * FROM trips", con, geom_col="geom")
+    >>> trips = ti.io.read_trips_postgis("SELECT * FROM trips", con)
+    >>> trips = ti.io.read_trips_postgis("SELECT * FROM trips", con, geom_col="geom", index_col="id",
+    ...                                  started_at="start_time", finished_at="end_time", user_id="USER",
+    ...                                  origin_staypoint_id="ORIGIN", destination_staypoint_id="DEST")
 
     """
-    trips = pd.read_sql(sql, con, index_col="id", **kwargs)
-
-    return ti.io.read_trips_gpd(trips)
+    if geom_col is None:
+        trips = pd.read_sql(
+            sql,
+            con,
+            index_col=index_col,
+            coerce_float=coerce_float,
+            params=params,
+            parse_dates=parse_dates,
+            chunksize=chunksize,
+        )
+    else:
+        trips = gpd.GeoDataFrame.from_postgis(
+            sql,
+            con,
+            geom_col=geom_col,
+            crs=crs,
+            index_col=index_col,
+            coerce_float=coerce_float,
+            parse_dates=parse_dates,
+            params=params,
+            chunksize=chunksize,
+        )
+
+    return ti.io.read_trips_gpd(trips, **kwargs)
 
 
 @_handle_con_string
 def write_trips_postgis(
     trips, name, con, schema=None, if_exists="fail", index=True, index_label=None, chunksize=None, dtype=None
 ):
-    trips.to_sql(
-        name,
-        con,
-        schema=schema,
-        if_exists=if_exists,
-        index=index,
-        index_label=index_label,
-        chunksize=chunksize,
-        dtype=dtype,
-    )
+    if isinstance(trips, gpd.GeoDataFrame):
+        trips.to_postgis(
+            name,
+            con,
+            schema=schema,
+            if_exists=if_exists,
+            index=index,
+            index_label=index_label,
+            chunksize=chunksize,
+            dtype=dtype,
+        )
+    else:  # is DataFrame
+        trips.to_sql(
+            name,
+            con,
+            schema=schema,
+            if_exists=if_exists,
+            index=index,
+            index_label=index_label,
+            chunksize=chunksize,
+            dtype=dtype,
+        )
 
 
 # helper docstring to change __doc__ of all write functions conveniently in one place
@@ -336,7 +620,7 @@ __doc = """Stores {long} to PostGIS. Usually, this is directly called on a {long
     Examples
     --------
     >>> {short}.as_{long}.to_postgis(conn_string, table_name)
-    >>> ti.io.postgis.write_{long}_postgis(pfs, conn_string, table_name)
+    >>> ti.io.postgis.write_{long}_postgis({short}, conn_string, table_name)
 """
 
 write_positionfixes_postgis.__doc__ = __doc.format(long="positionfixes", short="pfs")
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index a27ad58..39ce4f3 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -3,7 +3,7 @@ import warnings
 import geopandas as gpd
 import numpy as np
 import pandas as pd
-from shapely.geometry import MultiPoint
+from shapely.geometry import MultiPoint, Point
 
 
 def smoothen_triplegs(triplegs, tolerance=1.0, preserve_topology=True):
@@ -245,13 +245,13 @@ def generate_trips(spts, tpls, gap_threshold=15, add_geometry=True):
         origin_nan_rows = trips[pd.isna(trips["origin_staypoint_id"])].copy()
         trips.loc[pd.isna(trips["origin_staypoint_id"]), "origin_geom"] = origin_nan_rows.tpls.map(
             # from tpls table, get the first point of the first tripleg for the trip
-            lambda x: tpls.loc[x[0], tpls.geometry.name].boundary[0]
+            lambda x: Point(tpls.loc[x[0], tpls.geometry.name].coords[0])
         )
         # fill geometry for destionations staypoints that are NaN
         destination_nan_rows = trips[pd.isna(trips["destination_staypoint_id"])].copy()
         trips.loc[pd.isna(trips["destination_staypoint_id"]), "destination_geom"] = destination_nan_rows.tpls.map(
             # from tpls table, get the last point of the last tripleg on the trip
-            lambda x: tpls.loc[x[-1], tpls.geometry.name].boundary[1]
+            lambda x: Point(tpls.loc[x[-1], tpls.geometry.name].coords[-1])
         )
         # convert to GeoDataFrame with MultiPoint column
         trips["geom"] = [MultiPoint([x, y]) for x, y in zip(trips.origin_geom, trips.destination_geom)]


Test patch:
diff --git a/tests/io/test_postgis.py b/tests/io/test_postgis.py
index 4d69257..731dceb 100644
--- a/tests/io/test_postgis.py
+++ b/tests/io/test_postgis.py
@@ -1,14 +1,14 @@
-"""A copy of the geopandas postgis test to verify the continuous integration"""
 import datetime
 import os
 
 import geopandas as gpd
-import pandas as pd
-import pytest
 from geopandas.testing import assert_geodataframe_equal
+import pandas as pd
 from pandas.testing import assert_frame_equal
-from shapely.geometry import LineString, Point, Polygon
+import pytest
 import sqlalchemy
+from shapely.geometry import LineString, MultiPoint, Point, Polygon
+
 import trackintel as ti
 
 
@@ -275,6 +275,22 @@ class TestPositionfixes:
         finally:
             del_table(conn, table)
 
+    def test_non_standard_column_names(self, example_positionfixes, conn_postgis):
+        """Test renaming handled by read_positionfixes_gpd()."""
+        pfs = example_positionfixes.copy()
+        conn_string, conn = conn_postgis
+        table = "positionfixes"
+        sql = f"SELECT * FROM {table}"
+        geom_col = pfs.geometry.name
+        rename_dict = {"user_id": "USER", "tracked_at": "time"}
+        pfs.rename(rename_dict, inplace=True)
+        try:
+            pfs.as_positionfixes.to_postgis(table, conn_string)
+            pfs_db = ti.io.read_positionfixes_postgis(sql, conn_string, geom_col, index_col="id", **rename_dict)
+            assert_geodataframe_equal(example_positionfixes, pfs_db)
+        finally:
+            del_table(conn, table)
+
 
 class TestTriplegs:
     def test_write(self, example_triplegs, conn_postgis):
@@ -304,7 +320,7 @@ class TestTriplegs:
 
         try:
             tpls.as_triplegs.to_postgis(table, conn_string)
-            tpls_db = ti.io.read_triplegs_postgis(sql, conn_string, geom_col)
+            tpls_db = ti.io.read_triplegs_postgis(sql, conn_string, geom_col, index_col="id")
             assert_geodataframe_equal(tpls, tpls_db)
         finally:
             del_table(conn, table)
@@ -320,11 +336,27 @@ class TestTriplegs:
 
         try:
             tpls.as_triplegs.to_postgis(table, conn_string)
-            tpls_db = ti.io.read_triplegs_postgis(sql, conn_string, geom_col)
+            tpls_db = ti.io.read_triplegs_postgis(sql, conn_string, geom_col, index_col="id")
             assert_geodataframe_equal(tpls, tpls_db)
         finally:
             del_table(conn, table)
 
+    def test_non_standard_column_names(self, example_triplegs, conn_postgis):
+        """Test renaming handled by read_triplegs_gpd()."""
+        tpls = example_triplegs.copy()
+        conn_string, conn = conn_postgis
+        table = "triplegs"
+        sql = f"SELECT * FROM {table}"
+        geom_col = tpls.geometry.name
+        rename_dict = {"user_id": "USER", "started_at": "start_time", "finished_at": "end_time"}
+        tpls.rename(rename_dict, inplace=True)
+        try:
+            tpls.as_triplegs.to_postgis(table, conn_string)
+            tpls_db = ti.io.read_triplegs_postgis(sql, conn_string, geom_col, index_col="id", **rename_dict)
+            assert_geodataframe_equal(example_triplegs, tpls_db)
+        finally:
+            del_table(conn, table)
+
 
 class TestStaypoints:
     def test_write(self, example_staypoints, conn_postgis):
@@ -354,7 +386,7 @@ class TestStaypoints:
 
         try:
             spts.as_staypoints.to_postgis(table, conn_string)
-            spts_db = ti.io.read_staypoints_postgis(sql, conn_string, geom_col)
+            spts_db = ti.io.read_staypoints_postgis(sql, conn_string, geom_col, index_col="id")
             assert_geodataframe_equal(spts, spts_db)
         finally:
             del_table(conn, table)
@@ -369,7 +401,7 @@ class TestStaypoints:
         spts.crs = None
         try:
             spts.as_staypoints.to_postgis(table, conn_string)
-            spts_db = ti.io.read_staypoints_postgis(sql, conn_string, geom_col)
+            spts_db = ti.io.read_staypoints_postgis(sql, conn_string, geom_col, index_col="id")
             assert_geodataframe_equal(spts, spts_db)
         finally:
             del_table(conn, table)
@@ -403,7 +435,7 @@ class TestLocations:
 
         try:
             locs.as_locations.to_postgis(table, conn_string)
-            locs_db = ti.io.read_locations_postgis(sql, conn_string, geom_col)
+            locs_db = ti.io.read_locations_postgis(sql, conn_string, geom_col, index_col="id")
             assert_geodataframe_equal(locs, locs_db)
         finally:
             del_table(conn, table)
@@ -418,7 +450,7 @@ class TestLocations:
         locs.crs = None
         try:
             locs.as_locations.to_postgis(table, conn_string)
-            locs_db = ti.io.read_locations_postgis(sql, conn_string, geom_col)
+            locs_db = ti.io.read_locations_postgis(sql, conn_string, geom_col, index_col="id")
             assert_geodataframe_equal(locs, locs_db)
         finally:
             del_table(conn, table)
@@ -445,6 +477,23 @@ class TestLocations:
         finally:
             del_table(conn, table)
 
+    def test_non_standard_column_names(self, example_locations, conn_postgis):
+        """Test renaming handled by read_locations_gpd()."""
+        locs = example_locations.copy()
+        conn_string, conn = conn_postgis
+        table = "locations"
+        sql = f"SELECT * FROM {table}"
+        rename_dict = {"user_id": "USER", "center": "geom"}
+        locs.rename(rename_dict, inplace=True)
+        del rename_dict["center"]
+        geom_col = locs.geometry.name
+        try:
+            locs.as_locations.to_postgis(table, conn_string)
+            tpls_db = ti.io.read_locations_postgis(sql, conn_string, geom_col, index_col="id", **rename_dict)
+            assert_geodataframe_equal(example_locations, tpls_db)
+        finally:
+            del_table(conn, table)
+
 
 class TestTrips:
     def test_write(self, example_trips, conn_postgis):
@@ -470,11 +519,68 @@ class TestTrips:
 
         try:
             trips.as_trips.to_postgis(table, conn_string)
-            trips_db = ti.io.read_trips_postgis(sql, conn_string)
+            trips_db = ti.io.read_trips_postgis(sql, conn_string, index_col="id")
             assert_frame_equal(trips, trips_db)
         finally:
             del_table(conn, table)
 
+    def test_non_standard_column_names(self, example_trips, conn_postgis):
+        """Test renaming handled by read_trips_gpd()."""
+        trips = example_trips.copy()
+        conn_string, conn = conn_postgis
+        table = "trips"
+        sql = f"SELECT * FROM {table}"
+        rename_dict = {
+            "started_at": "start_time",
+            "finished_at": "end_time",
+            "user_id": "USER",
+            "origin_staypoint_id": "ORIGIN",
+            "destination_staypoint_id": "DEST",
+        }
+        trips.rename(rename_dict, inplace=True)
+        try:
+            trips.as_trips.to_postgis(table, conn_string)
+            tpls_db = ti.io.read_trips_postgis(sql, conn_string, index_col="id", **rename_dict)
+            assert_frame_equal(example_trips, tpls_db)
+        finally:
+            del_table(conn, table)
+
+    def test_write_with_geometry(self, example_trips, conn_postgis):
+        """Test if write of trips with geometry creates correct schema in database."""
+        mp1 = MultiPoint([(0.0, 0.0), (1.0, 1.0)])
+        mp2 = MultiPoint([(2.0, 2.0), (3.0, 3.0)])
+        trips = gpd.GeoDataFrame(example_trips, copy=True, geometry=[mp1, mp2, mp2], crs="EPSG:4326")
+        conn_string, conn = conn_postgis
+        table = "positionfixes"
+        try:
+            trips.as_trips.to_postgis(table, conn_string)
+            columns_db, dtypes = get_table_schema(conn, table)
+            columns = trips.columns.tolist() + [trips.index.name]
+            assert len(columns_db) == len(columns)
+            assert set(columns_db) == set(columns)
+            srid = _get_srid(trips)
+            geom_schema = f"geometry(MultiPoint,{srid})"
+            assert geom_schema in dtypes
+        finally:
+            del_table(conn, table)
+
+    def test_read_with_geometry(self, example_trips, conn_postgis):
+        """Test if read with geometry works."""
+        mp1 = MultiPoint([(0.0, 0.0), (1.0, 1.0)])
+        mp2 = MultiPoint([(2.0, 2.0), (3.0, 3.0)])
+        trips = gpd.GeoDataFrame(example_trips, copy=True, geometry=[mp1, mp2, mp2], crs="EPSG:4326")
+        conn_string, conn = conn_postgis
+        table = "trips"
+        sql = f"SELECT * FROM {table}"
+        geom_col = trips.geometry.name
+
+        try:
+            trips.as_trips.to_postgis(table, conn_string)
+            locs_db = ti.io.read_trips_postgis(sql, conn_string, geom_col=geom_col, index_col="id")
+            assert_geodataframe_equal(trips, locs_db)
+        finally:
+            del_table(conn, table)
+
 
 class TestGetSrid:
     def test_srid(self, example_positionfixes):
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 84f22b9..5ac1537 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -14,6 +14,29 @@ import trackintel as ti
 from trackintel.preprocessing.triplegs import generate_trips
 
 
+@pytest.fixture
+def example_triplegs():
+    """Generate input data for trip generation from geolife positionfixes"""
+    pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
+    pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
+    stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
+    pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+    return stps, tpls
+
+
+@pytest.fixture
+def example_triplegs_higher_gap_threshold():
+    """Generate input data for trip generation, but with a higher gap threshold in stp generation"""
+    # create trips from geolife (based on positionfixes)
+    pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
+    pfs, stps = pfs.as_positionfixes.generate_staypoints(
+        method="sliding", dist_threshold=25, time_threshold=5, gap_threshold=1e6
+    )
+    stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
+    pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+    return stps, tpls
+
+
 class TestSmoothen_triplegs:
     def test_smoothen_triplegs(self):
         tpls_file = os.path.join("tests", "data", "triplegs_with_too_many_points_test.csv")
@@ -35,13 +58,10 @@ class TestSmoothen_triplegs:
 class TestGenerate_trips:
     """Tests for generate_trips() method."""
 
-    def test_duplicate_columns(self):
+    def test_duplicate_columns(self, example_triplegs):
         """Test if running the function twice, the generated column does not yield exception in join statement"""
         # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        stps, tpls = example_triplegs
 
         # generate trips and a joint staypoint/triplegs dataframe
         stps_run_1, tpls_run_1, _ = generate_trips(stps, tpls, gap_threshold=15)
@@ -51,18 +71,13 @@ class TestGenerate_trips:
         assert set(tpls_run_1.columns) == set(tpls_run_2.columns)
         assert set(stps_run_1.columns) == set(stps_run_2.columns)
 
-    def test_generate_trips(self):
+    def test_generate_trips(self, example_triplegs_higher_gap_threshold):
         """Test if we can generate the example trips based on example data."""
         # load pregenerated trips
         trips_loaded = ti.read_trips_csv(os.path.join("tests", "data", "geolife_long", "trips.csv"), index_col="id")
 
-        # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(
-            method="sliding", dist_threshold=25, time_threshold=5, gap_threshold=1e6
-        )
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # create trips from geolife (based on positionfixes) - with gap_threshold 1e6
+        stps, tpls = example_triplegs_higher_gap_threshold
 
         # generate trips and a joint staypoint/triplegs dataframe
         stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
@@ -72,15 +87,10 @@ class TestGenerate_trips:
         # test if generated trips are equal
         assert_geodataframe_equal(trips_loaded, trips)
 
-    def test_trip_wo_geom(self):
+    def test_trip_wo_geom(self, example_triplegs_higher_gap_threshold):
         """Test if the add_geometry parameter shows correct behavior"""
-        # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(
-            method="sliding", dist_threshold=25, time_threshold=5, gap_threshold=1e6
-        )
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # create trips from geolife (based on positionfixes) - with gap_threshold 1e6
+        stps, tpls = example_triplegs_higher_gap_threshold
 
         # generate trips dataframe with geometry
         _, _, trips = generate_trips(stps, tpls, gap_threshold=15)
@@ -92,15 +102,10 @@ class TestGenerate_trips:
         # test if generated trips are equal
         assert_frame_equal(trips_wo_geom, trips)
 
-    def test_trip_coordinates(self):
+    def test_trip_coordinates(self, example_triplegs_higher_gap_threshold):
         """Test if coordinates of start and destination are correct"""
-        # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(
-            method="sliding", dist_threshold=25, time_threshold=5, gap_threshold=1e6
-        )
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # create trips from geolife (based on positionfixes) - with gap_threshold 1e6
+        stps, tpls = example_triplegs_higher_gap_threshold
 
         # generate trips and a joint staypoint/triplegs dataframe
         stps, tpls, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
@@ -133,16 +138,11 @@ class TestGenerate_trips:
 
             assert correct_dest_point == dest_point_trips
 
-    def test_accessor(self):
+    def test_accessor(self, example_triplegs):
         """Test if the accessor leads to the same results as the explicit function."""
-        # load pregenerated trips
-        trips_loaded = ti.read_trips_csv(os.path.join("tests", "data", "geolife_long", "trips.csv"), index_col="id")
 
-        # prepare data
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # get geolife test data (based on positionfixes)
+        stps, tpls = example_triplegs
 
         # generate trips using the explicit function import
         stps_expl, tpls_expl, trips_expl = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
@@ -155,16 +155,11 @@ class TestGenerate_trips:
         assert_geodataframe_equal(stps_expl, stps_acc)
         assert_geodataframe_equal(tpls_expl, tpls_acc)
 
-    def test_accessor_arguments(self):
+    def test_accessor_arguments(self, example_triplegs):
         """Test if the accessor is robust to different ways to receive arguments"""
-        # load pregenerated trips
-        trips_loaded = ti.read_trips_csv(os.path.join("tests", "data", "geolife_long", "trips.csv"), index_col="id")
 
-        # prepare data
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, spts = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        spts = spts.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
+        # get geolife test data (based on positionfixes)
+        spts, tpls = example_triplegs
 
         # accessor with only arguments (not allowed)
         with pytest.raises(AssertionError):
@@ -181,13 +176,10 @@ class TestGenerate_trips:
         assert_geodataframe_equal(tpls_1, tpls_2)
         assert_geodataframe_equal(trips_1, trips_2)
 
-    def test_generate_trips_missing_link(self):
+    def test_generate_trips_missing_link(self, example_triplegs):
         """Test nan is assigned for missing link between stps and trips, and tpls and trips."""
-        # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # get geolife test data (based on positionfixes)
+        stps, tpls = example_triplegs
 
         # generate trips and a joint staypoint/triplegs dataframe
         stps, tpls, _ = generate_trips(stps, tpls, gap_threshold=15)
@@ -195,13 +187,10 @@ class TestGenerate_trips:
         assert pd.isna(stps["prev_trip_id"]).any()
         assert pd.isna(stps["next_trip_id"]).any()
 
-    def test_generate_trips_dtype_consistent(self):
+    def test_generate_trips_dtype_consistent(self, example_triplegs):
         """Test the dtypes for the generated columns."""
-        # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # get geolife test data (based on positionfixes)
+        stps, tpls = example_triplegs
 
         # generate trips and a joint staypoint/triplegs dataframe
         stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
@@ -214,15 +203,12 @@ class TestGenerate_trips:
         assert stps["next_trip_id"].dtype == "Int64"
         assert tpls["trip_id"].dtype == "Int64"
 
-    def test_compare_to_old_trip_function(self):
+    def test_compare_to_old_trip_function(self, example_triplegs):
         """Test if we can generate the example trips based on example data."""
         # load pregenerated trips
 
-        # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # get geolife test data (based on positionfixes)
+        stps, tpls = example_triplegs
 
         # generate trips and a joint staypoint/triplegs dataframe
         stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
@@ -235,12 +221,10 @@ class TestGenerate_trips:
         assert_frame_equal(stps, stps_, check_like=True, check_index_type=False)
         assert_frame_equal(tpls, tpls_, check_like=True, check_index_type=False)
 
-    def test_generate_trips_index_start(self):
+    def test_generate_trips_index_start(self, example_triplegs):
         """Test the generated index start from 0 for different methods."""
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        # get geolife test data (based on positionfixes)
+        stps, tpls = example_triplegs
 
         # generate trips and a joint staypoint/triplegs dataframe
         _, _, trips = generate_trips(stps, tpls, gap_threshold=15)
@@ -307,20 +291,14 @@ class TestGenerate_trips:
         # test if generated staypoints/triplegs are equal (especially important for trip ids)
         assert_frame_equal(stps_tpls_loaded, stps_tpls, check_dtype=False)
 
-    def test_generate_trips_id_management(self):
+    def test_generate_trips_id_management(self, example_triplegs_higher_gap_threshold):
         """Test if we can generate the example trips based on example data."""
         stps_tpls_loaded = pd.read_csv(os.path.join("tests", "data", "geolife_long", "stps_tpls.csv"), index_col="id")
         stps_tpls_loaded["started_at"] = pd.to_datetime(stps_tpls_loaded["started_at"])
         stps_tpls_loaded["started_at_next"] = pd.to_datetime(stps_tpls_loaded["started_at_next"])
         stps_tpls_loaded["finished_at"] = pd.to_datetime(stps_tpls_loaded["finished_at"])
 
-        # create trips from geolife (based on positionfixes)
-        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(
-            method="sliding", dist_threshold=25, time_threshold=5, gap_threshold=1e6
-        )
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        stps, tpls = example_triplegs_higher_gap_threshold
 
         # generate trips and a joint staypoint/triplegs dataframe
         gap_threshold = 15
@@ -356,6 +334,20 @@ class TestGenerate_trips:
         assert (tpls_["trip_id"] == 0).all()
         assert len(trips) == 1
 
+    def test_loop_linestring_case(self, example_triplegs):
+        """Test corner case where a tripleg starts and ends at the same point"""
+        # input data: preprocessed stps and tpls
+        stps, tpls = example_triplegs
+
+        # add a tripleg with same start as end, by modifying the first tripleg
+        tpls.loc[0, "geom"] = LineString([(0, 0), (1, 1), (0, 0)])
+
+        # generate trips and a joint staypoint/triplegs dataframe
+        stps, tpls, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+
+        # test if start of first trip is (0,0)
+        assert trips.loc[0, "geom"][0] == Point(0, 0)
+
 
 def _create_debug_stps_tpls_data(stps, tpls, gap_threshold):
     """Preprocess stps and tpls for "test_generate_trips_*."""