From 22f7152329748f429e68ba1a0a0c8e5573415038 Mon Sep 17 00:00:00 2001 From: Brian Bergstrom Date: Sat, 22 Aug 2026 15:11:07 -0500 Subject: [PATCH 1/2] feat: accept closed LineStrings as AOI outlines on import Fixes #2285. GridService._adapt_feature_geometry() only accepted Polygon/MultiPolygon geometries, silently dropping (returning None) anything else - including LineStrings. This meant a closed area imported as a LineString (which is exactly what tools like geojson.io produce when converting a closed .osm way to GeoJSON) got rejected outright with "The AOI contains geometries which are not polygons or multipolygons", even though it's unambiguously a valid area outline. Now, a LineString whose first and last coordinates match (a closed ring) and has at least 4 points is converted into a Polygon using the same coordinates before the existing Polygon/MultiPolygon check runs. An open LineString (a real path, not a boundary) is still rejected, same as before - only genuinely closed rings get converted. Verified against 4 cases directly (couldn't import GridService in isolation - importing the backend package pulls in its full dependency tree beyond what this static method needs, same situation as PR #7319): - closed LineString -> now converted and accepted as a Polygon - open LineString -> still correctly rejected - existing Polygon input -> unchanged - a degenerate 3-point "closed" ring (below the 4-point minimum) -> still correctly rejected Co-Authored-By: Claude Sonnet 5 Signed-off-by: Brian Bergstrom --- backend/services/grid/grid_service.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/services/grid/grid_service.py b/backend/services/grid/grid_service.py index 7a634a42a0..808632ef33 100644 --- a/backend/services/grid/grid_service.py +++ b/backend/services/grid/grid_service.py @@ -179,12 +179,20 @@ def _adapt_feature_geometry(feature: geojson.Feature) -> geojson.Feature: :param feature: geojson.feature to be adapted :return: feature with geometry adapted """ - if isinstance( - feature.geometry, (geojson.geometry.Polygon, geojson.geometry.MultiPolygon) - ): + geometry = feature.geometry + if isinstance(geometry, geojson.geometry.LineString): + # A closed LineString (first/last coordinate match, at least 4 + # points) is unambiguously a polygon outline. Tools like + # geojson.io convert a closed .osm way to a LineString rather + # than a Polygon, which otherwise gets rejected outright. + coordinates = geometry["coordinates"] + if len(coordinates) >= 4 and coordinates[0] == coordinates[-1]: + geometry = geojson.geometry.Polygon([coordinates]) + + if isinstance(geometry, (geojson.geometry.Polygon, geojson.geometry.MultiPolygon)): # adapt the geometry for use as a shapely geometry # http://toblerity.org/shapely/manual.html#shapely.geometry.asShape - feature.geometry = shapely.geometry.shape(feature.geometry) + feature.geometry = shapely.geometry.shape(geometry) return feature else: return None From 474169661d14d9025a34ea8a6195201c066824bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:11:51 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- backend/services/grid/grid_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/services/grid/grid_service.py b/backend/services/grid/grid_service.py index 808632ef33..7d6ce81f2e 100644 --- a/backend/services/grid/grid_service.py +++ b/backend/services/grid/grid_service.py @@ -189,7 +189,9 @@ def _adapt_feature_geometry(feature: geojson.Feature) -> geojson.Feature: if len(coordinates) >= 4 and coordinates[0] == coordinates[-1]: geometry = geojson.geometry.Polygon([coordinates]) - if isinstance(geometry, (geojson.geometry.Polygon, geojson.geometry.MultiPolygon)): + if isinstance( + geometry, (geojson.geometry.Polygon, geojson.geometry.MultiPolygon) + ): # adapt the geometry for use as a shapely geometry # http://toblerity.org/shapely/manual.html#shapely.geometry.asShape feature.geometry = shapely.geometry.shape(geometry)