Skip to content

Commit a609176

Browse files
authored
feat: reduce geojson maps precision and remove ids (#1335)
1 parent 0b7c2b5 commit a609176

File tree

5 files changed

+206
-9
lines changed

5 files changed

+206
-9
lines changed

functions-python/helpers/locations.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def get_country_code(country_name: str) -> Optional[str]:
4646
if country:
4747
return country.alpha_2
4848

49-
# Try searching by name
49+
# Try searching with fuzzy matching
5050
countries = pycountry.countries.search_fuzzy(country_name)
5151
if countries:
5252
return countries[0].alpha_2
@@ -231,3 +231,50 @@ def get_geopolygons_covers(stop_point: WKTElement, db_session: Session):
231231
).all()
232232
)
233233
return geopolygons
234+
235+
236+
def round_geojson_coords(geometry, precision=5):
237+
"""
238+
Recursively round all coordinates in a GeoJSON geometry to the given precision.
239+
Handles Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection.
240+
"""
241+
geom_type = geometry.get("type")
242+
if geom_type == "GeometryCollection":
243+
return {
244+
"type": "GeometryCollection",
245+
"geometries": [
246+
round_geojson_coords(g, precision)
247+
for g in geometry.get("geometries", [])
248+
],
249+
}
250+
elif "coordinates" in geometry:
251+
return {
252+
**geometry,
253+
"coordinates": round_coords(geometry["coordinates"], precision),
254+
}
255+
else:
256+
return geometry
257+
258+
259+
def round_coords(coords, precision):
260+
"""
261+
Recursively round coordinates to the given precision.
262+
Handles nested lists of coordinates.
263+
Args:
264+
coords: A coordinate or list of coordinates (can be nested)
265+
precision: Number of decimal places to round to
266+
Returns:
267+
Rounded coordinates with the same structure as input
268+
"""
269+
if isinstance(coords, (list, tuple)):
270+
if coords and isinstance(coords[0], (list, tuple)):
271+
return [round_coords(c, precision) for c in coords]
272+
else:
273+
result = []
274+
for c in coords:
275+
if isinstance(c, (int, float)):
276+
result.append(round(float(c), precision))
277+
else:
278+
result.append(c)
279+
return result
280+
return coords

