diff --git a/CHANGELOG.md b/CHANGELOG.md index c662e1fa..8e33ce2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ the full readiness gate, one Alembic head and complete offline migration SQL. - Fixed fail-closed AdminVector GeoJSON serialization for Pandas timestamp properties exposed by the live NGI GeoPackage. +- Hardened shared AOI geometry normalization to persist valid 2D polygons when + an authoritative source supplies harmless zero-valued Z coordinates. ## Sprint 240 Operational forest, agriculture, nature and soil themes (2026-07-17) diff --git a/backend/app/utils/geometry.py b/backend/app/utils/geometry.py index 645ceae7..d12fd6f6 100644 --- a/backend/app/utils/geometry.py +++ b/backend/app/utils/geometry.py @@ -3,13 +3,14 @@ from __future__ import annotations from typing import Any from pyproj import Transformer +from shapely import force_2d from shapely.geometry import GeometryCollection, MultiPolygon, Polygon, box, shape from shapely.ops import transform from shapely.validation import make_valid def normalize_to_multipolygon(raw_geometry: dict[str, Any]) -> MultiPolygon: - geom = shape(raw_geometry) + geom = force_2d(shape(raw_geometry)) if geom.is_empty: raise ValueError("Geometry is empty") diff --git a/backend/tests/test_rc4_national_scope_operator.py b/backend/tests/test_rc4_national_scope_operator.py index ce0e81cf..042b5d89 100644 --- a/backend/tests/test_rc4_national_scope_operator.py +++ b/backend/tests/test_rc4_national_scope_operator.py @@ -15,6 +15,7 @@ if str(SCRIPTS) not in sys.path: sys.path.insert(0, str(SCRIPTS)) import provision_belgium_north_sea_scope as operator # noqa: E402 +from app.utils.geometry import normalize_to_multipolygon # noqa: E402 def marine_feature(identifier: str, geometry): @@ -77,6 +78,26 @@ def test_marine_scope_derivation_fails_when_a_required_unit_is_missing() -> None ) +def test_area_geometry_normalization_drops_source_z_dimension() -> None: + geometry = normalize_to_multipolygon( + { + "type": "Polygon", + "coordinates": [ + [ + [2.5, 49.5, 0], + [6.4, 49.5, 0], + [6.4, 51.5, 0], + [2.5, 51.5, 0], + [2.5, 49.5, 0], + ] + ], + } + ) + + assert geometry.has_z is False + assert geometry.geom_type == "MultiPolygon" + + def test_archive_extraction_accepts_one_safe_geopackage_and_rejects_traversal(tmp_path: Path) -> None: archive = tmp_path / "adminvector.zip" with zipfile.ZipFile(archive, "w") as handle: diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 9b3ad5e1..2b3e4f7f 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -30,6 +30,11 @@ serialization was corrected to explicit string conversion and guarded by a regression assertion; downloaded source artifacts remain operator cache only. +- The corrected Tower fetch-only run validated all seven generated artifacts + and 101 current MSP 2026 features. The first API persistence run then exposed + a separate live-only PostGIS constraint: AdminVector AOI boundaries carry a + zero-valued Z coordinate while `areas.geometry` is intentionally 2D. + Shared AOI normalization now removes Z before validation and persistence. - Froze the RC geography as all Belgian land plus the separately labelled territorial sea, EEZ and continental shelf.