Repository: mie-lab/trackintel

Issue:
start and endpoint for trip model
I think trips in trackintel should have the start and end point as geometry. Even though from the perspective of creating trips from staypoints this is redundant but in real-life use cases it might appear that we only analyze trips without having access to staypoints.

- Add columns geom_start and geom_end as required (and with a fixed name I guess...)
- Add column primary_mode as an optional column 

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/271?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
> Merging [#271](https://codecov.io/gh/mie-lab/trackintel/pull/271?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (567063e) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/2ec98ca8f63988788d323998ae4f6a9f9f62975f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (2ec98ca) will **decrease** coverage by `3.79%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mie-lab/trackintel/pull/271/graphs/tree.svg?width=650&height=150&src=pr&token=ITDZEEE1MO&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab)](https://codecov.io/gh/mie-lab/trackintel/pull/271?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab)

```diff
@@            Coverage Diff             @@
##           master     #271      +/-   ##
==========================================
- Coverage   95.11%   91.31%   -3.80%     
==========================================
  Files          38       38              
  Lines        1618     1647      +29     
  Branches      265      269       +4     
==========================================
- Hits         1539     1504      -35     
- Misses         41      104      +63     
- Partials       38       39       +1     
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/271?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/io/file.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9maWxlLnB5) | `97.08% <100.00%> (+0.06%)` | :arrow_up: |
| [trackintel/preprocessing/triplegs.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3RyaXBsZWdzLnB5) | `92.30% <100.00%> (+0.39%)` | :arrow_up: |
| [trackintel/io/postgis.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9wb3N0Z2lzLnB5) | `17.80% <0.00%> (-82.20%)` | :arrow_down: |
| [trackintel/model/trips.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwcy5weQ==) | `95.23% <0.00%> (-4.77%)` | :arrow_down: |
| [trackintel/model/locations.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9sb2NhdGlvbnMucHk=) | `96.00% <0.00%> (-4.00%)` | :arrow_down: |
| [trackintel/model/positionfixes.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9wb3NpdGlvbmZpeGVzLnB5) | `97.05% <0.00%> (-2.95%)` | :arrow_down: |
| [trackintel/model/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9zdGF5cG9pbnRzLnB5) | `97.22% <0.00%> (-2.78%)` | :arrow_down: |
| [trackintel/model/triplegs.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwbGVncy5weQ==) | `97.56% <0.00%> (-2.44%)` | :arrow_down: |
| [trackintel/preprocessing/positionfixes.py](https://codecov.io/gh/mie-lab/trackintel/pull/271/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3Bvc2l0aW9uZml4ZXMucHk=) | `97.04% <0.00%> (+1.04%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/271?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/271?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Last update [2ec98ca...567063e](https://codecov.io/gh/mie-lab/trackintel/pull/271?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).

henrymartin1: Results from our last meeting: 
- Geometry should be optional
- Multipoint with the first point of the first tripleg (or the last point of the last tripleg) can be used if the start/end point is missing because multipoint does not allow for missing geometries
henrymartin1: Reminder: In the end we will have to update the documentation
NinaWie: I changed the structure according to your comment @henrymartin1 such that the trips is always a GeoDataFrame, no matter if it contains a geometry column or not. However, I get an error and I'm confused about this test: https://github.com/mie-lab/trackintel/blob/e0c0cdd0d8472ba7b113b3819d062ea8abcd8168/tests/io/test_from_geopandas.py#L64
In all other `from_geopandas` tests, the reading from a .geojson file is compared to reading form a csv file. In contrast, in the trips tests the csv file is basically compared to a csv. Since one of them is a GeoDataFrame now, the test fails. 

I could now either 1) change the test such that the first one is also a gdf, which however makes the test quite useless (because we load the same csv file in both cases and then convert it to a gdf), or 2) generate a new geojson file from the `trips.csv` (which does not have a geometry column) or `geolife_long/trips.csv` (which has a geometry column). 
Let me know what you think.
NinaWie: Thanks for the review; I did the changes to the documentation as you suggested :) I also compiled it locally and - after removing some more spaces - it looked fine. Let me know if there is anything else to do

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 02f62d3..f6bf977 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,7 +66,7 @@ See [issue 117](https://github.com/mie-lab/trackintel/issues/117)
   - Staypoints: `GeoDataFrame (as trackintel staypoints)`
   - Triplegs: `GeoDataFrame (as trackintel triplegs)`
   - Locations: `GeoDataFrame (as trackintel locations)`  
-  - Trips: `DataFrame (as trackintel trips)`
+  - Trips: `(Geo)DataFrame (as trackintel trips)`
   - Tours: `DataFrame (as trackintel tours)` 
 
 ### Others
diff --git a/trackintel/io/file.py b/trackintel/io/file.py
index 363f93a..1681a2e 100644
--- a/trackintel/io/file.py
+++ b/trackintel/io/file.py
@@ -1,4 +1,5 @@
 import warnings
+from geopandas.geodataframe import GeoDataFrame
 
 import numpy as np
 import dateutil
@@ -8,6 +9,7 @@ import pandas as pd
 import pytz
 import warnings
 from shapely import wkt
+from shapely import geometry
 from shapely.geometry import Point
 
 
@@ -424,6 +426,7 @@ def read_trips_csv(*args, columns=None, tz=None, index_col=object(), **kwargs):
         The column names to rename in the format {'old_name':'trackintel_standard_name'}.
         The required columns for this function include: "user_id", "started_at",
         "finished_at", "origin_staypoint_id" and "destination_staypoint_id".
+        An optional column is "geom" of type MultiPoint, containing start and destination points of the trip
 
     tz : str, optional
         pytz compatible timezone string. If None UTC is assumed.
@@ -434,8 +437,8 @@ def read_trips_csv(*args, columns=None, tz=None, index_col=object(), **kwargs):
 
     Returns
     -------
-    trips : DataFrame (as trackintel trips)
-        A DataFrame containing the trips.
+    trips : (Geo)DataFrame (as trackintel trips)
+        A DataFrame containing the trips. GeoDataFrame if geometry column exists.
 
     Notes
     -----
@@ -445,10 +448,14 @@ def read_trips_csv(*args, columns=None, tz=None, index_col=object(), **kwargs):
     --------
     >>> trackintel.read_trips_csv('data.csv')
     >>> trackintel.read_trips_csv('data.csv', columns={'start_time':'started_at', 'User':'user_id'})
-        user_id                started_at               finished_at  origin_staypoint_id  destination_staypoint_id
+        user_id                started_at               finished_at  origin_staypoint_id  destination_staypoint_id\
     id
     0         1 2015-11-27 08:00:00+00:00 2015-11-27 08:15:00+00:00                    2                         5
     1         1 2015-11-27 08:20:22+00:00 2015-11-27 08:35:22+00:00                    5                         3
+                                geom  
+    id                                                     
+    0   MULTIPOINT (116.31842 39.98470, 116.29873 39.999729)
+    1   MULTIPOINT (116.29873 39.98402, 116.32480 40.009269)
     """
     columns = {} if columns is None else columns
 
@@ -472,6 +479,11 @@ def read_trips_csv(*args, columns=None, tz=None, index_col=object(), **kwargs):
         if not pd.api.types.is_datetime64tz_dtype(trips[col]):
             trips[col] = _localize_timestamp(dt_series=trips[col], pytz_tzinfo=tz, col_name=col)
 
+    # convert to geodataframe
+    if "geom" in trips.columns:
+        trips["geom"] = trips["geom"].apply(wkt.loads)
+        trips = gpd.GeoDataFrame(trips, geometry="geom")
+
     # assert validity of trips
     trips.as_trips
     return trips
@@ -485,13 +497,16 @@ def write_trips_csv(trips, filename, *args, **kwargs):
 
     Parameters
     ----------
-    trips : DataFrame (as trackintel trips)
+    trips : (Geo)DataFrame (as trackintel trips)
         The trips to store to the CSV file.
 
     filename : str
         The file to write to.
     """
     df = trips.copy()
+    if isinstance(df, GeoDataFrame):
+        geom_col_name = df.geometry.name
+        df[geom_col_name] = df[geom_col_name].apply(wkt.dumps)
     df.to_csv(filename, index=True, *args, **kwargs)
 
 
diff --git a/trackintel/model/trips.py b/trackintel/model/trips.py
index 73abb1c..3d57f22 100644
--- a/trackintel/model/trips.py
+++ b/trackintel/model/trips.py
@@ -3,6 +3,7 @@ from trackintel.io.postgis import write_trips_postgis
 from trackintel.io.file import write_trips_csv
 from trackintel.model.util import copy_docstring
 import pandas as pd
+import geopandas as gpd
 
 import trackintel as ti
 
@@ -17,7 +18,9 @@ class TripsAccessor(object):
     Requires at least the following columns:
     ['user_id', 'started_at', 'finished_at', 'origin_staypoint_id', 'destination_staypoint_id']
 
-    The 'index' of the GeoDataFrame will be treated as unique identifier of the `Trips`
+    The 'index' of the (Geo)DataFrame will be treated as unique identifier of the `Trips`
+
+    Trips have an optional geometry of type MultiPoint which describes the start and the end point of the trip
 
     For several usecases, the following additional columns are required:
     ['context', 'origin_activity', 'destination_activity', 'modes', 'primary_mode', 'tour_id']
@@ -25,15 +28,13 @@ class TripsAccessor(object):
     Notes
     -----
     `Trips` are an aggregation level in transport planning that summarize all movement and all non-essential actions
-    (e.g., waiting) between two relevant activities.
-
-    The following assumptions are implemented
-
-        - All movement before the first and after the last activity is omitted
+    (e.g., waiting) between two relevant activities. The following assumptions are implemented
         - If we do not record a person for more than `gap_threshold` minutes, we assume that the person performed an \
-            activity in the recording gap and split the trip at the gap.
-        - Trips that start/end in a recording gap can have an unknown origin/destination.
-        - There are no trips without a (recored) tripleg.
+          activity in the recording gap and split the trip at the gap.
+        - Trips that start/end in a recording gap can have an unknown origin/destination staypoint id.
+        - If the origin (or destination) staypoint is unknown (and a geometry column exists), the origin/destination
+          geometry is set as the first coordinate of the first tripleg (or the last coordinate of the last tripleg)
+        - There are no trips without a (recorded) tripleg.
 
     'started_at' and 'finished_at' are timezone aware pandas datetime objects.
 
@@ -65,6 +66,15 @@ class TripsAccessor(object):
             obj["finished_at"]
         ), "dtype of finished_at is {} but has to be datetime64 and timezone aware".format(obj["finished_at"].dtype)
 
