Repository: mie-lab/trackintel

Issue:
Put transformation of crs for plotting in designated function.
With #135 we introduced a check to transform the data to WGS84 for plotting.
Since this method is repeated four times, it would be good to create a function for that in `visualization.util`. This way we can test for bugs in one place and the plot function will be cleaner.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/147?src=pr&el=h1) Report
> Merging [#147](https://codecov.io/gh/mie-lab/trackintel/pull/147?src=pr&el=desc) (928c750) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/61b90fc4fb49783bc6201c5a96e3094168c57ff6?el=desc) (61b90fc) will **decrease** coverage by `0.90%`.
> The diff coverage is `94.11%`.

[![Impacted file tree graph](https://codecov.io/gh/mie-lab/trackintel/pull/147/graphs/tree.svg?width=650&height=150&src=pr&token=ITDZEEE1MO)](https://codecov.io/gh/mie-lab/trackintel/pull/147?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #147      +/-   ##
==========================================
- Coverage   75.12%   74.21%   -0.91%     
==========================================
  Files          34       34              
  Lines        1411     1400      -11     
  Branches      246      240       -6     
==========================================
- Hits         1060     1039      -21     
- Misses        277      285       +8     
- Partials       74       76       +2     
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/147?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [trackintel/visualization/util.py](https://codecov.io/gh/mie-lab/trackintel/pull/147/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC92aXN1YWxpemF0aW9uL3V0aWwucHk=) | `69.35% <88.88%> (+3.31%)` | :arrow_up: |
| [trackintel/visualization/locations.py](https://codecov.io/gh/mie-lab/trackintel/pull/147/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC92aXN1YWxpemF0aW9uL2xvY2F0aW9ucy5weQ==) | `51.61% <100.00%> (-17.84%)` | :arrow_down: |
| [trackintel/visualization/positionfixes.py](https://codecov.io/gh/mie-lab/trackintel/pull/147/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC92aXN1YWxpemF0aW9uL3Bvc2l0aW9uZml4ZXMucHk=) | `60.00% <100.00%> (+4.00%)` | :arrow_up: |
| [trackintel/visualization/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/147/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC92aXN1YWxpemF0aW9uL3N0YXlwb2ludHMucHk=) | `60.60% <100.00%> (-20.98%)` | :arrow_down: |
| [trackintel/visualization/triplegs.py](https://codecov.io/gh/mie-lab/trackintel/pull/147/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC92aXN1YWxpemF0aW9uL3RyaXBsZWdzLnB5) | `44.00% <100.00%> (-16.00%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/147?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/147?src=pr&el=footer). Last update [61b90fc...928c750](https://codecov.io/gh/mie-lab/trackintel/pull/147?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

bifbof: > The tests contain some warnings, can you try to fix the warnings before we continue?
 
Some are from older commits, I'll fix them.

Should we move all the other tests for visualization also in this PR or should we rather do that in a seperate one?

Patch:
diff --git a/trackintel/model/positionfixes.py b/trackintel/model/positionfixes.py
index 6cb3639..9d8bf05 100644
--- a/trackintel/model/positionfixes.py
+++ b/trackintel/model/positionfixes.py
@@ -16,7 +16,7 @@ class PositionfixesAccessor(object):
     of the `Positionfixes`
 
     For several usecases, the following additional columns are required:
-    ``['elevation', 'accuracy', 'tracking_tech', 'context']``
+    ``['elevation', 'accuracy', 'tracking_tech', 'context', 'staypoint_id', 'tripleg_id']``
 
     Notes
     -------
diff --git a/trackintel/model/staypoints.py b/trackintel/model/staypoints.py
index 733c1d3..f1cf93b 100644
--- a/trackintel/model/staypoints.py
+++ b/trackintel/model/staypoints.py
@@ -16,7 +16,8 @@ class StaypointsAccessor(object):
     of the `Staypoints`
 
     For several usecases, the following additional columns are required:
-    ``['elevation', 'radius', 'context', 'purpose', 'activity', 'next_trip_id', 'prev_trip_id', 'trip_id']``
+    ``['elevation', 'radius', 'context', 'purpose', 'activity', 'next_trip_id', 'prev_trip_id', 'trip_id',
+    location_id]``
 
     Notes
     -------
diff --git a/trackintel/model/tours.py b/trackintel/model/tours.py
index 378809b..3e06beb 100644
--- a/trackintel/model/tours.py
+++ b/trackintel/model/tours.py
@@ -6,7 +6,7 @@ class ToursAccessor(object):
     """A pandas accessor to treat DataFrames as collections of tours. 
 
     Requires at least the following columns: 
-    ``['user_id', 'started_at', 'finished_at', 'origin_destination_location_id', 'journey']``
+    ``['user_id', 'started_at', 'finished_at', 'origin_staypoint_id', 'journey']``
 
     The ``index`` of the GeoDataFrame will be treated as unique identifier of the `trips`
 
diff --git a/trackintel/model/trips.py b/trackintel/model/trips.py
index 8d9f712..ed1cec7 100644
--- a/trackintel/model/trips.py
+++ b/trackintel/model/trips.py
@@ -16,7 +16,7 @@ class TripsAccessor(object):
     The ``index`` of the GeoDataFrame will be treated as unique identifier of the `trips`
 
     For several usecases, the following additional columns are required:
-    ``['context', 'origin_activity', 'destination_activity', 'modes', 'primary_mode']``
+    ``['context', 'origin_activity', 'destination_activity', 'modes', 'primary_mode', 'tour_id']``
 
     Notes
     -----
diff --git a/trackintel/visualization/locations.py b/trackintel/visualization/locations.py
index 20d742f..072b2ea 100644
--- a/trackintel/visualization/locations.py
+++ b/trackintel/visualization/locations.py
@@ -4,7 +4,7 @@ import warnings
 
 from trackintel.geogr.distances import meters_to_decimal_degrees
 from trackintel.visualization.osm import plot_osm_streets
-from trackintel.visualization.util import regular_figure, save_fig
+from trackintel.visualization.util import regular_figure, save_fig, transform_gdf_to_wgs84
 
 
 def plot_center_of_locations(locations, out_filename=None, radius=None, positionfixes=None, 
@@ -45,13 +45,7 @@ def plot_center_of_locations(locations, out_filename=None, radius=None, position
         _, ax = regular_figure()
     else:
         ax = axis
-
-    crs_wgs84 = 'EPSG:4326'
-    if locations.crs is None:
-        warnings.warn("Coordinate System (CRS) is not set, default to WGS84.")
-        locations.crs = crs_wgs84
-    elif locations.crs != crs_wgs84:
-        locations = locations.to_crs(crs_wgs84)
+    locations = transform_gdf_to_wgs84(locations)
 
     if staypoints is not None:
         staypoints.as_staypoints.plot(radius=staypoints_radius,
diff --git a/trackintel/visualization/positionfixes.py b/trackintel/visualization/positionfixes.py
index 1b10a67..7e5d57e 100644
--- a/trackintel/visualization/positionfixes.py
+++ b/trackintel/visualization/positionfixes.py
@@ -1,7 +1,7 @@
 import matplotlib.pyplot as plt
 import warnings
 from trackintel.visualization.osm import plot_osm_streets
-from trackintel.visualization.util import regular_figure, save_fig
+from trackintel.visualization.util import regular_figure, save_fig, transform_gdf_to_wgs84
 
 
 def plot_positionfixes(positionfixes, out_filename=None, plot_osm=False, axis=None):
@@ -33,13 +33,7 @@ def plot_positionfixes(positionfixes, out_filename=None, plot_osm=False, axis=No
         _, ax = regular_figure()
     else:
         ax = axis
-
-    crs_wgs84 = 'EPSG:4326'
-    if positionfixes.crs is None:
-        warnings.warn("Coordinate System (CRS) is not set, default to WGS84.")
-        positionfixes.crs = crs_wgs84
-    elif positionfixes.crs != crs_wgs84:
-        positionfixes = positionfixes.to_crs(crs_wgs84)
+    positionfixes = transform_gdf_to_wgs84(positionfixes)
 
     if plot_osm:
         west = positionfixes.geometry.x.min()
diff --git a/trackintel/visualization/staypoints.py b/trackintel/visualization/staypoints.py
index a88e859..99fa511 100644
--- a/trackintel/visualization/staypoints.py
+++ b/trackintel/visualization/staypoints.py
@@ -4,7 +4,7 @@ import warnings
 
 from trackintel.geogr.distances import meters_to_decimal_degrees
 from trackintel.visualization.osm import plot_osm_streets
-from trackintel.visualization.util import regular_figure, save_fig
+from trackintel.visualization.util import regular_figure, save_fig, transform_gdf_to_wgs84
 
 
 def plot_staypoints(staypoints, out_filename=None, radius=None, positionfixes=None, plot_osm=False, axis=None):
@@ -42,13 +42,7 @@ def plot_staypoints(staypoints, out_filename=None, radius=None, positionfixes=No
     else:
         ax = axis
     name_geocol = staypoints.geometry.name
-
-    crs_wgs84 = 'EPSG:4326'
-    if staypoints.crs is None:
-        warnings.warn("Coordinate System (CRS) is not set, default to WGS84.")
-        staypoints.crs = crs_wgs84
-    elif staypoints.crs != crs_wgs84:
-        staypoints = staypoints.to_crs(crs_wgs84)
+    staypoints = transform_gdf_to_wgs84(staypoints)
 
     if positionfixes is not None:
         positionfixes.as_positionfixes.plot(plot_osm=plot_osm, axis=ax)
diff --git a/trackintel/visualization/triplegs.py b/trackintel/visualization/triplegs.py
index 98e2cc9..ca249a4 100644
--- a/trackintel/visualization/triplegs.py
+++ b/trackintel/visualization/triplegs.py
@@ -3,7 +3,7 @@ import matplotlib.pyplot as plt
 import warnings
 
 from trackintel.visualization.osm import plot_osm_streets
-from trackintel.visualization.util import regular_figure, save_fig
+from trackintel.visualization.util import regular_figure, save_fig, transform_gdf_to_wgs84
 
 
 def plot_triplegs(triplegs, out_filename=None, positionfixes=None, staypoints=None, 
@@ -39,13 +39,7 @@ def plot_triplegs(triplegs, out_filename=None, positionfixes=None, staypoints=No
         _, ax = regular_figure()
     else:
         ax = axis
-
-    crs_wgs84 = 'EPSG:4326'
-    if triplegs.crs is None:
-        warnings.warn("Coordinate System (CRS) is not set, default to WGS84.")
-        triplegs.crs = crs_wgs84
-    elif triplegs.crs != crs_wgs84:
-        triplegs = triplegs.to_crs(crs_wgs84)
+    triplegs = transform_gdf_to_wgs84(triplegs)
 
     if staypoints is not None:
         staypoints.as_staypoints.plot(radius=staypoints_radius,
diff --git a/trackintel/visualization/util.py b/trackintel/visualization/util.py
index 851531b..5a370d6 100644
--- a/trackintel/visualization/util.py
+++ b/trackintel/visualization/util.py
@@ -1,5 +1,6 @@
 import time
 import logging
+import warnings
 
 import matplotlib
 import matplotlib.pyplot as plt
@@ -118,3 +119,25 @@ def save_fig(out_filename, tight='tight', formats=['png', 'pdf']):
         logging.info("...took {} s!".format(round(time.time() - ts, 2)))
     plt.close()
     logging.info("Finished!")
+
+
+def transform_gdf_to_wgs84(gdf):
+    """Transforms a GeoDataFrame into WGS84.
+
+    Additionally checks if data has CRS or is already in WGS84.
+
+    Parameters
+    ----------
+    gdf : GeoDataFrame
+
+    Example
+    -------
+    >>> gdf = transform_gdf_to_wgs84(gdf)
+    """
+    crs_wgs84 = 'EPSG:4326'
+    if gdf.crs is None:
+        warnings.warn("Coordinate System (CRS) is not set, default to WGS84.")
+        gdf.crs = crs_wgs84
+    elif gdf.crs != crs_wgs84:
+        gdf = gdf.to_crs(crs_wgs84)
+    return gdf


Test patch:
diff --git a/tests/test_visualization.py b/tests/test_visualization.py
index cea364f..df67b96 100644
--- a/tests/test_visualization.py
+++ b/tests/test_visualization.py
@@ -13,7 +13,7 @@ class TestIO:
 
         tmp_file = os.path.join('tests', 'data', 'positionfixes_plot.png')
         pfs_file = os.path.join('tests', 'data', 'positionfixes.csv')
-        pfs = ti.read_positionfixes_csv(pfs_file, sep=';', index_col='id')
+        pfs = ti.read_positionfixes_csv(pfs_file, sep=';', index_col='id', crs='EPSG:4326')
         pfs.as_positionfixes.plot(out_filename=tmp_file, plot_osm=False)
         assert os.path.exists(tmp_file)
         os.remove(tmp_file)
@@ -23,10 +23,10 @@ class TestIO:
 
         tmp_file = os.path.join('tests', 'data', 'triplegs_plot.png')
         pfs_file = os.path.join('tests', 'data', 'positionfixes.csv')
-        pfs = ti.read_positionfixes_csv(pfs_file, sep=';', index_col='id')
+        pfs = ti.read_positionfixes_csv(pfs_file, sep=';', index_col='id', crs='EPSG:4326')
 
         tpls_file = os.path.join('tests', 'data', 'triplegs.csv')
-        tpls = ti.read_triplegs_csv(tpls_file, sep=';', index_col='id')
+        tpls = ti.read_triplegs_csv(tpls_file, sep=';', index_col='id', crs='EPSG:4326')
         tpls.as_triplegs.plot(out_filename=tmp_file, positionfixes=pfs, plot_osm=False)
         assert os.path.exists(tmp_file)
         os.remove(tmp_file)
@@ -36,10 +36,10 @@ class TestIO:
 
         tmp_file = os.path.join('tests', 'data', 'staypoints_plot.png')
         pfs_file = os.path.join('tests', 'data', 'positionfixes.csv')
-        pfs = ti.read_positionfixes_csv(pfs_file, sep=';', index_col='id')
+        pfs = ti.read_positionfixes_csv(pfs_file, sep=';', index_col='id', crs='EPSG:4326')
 
         stps_file = os.path.join('tests', 'data', 'staypoints.csv')
-        stps = ti.read_staypoints_csv(stps_file, sep=';', index_col='id')
+        stps = ti.read_staypoints_csv(stps_file, sep=';', index_col='id', crs='EPSG:4326')
         stps.as_staypoints.plot(out_filename=tmp_file, radius=0.01, positionfixes=pfs, plot_osm=False)
         assert os.path.exists(tmp_file)
         os.remove(tmp_file)
@@ -48,46 +48,13 @@ class TestIO:
         """Use trackintel visualization function to plot locations and check if file exists."""
 
         tmp_file = os.path.join("tests", "data", "locations_plot.png")
-        pfs = pfs = ti.read_positionfixes_csv('tests/data/positionfixes.csv', sep=';')
-        stps = ti.read_staypoints_csv('tests/data/staypoints.csv', sep=';')
-        locs = ti.read_locations_csv('tests/data/locations.csv', sep=';')
+        pfs_file = os.path.join('tests', 'data', 'positionfixes.csv')
+        pfs = pfs = ti.read_positionfixes_csv(pfs_file, sep=';', index_col='id', crs='EPSG:4326')
+        stps_file = os.path.join('tests', 'data', 'staypoints.csv')
+        stps = ti.read_staypoints_csv(stps_file, sep=';', index_col='id', crs='EPSG:4326')
+        locs_file = os.path.join('tests', 'data', 'locations.csv')
+        locs = ti.read_locations_csv(locs_file, sep=';', index_col='id', crs='EPSG:4326')
         locs.as_locations.plot(out_filename=tmp_file, radius=120, positionfixes=pfs,
                                staypoints=stps, staypoints_radius=100, plot_osm=False)
         assert os.path.exists(tmp_file)
         os.remove(tmp_file)
-
-    def test_positionfixes_crs_warning(self):
-        """Check if warning is raised for data without crs."""
-
-        file = os.path.join('tests', 'data', 'positionfixes.csv')
-        _, ax = mpl.pyplot.subplots()
-        pfs = ti.read_positionfixes_csv(file, sep=';')
-        with pytest.warns(UserWarning):
-            pfs.as_positionfixes.plot(axis=ax)
-
-    def test_staypoints_crs_warning(self):
-        """Check if warning is raised for data without crs."""
-
-        file = os.path.join('tests', 'data', 'staypoints.csv')
-        _, ax = mpl.pyplot.subplots()
-        pfs = ti.read_staypoints_csv(file, sep=';')
-        with pytest.warns(UserWarning):
-            pfs.as_staypoints.plot(axis=ax)
-
-    def test_triplegs_crs_warning(self):
-        """Check if warning is raised for data without crs."""
-
-        file = os.path.join('tests', 'data', 'triplegs.csv')
-        _, ax = mpl.pyplot.subplots()
-        pfs = ti.read_triplegs_csv(file, sep=';')
-        with pytest.warns(UserWarning):
-            pfs.as_triplegs.plot(axis=ax)
-
-    def test_locations_crs_warning(self):
-        """Check if warning is raised for data without crs."""
-
-        file = os.path.join('tests', 'data', 'locations.csv')
-        _, ax = mpl.pyplot.subplots()
-        pfs = ti.read_locations_csv(file, sep=';')
-        with pytest.warns(UserWarning):
-            pfs.as_locations.plot(axis=ax)
diff --git a/tests/visualization/__init__.py b/tests/visualization/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/visualization/test_util.py b/tests/visualization/test_util.py
new file mode 100644
index 0000000..7f24588
--- /dev/null
+++ b/tests/visualization/test_util.py
@@ -0,0 +1,24 @@
+import os
+import pytest
+from geopandas.testing import assert_geodataframe_equal
+
+import trackintel as ti
+
+
+class TestTransform_gdf_to_wgs84:
+    def test_transformation(self):
+        """Check if data gets transformed."""
+
+        file = os.path.join('tests', 'data', 'positionfixes.csv')
+        pfs = ti.read_positionfixes_csv(file, sep=';', crs='EPSG:4326', index_col=None)
+        pfs_2056 = pfs.to_crs("EPSG:2056")
+        pfs_4326 = ti.visualization.util.transform_gdf_to_wgs84(pfs_2056)
+        assert_geodataframe_equal(pfs, pfs_4326, check_less_precise=True)
+
+    def test_crs_warning(self):
+        """Check if warning is raised for data without crs."""
+
+        file = os.path.join('tests', 'data', 'positionfixes.csv')
+        pfs = ti.read_positionfixes_csv(file, sep=';', crs=None, index_col=None)
+        with pytest.warns(UserWarning):
+            ti.visualization.util.transform_gdf_to_wgs84(pfs)