functions-python/helpers/tests/test_locations.py

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Unit tests for locations helper module."""
2-
2+
import math
33
import unittest
44
from unittest.mock import MagicMock
55

@@ -16,6 +16,8 @@
1616
select_highest_level_polygon,
1717
select_lowest_level_polygon,
1818
get_country_code_from_polygons,
19+
round_geojson_coords,
20+
round_coords,
1921
)
2022
from unittest.mock import patch
2123

@@ -455,3 +457,154 @@ def test_get_country_code_from_polygons_all_none_admin_levels_returns_one_with_c
455457
]
456458
result = get_country_code_from_polygons(polys)
457459
self.assertIn(result, {"US", "CA"})
460+
461+
def _coords_equal(self, a, b, abs_tol=1e-5):
462+
if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
463+
if len(a) != len(b):
464+
return False
465+
return all(self._coords_equal(x, y, abs_tol=abs_tol) for x, y in zip(a, b))
466+
elif isinstance(a, (list, tuple)) or isinstance(b, (list, tuple)):
467+
return False
468+
else:
469+
return math.isclose(a, b, abs_tol=abs_tol)
470+
471+
def test_round_point(self):
472+
geom = {"type": "Point", "coordinates": [1.1234567, 2.9876543]}
473+
rounded = round_geojson_coords(geom, precision=5)
474+
assert self._coords_equal(rounded["coordinates"], [1.12346, 2.98765])
475+
476+
def test_round_linestring(self):
477+
geom = {
478+
"type": "LineString",
479+
"coordinates": [[1.1234567, 2.9876543], [3.1111111, 4.9999999]],
480+
}
481+
rounded = round_geojson_coords(geom, precision=5)
482+
assert self._coords_equal(
483+
rounded["coordinates"], [[1.12346, 2.98765], [3.11111, 5.0]]
484+
)
485+
486+
def test_round_polygon(self):
487+
geom = {
488+
"type": "Polygon",
489+
"coordinates": [
490+
[[1.1234567, 2.9876543], [3.1111111, 4.9999999], [1.1234567, 2.9876543]]
491+
],
492+
}
493+
rounded = round_geojson_coords(geom, precision=5)
494+
assert self._coords_equal(
495+
rounded["coordinates"],
496+
[[[1.12346, 2.98765], [3.11111, 5.0], [1.12346, 2.98765]]],
497+
)
498+
499+
def test_round_multipoint(self):
500+
geom = {
501+
"type": "MultiPoint",
502+
"coordinates": [[1.1234567, 2.9876543], [3.1111111, 4.9999999]],
503+
}
504+
rounded = round_geojson_coords(geom, precision=5)
505+
assert self._coords_equal(
506+
rounded["coordinates"], [[1.12346, 2.98765], [3.11111, 5.0]]
507+
)
508+
509+
def test_round_multilinestring(self):
510+
geom = {
511+
"type": "MultiLineString",
512+
"coordinates": [
513+
[[1.1234567, 2.9876543], [3.1111111, 4.9999999]],
514+
[[5.5555555, 6.6666666], [7.7777777, 8.8888888]],
515+
],
516+
}
517+
rounded = round_geojson_coords(geom, precision=5)
518+
assert self._coords_equal(
519+
rounded["coordinates"],
520+
[
521+
[[1.12346, 2.98765], [3.11111, 5.0]],
522+
[[5.55556, 6.66667], [7.77778, 8.88889]],
523+
],
524+
)
525+
526+
def test_round_multipolygon(self):
527+
geom = {
528+
"type": "MultiPolygon",
529+
"coordinates": [
530+
[
531+
[
532+
[1.1234567, 2.9876543],
533+
[3.1111111, 4.9999999],
534+
[1.1234567, 2.9876543],
535+
]
536+
],
537+
[
538+
[
539+
[5.5555555, 6.6666666],
540+
[7.7777777, 8.8888888],
541+
[5.5555555, 6.6666666],
542+
]
543+
],
544+
],
545+
}
546+
rounded = round_geojson_coords(geom, precision=5)
547+
assert self._coords_equal(
548+
rounded["coordinates"],
549+
[
550+
[[[1.12346, 2.98765], [3.11111, 5.0], [1.12346, 2.98765]]],
551+
[[[5.55556, 6.66667], [7.77778, 8.88889], [5.55556, 6.66667]]],
552+
],
553+
)
554+
555+
def test_round_geometrycollection(self):
556+
geom = {
557+
"type": "GeometryCollection",
558+
"geometries": [
559+
{"type": "Point", "coordinates": [1.1234567, 2.9876543]},
560+
{
561+
"type": "LineString",
562+
"coordinates": [[3.1111111, 4.9999999], [5.5555555, 6.6666666]],
563+
},
564+
],
565+
}
566+
rounded = round_geojson_coords(geom, precision=5)
567+
assert self._coords_equal(
568+
rounded["geometries"][0]["coordinates"], [1.12346, 2.98765]
569+
)
570+
assert self._coords_equal(
571+
rounded["geometries"][1]["coordinates"],
572+
[[3.11111, 5.0], [5.55556, 6.66667]],
573+
)
574+
575+
def test_empty_coords(self):
576+
geom = {"type": "Point", "coordinates": []}
577+
rounded = round_geojson_coords(geom, precision=5)
578+
assert rounded["coordinates"] == []
579+
580+
def test_non_list_coords(self):
581+
geom = {"type": "Point", "coordinates": 1.1234567}
582+
rounded = round_geojson_coords(geom, precision=5)
583+
assert rounded["coordinates"] == 1.1234567
584+
585+
def test_round_coords_single_float(self):
586+
assert (
587+
round_coords(1.1234567, 5) == 1.1234567
588+
) # Non-list input returns unchanged
589+
590+
def test_round_coords_list_of_floats(self):
591+
assert round_coords([1.1234567, 2.9876543], 5) == [1.12346, 2.98765]
592+
593+
def test_round_coords_tuple_of_floats(self):
594+
assert round_coords((1.1234567, 2.9876543), 5) == [1.12346, 2.98765]
595+
596+
def test_round_coords_nested_lists(self):
597+
coords = [[[1.1234567, 2.9876543], [3.1111111, 4.9999999]]]
598+
expected = [[[1.12346, 2.98765], [3.11111, 5.0]]]
599+
assert round_coords(coords, 5) == expected
600+
601+
def test_round_coords_empty_list(self):
602+
assert round_coords([], 5) == []
603+
604+
def test_round_coords_non_numeric(self):
605+
assert round_coords("not_a_number", 5) == "not_a_number"
606+
607+
def test_round_coords_mixed_types(self):
608+
coords = [1.1234567, "foo", 2.9876543]
609+
expected = [1.12346, "foo", 2.98765]
610+
assert round_coords(coords, 5) == expected

functions-python/reverse_geolocation/src/reverse_geolocation_processor.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
)
3737
from shared.dataset_service.dataset_service_commons import Status
3838

39-
from shared.helpers.locations import ReverseGeocodingStrategy
39+
from shared.helpers.locations import ReverseGeocodingStrategy, round_geojson_coords
4040
from shared.helpers.logger import get_logger
4141
from shared.helpers.runtime_metrics import track_metrics
4242
from shared.helpers.utils import (
@@ -169,9 +169,10 @@ def create_geojson_aggregate(
169169
"features": [
170170
{
171171
"type": "Feature",
172-
"geometry": mapping(geo_polygon_count[osm_id]["clipped_geometry"]),
172+
"geometry": round_geojson_coords(
173+
mapping(geo_polygon_count[osm_id]["clipped_geometry"])
174+
),
173175
"properties": {
174-
"osm_id": osm_id,
175176
"country_code": geo_polygon_count[osm_id]["group"].iso_3166_1_code,
176177
"subdivision_code": geo_polygon_count[osm_id][
177178
"group"

functions-python/reverse_geolocation/tests/test_reverse_geolocation_processor.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ def test_create_geojson_aggregate(self, mock_getenv, mock_storage_client):
329329
self.assertEqual(geojson_data["type"], "FeatureCollection")
330330
self.assertEqual(len(geojson_data["features"]), 1)
331331
feature = geojson_data["features"][0]
332-
self.assertEqual(feature["properties"]["osm_id"], str(geopolygon_1.osm_id))
333332
self.assertEqual(
334333
feature["properties"]["country_code"], geopolygon_1.iso_3166_1_code
335334
)

web-app/src/app/components/PopupTable.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ interface PopupTableProps {
1818
}
1919

2020
const fieldDescriptions: Record<string, { description?: string }> = {
21-
osm_id: {
22-
description: 'OpenStreetMap ID of the geographic area.',
23-
},
2421
stops_in_area: {
2522
description:
2623
'This is the number of stops in stops.txt that are located within this geographic area.',

0 commit comments

Comments
 (0)