+        # Check geometry if Trips is a GeoDataFrame
+        if isinstance(obj, gpd.GeoDataFrame):
+            # check geometry
+            assert obj.geometry.is_valid.all(), (
+                "Not all geometries are valid. Try x[~ x.geometry.is_valid] " "where x is you GeoDataFrame"
+            )
+            if obj.geometry.iloc[0].geom_type != "MultiPoint":
+                raise AttributeError("The geometry must be a MultiPoint (only first checked).")
+
     def plot(self, *args, **kwargs):
         """
         Plot this collection of trips.
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index ef18e06..776c3b3 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -2,6 +2,10 @@ import warnings
 
 import numpy as np
 import pandas as pd
+from shapely import geometry
+from tqdm import tqdm
+from shapely.geometry import MultiPoint, Point
+import geopandas as gpd
 
 
 def smoothen_triplegs(triplegs, tolerance=1.0, preserve_topology=True):
@@ -36,7 +40,7 @@ def smoothen_triplegs(triplegs, tolerance=1.0, preserve_topology=True):
     return ret_tpls
 
 
-def generate_trips(spts, tpls, gap_threshold=15):
+def generate_trips(spts, tpls, gap_threshold=15, add_geometry=True):
     """Generate trips based on staypoints and triplegs.
 
     Parameters
