From 7d8067b5f7827b7cc1c3157b4fda6c966cf3bed7 Mon Sep 17 00:00:00 2001 From: jross Date: Thu, 25 Jun 2026 13:36:52 -0600 Subject: [PATCH] Supply bbox on gpkg publish so GeoServer skips getBounds Even with 2D geometries, GeoServer still hit a 3D-CRS error computing the layer extent when publishing the featuretype: 500 ... java.io.IOException: Error occured calculating bounds for nm_tds_summary Compute the bbox from the GeoDataFrame and pass nativeBoundingBox + latLonBoundingBox (EPSG:4326) in the featuretype POST, with recalculate= empty so GeoServer keeps the supplied boxes instead of calling getBounds on the gpkg store. Co-Authored-By: Claude Opus 4.8 --- orchestration/assets/products.py | 14 +++++++---- orchestration/resources/geoserver.py | 35 ++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/orchestration/assets/products.py b/orchestration/assets/products.py index 9862d0a..4b7ad7d 100644 --- a/orchestration/assets/products.py +++ b/orchestration/assets/products.py @@ -161,11 +161,12 @@ def _combine_asset( return _combine_asset -def _geojson_to_geopackage(geojson_path: Path, layer_name: str, out_dir: Path) -> Path: +def _geojson_to_geopackage(geojson_path: Path, layer_name: str, out_dir: Path): """Convert a GeoJSON file to a GeoPackage whose layer (table) is named *layer_name* (so the published GeoServer layer is named *layer_name*). GeoPackage is a single file with no field-name length limit, unlike the - zipped ESRI Shapefile this replaces. Returns the path to the .gpkg.""" + zipped ESRI Shapefile this replaces. Returns (gpkg_path, bbox) where bbox is + (minx, miny, maxx, maxy) in EPSG:4326.""" gdf = gpd.read_file(geojson_path) if gdf.empty: raise ValueError(f"{layer_name}: GeoJSON has no features; nothing to publish") @@ -177,9 +178,13 @@ def _geojson_to_geopackage(geojson_path: Path, layer_name: str, out_dir: Path) - # computing bounds, so flatten to 2D — elevation remains an attribute. gdf["geometry"] = gdf.geometry.force_2d() + # Bounds are passed to GeoServer explicitly so it never calls getBounds on + # the gpkg store (which still trips the 3D-CRS bug at publish time). + minx, miny, maxx, maxy = (float(v) for v in gdf.total_bounds) + gpkg_path = out_dir / f"{layer_name}.gpkg" gdf.to_file(gpkg_path, driver="GPKG", layer=layer_name) - return gpkg_path + return gpkg_path, (minx, miny, maxx, maxy) def _build_geoserver_asset(product: dict, group: str): @@ -203,12 +208,13 @@ def _geoserver_asset( with tempfile.TemporaryDirectory() as tmpdir: geojson = Path(tmpdir) / f"{pid}.geojson" gcs.download_latest(pid, str(geojson)) - gpkg_path = _geojson_to_geopackage(geojson, pid, Path(tmpdir)) + gpkg_path, bbox = _geojson_to_geopackage(geojson, pid, Path(tmpdir)) actions = geoserver.publish_geopackage( pid, str(gpkg_path), title=product.get("title", pid), abstract=product.get("description", ""), + bbox=bbox, ) except Exception: error = traceback.format_exc() diff --git a/orchestration/resources/geoserver.py b/orchestration/resources/geoserver.py index 8411ee6..3e0dac7 100644 --- a/orchestration/resources/geoserver.py +++ b/orchestration/resources/geoserver.py @@ -75,6 +75,7 @@ def publish_geopackage( gpkg_path: str, title: Optional[str] = None, abstract: Optional[str] = None, + bbox: Optional[tuple] = None, ) -> dict: """Create-or-update a native GeoPackage datastore named *layer_name* from the GeoPackage at *gpkg_path* and publish its layer. Idempotent — @@ -122,19 +123,33 @@ def publish_geopackage( # written by geopandas (== layer_name); name is the published layer # name. The store was recreated above, so this POST always creates a # fresh featuretype. srs is set explicitly — the data is WGS84. - ft_url = f"{base}/rest/workspaces/{ws}/datastores/{layer_name}/featuretypes" + feature_type: dict = { + "name": layer_name, + "nativeName": layer_name, + "title": title or layer_name, + "abstract": abstract or "", + "srs": "EPSG:4326", + "nativeCRS": "EPSG:4326", + } + # Supply the bounding boxes so GeoServer does not call getBounds on the + # gpkg store — that path still trips a 3D-CRS dimension error at publish. + # recalculate= (empty) tells GeoServer to keep the supplied boxes as-is. + recalc = "" + if bbox is not None: + minx, miny, maxx, maxy = bbox + box = { + "minx": minx, "miny": miny, "maxx": maxx, "maxy": maxy, + "crs": "EPSG:4326", + } + feature_type["nativeBoundingBox"] = box + feature_type["latLonBoundingBox"] = dict(box) + recalc = "?recalculate=" + + ft_url = f"{base}/rest/workspaces/{ws}/datastores/{layer_name}/featuretypes{recalc}" r = requests.post( ft_url, auth=auth, - json={ - "featureType": { - "name": layer_name, - "nativeName": layer_name, - "title": title or layer_name, - "abstract": abstract or "", - "srs": "EPSG:4326", - } - }, + json={"featureType": feature_type}, headers={"Content-Type": "application/json"}, timeout=self.timeout, )