From a0b6a0270164fc944e88811044694ff877603778 Mon Sep 17 00:00:00 2001 From: Clemens Hahn Date: Wed, 7 Oct 2026 10:30:27 +0200 Subject: [PATCH] Report a pyproj CRS projection as a string A TrajectoryCollection built from a GeoDataFrame keeps a pyproj CRS, one built from x/y the string it was given. The analyzer passed the CRS into its result, json.dumps rejected it, and the event handler's fail-safe wrote `[]` instead of the statistics. On master since 2026-09-23 for every App that hands on such a collection; on develop for every Python App after `move2_loc to MovingPandas`, which builds its points as a GeoDataFrame since link-r-python v2.2.1. Not gzip: an uncompressed pickle fails alike. The test applies the handler's JSON encoding. Comparing the CRS with 'EPSG:4326' would pass regardless, as pyproj.CRS equals its string. Co-Authored-By: Claude Opus 5.5 --- .../analyzer.py | 6 +++- .../test_analyzer.py | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/analyzer/moving_pandas_trajectory_collection/analyzer.py b/src/analyzer/moving_pandas_trajectory_collection/analyzer.py index 0690872..d3dd03b 100644 --- a/src/analyzer/moving_pandas_trajectory_collection/analyzer.py +++ b/src/analyzer/moving_pandas_trajectory_collection/analyzer.py @@ -1,4 +1,5 @@ from geopandas import GeoDataFrame +from pyproj import CRS from src.analyzer.base_analyzer import BaseAnalyzer from src.moveapps_pickle import MoveAppsPickle @@ -55,7 +56,10 @@ def __inspect(self, data: gpd.GeoDataFrame, movingpandas: mpd.TrajectoryCollecti geom_col = movingpandas.get_geom_col() number_positions_by_track = dict(data.dissolve(by=traj_id_col, aggfunc={traj_id_col: "count"}).drop(geom_col, axis=1).rename(columns={traj_id_col: "count"}).reset_index().values) - projection = movingpandas.get_crs() + # built from a GeoDataFrame, a collection keeps a `pyproj.CRS`, which JSON cannot encode; + # built from x/y, the string it was given. Both report as e.g. 'EPSG:4326' + crs = movingpandas.get_crs() + projection = crs.srs if isinstance(crs, CRS) else crs #positions_bounding_box = data.total_bounds.tolist() positions_bounding_box = data.total_bounds diff --git a/test/analyzer/moving_pandas_trajectory_collection/test_analyzer.py b/test/analyzer/moving_pandas_trajectory_collection/test_analyzer.py index 42d58eb..7d1a133 100644 --- a/test/analyzer/moving_pandas_trajectory_collection/test_analyzer.py +++ b/test/analyzer/moving_pandas_trajectory_collection/test_analyzer.py @@ -1,6 +1,13 @@ +import json import os.path import tempfile from unittest import TestCase + +import geopandas as gpd +import movingpandas as mpd +import pandas as pd +from pyproj import CRS + from src.analyzer.moving_pandas_trajectory_collection.analyzer import MovingPandasAnalyzer from test.config.definitions import ROOT_DIR from test.config.fixtures import gzip_copy @@ -78,5 +85,29 @@ def test_it_should_analyze_a_gzip_compressed_input4_LatLon(self): self.assertEqual(actual[10]['number_positions_by_track'], {'X742..deploy_id.56853924.': 3265, 'X746..deploy_id.20813885.': 3010, 'X749..deploy_id.20813892.': 1495}) self.assertEqual(actual[12]['n'], ['non-empty-result']) + def test_it_should_report_the_projection_of_a_collection_built_from_a_geodataframe(self): + # arrange: built from a GeoDataFrame, a collection keeps a `pyproj.CRS`, not the string it + # was given - e.g. the output of `move2_loc to MovingPandas` since link-r-python v2.2.1 + collection = self.__collection_built_from_a_geodataframe() + self.assertIsInstance(collection.get_crs(), CRS) + output = os.path.join(self.tmp.name, "output_file") + pd.to_pickle(collection, output, compression='gzip') + + # act: the result leaves the cargo-agent as JSON, so it has to survive the encoding + actual = json.loads(json.dumps(self.sut.analyze(path=output))) + + # assert + self.assertEqual(actual[7]['projection'], 'EPSG:4326') + self.assertEqual(actual[12]['n'], ['non-empty-result']) + + @staticmethod + def __collection_built_from_a_geodataframe() -> mpd.TrajectoryCollection: + points = gpd.GeoDataFrame( + {'track': ['a', 'a', 'b', 'b'], 't': pd.date_range('2026-01-01', periods=4, freq='h')}, + geometry=gpd.points_from_xy([0.0, 1.0, 2.0, 3.0], [0.0, 1.0, 2.0, 3.0]), + crs='EPSG:4326', + ) + return mpd.TrajectoryCollection(points, traj_id_col='track', t='t') + def __test_file(self, file_name) -> str: return os.path.join(ROOT_DIR, 'test', 'resources', 'moving_pandas_trajectory_collection', file_name)