@@ -49,6 +53,10 @@ def generate_trips(spts, tpls, gap_threshold=15):
         Maximum allowed temporal gap size in minutes. If tracking data is missing for more than
         `gap_threshold` minutes, then a new trip begins after the gap.
 
+    add_geometry : bool default True
+        If True, the start and end coordinates of each trip are added to the output table in a geometry column "geom"
+        of type MultiPoint. Set `add_geometry=False` for better runtime performance (if coordinates are not required).
+
     print_progress : bool, default False
         If print_progress is True, the progress bar is displayed
 
@@ -60,7 +68,7 @@ def generate_trips(spts, tpls, gap_threshold=15):
     triplegs: GeoDataFrame (as trackintel triplegs)
         The original triplegs with a new column ``[`trip_id`]``.
 
-    trips: GeoDataFrame (as trackintel trips)
+    trips: (Geo)DataFrame (as trackintel trips)
         The generated trips.
 
     Notes
@@ -74,7 +82,12 @@ def generate_trips(spts, tpls, gap_threshold=15):
         - If we do not record a person for more than `gap_threshold` minutes,
           we assume that the person performed an activity in the recording gap and split the trip at the gap.
         - Trips that start/end in a recording gap can have an unknown origin/destination
-        - There are no trips without a (recored) tripleg
+        - There are no trips without a (recorded) tripleg
+        - Trips optionally have their start and end point as geometry of type MultiPoint, if `add_geometry==True`
+        - If the origin (or destination) staypoint is unknown, and `add_geometry==True`, the origin (and destination)
+          geometry is set as the first coordinate of the first tripleg (or the last coordinate of the last tripleg),
+          respectively. Trips with missing values can still be identified via col `origin_staypoint_id`.
+
 
     Examples
     --------
