From a18d38547c09f273a2da1afd8b13514c7d459f3e Mon Sep 17 00:00:00 2001 From: Emir Taha Demir <76969849+EmirTD@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:01:18 +0300 Subject: [PATCH 1/6] Fix --boundary crash on fiona 1.10 (CRSError: Invalid PROJ string: EPSG:4326) fiona.crs.to_string() is deprecated in fiona 1.10 and returns "EPSG:4326" instead of a PROJ4 string. GeoJSON is always WGS84 (RFC 7946), so use CRS.from_epsg(4326) directly. --- opendm/boundary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendm/boundary.py b/opendm/boundary.py index 86ea5ae25..ab02896a5 100644 --- a/opendm/boundary.py +++ b/opendm/boundary.py @@ -64,7 +64,7 @@ def load_boundary(boundary_json, reproject_to_proj4=None): dimensions = len(coords[0]) if reproject_to_proj4 is not None: - t = transformer(CRS.from_proj4(fiona.crs.to_string(src.crs)), + t = transformer(CRS.from_epsg(4326), CRS.from_proj4(reproject_to_proj4)) coords = [t.TransformPoint(*c)[:dimensions] for c in coords] From 065ff95f263f1d10f9d26d07f56f950278449227 Mon Sep 17 00:00:00 2001 From: Emir Taha Demir <76969849+EmirTD@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:36:13 +0300 Subject: [PATCH 2/6] Add regression test for load_boundary GeoJSON reprojection (#2039) --- tests/test_boundary.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/test_boundary.py diff --git a/tests/test_boundary.py b/tests/test_boundary.py new file mode 100644 index 000000000..62685493c --- /dev/null +++ b/tests/test_boundary.py @@ -0,0 +1,47 @@ +import unittest + +from opendm.boundary import load_boundary + + +class TestBoundary(unittest.TestCase): + def setUp(self): + # A simple WGS84 (EPSG:4326) GeoJSON polygon + self.boundary = { + 'type': 'Feature', + 'properties': {'name': 'boundary'}, + 'geometry': { + 'type': 'Polygon', + 'coordinates': [[ + [-91.99544, 46.84260], + [-91.99417, 46.84260], + [-91.99417, 46.84337], + [-91.99544, 46.84337], + [-91.99544, 46.84260], + ]] + } + } + + def test_load_boundary_without_reprojection(self): + # Without a target CRS the coordinates are returned unchanged + coords = load_boundary(self.boundary) + self.assertEqual(len(coords), 5) + self.assertAlmostEqual(coords[0][0], -91.99544, places=5) + self.assertAlmostEqual(coords[0][1], 46.84260, places=5) + + def test_load_boundary_reprojects_to_utm(self): + # Regression test for https://github.com/OpenDroneMap/ODM/issues/2039 + # On fiona >= 1.10, fiona.crs.to_string() returns "EPSG:4326" instead of a + # PROJ4 string, which made CRS.from_proj4() raise + # "CRSError: Invalid PROJ string: EPSG:4326". GeoJSON is always WGS84 + # (RFC 7946), so load_boundary must reproject without raising. + utm15n = '+proj=utm +zone=15 +datum=WGS84 +units=m +no_defs' + coords = load_boundary(self.boundary, utm15n) + self.assertEqual(len(coords), 5) + # Reprojected coordinates are in meters (UTM), not degrees + for x, y in coords: + self.assertGreater(abs(x), 1000) + self.assertGreater(abs(y), 1000) + + +if __name__ == '__main__': + unittest.main() From 6fc2d24dde2c5163492b67a10e3034bbf6b9f451 Mon Sep 17 00:00:00 2001 From: Emir Taha Demir <76969849+EmirTD@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:28:47 +0300 Subject: [PATCH 3/6] Honor source CRS instead of assuming WGS84 --- opendm/boundary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendm/boundary.py b/opendm/boundary.py index ab02896a5..cc95aeb11 100644 --- a/opendm/boundary.py +++ b/opendm/boundary.py @@ -64,7 +64,7 @@ def load_boundary(boundary_json, reproject_to_proj4=None): dimensions = len(coords[0]) if reproject_to_proj4 is not None: - t = transformer(CRS.from_epsg(4326), + t = transformer(CRS.from_user_input(src.crs), CRS.from_proj4(reproject_to_proj4)) coords = [t.TransformPoint(*c)[:dimensions] for c in coords] From a80df8e64ed03840f3494c3cf50a1a6f50cbee0d Mon Sep 17 00:00:00 2001 From: Emir Taha Demir <76969849+EmirTD@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:30:04 +0300 Subject: [PATCH 4/6] Add test that load_boundary honors non-WGS84 source CRS --- tests/test_boundary.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/test_boundary.py b/tests/test_boundary.py index 62685493c..0215a7b3f 100644 --- a/tests/test_boundary.py +++ b/tests/test_boundary.py @@ -3,6 +3,85 @@ from opendm.boundary import load_boundary +class TestBoundary(unittest.TestCase): + def setUp(self): + # A simple WGS84 (EPSG:4326) GeoJSON polygon + self.boundary = { + 'type': 'Feature', + 'properties': {'name': 'boundary'}, + 'geometry': { + 'type': 'Polygon', + 'coordinates': [[ + [-91.99544, 46.84260], + [-91.99417, 46.84260], + [-91.99417, 46.84337], + [-91.99544, 46.84337], + [-91.99544, 46.84260], + ]] + } + } + + def test_load_boundary_without_reprojection(self): + # Without a target CRS the coordinates are returned unchanged + coords = load_boundary(self.boundary) + self.assertEqual(len(coords), 5) + self.assertAlmostEqual(coords[0][0], -91.99544, places=5) + self.assertAlmostEqual(coords[0][1], 46.84260, places=5) + + def test_load_boundary_reprojects_to_utm(self): + # Regression test for https://github.com/OpenDroneMap/ODM/issues/2039 + # On fiona >= 1.10, fiona.crs.to_string() returns "EPSG:4326" instead of a + # PROJ4 string, which made CRS.from_proj4() raise + # "CRSError: Invalid PROJ string: EPSG:4326". load_boundary must reproject + # a WGS84 GeoJSON boundary without raising. + utm15n = '+proj=utm +zone=15 +datum=WGS84 +units=m +no_defs' + coords = load_boundary(self.boundary, utm15n) + self.assertEqual(len(coords), 5) + # Reprojected coordinates are in meters (UTM), not degrees + for x, y in coords: + self.assertGreater(abs(x), 1000) + self.assertGreater(abs(y), 1000) + + def test_load_boundary_honors_non_wgs84_source_crs(self): + # GeoJSON may declare a CRS other than WGS84 (allowed by RFC 7946 and + # written in practice by tools like QGIS). load_boundary must reproject + # from the file's declared CRS, not assume EPSG:4326. + boundary_utm = { + 'type': 'FeatureCollection', + 'crs': {'type': 'name', + 'properties': {'name': 'urn:ogc:def:crs:EPSG::32635'}}, + 'features': [{ + 'type': 'Feature', + 'properties': {}, + 'geometry': { + 'type': 'Polygon', + 'coordinates': [[ + [500000, 4540000], + [501000, 4540000], + [501000, 4541000], + [500000, 4541000], + [500000, 4540000], + ]] + } + }] + } + wgs84 = '+proj=longlat +datum=WGS84 +no_defs' + coords = load_boundary(boundary_utm, wgs84) + self.assertEqual(len(coords), 5) + # EPSG:32635 (UTM 35N) easting 500000 / northing ~4540000 is ~27E, ~41N. + # If the source CRS were wrongly assumed to be WGS84, these would not land here. + for lon, lat in coords: + self.assertTrue(26 < lon < 28, "unexpected lon: %s" % lon) + self.assertTrue(40 < lat < 42, "unexpected lat: %s" % lat) + + +if __name__ == '__main__': + unittest.main() +import unittest + +from opendm.boundary import load_boundary + + class TestBoundary(unittest.TestCase): def setUp(self): # A simple WGS84 (EPSG:4326) GeoJSON polygon From 4b4fe4ee522833189e4d09c4d4c79be36cc9064b Mon Sep 17 00:00:00 2001 From: Emir Taha Demir <76969849+EmirTD@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:34:36 +0300 Subject: [PATCH 5/6] Remove duplicated test block in test_boundary.py --- tests/test_boundary.py | 45 ------------------------------------------ 1 file changed, 45 deletions(-) diff --git a/tests/test_boundary.py b/tests/test_boundary.py index 0215a7b3f..58a937158 100644 --- a/tests/test_boundary.py +++ b/tests/test_boundary.py @@ -77,50 +77,5 @@ def test_load_boundary_honors_non_wgs84_source_crs(self): if __name__ == '__main__': unittest.main() -import unittest - -from opendm.boundary import load_boundary - - -class TestBoundary(unittest.TestCase): - def setUp(self): - # A simple WGS84 (EPSG:4326) GeoJSON polygon - self.boundary = { - 'type': 'Feature', - 'properties': {'name': 'boundary'}, - 'geometry': { - 'type': 'Polygon', - 'coordinates': [[ - [-91.99544, 46.84260], - [-91.99417, 46.84260], - [-91.99417, 46.84337], - [-91.99544, 46.84337], - [-91.99544, 46.84260], - ]] - } - } - - def test_load_boundary_without_reprojection(self): - # Without a target CRS the coordinates are returned unchanged - coords = load_boundary(self.boundary) - self.assertEqual(len(coords), 5) - self.assertAlmostEqual(coords[0][0], -91.99544, places=5) - self.assertAlmostEqual(coords[0][1], 46.84260, places=5) - - def test_load_boundary_reprojects_to_utm(self): - # Regression test for https://github.com/OpenDroneMap/ODM/issues/2039 - # On fiona >= 1.10, fiona.crs.to_string() returns "EPSG:4326" instead of a - # PROJ4 string, which made CRS.from_proj4() raise - # "CRSError: Invalid PROJ string: EPSG:4326". GeoJSON is always WGS84 - # (RFC 7946), so load_boundary must reproject without raising. - utm15n = '+proj=utm +zone=15 +datum=WGS84 +units=m +no_defs' - coords = load_boundary(self.boundary, utm15n) - self.assertEqual(len(coords), 5) - # Reprojected coordinates are in meters (UTM), not degrees - for x, y in coords: - self.assertGreater(abs(x), 1000) - self.assertGreater(abs(y), 1000) - - if __name__ == '__main__': unittest.main() From cbf05289de25dbad208e163c4e95dd19efecd8c2 Mon Sep 17 00:00:00 2001 From: Emir Taha Demir <76969849+EmirTD@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:35:24 +0300 Subject: [PATCH 6/6] Remove redundant main check in test_boundary.py Removed duplicate unittest.main() call. --- tests/test_boundary.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_boundary.py b/tests/test_boundary.py index 58a937158..dc722017b 100644 --- a/tests/test_boundary.py +++ b/tests/test_boundary.py @@ -77,5 +77,3 @@ def test_load_boundary_honors_non_wgs84_source_crs(self): if __name__ == '__main__': unittest.main() -if __name__ == '__main__': - unittest.main()