Repository: mie-lab/trackintel

Issue:
ENH/PERF for generate_locations
The performance of per-user location generation can be enhanced using .groupby() (Line 81-99). Pay attention to the unique labels across users (Line 92-96). 
An additional parameter `print_progress` can be added to monitor the per-user progress.
See `generate_trips` function for examples.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/232?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
> Merging [#232](https://codecov.io/gh/mie-lab/trackintel/pull/232?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (0a4e80b) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/76bdb34fd7c0ba29ccd40cef0b0991d351066f19?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (76bdb34) will **decrease** coverage by `4.30%`.
> The diff coverage is `86.66%`.

[![Impacted file tree graph](https://codecov.io/gh/mie-lab/trackintel/pull/232/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/232?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     #232      +/-   ##
==========================================
- Coverage   94.89%   90.59%   -4.31%     
==========================================
  Files          37       37              
  Lines        1548     1552       +4     
  Branches      259      257       -2     
==========================================
- Hits         1469     1406      -63     
- Misses         41      107      +66     
- Partials       38       39       +1     
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/232?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/preprocessing/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/232/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3N0YXlwb2ludHMucHk=) | `89.55% <86.66%> (+0.66%)` | :arrow_up: |
| [trackintel/io/postgis.py](https://codecov.io/gh/mie-lab/trackintel/pull/232/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/232/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/232/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/visualization/util.py](https://codecov.io/gh/mie-lab/trackintel/pull/232/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC92aXN1YWxpemF0aW9uL3V0aWwucHk=) | `94.44% <0.00%> (-3.71%)` | :arrow_down: |
| [trackintel/model/positionfixes.py](https://codecov.io/gh/mie-lab/trackintel/pull/232/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/triplegs.py](https://codecov.io/gh/mie-lab/trackintel/pull/232/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.22% <0.00%> (-2.78%)` | :arrow_down: |
| [trackintel/model/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/232/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: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/232?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/232?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Last update [76bdb34...0a4e80b](https://codecov.io/gh/mie-lab/trackintel/pull/232?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).


Patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..a16ce8b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* linguist-vendored
+*.py linguist-vendored=false
\ No newline at end of file
diff --git a/docs/environment.yml b/docs/environment.yml
index 48537bc..4750c75 100644
--- a/docs/environment.yml
+++ b/docs/environment.yml
@@ -15,7 +15,6 @@ dependencies:
 - geoalchemy2
 - osmnx
 - psycopg2
-- imageio
 - sphinx
 - sphinx_rtd_theme
 - tqdm
diff --git a/docs/modules/io.rst b/docs/modules/io.rst
index c1091f3..d557b4e 100644
--- a/docs/modules/io.rst
+++ b/docs/modules/io.rst
@@ -87,4 +87,10 @@ Predefined dataset readers
 ==========================
 We also provide functionality to parse well-known datasets directly into the trackintel framework.
 
+Geolife
+-----------
+We support easy parsing of the Geolife dataset including available mode labels.
+
 .. autofunction:: trackintel.io.dataset_reader.read_geolife
+
+.. autofunction:: trackintel.io.dataset_reader.geolife_add_modes_to_triplegs
diff --git a/environment.yml b/environment.yml
index 1739723..775c6bd 100644
--- a/environment.yml
+++ b/environment.yml
@@ -15,6 +15,5 @@ dependencies:
 - geoalchemy2 
 - osmnx 
 - psycopg2
-- imageio
 - tqdm
 - similaritymeasures
diff --git a/requirements.txt b/requirements.txt
index 9dff552..3297999 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,5 @@ pint
 geoalchemy2 
 osmnx 
 psycopg2
-imageio
 tqdm
 similaritymeasures
\ No newline at end of file
diff --git a/trackintel/analysis/label.py b/trackintel/analysis/label.py
index 9f90f8c..6ee8516 100644
--- a/trackintel/analysis/label.py
+++ b/trackintel/analysis/label.py
@@ -1,10 +1,11 @@
 import datetime
+
 import numpy as np
 
 from trackintel.geogr.distances import check_gdf_crs, calculate_haversine_length
 
 
-def create_activity_flag(staypoints, method="time_threshold", time_threshold=5.0, activity_column_name="activity"):
+def create_activity_flag(staypoints, method="time_threshold", time_threshold=15.0, activity_column_name="activity"):
     """
     Add a flag whether or not a staypoint is considered an activity.
 
@@ -17,7 +18,7 @@ def create_activity_flag(staypoints, method="time_threshold", time_threshold=5.0
 
         - 'time_threshold' : All staypoints with a duration greater than the time_threshold are considered an activity.
 
-    time_threshold : float, default = 5 (minutes)
+    time_threshold : float, default = 15 (minutes)
         The time threshold for which a staypoint is considered an activity in minutes. Used by method 'time_threshold'
 
     activity_column_name : str , default = 'activity'
@@ -30,7 +31,7 @@ def create_activity_flag(staypoints, method="time_threshold", time_threshold=5.0
 
     Examples
     --------
-    >>> stps  = stps.as_staypoints.create_activity_flag(method='time_threshold', time_threshold=5)
+    >>> stps  = stps.as_staypoints.create_activity_flag(method='time_threshold', time_threshold=15)
     >>> print(stps['activity'])
     """
     if method == "time_threshold":
diff --git a/trackintel/analysis/tracking_quality.py b/trackintel/analysis/tracking_quality.py
index 67085b8..62c2607 100644
--- a/trackintel/analysis/tracking_quality.py
+++ b/trackintel/analysis/tracking_quality.py
@@ -227,26 +227,29 @@ def _split_overlaps(source, granularity="day"):
     # Iteratively split one day/hour from multi day/hour entries until no entry spans over multiple days/hours
     while change_flag.sum() > 0:
 
-        # calculate new finished_at timestamp (1sec before midnight)
+        # calculate new finished_at timestamp (00:00 midnight)
         finished_at_temp = df.loc[change_flag, "finished_at"].copy()
         if granularity == "day":
             df.loc[change_flag, "finished_at"] = df.loc[change_flag, "started_at"].apply(
-                lambda x: x.replace(hour=23, minute=59, second=59)
+                lambda x: x.replace(hour=23, minute=59, second=59) + datetime.timedelta(seconds=1)
             )
         elif granularity == "hour":
             df.loc[change_flag, "finished_at"] = df.loc[change_flag, "started_at"].apply(
-                lambda x: x.replace(minute=59, second=59)
+                lambda x: x.replace(minute=59, second=59) + datetime.timedelta(seconds=1)
             )
 
         # create new entries with remaining timestamp
         new_df = df.loc[change_flag].copy()
-        new_df.loc[change_flag, "started_at"] = df.loc[change_flag, "finished_at"] + datetime.timedelta(seconds=1)
+        new_df.loc[change_flag, "started_at"] = df.loc[change_flag, "finished_at"]
         new_df.loc[change_flag, "finished_at"] = finished_at_temp
 
         df = df.append(new_df, ignore_index=True, sort=True)
 
         change_flag = __get_split_index(df, granularity=granularity)
 
+    if "duration" in df.columns:
+        df["duration"] = df["finished_at"] - df["started_at"]
+
     return df
 
 
@@ -268,9 +271,9 @@ def __get_split_index(df, granularity="day"):
     change_flag: pd.Series
         Boolean index indicating which records needs to be splitted
     """
-    change_flag = df["started_at"].dt.date != df["finished_at"].dt.date
+    change_flag = df["started_at"].dt.date != (df["finished_at"] - pd.to_timedelta("1s")).dt.date
     if granularity == "hour":
-        hour_flag = df["started_at"].dt.hour != df["finished_at"].dt.hour
+        hour_flag = df["started_at"].dt.hour != (df["finished_at"] - pd.to_timedelta("1s")).dt.hour
         # union of day and hour change flag
         change_flag = change_flag | hour_flag
 
diff --git a/trackintel/io/dataset_reader.py b/trackintel/io/dataset_reader.py
index 201c958..9f3d98e 100644
--- a/trackintel/io/dataset_reader.py
+++ b/trackintel/io/dataset_reader.py
@@ -9,6 +9,7 @@ import numpy as np
 import pandas as pd
 from shapely.geometry import Point
 from sklearn.neighbors import NearestNeighbors
+from tqdm import tqdm
 
 FEET2METER = 0.3048
 
@@ -17,7 +18,7 @@ CRS_WGS84 = "epsg:4326"
 from trackintel.preprocessing.util import calc_temp_overlap
 
 
-def read_geolife(geolife_path):
+def read_geolife(geolife_path, print_progress=False):
     """
     Read raw geolife data and return trackintel positionfixes.
 
@@ -28,20 +29,27 @@ def read_geolife(geolife_path):
     geolife_path: str
         path to the directory with the geolife data
 
+    print_progress: Bool, default False
+        Show per-user progress if set to True.
+
     Returns
     -------
     gdf: GeoDataFrame (as trackintel positionfixes)
         Contains all loaded geolife positionfixes
 
     labels: dict
-        Dictionary with the available (optional) mode labels.
+        Dictionary with the available mode labels.
 
     Notes
     -----
-    The geopandas dataframe has the following columns and datatype: 'lat': float64, Latitude WGS84; 'lon': float64, Latitude WGS84; 'elevation': float64, in meters;
-    'tracked_at': datetime64[ns]; 'user_id': int64; 'geom': geopandas/shapely geometry; 'accuracy': None;
+    The geopandas dataframe has the following columns and datatype: 'lat': float64, Latitude WGS84;
+    'lon': float64, Longitude WGS84; 'elevation': float64, in meters; 'tracked_at': datetime64[ns];
+    'user_id': int64; 'geom': geopandas/shapely geometry; 'accuracy': None;
 
+    For some users, travel mode labels are provided as .txt file. These labels are read and returned as label dictionary.
     The label dictionary contains the user ids as keys and DataFrames with the available labels as values.
+    Labels can be added to each user at the tripleg level, see
+    :func:`trackintel.io.dataset_reader.geolife_add_modes_to_triplegs` for more details.
 
     The folder structure within the geolife directory needs to be identical with the folder structure
     available from the official download. The means that the top level folder (provided with 'geolife_path')
@@ -52,17 +60,22 @@ def read_geolife(geolife_path):
     | │   ├── Trajectory
     | │   │   ├── 20081023025304.plt
     | │   │   ├── 20081024020959.plt
-    | │   │   └── 20081026134407.plt
+    | │   │   ├── 20081026134407.plt
+    | │   │   └── ...
     | ├── 001
     | │   ├── Trajectory
     | │   │   └── ...
+    | │   ...
+    | ├── 010
+    | │   ├── labels.txt
+    | │   ├── Trajectory
+    | │   │   └── ...
     | └── ...
 
     the geolife dataset as it can be downloaded from:
 
     https://www.microsoft.com/en-us/research/publication/geolife-gps-trajectory-dataset-user-guide/
 
-
     References
     ----------
     [1] Yu Zheng, Lizhu Zhang, Xing Xie, Wei-Ying Ma. Mining interesting locations and travel sequences from
@@ -77,7 +90,8 @@ def read_geolife(geolife_path):
 
     Example
     ----------
-    >>> geolife_pfs, labels = read_geolife(os.path.join('downloads', 'Geolife Trajectories 1.3'))
+    >>> from trackintel.io.dataset_reader import read_geolife
+    >>> pfs, mode_labels = read_geolife(os.path.join('downloads', 'Geolife Trajectories 1.3'))
     """
     geolife_path = os.path.join(geolife_path, "*")
     user_folder = sorted(glob.glob(geolife_path))
@@ -88,7 +102,7 @@ def read_geolife(geolife_path):
     if len(user_folder) == 0:
         raise NameError("No folders found with working directory {} and path {}".format(os.getcwd(), geolife_path))
 
-    for user_folder_this in user_folder:
+    for user_folder_this in tqdm(user_folder, disable=not print_progress):
 
         # skip files
         if not os.path.isdir(user_folder_this):
@@ -115,8 +129,6 @@ def read_geolife(geolife_path):
             )
             raise ValueError(errmsg) from err
 
-        print("start importing geolife user_id: ", user_id)
-
         input_files = sorted(glob.glob(os.path.join(user_folder_this, "Trajectory", "*.plt")))
         df_list_days = []
 
@@ -145,7 +157,6 @@ def read_geolife(geolife_path):
         # concat all days of a user into a single dataframe
         df_user_this = pd.concat(df_list_days, axis=0, ignore_index=True)
         label_dict[user_id] = labels
-        print("finished user_id: ", user_id)
 
         df_list_users.append(df_user_this)
 
@@ -189,6 +200,14 @@ def geolife_add_modes_to_triplegs(
     -------
     tpls : GeoDataFrame (as trackintel triplegs)
         triplegs with mode labels.
+
+    Example
+    ----------
+    >>> from trackintel.io.dataset_reader import read_geolife, geolife_add_modes_to_triplegs
+    >>> pfs, mode_labels = read_geolife(os.path.join('downloads', 'Geolife Trajectories 1.3'))
+    >>> pfs, spts = pfs.as_positionfixes.generate_staypoints()
+    >>> pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
+    >>> tpls = geolife_add_modes_to_triplegs(tpls, mode_labels)
     """
     tpls = tpls_in.copy()
     # temp time fields for nn query
diff --git a/trackintel/model/triplegs.py b/trackintel/model/triplegs.py
index f57ca41..f778cd0 100644
--- a/trackintel/model/triplegs.py
+++ b/trackintel/model/triplegs.py
@@ -103,6 +103,23 @@ class TriplegsAccessor(object):
         """
         return ti.preprocessing.filter.spatial_filter(self._obj, *args, **kwargs)
 
+    def generate_trips(self, *args, **kwargs):
+        """
+        Generate trips based on staypoints and triplegs.
+
+        See :func:`trackintel.preprocessing.triplegs.generate_trips`.
+        """
+        # if spts in kwargs: 'spts' can not be in args as it would be the first argument
+        if "stps_input" in kwargs:
+            return ti.preprocessing.triplegs.generate_trips(tpls_input=self._obj, **kwargs)
+        # if 'spts' no in kwargs it has to be the first argument in 'args'
+        else:
+            assert len(args) <= 1, (
+                "All arguments except 'stps_input' have to be given as keyword arguments. You gave"
+                f" {args[1:]} as positional arguments."
+            )
+            return ti.preprocessing.triplegs.generate_trips(stps_input=args[0], tpls_input=self._obj, **kwargs)
+
     def predict_transport_mode(self, *args, **kwargs):
         """
         Predict/impute the transport mode with which each tripleg was likely covered.
diff --git a/trackintel/preprocessing/positionfixes.py b/trackintel/preprocessing/positionfixes.py
index 46afbf9..54daa77 100644
--- a/trackintel/preprocessing/positionfixes.py
+++ b/trackintel/preprocessing/positionfixes.py
@@ -1,10 +1,9 @@
+import datetime
+
 import geopandas as gpd
 import numpy as np
 import pandas as pd
 from shapely.geometry import LineString, Point
-from sklearn.cluster import DBSCAN
-
-import datetime
 from tqdm import tqdm
 
 from trackintel.geogr.distances import haversine_dist
@@ -36,7 +35,8 @@ def generate_staypoints(
 
     dist_threshold : float, default 100
         The distance threshold for the 'sliding' method, i.e., how far someone has to travel to
-        generate a new staypoint. Units depend on the dist_func parameter.
+        generate a new staypoint. Units depend on the dist_func parameter. If 'distance_metric' is 'haversine' the
+        unit is in meters
 
     time_threshold : float, default 5.0 (minutes)
         The time threshold for the 'sliding' method in minutes.
@@ -354,7 +354,7 @@ def generate_triplegs(pfs_input, stps_input, method="between_staypoints", gap_th
         posfix_grouper = pfs.groupby("tripleg_id")
 
         tpls = posfix_grouper.agg(
-            {"user_id": ["mean"], "tracked_at": [min, max], "geom": list}
+            {"user_id": ["mean"], "tracked_at": [min, max], pfs.geometry.name: list}
         )  # could add a "number of pfs": can be any column "count"
 
         # prepare dataframe: Rename columns; read/set geometry/crs;
diff --git a/trackintel/preprocessing/staypoints.py b/trackintel/preprocessing/staypoints.py
index 16e2a09..b366e6b 100644
--- a/trackintel/preprocessing/staypoints.py
+++ b/trackintel/preprocessing/staypoints.py
@@ -1,14 +1,23 @@
 from math import radians
 
 import numpy as np
+import geopandas as gpd
+import pandas as pd
 from shapely.geometry import Point
 from sklearn.cluster import DBSCAN
+from tqdm import tqdm
 
 from trackintel.geogr.distances import meters_to_decimal_degrees
 
 
 def generate_locations(
-    staypoints, method="dbscan", epsilon=100, num_samples=1, distance_metric="haversine", agg_level="user"
+    staypoints,
+    method="dbscan",
+    epsilon=100,
+    num_samples=1,
+    distance_metric="haversine",
+    agg_level="user",
+    print_progress=False,
 ):
     """
     Generate locations from the staypoints.
@@ -36,10 +45,12 @@ def generate_locations(
 
     agg_level: {'user','dataset'}
         The level of aggregation when generating locations:
-
         - 'user'      : locations are generated independently per-user.
         - 'dataset'   : shared locations are generated for all users.
 
+    print_progress : bool, default False
+        If print_progress is True, the progress bar is displayed
+
     Returns
     -------
     ret_sp: GeoDataFrame (as trackintel staypoints)
@@ -59,6 +70,8 @@ def generate_locations(
 
     # initialize the return GeoDataFrames
     ret_stps = staypoints.copy()
+    ret_stps = ret_stps.sort_values(["user_id", "started_at"])
+    geo_col = ret_stps.geometry.name
 
     if method == "dbscan":
 
@@ -72,27 +85,44 @@ def generate_locations(
             db = DBSCAN(eps=epsilon, min_samples=num_samples, algorithm="ball_tree", metric=distance_metric)
 
         if agg_level == "user":
-            location_id_counter = 0
-            # TODO: change into groupby
-            for user_id_this in ret_stps["user_id"].unique():
-                # Slice staypoints array by user. This is not a copy!
-                user_staypoints = ret_stps[ret_stps["user_id"] == user_id_this]
+            if print_progress:
+                tqdm.pandas(desc="User location generation")
+                ret_stps = ret_stps.groupby("user_id", as_index=False).progress_apply(
+                    _generate_locations_per_user,
+                    geo_col=geo_col,
+                    distance_metric=distance_metric,
+                    db=db,
+                )
+            else:
+                ret_stps = ret_stps.groupby("user_id", as_index=False).apply(
+                    _generate_locations_per_user,
+                    geo_col=geo_col,
+                    distance_metric=distance_metric,
+                    db=db,
+                )
 
-                if distance_metric == "haversine":
-                    # the input is converted to list of (lat, lon) tuples in radians unit
-                    p = np.array([[radians(g.y), radians(g.x)] for g in user_staypoints.geometry])
-                else:
-                    p = np.array([[g.x, g.y] for g in user_staypoints.geometry])
-                labels = db.fit_predict(p)
+            # keeping track of noise labels
+            ret_stps_non_noise_labels = ret_stps[ret_stps["location_id"] != -1]
+            ret_stps_noise_labels = ret_stps[ret_stps["location_id"] == -1]
+
+            # sort so that the last location id of a user = max(location id)
+            ret_stps_non_noise_labels = ret_stps_non_noise_labels.sort_values(["user_id", "location_id"])
+
+            # identify start positions of new user_ids
+            start_of_user_id = ret_stps_non_noise_labels["user_id"] != ret_stps_non_noise_labels["user_id"].shift(1)
+
+            # calculate the offset (= last location id of the previous user)
+            # multiplication is to mask all positions where no new user starts and addition is to have a +1 when a
+            # new user starts
+            loc_id_offset = ret_stps_non_noise_labels["location_id"].shift(1) * start_of_user_id + start_of_user_id
 
-                # enforce unique lables across all users without changing noise labels
-                max_label = np.max(labels)
-                labels[labels != -1] = labels[labels != -1] + location_id_counter
-                if max_label > -1:
-                    location_id_counter = location_id_counter + max_label + 1
+            # fill first nan with 0 and create the cumulative sum
+            loc_id_offset = loc_id_offset.fillna(0).cumsum()
+
+            ret_stps_non_noise_labels["location_id"] = ret_stps["location_id"] + loc_id_offset
+            ret_stps = gpd.GeoDataFrame(pd.concat([ret_stps_non_noise_labels, ret_stps_noise_labels]), geometry=geo_col)
+            ret_stps.sort_values(["user_id", "started_at"], inplace=True)
 
-                # add staypoint - location matching to original staypoints
-                ret_stps.loc[user_staypoints.index, "location_id"] = labels
         else:
             if distance_metric == "haversine":
                 # the input is converted to list of (lat, lon) tuples in radians unit
@@ -111,7 +141,9 @@ def generate_locations(
         else:
             ## generate user-location pairs with same geometries across users
             # get user-location pairs
-            ret_loc = temp_sp.dissolve(by=["user_id", "location_id"], as_index=False).drop(columns={"geom"})
+            ret_loc = temp_sp.dissolve(by=["user_id", "location_id"], as_index=False).drop(
+                columns={temp_sp.geometry.name}
+            )
             # get location geometries
             geom_df = temp_sp.dissolve(by=["location_id"], as_index=False).drop(columns={"user_id"})
             # merge pairs with location geometries
@@ -124,17 +156,17 @@ def generate_locations(
         # locations with only one staypoints is of type "Point"
         point_idx = ret_loc.geom_type == "Point"
         if not ret_loc.loc[point_idx].empty:
-            ret_loc.loc[point_idx, "center"] = ret_loc.loc[point_idx, "geom"]
+            ret_loc.loc[point_idx, "center"] = ret_loc.loc[point_idx, ret_loc.geometry.name]
         # locations with multiple staypoints is of type "MultiPoint"
         if not ret_loc.loc[~point_idx].empty:
-            ret_loc.loc[~point_idx, "center"] = ret_loc.loc[~point_idx, "geom"].apply(
+            ret_loc.loc[~point_idx, "center"] = ret_loc.loc[~point_idx, ret_loc.geometry.name].apply(
                 lambda p: Point(np.array(p)[:, 0].mean(), np.array(p)[:, 1].mean())
             )
 
         # extent is the convex hull of the geometry
         ret_loc["extent"] = None  # initialize
         if not ret_loc.empty:
-            ret_loc["extent"] = ret_loc["geom"].apply(lambda p: p.convex_hull)
+            ret_loc["extent"] = ret_loc[ret_loc.geometry.name].apply(lambda p: p.convex_hull)
             # convex_hull of one point would be a Point and two points a Linestring,
             # we change them into Polygon by creating a buffer of epsilon around them.
             pointLine_idx = (ret_loc["extent"].geom_type == "LineString") | (ret_loc["extent"].geom_type == "Point")
@@ -169,3 +201,21 @@ def generate_locations(
     ret_loc["user_id"] = ret_loc["user_id"].astype(ret_stps["user_id"].dtype)
 
     return ret_stps, ret_loc
+
+
+def _generate_locations_per_user(user_staypoints, distance_metric, db, geo_col):
+    """function called after groupby: should only contain records of one user;
+    see generate_locations() function for parameter meaning."""
+
+    if distance_metric == "haversine":
+        # the input is converted to list of (lat, lon) tuples in radians unit
+        p = np.array([[radians(q.y), radians(q.x)] for q in (user_staypoints[geo_col])])
+    else:
+        p = np.array([[q.x, q.y] for q in (user_staypoints[geo_col])])
+    labels = db.fit_predict(p)
+
+    # add staypoint - location matching to original staypoints
+    user_staypoints["location_id"] = labels
+    user_staypoints = gpd.GeoDataFrame(user_staypoints, geometry=geo_col)
+
+    return user_staypoints
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index f599a38..50de0eb 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -1,4 +1,3 @@
-import copy
 import datetime
 
 import numpy as np
@@ -53,6 +52,9 @@ def generate_trips(stps_input, tpls_input, gap_threshold=15, print_progress=Fals
         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.
 
+    print_progress : bool, default False
+        If print_progress is True, the progress bar is displayed
+
     Returns
     -------
     staypoints: GeoDataFrame (as trackintel staypoints)
@@ -80,7 +82,12 @@ def generate_trips(stps_input, tpls_input, gap_threshold=15, print_progress=Fals
 
     Examples
     --------
+    >>> from trackintel.preprocessing.triplegs import generate_trips
     >>> staypoints, triplegs, trips = generate_trips(staypoints, triplegs)
+
+    trips can also be directly generated using the tripleg accessor
+    >>> staypoints, triplegs, trips = triplegs.as_triplegs.generate_trips(staypoints)
+
     """
     assert (
         "activity" in stps_input.columns


Test patch:
diff --git a/ci/envs/37-latest-conda-forge.yaml b/ci/envs/37-latest-conda-forge.yaml
index 90e4c16..66f5129 100644
--- a/ci/envs/37-latest-conda-forge.yaml
+++ b/ci/envs/37-latest-conda-forge.yaml
@@ -15,7 +15,6 @@ dependencies:
 - geoalchemy2 
 - osmnx 
 - psycopg2
-- imageio
 - pytest
 - pytest-cov
 - pytest-xdist
diff --git a/ci/envs/38-latest-conda-forge.yaml b/ci/envs/38-latest-conda-forge.yaml
index 8ab82f2..81dddb0 100644
--- a/ci/envs/38-latest-conda-forge.yaml
+++ b/ci/envs/38-latest-conda-forge.yaml
@@ -15,7 +15,6 @@ dependencies:
 - geoalchemy2 
 - osmnx 
 - psycopg2
-- imageio
 - pytest
 - pytest-cov
 - pytest-xdist
diff --git a/ci/envs/39-latest-conda-forge.yaml b/ci/envs/39-latest-conda-forge.yaml
index a9ae0da..da5a726 100644
--- a/ci/envs/39-latest-conda-forge.yaml
+++ b/ci/envs/39-latest-conda-forge.yaml
@@ -15,7 +15,6 @@ dependencies:
 - geoalchemy2 
 - osmnx 
 - psycopg2
-- imageio
 - pytest
 - pytest-cov
 - pytest-xdist
diff --git a/tests/analysis/test_label.py b/tests/analysis/test_label.py
index a6f3ec1..fdceefd 100644
--- a/tests/analysis/test_label.py
+++ b/tests/analysis/test_label.py
@@ -1,6 +1,7 @@
 import os
-import pandas as pd
+
 import numpy as np
+import pandas as pd
 import pytest
 
 import trackintel as ti
@@ -18,7 +19,7 @@ class TestCreate_activity_flag:
         activity_true = stps_test["activity"].copy()
         stps_test["activity"] = False
 
-        stps_test = stps_test.as_staypoints.create_activity_flag()
+        stps_test = stps_test.as_staypoints.create_activity_flag(method="time_threshold", time_threshold=5.0)
 
         pd.testing.assert_series_equal(stps_test["activity"], activity_true)
 
diff --git a/tests/analysis/test_tracking_quality.py b/tests/analysis/test_tracking_quality.py
index 94ecdbf..c3e5574 100644
--- a/tests/analysis/test_tracking_quality.py
+++ b/tests/analysis/test_tracking_quality.py
@@ -18,8 +18,20 @@ def testdata_stps_tpls_geolife_long():
     return stps_tpls
 
 
+@pytest.fixture
+def testdata_all_geolife_long():
+    """Generate stps, tpls and trips of the original pfs for subsequent testing."""
+    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, method="between_staypoints")
+    stps, tpls, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+
+    return stps, tpls, trips
+
+
 class TestTemporal_tracking_quality:
-    """Tests for the temporal_tracking_quality() function and its subfunctions."""
+    """Tests for the temporal_tracking_quality() function."""
 
     def test_tracking_quality_all(self, testdata_stps_tpls_geolife_long):
         """Test if the calculated total tracking quality is correct."""
@@ -124,7 +136,7 @@ class TestTemporal_tracking_quality:
         quality = ti.analysis.tracking_quality.temporal_tracking_quality(stps_tpls, granularity="hour")
 
         assert quality_manual == quality.loc[(quality["user_id"] == 0) & (quality["hour"] == 2), "quality"].values[0]
-        assert (quality["quality"] < 1).all()
+        assert (quality["quality"] <= 1).all()
 
     def test_tracking_quality_error(self, testdata_stps_tpls_geolife_long):
         """Test if the an error is raised when passing unknown 'granularity' to temporal_tracking_quality()."""
@@ -161,6 +173,37 @@ class TestTemporal_tracking_quality:
         with pytest.raises(AttributeError):
             ti.analysis.tracking_quality._get_tracking_quality_user(user_0, granularity="random")
 
+    def test_staypoints_accessors(self, testdata_all_geolife_long):
+        """Test tracking_quality calculation from staypoints accessor."""
+        stps, _, _ = testdata_all_geolife_long
+
+        # for staypoints
+        stps_quality_accessor = stps.as_staypoints.temporal_tracking_quality()
+        stps_quality_method = ti.analysis.tracking_quality.temporal_tracking_quality(stps)
+        pd.testing.assert_frame_equal(stps_quality_accessor, stps_quality_method)
+
+    def test_triplegs_accessors(self, testdata_all_geolife_long):
+        """Test tracking_quality calculation from triplegs accessor."""
+        _, tpls, _ = testdata_all_geolife_long
+
+        # for triplegs
+        tpls_quality_accessor = tpls.as_triplegs.temporal_tracking_quality()
+        tpls_quality_method = ti.analysis.tracking_quality.temporal_tracking_quality(tpls)
+        pd.testing.assert_frame_equal(tpls_quality_accessor, tpls_quality_method)
+
+    def test_trips_accessors(self, testdata_all_geolife_long):
+        """Test tracking_quality calculation from trips accessor."""
+        _, _, trips = testdata_all_geolife_long
+
+        # for trips
+        trips_quality_accessor = trips.as_trips.temporal_tracking_quality()
+        trips_quality_method = ti.analysis.tracking_quality.temporal_tracking_quality(trips)
+        pd.testing.assert_frame_equal(trips_quality_accessor, trips_quality_method)
+
+
+class TestSplit_overlaps:
+    """Tests for the _split_overlaps() function."""
+
     def test_split_overlaps_days(self, testdata_stps_tpls_geolife_long):
         """Test if _split_overlaps() function can split records that span several days."""
         stps_tpls = testdata_stps_tpls_geolife_long
@@ -174,7 +217,7 @@ class TestTemporal_tracking_quality:
         splitted = ti.analysis.tracking_quality._split_overlaps(stps_tpls, granularity="day")
 
         # no record spans several days after the split
-        multi_day_records = splitted["finished_at"].dt.day - splitted["started_at"].dt.day
+        multi_day_records = (splitted["finished_at"] - pd.to_timedelta("1s")).dt.day - splitted["started_at"].dt.day
         assert (multi_day_records == 0).all()
 
     def test_split_overlaps_hours(self, testdata_stps_tpls_geolife_long):
@@ -190,7 +233,7 @@ class TestTemporal_tracking_quality:
         splitted = ti.analysis.tracking_quality._split_overlaps(stps_tpls, granularity="hour")
 
         # no record spans several hours after the split
-        hour_diff = splitted["finished_at"].dt.hour - splitted["started_at"].dt.hour
+        hour_diff = (splitted["finished_at"] - pd.to_timedelta("1s")).dt.hour - splitted["started_at"].dt.hour
         assert (hour_diff == 0).all()
 
     def test_split_overlaps_hours_case2(self, testdata_stps_tpls_geolife_long):
@@ -203,10 +246,10 @@ class TestTemporal_tracking_quality:
         head2["finished_at"] = head2.apply(lambda x: x["started_at"].replace(day=x["started_at"].day + 1), axis=1)
 
         # the records have the same hour
-        hour_diff = head2["finished_at"].dt.hour - head2["started_at"].dt.hour
+        hour_diff = (head2["finished_at"] - pd.to_timedelta("1s")).dt.hour - head2["started_at"].dt.hour
         assert (hour_diff == 0).all()
         # but have different days
-        day_diff = head2["finished_at"].dt.day - head2["started_at"].dt.day
+        day_diff = (head2["finished_at"] - pd.to_timedelta("1s")).dt.day - head2["started_at"].dt.day
         assert (day_diff > 0).all()
 
         # split the records according to hour
@@ -214,28 +257,22 @@ class TestTemporal_tracking_quality:
         splitted = ti.analysis.tracking_quality._split_overlaps(head2, granularity="hour")
 
         # no record has different days after the split
-        day_diff = splitted["finished_at"].dt.day - splitted["started_at"].dt.day
+        day_diff = (splitted["finished_at"] - pd.to_timedelta("1s")).dt.day - splitted["started_at"].dt.day
         assert (day_diff == 0).all()
 
-    def test_accessors(self):
-        """The result obtained from model accessors should be the same as calling the function."""
-        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, method="between_staypoints")
-        stps, tpls, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+    def test_split_overlaps_duration(self, testdata_stps_tpls_geolife_long):
+        """Test if the column 'duration' gets updated after using the _split_overlaps() function."""
+        stps_tpls = testdata_stps_tpls_geolife_long
+        # initiate the duration column
+        stps_tpls["duration"] = stps_tpls["finished_at"] - stps_tpls["started_at"]
+        stps_tpls.reset_index(inplace=True)
 
-        # for staypoints
-        stps_quality_accessor = stps.as_staypoints.temporal_tracking_quality()
-        stps_quality_method = ti.analysis.tracking_quality.temporal_tracking_quality(stps)
-        pd.testing.assert_frame_equal(stps_quality_accessor, stps_quality_method)
+        # split the records according to day
+        splitted_day = ti.analysis.tracking_quality._split_overlaps(stps_tpls, granularity="day")
 
-        # for triplegs
-        tpls_quality_accessor = tpls.as_triplegs.temporal_tracking_quality()
-        tpls_quality_method = ti.analysis.tracking_quality.temporal_tracking_quality(tpls)
-        pd.testing.assert_frame_equal(tpls_quality_accessor, tpls_quality_method)
+        # split the records according to hour
+        splitted_hour = ti.analysis.tracking_quality._split_overlaps(stps_tpls, granularity="hour")
 
-        # for trips
-        trips_quality_accessor = trips.as_trips.temporal_tracking_quality()
-        trips_quality_method = ti.analysis.tracking_quality.temporal_tracking_quality(trips)
-        pd.testing.assert_frame_equal(trips_quality_accessor, trips_quality_method)
+        # test "duration" is recalculated after the split
+        assert splitted_day["duration"].sum() == stps_tpls["duration"].sum()
+        assert splitted_hour["duration"].sum() == stps_tpls["duration"].sum()
diff --git a/tests/io/test_dataset_reader.py b/tests/io/test_dataset_reader.py
index 93be32d..510d205 100644
--- a/tests/io/test_dataset_reader.py
+++ b/tests/io/test_dataset_reader.py
@@ -81,13 +81,26 @@ def impossible_matching_data():
 class TestReadGeolife:
     def test_loop_read(self):
         """Use read_geolife reader, store posfix as .csv, load them again."""
-        pfs, _ = read_geolife(os.path.join("tests", "data", "geolife"))
+        pfs, _ = read_geolife(os.path.join("tests", "data", "geolife"), print_progress=True)
         tmp_file = os.path.join("tests", "data", "positionfixes_test.csv")
         pfs.as_positionfixes.to_csv(tmp_file)
         pfs2 = ti.read_positionfixes_csv(tmp_file, index_col="id")[pfs.columns]
         os.remove(tmp_file)
         assert np.isclose(0, (pfs.lat - pfs2.lat).abs().sum())
 
+    def test_print_progress_flag(self, capsys):
+        """Test if the print_progress par controls the printing behavior"""
+        g_path = os.path.join("tests", "data", "geolife")
+        pfs, _ = read_geolife(g_path, print_progress=True)
+        captured_print = capsys.readouterr()
+        assert captured_print.err != ""
+
+        pfs, _ = read_geolife(g_path, print_progress=False)
+        captured_noprint = capsys.readouterr()
+        assert captured_noprint.err == ""
+
+        assert True
+
     def test_label_reading(self):
         """test data types of the labels returned by read_geolife"""
 
diff --git a/tests/preprocessing/test_staypoints.py b/tests/preprocessing/test_staypoints.py
index 00c7fc1..f1c5339 100644
--- a/tests/preprocessing/test_staypoints.py
+++ b/tests/preprocessing/test_staypoints.py
@@ -13,7 +13,8 @@ from trackintel.geogr.distances import calculate_distance_matrix
 class TestGenerate_locations:
     """Tests for generate_locations() method."""
 
-    def test_generate_locations_dbscan_hav_euc(self):
+    def test_dbscan_hav_euc(self):
+        """Test if using haversine and euclidean distances will generate the same location result."""
         stps_file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
         stps = ti.read_staypoints_csv(stps_file, tz="utc", index_col="id")
 
@@ -31,9 +32,10 @@ class TestGenerate_locations:
             method="dbscan", epsilon=100, num_samples=0, distance_metric="euclidean", agg_level="dataset"
         )
 
-        assert len(loc_har) == len(loc_eu), "The #location should be the same for haversine" + "and euclidean distances"
+        assert len(loc_har) == len(loc_eu)
 
-    def test_generate_locations_dbscan_haversine(self):
+    def test_dbscan_haversine(self):
+        """Test haversine dbscan location result with manually calling the DBSCAN method."""
         stps_file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
         stps = ti.read_staypoints_csv(stps_file, tz="utc", index_col="id")
 
@@ -47,9 +49,10 @@ class TestGenerate_locations:
         db = DBSCAN(eps=10, min_samples=0, metric="precomputed")
         labels = db.fit_predict(sp_distance_matrix)
 
-        assert len(set(locs.index)) == len(set(labels)), "The number of locations should be the same"
+        assert len(set(locs.index)) == len(set(labels))
 
-    def test_generate_locations_dbscan_loc(self):
+    def test_dbscan_loc(self):
+        """Test haversine dbscan location result with manually grouping the locations method."""
         stps_file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
         stps = ti.read_staypoints_csv(stps_file, tz="utc", index_col="id")
         stps, locs = stps.as_staypoints.generate_locations(
@@ -75,10 +78,11 @@ class TestGenerate_locations:
         other_locs = gpd.GeoDataFrame(other_locs, geometry="center", crs=stps.crs)
         other_locs.set_index("id", inplace=True)
 
-        assert all(other_locs["center"] == locs["center"]), "The location geometry should be the same"
-        assert all(other_locs.index == locs.index), "The location id should be the same"
+        assert all(other_locs["center"] == locs["center"])
+        assert all(other_locs.index == locs.index)
 
-    def test_generate_locations_dbscan_user_dataset(self):
+    def test_dbscan_user_dataset(self):
+        """Test user and dataset location generation."""
         stps_file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
         stps = ti.read_staypoints_csv(stps_file, tz="utc", index_col="id")
         # take the first row and duplicate once
@@ -95,12 +99,13 @@ class TestGenerate_locations:
         _, locs_us = stps.as_staypoints.generate_locations(
             method="dbscan", epsilon=10, num_samples=0, distance_metric="haversine", agg_level="user"
         )
-        loc_ds_num = len(locs_ds.index.unique())
-        loc_us_num = len(locs_us.index.unique())
-        assert loc_ds_num == 1, "Considering all staypoints at once, there should be only one location"
-        assert loc_us_num == 2, "Considering user staypoints separately, there should be two locations"
+        loc_dataset_num = len(locs_ds.index.unique())
+        loc_user_num = len(locs_us.index.unique())
+        assert loc_dataset_num == 1
+        assert loc_user_num == 2
 
-    def test_generate_locations_dbscan_min(self):
+    def test_dbscan_min(self):
+        """Test with small epsilon parameter."""
         pfs_file = os.path.join("tests", "data", "positionfixes.csv")
         pfs = ti.read_positionfixes_csv(pfs_file, sep=";", tz="utc", index_col="id")
         _, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=0, time_threshold=0)
@@ -110,10 +115,12 @@ class TestGenerate_locations:
         _, locs_data = stps.as_staypoints.generate_locations(
             method="dbscan", epsilon=1e-18, num_samples=0, agg_level="dataset"
         )
-        assert len(locs_user) == len(stps), "With small hyperparameters, clustering should not reduce the number"
-        assert len(locs_data) == len(stps), "With small hyperparameters, clustering should not reduce the number"
+        # With small hyperparameters, clustering should not reduce the number
+        assert len(locs_user) == len(stps)
+        assert len(locs_data) == len(stps)
 
-    def test_generate_locations_dbscan_max(self):
+    def test_dbscan_max(self):
+        """Test with large epsilon parameter."""
         pfs_file = os.path.join("tests", "data", "positionfixes.csv")
         pfs = ti.read_positionfixes_csv(pfs_file, sep=";", tz="utc", index_col="id")
         _, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=0, time_threshold=0)
@@ -123,10 +130,11 @@ class TestGenerate_locations:
         _, locs_data = stps.as_staypoints.generate_locations(
             method="dbscan", epsilon=1e18, num_samples=1000, agg_level="dataset"
         )
-        assert len(locs_user) == 0, "With large hyperparameters, every user location is an outlier"
-        assert len(locs_data) == 0, "With large hyperparameters, every dataset location is an outlier"
+        # "With large epsilon, every user location is an outlier"
+        assert len(locs_user) == 0
+        assert len(locs_data) == 0
 
-    def test_generate_locations_missing_link(self):
+    def test_missing_link(self):
         """Test nan is assigned for missing link between stps and locs."""
         pfs_file = os.path.join("tests", "data", "positionfixes.csv")
         pfs = ti.read_positionfixes_csv(pfs_file, sep=";", tz="utc", index_col="id")
@@ -137,7 +145,26 @@ class TestGenerate_locations:
 
         assert pd.isna(stps["location_id"]).any()
 
-    def test_generate_locations_dtype_consistent(self):
+    def test_num_samples_high(self):
+        """Test higher values of num_samples for generate_locations."""
+        stps_file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
+        stps = ti.read_staypoints_csv(stps_file, tz="utc", index_col="id")
+        stps_ns_5, _ = stps.as_staypoints.generate_locations(
+            epsilon=50, distance_metric="haversine", agg_level="user", num_samples=2
+        )
+        non_noise_stps = stps_ns_5[stps_ns_5["location_id"] != -1]
+
+        # group_by_user_id and check that no two different user ids share a common location id
+        grouped = list(non_noise_stps.groupby(["user_id"])["location_id"].unique())
+        loc_set = []
+        for loc_list in grouped:
+            loc_set.append(set(loc_list))
+
+        # we assert that the count of overlaps is equal to the count of users
+        # (each user has overlap with the same user)
+        assert sum([int(len(p & q) > 0) for p in loc_set for q in loc_set]) == len(loc_set)
+
+    def test_dtype_consistent(self):
         """Test the dtypes for the generated columns."""
         stps_file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
         stps = ti.read_staypoints_csv(stps_file, tz="utc", index_col="id")
@@ -157,17 +184,29 @@ class TestGenerate_locations:
         assert stps["location_id"].dtype == "Int64"
         assert locs.index.dtype == "int64"
 
-    def test_generate_locations_index_start(self):
+    def test_index_start(self):
         """Test the generated index start from 0 for different methods."""
         stps_file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
         stps = ti.read_staypoints_csv(stps_file, tz="utc", index_col="id")
 
-        _, locs_ds = stps.as_staypoints.generate_locations(
-            method="dbscan", epsilon=10, num_samples=0, distance_metric="haversine", agg_level="dataset"
-        )
-        _, locs_us = stps.as_staypoints.generate_locations(
-            method="dbscan", epsilon=10, num_samples=0, distance_metric="haversine", agg_level="user"
-        )
-
-        assert (locs_ds.index == np.arange(len(locs_ds))).any()
-        assert (locs_us.index == np.arange(len(locs_us))).any()
+        distance_metric_ls = ["haversine", "euclidean"]
+        agg_level_ls = ["dataset", "user"]
+        for distance_metric in distance_metric_ls:
+            for agg_level in agg_level_ls:
+                _, locations = stps.as_staypoints.generate_locations(
+                    method="dbscan", epsilon=10, num_samples=0, distance_metric=distance_metric, agg_level=agg_level
+                )
+                assert (locations.index == np.arange(len(locations))).any()
+
+    def test_print_progress_flag(self, capsys):
+        """Test if the print_progress bar controls the printing behavior."""
+        file = os.path.join("tests", "data", "geolife", "geolife_staypoints.csv")
+        staypoints = ti.read_staypoints_csv(file, tz="utc", index_col="id")
+
+        staypoints.as_staypoints.generate_locations(print_progress=True)
+        captured_print = capsys.readouterr()
+        assert captured_print.err != ""
+
+        staypoints.as_staypoints.generate_locations(print_progress=False)
+        captured_print = capsys.readouterr()
+        assert captured_print.err == ""
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 40e3388..80e8279 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -3,6 +3,8 @@ import os
 import geopandas as gpd
 import numpy as np
 import pandas as pd
+import pytest
+from geopandas.testing import assert_geodataframe_equal
 from shapely.geometry import Point, LineString
 
 import trackintel as ti
@@ -63,6 +65,54 @@ class TestGenerate_trips:
         # test if generated trips are equal
         pd.testing.assert_frame_equal(trips_loaded, trips)
 
+    def test_accessor(self):
+        """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)
+
+        # generate trips using the explicit function import
+        stps_expl, tpls_expl, trips_expl = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+
+        # generate trips using the accessor
+        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(stps_expl, stps_acc)
+        assert_geodataframe_equal(tpls_expl, tpls_acc)
+
+    def test_accessor_arguments(self):
+        """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, 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)
+
+        # accessor with only arguments (not allowed)
+        with pytest.raises(AssertionError):
+            _, _, _ = tpls.as_triplegs.generate_trips(stps, 15)
+
+        # accessor with only keywords
+        stps_1, tpls_1, trips_1 = tpls.as_triplegs.generate_trips(stps_input=stps, gap_threshold=15)
+
+        # accessor with mixed arguments/keywords
+        stps_2, tpls_2, trips_2 = tpls.as_triplegs.generate_trips(stps, gap_threshold=15)
+
+        # test if generated trips are equal (1,2)
+        assert_geodataframe_equal(stps_1, stps_2)
+        assert_geodataframe_equal(tpls_1, tpls_2)
+        pd.testing.assert_frame_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."""
         # create trips from geolife (based on positionfixes)