@@ -114,6 +127,9 @@ def generate_trips(spts, tpls, gap_threshold=15):
             tpls[["started_at", "finished_at", "user_id", "type"]],
         ]
     )
+    if add_geometry:
+        spts_tpls["geom"] = pd.concat([spts.geometry, tpls.geometry])
+
     # transform nan to bool
     spts_tpls["activity"].fillna(False, inplace=True)
 
@@ -193,6 +209,11 @@ def generate_trips(spts, tpls, gap_threshold=15):
     trips_with_act["origin_staypoint_id"] = trips_with_act["spts_tpls_id"].shift(1)
     trips_with_act["destination_staypoint_id"] = trips_with_act["spts_tpls_id"].shift(-1)
 
+    # add geometry for start and end points
+    if add_geometry:
+        trips_with_act["origin_geom"] = trips_with_act["geom"].shift(1)
+        trips_with_act["destination_geom"] = trips_with_act["geom"].shift(-1)
+
     # add prev_trip_id and next_trip_id for activity staypoints
     trips_with_act["prev_trip_id"] = trips_with_act["trip_id"].shift(1)
     trips_with_act["next_trip_id"] = trips_with_act["trip_id"].shift(-1)
@@ -218,6 +239,28 @@ def generate_trips(spts, tpls, gap_threshold=15):
         axis=1,
     )
 
+    # fill missing points and convert to MultiPoint
+    # for all trips with missing 'origin_staypoint_id' we now assign the startpoint of the first tripleg of the trip.
+    # for all tripls with missing 'destination_staypoint_id' we now assign the endpoint of the last tripleg of the trip.
+    if add_geometry:
+        # fill geometry for origin staypoints that are NaN
+        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]
+        )
+        # 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]
+        )
+        # convert to GeoDataFrame with MultiPoint column
+        trips["geom"] = [MultiPoint([x, y]) for x, y in zip(trips.origin_geom, trips.destination_geom)]
+        trips = gpd.GeoDataFrame(trips, geometry="geom")
+        # cleanup
+        trips.drop(["origin_geom", "destination_geom"], inplace=True, axis=1)
+
     # now handle the data that is aggregated in the trips
     # assign trip_id to tpls
     temp = trips.explode("tpls")


