From 9069fe549775c8b1cc1da1a49712fd9c30bcfca0 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 11 Jun 2026 14:49:00 +0200 Subject: [PATCH] feat: add MVT simplify option --- tests/routes/test_items.py | 17 ++++------------- tests/routes/test_tiles.py | 28 ++++++++++++++++++++++++++++ tipg/collections.py | 8 +++++++- tipg/factory.py | 7 +++++++ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/tests/routes/test_items.py b/tests/routes/test_items.py index dbec58c1..a05eca23 100644 --- a/tests/routes/test_items.py +++ b/tests/routes/test_items.py @@ -688,6 +688,9 @@ def test_items_geometry_return_options(app): } Items.model_validate(body) + response = app.get("/collections/public.landsat_wrs/items?ids=1") + geom = response.json()["features"][0]["geometry"] + response = app.get("/collections/public.landsat_wrs/items?ids=1&simplify=.001") assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" @@ -697,19 +700,7 @@ def test_items_geometry_return_options(app): assert body["numberReturned"] == 1 assert body["features"][0]["id"] == 1 assert body["features"][0]["properties"]["ogc_fid"] == 1 - assert body["features"][0]["geometry"] == { - "coordinates": [ - [ - [-10.803, 80.989], - [-8.974, 80.342], - [-16.985, 79.689], - [-22.215, 81.092], - [-13.255, 81.856], - [-10.803, 80.989], - ] - ], - "type": "Polygon", - } + assert body["features"][0]["geometry"] != geom Items.model_validate(body) diff --git a/tests/routes/test_tiles.py b/tests/routes/test_tiles.py index b96a5c48..e0b6a2b6 100644 --- a/tests/routes/test_tiles.py +++ b/tests/routes/test_tiles.py @@ -247,3 +247,31 @@ def test_stylejson(app): "/collections/public.landsat/tiles/WebMercatorQuad/style.json?geom-column=centroid" ) assert response.status_code == 200 + + +def test_tile_simplify(app): + """Test tile simplification.""" + name = "landsat_wrs" + response = app.get( + f"/collections/public.{name}/tiles/WebMercatorQuad/0/0/0?limit=1" + ) + assert response.status_code == 200 + decoded = mapbox_vector_tile.decode(response.content) + assert len(decoded["default"]["features"]) == 1 + geom = decoded["default"]["features"][0]["geometry"] + + response = app.get( + f"/collections/public.{name}/tiles/WebMercatorQuad/0/0/0?limit=1&simplify=0.01" + ) + assert response.status_code == 200 + decoded = mapbox_vector_tile.decode(response.content) + assert len(decoded["default"]["features"]) == 1 + assert decoded["default"]["features"][0]["properties"] == { + "id": "1286", + "ogc_fid": 796, + "path": 182, + "pr": "182047", + "row": 47, + } + assert decoded["default"]["features"][0]["id"] == 0 + assert decoded["default"]["features"][0]["geometry"] != geom diff --git a/tipg/collections.py b/tipg/collections.py index 88e939e5..4f24211f 100644 --- a/tipg/collections.py +++ b/tipg/collections.py @@ -673,7 +673,7 @@ def _geom_expr( g = f"ST_Envelope({g})" elif simplify: s = float(simplify) - g = f"ST_SnapToGrid(ST_Simplify({g}, {s}), {s})" + g = f"ST_SnapToGrid(ST_SimplifyPreserveTopology({g}, {s}), {s})" return g @@ -711,12 +711,16 @@ def _select_mvt( geometry_column: Column, tms: TileMatrixSet, tile: Tile, + simplify: float | None = None, ) -> str: """Build the SELECT clause that emits an MVT geometry per row.""" cols = self._select_property_columns(properties) geom = f"CAST({_quote_ident(geometry_column.name)} AS geometry)" + if simplify: + geom = f"ST_SnapToGrid(ST_SimplifyPreserveTopology({geom}, {simplify}), {simplify})" + # For tiles that fall outside the TMS's natural domain (e.g. an # over-zoomed corner tile), clip the source geometry to the TMS's # geographic bbox before reprojecting — otherwise ST_AsMVTGeom can @@ -968,6 +972,7 @@ async def get_tile( geom: Optional[str] = None, dt: Optional[str] = None, limit: Optional[int] = None, + simplify: float | None = None, ): """Build query to get Vector Tile.""" limit = limit or mvt_settings.max_features_per_tile @@ -1001,6 +1006,7 @@ async def get_tile( geometry_column=geometry_column, tms=tms, tile=tile, + simplify=simplify, ), self._from(function_parameters, params), self._where_clause(where_filter), diff --git a/tipg/factory.py b/tipg/factory.py index bb7879e2..7e0a7af8 100644 --- a/tipg/factory.py +++ b/tipg/factory.py @@ -1668,6 +1668,12 @@ async def collection_get_tile( description="Limits the number of features in the response. Defaults to 10000 or TIPG_MAX_FEATURES_PER_TILE environment variable." ), ] = None, + simplify: Annotated[ + float | None, + Query( + description="Simplify the output geometry to given threshold in the units of the output CRS.", + ), + ] = None, ): """Return Vector Tile.""" tms = self.supported_tms.get(tileMatrixSetId) @@ -1688,6 +1694,7 @@ async def collection_get_tile( limit=limit, geom=geom_column, dt=datetime_column, + simplify=simplify, ) return Response(tile, media_type=MediaType.mvt.value)