Repository: mie-lab/trackintel
Pull request #221 (2021-05-23 10:06:01)

ENH: tracking_quality function
Two enhancements for the `tracking_quality()`/`_split_overlaps()` function:
- previously `_split_overlaps` split records that span several days in midnight, e.g., a record that span 2 days is split into _started_at - 23:59:59_ and _00:00:00 - finished_at_, which means 1 second is lost whenever we perform a split. Now this is changed into _started_at - 00:00:00_ and _00:00:00 - finished_at_. Tests are changed accordingly.
- previously `_split_overlaps` did not deal with `duration` column. Now, this column is updated in the function.

codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/221?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
> Merging [#221](https://codecov.io/gh/mie-lab/trackintel/pull/221?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (c3d9d43) 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.18%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mie-lab/trackintel/pull/221/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/221?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     #221      +/-   ##
==========================================
- Coverage   94.89%   90.70%   -4.19%     
==========================================
  Files          37       37              
  Lines        1548     1550       +2     
  Branches      259      260       +1     
==========================================
- Hits         1469     1406      -63     
- Misses         41      106      +65     
  Partials       38       38              
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/221?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/analysis/tracking\_quality.py](https://codecov.io/gh/mie-lab/trackintel/pull/221/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9hbmFseXNpcy90cmFja2luZ19xdWFsaXR5LnB5) | `98.82% <100.00%> (+0.02%)` | :arrow_up: |
| [trackintel/io/postgis.py](https://codecov.io/gh/mie-lab/trackintel/pull/221/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/221/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/221/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/221/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/221/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/221/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/221?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/221?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Last update [76bdb34...c3d9d43](https://codecov.io/gh/mie-lab/trackintel/pull/221?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/README.md b/README.md
index f7e502e..eb1655f 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 
 [![PyPI version](https://badge.fury.io/py/trackintel.svg)](https://badge.fury.io/py/trackintel)
-[![Build Status](https://travis-ci.org/mie-lab/trackintel.svg?branch=master)](https://travis-ci.org/mie-lab/trackintel)
+[![Actions Status](https://github.com/mie-lab/trackintel/workflows/Tests/badge.svg)](https://github.com/mie-lab/trackintel/actions?query=workflow%3ATests)
 [![Documentation Status](https://readthedocs.org/projects/trackintel/badge/?version=latest)](https://trackintel.readthedocs.io/en/latest/?badge=latest)
 [![codecov.io](https://codecov.io/gh/mie-lab/trackintel/coverage.svg?branch=master)](https://codecov.io/gh/mie-lab/trackintel)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
diff --git a/ROADMAP.md b/ROADMAP.md
index a74c214..875c4fd 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,10 +2,20 @@
 
 This document explains the functionality planned for various releases.
 
+Ideas that are planned:
+## v1.1.0
 
-## v2.0.0
 
-Ideas for a later version.
+* 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:
 
 I/O:
 * Read *customermovements*, *tours* from files.
@@ -16,33 +26,26 @@ I/O:
 
 
 Preprocessing:
-* 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)).
 * Short walks to/from cars/buses/etc.: These are often not recognized by the tracking applications.
-* Map match *triplegs* (based on transport mode identification). 
 * Imputation of trivial gaps in tracking data (*triplegs* and *staypoints*).
 * Simple outlier filtering for *triplegs*
 
 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. 
-* Add context data to movement trajectories (using spatio-temporal map algebra).
 * 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).
 
 Analysis
 * Mobility behaviour (and its changes): automatic detection of uncharacteristic changes in mobility patterns.
 * User profiling and clustering.
-* Anomaly detection (based on properties of movement data as well as contextual factors).
 * Extract *customermovements* from *triplegs* and *trips*.
-* next place prediction (e.g., markov-model)
 * 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)
 
 Visualization 
 * Visualize *customermovements*, *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*.
+* 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
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/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..dec9390 100644
--- a/trackintel/preprocessing/staypoints.py
+++ b/trackintel/preprocessing/staypoints.py
@@ -111,7 +111,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 +126,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")
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index f599a38..45103be 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -1,4 +1,3 @@
-import copy
 import datetime
 
 import numpy as np
@@ -80,7 +79,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/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_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)
diff --git a/tests/visualization/test_locations.py b/tests/visualization/test_locations.py
index 9dbe46e..8cc7f38 100644
--- a/tests/visualization/test_locations.py
+++ b/tests/visualization/test_locations.py
@@ -29,7 +29,7 @@ class TestPlot_locations:
     def test_locations_plot(self, test_data):
         """Use trackintel visualization function to plot locations and check if the file exists."""
         pfs, stps, locs = test_data
-        tmp_file = os.path.join("tests", "data", "locations_plot.png")
+        tmp_file = os.path.join("tests", "data", "locations_plot1.png")
         locs.as_locations.plot(
             out_filename=tmp_file, radius=200, positionfixes=pfs, staypoints=stps, staypoints_radius=100, plot_osm=False
         )
@@ -39,7 +39,7 @@ class TestPlot_locations:
     def test_axis(self, test_data):
         """Test the use of regular_figure() to create axis."""
         pfs, _, locs = test_data
-        tmp_file = os.path.join("tests", "data", "locations_plot.png")
+        tmp_file = os.path.join("tests", "data", "locations_plot2.png")
         _, ax = regular_figure()
 
         locs.as_locations.plot(
@@ -54,8 +54,8 @@ class TestPlot_locations:
 
     def test_parameter(self, test_data):
         """Test other parameter configurations."""
-        pfs, _, locs = test_data
-        tmp_file = os.path.join("tests", "data", "locations_plot.png")
+        _, _, locs = test_data
+        tmp_file = os.path.join("tests", "data", "locations_plot3.png")
 
         # plot only location
         locs.as_locations.plot(out_filename=tmp_file, plot_osm=True)
diff --git a/tests/visualization/test_staypoints.py b/tests/visualization/test_staypoints.py
index 1b6c448..7f3df7c 100644
--- a/tests/visualization/test_staypoints.py
+++ b/tests/visualization/test_staypoints.py
@@ -23,7 +23,7 @@ class TestPlot_staypoints:
     def test_staypoints_plot(self, test_data):
         """Use trackintel visualization function to plot staypoints and check if the file exists."""
         pfs, stps = test_data
-        tmp_file = os.path.join("tests", "data", "staypoints_plot.png")
+        tmp_file = os.path.join("tests", "data", "staypoints_plot1.png")
 
         stps.as_staypoints.plot(out_filename=tmp_file, radius=100, positionfixes=pfs, plot_osm=False)
         assert os.path.exists(tmp_file)
@@ -32,7 +32,7 @@ class TestPlot_staypoints:
     def test_parameter(self, test_data):
         """Test other parameter configurations."""
         pfs, stps = test_data
-        tmp_file = os.path.join("tests", "data", "staypoints_plot.png")
+        tmp_file = os.path.join("tests", "data", "staypoints_plot2.png")
 
         # no radius
         stps.as_staypoints.plot(out_filename=tmp_file, positionfixes=pfs, plot_osm=False)
@@ -40,6 +40,6 @@ class TestPlot_staypoints:
         os.remove(tmp_file)
 
         # with osm
-        stps.as_staypoints.plot(out_filename="staypoints_plot", plot_osm=True)
-        assert os.path.exists("staypoints_plot.png")
-        os.remove("staypoints_plot.png")
+        stps.as_staypoints.plot(out_filename=tmp_file, plot_osm=True)
+        assert os.path.exists(tmp_file)
+        os.remove(tmp_file)
diff --git a/tests/visualization/test_triplegs.py b/tests/visualization/test_triplegs.py
index 03918ef..ce19609 100644
--- a/tests/visualization/test_triplegs.py
+++ b/tests/visualization/test_triplegs.py
@@ -27,7 +27,7 @@ class TestPlot_triplegs:
         """Use trackintel visualization function to plot triplegs and check if the file exists."""
         pfs, stps, tpls = test_data
 
-        tmp_file = os.path.join("tests", "data", "triplegs_plot.png")
+        tmp_file = os.path.join("tests", "data", "triplegs_plot1.png")
         tpls.as_triplegs.plot(out_filename=tmp_file, positionfixes=pfs, staypoints=stps, plot_osm=False)
         assert os.path.exists(tmp_file)
         os.remove(tmp_file)
@@ -35,7 +35,7 @@ class TestPlot_triplegs:
     def test_axis(self, test_data):
         """Test the use of regular_figure() to create axis."""
         _, _, tpls = test_data
-        tmp_file = os.path.join("tests", "data", "triplegs_plot.png")
+        tmp_file = os.path.join("tests", "data", "triplegs_plot2.png")
         _, ax = regular_figure()
 
         tpls.as_triplegs.plot(out_filename=tmp_file, axis=ax)
@@ -45,7 +45,7 @@ class TestPlot_triplegs:
     def test_parameter(self, test_data):
         """Test other parameter configurations."""
         _, _, tpls = test_data
-        tmp_file = os.path.join("tests", "data", "triplegs_plot.png")
+        tmp_file = os.path.join("tests", "data", "triplegs_plot3.png")
 
         # test plot_osm
         tpls.as_triplegs.plot(out_filename=tmp_file, plot_osm=True)