Test patch:
diff --git a/tests/data/geolife_long/trips.csv b/tests/data/geolife_long/trips.csv
index 6a4ea07..e910b17 100644
--- a/tests/data/geolife_long/trips.csv
+++ b/tests/data/geolife_long/trips.csv
@@ -1,15 +1,15 @@
-id,user_id,started_at,finished_at,origin_staypoint_id,destination_staypoint_id
-0,0,2008-10-23 02:53:04+00:00,2008-10-23 03:05:00+00:00,,0.0
-1,0,2008-10-23 04:08:07+00:00,2008-10-23 04:34:37+00:00,0.0,1.0
-2,0,2008-10-23 09:42:25+00:00,2008-10-23 10:45:41+00:00,1.0,5.0
-3,0,2008-10-23 11:09:22+00:00,2008-10-23 11:10:57+00:00,5.0,6.0
-4,0,2008-10-24 02:09:59+00:00,2008-10-24 02:47:06+00:00,6.0,
-5,1,2008-10-23 05:53:05+00:00,2008-10-23 06:01:42+00:00,,10.0
-6,1,2008-10-23 10:32:53+00:00,2008-10-23 11:10:19+00:00,10.0,11.0
-7,1,2008-10-23 11:49:08+00:00,2008-10-23 11:49:48+00:00,11.0,12.0
-8,1,2008-10-23 23:43:46+00:00,2008-10-24 00:23:03+00:00,12.0,14.0
-9,1,2008-10-24 01:45:41+00:00,2008-10-24 02:02:46+00:00,14.0,15.0
-10,1,2008-10-24 02:28:19+00:00,2008-10-24 02:31:32+00:00,15.0,16.0
-11,1,2008-10-24 03:16:35+00:00,2008-10-24 04:12:50+00:00,16.0,20.0
-12,1,2008-10-24 05:28:05+00:00,2008-10-24 05:39:50+00:00,20.0,21.0
-13,1,2008-10-24 06:08:42+00:00,2008-10-24 06:35:50+00:00,21.0,
+id,user_id,started_at,finished_at,origin_staypoint_id,destination_staypoint_id,geom
+0,0,2008-10-23 02:53:04+00:00,2008-10-23 03:05:00+00:00,,0.0,"MULTIPOINT (116.3184169999999966 39.9847019999999986, 116.2987250000000046 39.9840190000000035)"
+1,0,2008-10-23 04:08:07+00:00,2008-10-23 04:34:37+00:00,0.0,1.0,"MULTIPOINT (116.2987250000000046 39.9840190000000035, 116.3248009999999937 39.9997290000000021)"
+2,0,2008-10-23 09:42:25+00:00,2008-10-23 10:45:41+00:00,1.0,5.0,"MULTIPOINT (116.3248009999999937 39.9997290000000021, 116.3216824999999943 40.0089049999999986)"
+3,0,2008-10-23 11:09:22+00:00,2008-10-23 11:10:57+00:00,5.0,6.0,"MULTIPOINT (116.3216824999999943 40.0089049999999986, 116.3209779999999967 40.0092690000000033)"
+4,0,2008-10-24 02:09:59+00:00,2008-10-24 02:47:06+00:00,6.0,,"MULTIPOINT (116.3209779999999967 40.0092690000000033, 116.3211620000000011 40.0092089999999985)"
+5,1,2008-10-23 05:53:05+00:00,2008-10-23 06:01:42+00:00,,10.0,"MULTIPOINT (116.3192360000000036 39.9840939999999989, 116.3275380000000041 39.9780510000000007)"
+6,1,2008-10-23 10:32:53+00:00,2008-10-23 11:10:19+00:00,10.0,11.0,"MULTIPOINT (116.3275380000000041 39.9780510000000007, 116.3064135000000050 40.0138019999999983)"
+7,1,2008-10-23 11:49:08+00:00,2008-10-23 11:49:48+00:00,11.0,12.0,"MULTIPOINT (116.3064135000000050 40.0138019999999983, 116.3065320000000042 40.0138200000000026)"
+8,1,2008-10-23 23:43:46+00:00,2008-10-24 00:23:03+00:00,12.0,14.0,"MULTIPOINT (116.3065320000000042 40.0138200000000026, 116.3266370000000052 39.9779920000000004)"
+9,1,2008-10-24 01:45:41+00:00,2008-10-24 02:02:46+00:00,14.0,15.0,"MULTIPOINT (116.3266370000000052 39.9779920000000004, 116.3084029999999984 39.9810390000000027)"
+10,1,2008-10-24 02:28:19+00:00,2008-10-24 02:31:32+00:00,15.0,16.0,"MULTIPOINT (116.3084029999999984 39.9810390000000027, 116.3101889999999941 39.9813740000000024)"
+11,1,2008-10-24 03:16:35+00:00,2008-10-24 04:12:50+00:00,16.0,20.0,"MULTIPOINT (116.3101889999999941 39.9813740000000024, 116.3267620000000022 39.9779330000000002)"
+12,1,2008-10-24 05:28:05+00:00,2008-10-24 05:39:50+00:00,20.0,21.0,"MULTIPOINT (116.3267620000000022 39.9779330000000002, 116.3112610000000018 39.9827119999999994)"
+13,1,2008-10-24 06:08:42+00:00,2008-10-24 06:35:50+00:00,21.0,,"MULTIPOINT (116.3112610000000018 39.9827119999999994, 116.3270629999999954 39.9778990000000007)"
diff --git a/tests/data/trips_mod_columns.csv b/tests/data/trips_mod_columns.csv
index 92bffb5..05f507a 100644
--- a/tests/data/trips_mod_columns.csv
+++ b/tests/data/trips_mod_columns.csv
@@ -1,5 +1,5 @@
-id;user_id;started_at;finished_at;orig_stp;dest_stp
-0;1;2015-11-27T08:00:00Z;2015-11-27T08:15:00Z;2;5
-1;1;2015-11-27T08:20:22Z;2015-11-27T08:35:22Z;2;5
-2;1;2015-11-27T12:39:28Z;2015-11-27T12:55:28Z;2;5
-3;1;2015-11-27T13:57:20Z;2015-11-27T14:13:20Z;2;5
\ No newline at end of file
+id;user_id;started_at;finished_at;orig_stp;dest_stp;geom
+0;1;2015-11-27T08:00:00Z;2015-11-27T08:15:00Z;2;5;MULTIPOINT (116.3184169999999966 39.9847019999999986, 116.2987250000000046 39.9840190000000035)
+1;1;2015-11-27T08:20:22Z;2015-11-27T08:35:22Z;2;5;MULTIPOINT (116.3184169999999966 39.9847019999999986, 116.2987250000000046 39.9840190000000035)
+2;1;2015-11-27T12:39:28Z;2015-11-27T12:55:28Z;2;5;MULTIPOINT (116.3184169999999966 39.9847019999999986, 116.2987250000000046 39.9840190000000035)
+3;1;2015-11-27T13:57:20Z;2015-11-27T14:13:20Z;2;5;MULTIPOINT (116.3184169999999966 39.9847019999999986, 116.2987250000000046 39.9840190000000035)
diff --git a/tests/io/test_file.py b/tests/io/test_file.py
index aee483e..67e027b 100644
--- a/tests/io/test_file.py
+++ b/tests/io/test_file.py
@@ -264,7 +264,8 @@ class TestTrips:
         trips = ti.read_trips_csv(orig_file, sep=";", index_col="id")
         column_mapping = {"orig_stp": "origin_staypoint_id", "dest_stp": "destination_staypoint_id"}
         mod_trips = ti.read_trips_csv(mod_file, columns=column_mapping, sep=";", index_col="id")
