Repository: mie-lab/trackintel
Pull request #570 (2023-11-08 13:43:12)

ENH: Add gpx reader for single user
That was a lot simpler than I thought.
Currently we assign to all positionfixes `user_id = 0`.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/mie-lab/trackintel/pull/570?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`bd83b61`)](https://app.codecov.io/gh/mie-lab/trackintel/commit/bd83b611dc8c0cc88ab47dd8ce8c870be647aaf9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) 93.10% compared to head [(`64433cc`)](https://app.codecov.io/gh/mie-lab/trackintel/pull/570?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) 93.15%.
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #570      +/-   ##
==========================================
+ Coverage   93.10%   93.15%   +0.05%     
==========================================
  Files          33       33              
  Lines        2088     2104      +16     
  Branches      373      374       +1     
==========================================
+ Hits         1944     1960      +16     
  Misses        130      130              
  Partials       14       14              
```


| [Files](https://app.codecov.io/gh/mie-lab/trackintel/pull/570?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/io/dataset\_reader.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/570?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9kYXRhc2V0X3JlYWRlci5weQ==) | `50.41% <100.00%> (+3.51%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mie-lab/trackintel/pull/570?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).

bifbof: It can generate them for Strava (I haven't tried Henry's) and at first glance the staypoints and triplegs seem fine.

Patch:
diff --git a/docs/modules/io.rst b/docs/modules/io.rst
index a9dab6e..a6656cc 100644
--- a/docs/modules/io.rst
+++ b/docs/modules/io.rst
@@ -104,3 +104,10 @@ We support easy parsing of the Geolife dataset including available mode labels.
 .. autofunction:: trackintel.io.read_geolife
 
 .. autofunction:: trackintel.io.geolife_add_modes_to_triplegs
+
+
+GPX
+-----------
+Load multiple tracks of the same user with this function.
+
+.. autofunction:: trackintel.io.read_gpx
diff --git a/trackintel/io/__init__.py b/trackintel/io/__init__.py
index 0ab916c..517ea8a 100644
--- a/trackintel/io/__init__.py
+++ b/trackintel/io/__init__.py
@@ -37,6 +37,7 @@ from .from_geopandas import read_tours_gpd
 from .dataset_reader import read_geolife
 from .dataset_reader import read_mzmv
 from .dataset_reader import geolife_add_modes_to_triplegs
+from .dataset_reader import read_gpx
 
 __all__ = [
     # positionfixes
@@ -79,4 +80,5 @@ __all__ = [
     "read_geolife",
     "read_mzmv",
     "geolife_add_modes_to_triplegs",
+    "read_gpx",
 ]
diff --git a/trackintel/io/dataset_reader.py b/trackintel/io/dataset_reader.py
index b1839bf..25c4cc3 100644
--- a/trackintel/io/dataset_reader.py
+++ b/trackintel/io/dataset_reader.py
@@ -13,6 +13,7 @@ from tqdm import tqdm
 
 from trackintel.preprocessing.util import calc_temp_overlap
 from trackintel import Positionfixes, Staypoints, Triplegs
+from trackintel.io import read_positionfixes_gpd
 
 FEET2METER = 0.3048
 CRS_WGS84 = 4326
@@ -683,3 +684,50 @@ def _mzmv_generate_sp(tpls, zf):
     added_cols = [b + c for b in ("S_", "Z_") for c in col[-7:]]
     tpls.drop(columns=added_cols, inplace=True)
     return sp
+
+
+def read_gpx(path):
+    """
+    Read gpx data and return it as Positionfixes of a single user
+
+    Parameters
+    ----------
+    path : str
+        Path to directory of gpx files. Non gpx files are ignored.
+
+    Returns
+    -------
+    Positionfixes
+    """
+    pattern = os.path.join(path, "*.gpx")
+    pfs_list = []
+    track_fid_offset = 0
+    # sorted to make result deterministic
+    for file in sorted(glob.glob(pattern)):
+        pfs = _read_single_gpx_file(file)
+        # give each track an unique ID
+        pfs["track_fid"] += track_fid_offset
+        track_fid_offset = pfs["track_fid"].max() + 1
+        pfs_list.append(pfs)
+    return pd.concat(pfs_list, ignore_index=True)
+
+
+def _read_single_gpx_file(path):
+    """
+    Read track points out from single gpx file
+
+    Extension types are not supported by fiona and therefore dropped.
+
+    Parameters
+    ----------
+    path : str
+        Path to `.gpx` file with track
+
+    Returns
+    -------
+    Positionfixes
+    """
+    gdf = gpd.read_file(path, layer="track_points")
+    gdf = gdf.dropna(axis="columns", how="all")  # drop empty columns
+    gdf["user_id"] = 0  # maybe we have a smarter way for this
+    return read_positionfixes_gpd(gdf, tracked_at="time")


Test patch:
diff --git a/tests/data/gpx_data/track1.gpx b/tests/data/gpx_data/track1.gpx
new file mode 100644
index 0000000..3e69c51
--- /dev/null
+++ b/tests/data/gpx_data/track1.gpx
@@ -0,0 +1,16 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<gpx version="1.1" creator="HiWi" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
+<metadata>
+    <name>2023-11-08_15-56_Tue</name>
+    <time>2023-11-08T15:00:00Z</time>
+  </metadata>
+<trk>
+    <trkseg>
+      <trkpt lat="47.0" lon="8.0">
+        <ele>1000.0</ele>
+        <time>2023-11-08T10:00:00Z</time>
+        <hdop>10.0</hdop>
+      </trkpt>
+    </trkseg>
+  </trk>
+</gpx>
\ No newline at end of file
diff --git a/tests/data/gpx_data/track2.gpx b/tests/data/gpx_data/track2.gpx
new file mode 100644
index 0000000..ba45316
--- /dev/null
+++ b/tests/data/gpx_data/track2.gpx
@@ -0,0 +1,24 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<gpx version="1.1" creator="HiWi" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
+<metadata>
+    <name>2023-11-08_15-56_Tue</name>
+    <time>2023-11-08T15:00:00Z</time>
+  </metadata>
+<trk>
+    <trkseg>
+      <trkpt lat="48.0" lon="9.0">
+        <ele>1001.0</ele>
+        <time>2023-11-08T11:00:00Z</time>
+        <hdop>11.0</hdop>
+        <extensions> <!-- cannot be read by fiora -->
+          <osmand:speed>0.2</osmand:speed>
+        </extensions>
+      </trkpt>
+      <trkpt lat="49.0" lon="10.0">
+        <ele>1002.0</ele>
+        <time>2023-11-08T12:00:00Z</time>
+        <hdop>12.0</hdop>
+      </trkpt>
+    </trkseg>
+  </trk>
+</gpx>
\ No newline at end of file
diff --git a/tests/io/test_dataset_reader.py b/tests/io/test_dataset_reader.py
index 7e5f4b9..d963554 100644
--- a/tests/io/test_dataset_reader.py
+++ b/tests/io/test_dataset_reader.py
@@ -7,7 +7,8 @@ from geopandas.testing import assert_geodataframe_equal
 from shapely.geometry import Point
 
 import trackintel as ti
-from trackintel.io.dataset_reader import _get_df, _get_labels, geolife_add_modes_to_triplegs, read_geolife
+from trackintel.io.dataset_reader import _get_df, _get_labels, geolife_add_modes_to_triplegs, read_geolife, read_gpx
+from trackintel import Positionfixes
 
 
 @pytest.fixture
@@ -244,3 +245,26 @@ class TestGeolife_add_modes_to_triplegs:
 
         tpls = geolife_add_modes_to_triplegs(tpls, labels)
         assert pd.isna(tpls.iloc[0]["mode"])
+
+
+class TestGpx_reader:
+    def test_simple_input(self):
+        """Test if gpx_reader works on simple input"""
+        pfs = read_gpx(os.path.join("tests", "data", "gpx_data"))
+        elev = 1000.0
+        h = pd.Timedelta(hours=1)
+        time = pd.Timestamp("2023-11-08 10:00:00+00:00")
+        hdop = 10.0
+        x, y = 8, 47
+        data = {
+            "track_fid": [0, 1, 1],  # track 0 with 1 point, track 1 with 2 points
+            "track_seg_id": [0, 0, 0],  # each track has 1 segment
+            "track_seg_point_id": [0, 0, 1],
+            "ele": [elev, elev + 1, elev + 2],
+            "tracked_at": [time, time + h, time + 2 * h],
+            "hdop": [hdop, hdop + 1, hdop + 2],
+            "geometry": [Point(x, y), Point(x + 1, y + 1), Point(x + 2, y + 2)],
+            "user_id": [0, 0, 0],
+        }
+        pfs_test = Positionfixes(data, geometry="geometry", crs=4326)
+        assert_geodataframe_equal(pfs, pfs_test)