-        assert mod_trips.equals(trips)
+        mod_trips_wo_geom = pd.DataFrame(mod_trips.drop(columns=["geom"]))
+        assert mod_trips_wo_geom.equals(trips)
 
         trips["started_at"] = trips["started_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
         trips["finished_at"] = trips["finished_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
diff --git a/tests/io/test_postgis_gpd.py b/tests/io/test_postgis_gpd.py
index f4ab5ec..4d69257 100644
--- a/tests/io/test_postgis_gpd.py
+++ b/tests/io/test_postgis_gpd.py
@@ -154,7 +154,7 @@ def example_trips():
             "destination_staypoint_id": 1,
         },
     ]
-    trips = gpd.GeoDataFrame(data=list_dict)
+    trips = pd.DataFrame(data=list_dict)
     trips.index.name = "id"
     assert trips.as_trips
     return trips
diff --git a/tests/model/test_trips.py b/tests/model/test_trips.py
index 44c1dcc..f2b87ab 100644
--- a/tests/model/test_trips.py
+++ b/tests/model/test_trips.py
@@ -1,17 +1,46 @@
 import os
 import pytest
+from shapely.geometry import Point, MultiPoint
 
 import trackintel as ti
 
 
+@pytest.fixture
+def testdata_trips():
+    """Read trips test data from file."""
+    test_trips = ti.read_trips_csv(os.path.join("tests", "data", "geolife_long", "trips.csv"), index_col="id")
+    return test_trips
+
+
 class TestTrips:
     """Tests for the TripsAccessor."""
 
-    def test_accessor(self):
+    def test_accessor(self, testdata_trips):
         """Test if the as_trips accessor checks the required column for trips."""
-        trips = ti.read_trips_csv(os.path.join("tests", "data", "geolife_long", "trips.csv"), index_col="id")
+        trips = testdata_trips.copy()
         assert trips.as_trips
 
         # user_id
         with pytest.raises(AttributeError):
             trips.drop(["user_id"], axis=1).as_trips
+
+    def test_accessor_geometry_type(self, testdata_trips):
+        """Test if the as_trips accessor requires MultiPoint geometry."""
+        trips_wrong_geom = testdata_trips.copy()
+
+        # check geometry type
+        with pytest.raises(AttributeError, match="The geometry must be a MultiPoint"):
+            trips_wrong_geom["geom"] = Point([(13.476808430, 48.573711823)])
+            trips_wrong_geom.as_trips
+
+    def test_accessor_geometry_name(self, testdata_trips):
+        """Test that it also works with a different geometry column name"""
+        trips_other_geom_name = testdata_trips.copy()
+
+        trips_other_geom_name["other_geom"] = Point(13.476808430, 48.573711823)
+        trips_other_geom_name.set_geometry("other_geom", inplace=True)
+        trips_other_geom_name.drop(columns=["geom"], inplace=True)
+
+        # check that it works with other geometry name
+        with pytest.raises(AttributeError, match="The geometry must be a MultiPoint"):
+            trips_other_geom_name.as_trips
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 4a7aa1d..58f5c55 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -7,6 +7,7 @@ import pandas as pd
 import pytest
 from geopandas.testing import assert_geodataframe_equal
 from pandas.testing import assert_frame_equal, assert_series_equal
+from shapely import geometry
 from shapely.geometry import LineString, Point
 
 import trackintel as ti
@@ -65,8 +66,72 @@ class TestGenerate_trips:
 
         # generate trips and a joint staypoint/triplegs dataframe
         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)
+
+    def test_trip_wo_geom(self):
+        """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)
+
+        # generate trips dataframe with geometry
+        _, _, trips = generate_trips(stps, tpls, gap_threshold=15)
+        trips = pd.DataFrame(trips.drop(["geom"], axis=1))
+
+        # generate trips without geometry
+        _, _, trips_wo_geom = generate_trips(stps, tpls, gap_threshold=15, add_geometry=False)
+
         # test if generated trips are equal
-        assert_frame_equal(trips_loaded, trips)
+        assert_frame_equal(trips_wo_geom, trips)
+
+    def test_trip_coordinates(self):
+        """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)
+
+        # generate trips and a joint staypoint/triplegs dataframe
+        stps, tpls, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+
+        # Check start and destination points of all rows
+        for i, row in trips.iterrows():
+            start_point_trips = row["geom"][0]  # get origin Point in generated trips
+            if not pd.isna(row["origin_staypoint_id"]):
+                # compare to the Point in the staypoints
+                correct_start_point = stps.loc[row["origin_staypoint_id"], "geom"]
+            else:
+                # check if it is the first point of the tripleg
+                # get all triplegs on this trip
+                tpls_on_trip = tpls[tpls["trip_id"] == row.name]
+                # correct point is the first point on the tripleg
+                correct_start_point, _ = tpls_on_trip.iloc[0]["geom"].boundary
+
+            assert correct_start_point == start_point_trips
+
+            dest_point_trips = row["geom"][1]  # get destination Point in generated trips
+            if not pd.isna(row["destination_staypoint_id"]):
+                correct_dest_point = stps.loc[row["destination_staypoint_id"], "geom"]
+                # compare to the Point in the staypoints
+            else:
+                # check if it is the last point of the tripleg
+                # get all triplegs on this trip
+                tpls_on_trip = tpls[tpls["trip_id"] == row.name]
+                # correct point is the first point on the tripleg
+                _, correct_dest_point = tpls_on_trip.iloc[-1]["geom"].boundary
+
+            assert correct_dest_point == dest_point_trips
 
     def test_accessor(self):
         """Test if the accessor leads to the same results as the explicit function."""
@@ -86,7 +151,7 @@ class TestGenerate_trips:
         stps_acc, tpls_acc, trips_acc = tpls.as_triplegs.generate_trips(stps, gap_threshold=15)
 
         # test if generated trips are equal
-        pd.testing.assert_frame_equal(trips_expl, trips_acc)
+        assert_geodataframe_equal(trips_expl, trips_acc)
         assert_geodataframe_equal(stps_expl, stps_acc)
         assert_geodataframe_equal(tpls_expl, tpls_acc)
 
@@ -114,7 +179,7 @@ class TestGenerate_trips:
         # test if generated trips are equal (1,2)
         assert_geodataframe_equal(spts_1, spts_2)
         assert_geodataframe_equal(tpls_1, tpls_2)
-        pd.testing.assert_frame_equal(trips_1, trips_2)
+        assert_geodataframe_equal(trips_1, trips_2)
 
     def test_generate_trips_missing_link(self):
         """Test nan is assigned for missing link between stps and trips, and tpls and trips."""
@@ -162,6 +227,7 @@ class TestGenerate_trips:
         # generate trips and a joint staypoint/triplegs dataframe
         stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
         stps_, tpls_, trips_ = _generate_trips_old(stps, tpls, gap_threshold=15)
+        trips.drop(columns=["geom"], inplace=True)
 
         # test if generated trips are equal
         # ignore column order and index dtype
@@ -226,7 +292,7 @@ class TestGenerate_trips:
         assert tpls_in.as_triplegs
 
         # load ground truth data
-        trips_loaded = pd.read_csv(os.path.join("tests", "data", "trips", "trips_gaps.csv"), index_col="id")
+        trips_loaded = ti.read_trips_csv(os.path.join("tests", "data", "trips", "trips_gaps.csv"), index_col="id")
         trips_loaded["started_at"] = pd.to_datetime(trips_loaded["started_at"], utc=True)
         trips_loaded["finished_at"] = pd.to_datetime(trips_loaded["finished_at"], utc=True)
 
@@ -236,11 +302,11 @@ class TestGenerate_trips:
         stps_tpls_loaded["finished_at"] = pd.to_datetime(stps_tpls_loaded["finished_at"], utc=True)
 
         # generate trips and a joint staypoint/triplegs dataframe
-        stps_proc, tpls_proc, trips = generate_trips(stps_in, tpls_in, gap_threshold=gap_threshold)
+        stps_proc, tpls_proc, trips = generate_trips(stps_in, tpls_in, gap_threshold=gap_threshold, add_geometry=False)
         stps_tpls = _create_debug_stps_tpls_data(stps_proc, tpls_proc, gap_threshold=gap_threshold)
 
         # test if generated trips are equal
-        assert_frame_equal(trips_loaded, trips)
+        pd.testing.assert_frame_equal(trips_loaded, trips)
 
         # test if generated staypoints/triplegs are equal (especially important for trip ids)
         assert_frame_equal(stps_tpls_loaded, stps_tpls, check_dtype=False)
@@ -287,7 +353,7 @@ class TestGenerate_trips:
         spts_tpls = pd.DataFrame(spts_tpls)
         spts = spts_tpls[spts_tpls["type"] == "staypoint"]
         tpls = spts_tpls[spts_tpls["type"] == "tripleg"]
-        spts_, tpls_, trips = generate_trips(spts, tpls)
+        spts_, tpls_, trips = generate_trips(spts, tpls, add_geometry=False)
         trip_id_truth = pd.Series([None, None, None, 0, None], dtype="Int64")
         trip_id_truth.index = spts_.index  # don't check index
         assert_series_equal(spts_["trip_id"], trip_id_truth, check_names=False)