Aggregate regional bathymetry partitions
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-17 17:08:35 +02:00
parent 24247746c1
commit 4fd6c06f5c
17 changed files with 661 additions and 29 deletions
+6
View File
@@ -24,6 +24,12 @@
with search and pagination so complete regional workspaces remain usable. with search and pagination so complete regional workspaces remain usable.
- Made VRBG boundary labels coverage-aware so Flanders is never presented as - Made VRBG boundary labels coverage-aware so Flanders is never presented as
the Kempen transport region. the Kempen transport region.
- Replaced the regional Waterbodem representative-Dataset shortcut with a
manifest-aware PostGIS selection across all intersecting VHA municipality
partitions. Exact municipality selection, full-Flanders counts, configured
metrics, GeoJSON and server-side exports now use the same governed path.
- Made the Waterbodem theme report the actual regional totals and partition
count instead of the feature/document count of one arbitrary municipality.
## Sprint 235 Governed bathymetry profiles and Belgian scale architecture (2026-07-17) ## Sprint 235 Governed bathymetry profiles and Belgian scale architecture (2026-07-17)
+8
View File
@@ -1743,6 +1743,14 @@ manifest after every partition. Repeating it reuses completed source
identities; `--force` deliberately refreshes them. A partial `--members` or identities; `--force` deliberately refreshes them. A partial `--members` or
`--max-partitions` run never marks regional coverage complete. `--max-partitions` run never marks regional coverage complete.
Regional map analysis uses
`POST /api/v1/projects/{project_id}/datasets/bathymetry/profiles/partitions/select`.
It selects the latest complete manifest, prefilters overlapping municipality
partitions and performs one PostGIS query over their persisted
`vector_features`. Municipality Areas use only their exact partition. The
response and server-side map export retain every contributing Dataset id and
never substitute a single municipality Dataset for all of Flanders.
Inspect the MDK North Sea WCS without downloading coverage: Inspect the MDK North Sea WCS without downloading coverage:
```bash ```bash
+35
View File
@@ -257,6 +257,41 @@ def finalize_bathymetry_profile_partitions(
return envelope(BathymetryProfileAcquisitionService.finalize_partitions(db, project_id, payload)) return envelope(BathymetryProfileAcquisitionService.finalize_partitions(db, project_id, payload))
@router.post("/datasets/bathymetry/profiles/partitions/select", response_model=dict)
def select_bathymetry_profile_partitions(
project_id: UUID,
payload: VectorSelectionRequest,
db: Session = Depends(get_db),
):
selection_geometry = None
selection_area_id = None
partition_area_id = None
if payload.area_id is not None:
selection_area = db.get(Area, payload.area_id)
if selection_area is None or selection_area.project_id != project_id:
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
selection_geometry, _covers_full_area = VectorFeatureService.constrain_bbox_to_area(
payload.bbox.model_dump(),
selection_area.geometry,
)
selection_area_id = selection_area.id
if str(selection_area.name or "").lower().startswith("gemeente "):
partition_area_id = selection_area.id
result = VectorFeatureService.select_partitioned_features_by_bbox(
db,
project_id=project_id,
source_name=BathymetryProfileAcquisitionService.PROVIDER,
partition_scope_key="flanders",
bbox=payload.bbox.model_dump(),
limit=payload.limit,
selection_geometry=selection_geometry,
selection_area_id=selection_area_id,
partition_area_id=partition_area_id,
)
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
@router.post("/datasets/thematic-raster/acquire", response_model=dict) @router.post("/datasets/thematic-raster/acquire", response_model=dict)
def acquire_bounded_thematic_raster( def acquire_bounded_thematic_raster(
project_id: UUID, project_id: UUID,
+3 -2
View File
@@ -56,6 +56,7 @@ class MapResultExportRequest(BaseModel):
area_id: UUID | None = None area_id: UUID | None = None
partitioned: bool = False partitioned: bool = False
product_key: str | None = None product_key: str | None = None
partition_scope_key: str | None = None
theme_id: str | None = None theme_id: str | None = None
name: str | None = None name: str | None = None
@@ -67,8 +68,8 @@ class MapResultExportRequest(BaseModel):
self.earlier_dataset_id is None or self.later_dataset_id is None self.earlier_dataset_id is None or self.later_dataset_id is None
): ):
raise ValueError("earlier_dataset_id and later_dataset_id are required for evolution exports") raise ValueError("earlier_dataset_id and later_dataset_id are required for evolution exports")
if self.partitioned and not self.product_key: if self.partitioned and not self.product_key and not self.partition_scope_key:
raise ValueError("product_key is required for partitioned raster exports") raise ValueError("product_key or partition_scope_key is required for partitioned exports")
return self return self
+5
View File
@@ -251,3 +251,8 @@ class VectorSelectionResponse(BaseModel):
truncated: bool truncated: bool
geojson: dict geojson: dict
summary: VectorSelectionSummary | None = None summary: VectorSelectionSummary | None = None
partition_count: int | None = None
available_partition_count: int | None = None
partition_scope_key: str | None = None
source_name: str | None = None
dataset_ids: list[UUID] | None = None
+91
View File
@@ -87,6 +87,16 @@ class ExportService:
if not dataset or dataset.project_id != payload.project_id: if not dataset or dataset.project_id != payload.project_id:
raise AppError(code="DATASET_NOT_FOUND", message="Dataset not found", status_code=404) raise AppError(code="DATASET_NOT_FOUND", message="Dataset not found", status_code=404)
if dataset.dataset_type in DatasetService.VECTOR_TYPES: if dataset.dataset_type in DatasetService.VECTOR_TYPES:
if payload.partitioned:
return ExportService.export_partitioned_vector_selection_geojson(
db,
dataset,
payload.bbox.model_dump(mode="json"),
partition_scope_key=payload.partition_scope_key or "",
area_id=payload.area_id,
name=payload.name,
limit=1000,
)
return ExportService.export_vector_selection_geojson( return ExportService.export_vector_selection_geojson(
db, db,
dataset.id, dataset.id,
@@ -198,6 +208,87 @@ class ExportService:
) )
return ExportService._create_response(export) return ExportService._create_response(export)
@staticmethod
def export_partitioned_vector_selection_geojson(
db: Session,
dataset: Dataset,
bbox: dict[str, Any],
*,
partition_scope_key: str,
area_id: uuid.UUID | None = None,
limit: int = 1000,
name: str | None = None,
) -> ExportCreateResponse:
if dataset.source_name != "vmm_vha_bathymetry_profiles" or partition_scope_key != "flanders":
raise AppError(
code="PARTITIONED_VECTOR_EXPORT_UNSUPPORTED",
message="This vector source does not expose a governed partitioned export",
details={
"source_name": dataset.source_name,
"partition_scope_key": partition_scope_key,
},
status_code=400,
)
selection_geometry = None
partition_area_id = None
if area_id is not None:
area = db.get(Area, area_id)
if not area or area.project_id != dataset.project_id:
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
selection_geometry, _covers_full_area = VectorFeatureService.constrain_bbox_to_area(
bbox,
area.geometry,
)
if str(area.name or "").lower().startswith("gemeente "):
partition_area_id = area.id
selection = VectorFeatureService.select_partitioned_features_by_bbox(
db,
project_id=dataset.project_id,
source_name=dataset.source_name,
partition_scope_key=partition_scope_key,
bbox=bbox,
limit=limit,
selection_geometry=selection_geometry,
selection_area_id=area_id,
partition_area_id=partition_area_id,
)
filename = ExportService._filename(name, "bathymetry-profile-selection.geojson", ".geojson")
export_path = StorageService.dataset_export_path(
str(dataset.project_id),
str(dataset.id),
filename,
)
metadata = {
"source": "partitioned_vector_selection",
"project_id": str(dataset.project_id),
"representative_dataset_id": str(dataset.id),
"dataset_ids": [str(value) for value in selection["dataset_ids"]],
"source_name": dataset.source_name,
"partition_scope_key": partition_scope_key,
"partition_count": selection["partition_count"],
"available_partition_count": selection["available_partition_count"],
"selection_bbox": selection["selection_bbox"],
"selection_area_id": selection.get("selection_area_id"),
"feature_count": selection["feature_count"],
"total_feature_count": selection["total_feature_count"],
"limit": selection["limit"],
"truncated": selection["truncated"],
"source_table": "vector_features",
"server_recomputed": True,
}
export = ExportService._write_json_export(
db,
project_id=dataset.project_id,
analysis_run_id=None,
export_type="partitioned_vector_selection_geojson",
storage_path=export_path,
content=selection["geojson"],
metadata=metadata,
)
return ExportService._create_response(export)
@staticmethod @staticmethod
def export_vector_selection_geojson( def export_vector_selection_geojson(
db: Session, db: Session,
+180 -1
View File
@@ -137,6 +137,8 @@ SELECTION_FILTER_VALUE_ALIASES: dict[tuple[str, str], tuple[str, ...]] = {
class VectorFeatureService: class VectorFeatureService:
MAX_VECTOR_PARTITIONS = 500
@staticmethod @staticmethod
def _expanded_selection_filter_values(filter_property: str, filter_values: list[Any]) -> list[str]: def _expanded_selection_filter_values(filter_property: str, filter_values: list[Any]) -> list[str]:
expanded: list[str] = [] expanded: list[str] = []
@@ -307,6 +309,178 @@ class VectorFeatureService:
return {"min_x": min_x, "min_y": min_y, "max_x": max_x, "max_y": max_y, "crs": "EPSG:4326"} return {"min_x": min_x, "min_y": min_y, "max_x": max_x, "max_y": max_y, "crs": "EPSG:4326"}
@staticmethod
def _dataset_bbox_intersects(
dataset: Dataset,
bbox: dict[str, float | str],
) -> bool:
source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {}
source_bbox = source_metadata.get("bbox_epsg4326")
if not isinstance(source_bbox, list) or len(source_bbox) != 4:
return True
try:
min_x, min_y, max_x, max_y = (float(value) for value in source_bbox)
except (TypeError, ValueError):
return True
return not (
max_x <= float(bbox["min_x"])
or min_x >= float(bbox["max_x"])
or max_y <= float(bbox["min_y"])
or min_y >= float(bbox["max_y"])
)
@staticmethod
def _latest_complete_partition_manifest(
datasets: Iterable[Dataset],
*,
source_name: str,
partition_scope_key: str,
) -> list[Dataset]:
groups: dict[str, list[Dataset]] = {}
for dataset in datasets:
source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {}
manifest_sha256 = str(source_metadata.get("partition_manifest_sha256") or "")
if (
dataset.source_name != source_name
or dataset.dataset_type not in {"vector", "geojson"}
or dataset.status != "ready"
or dataset.area_id is None
or source_metadata.get("regional_partitions_complete") is not True
or source_metadata.get("partition_scope_key") != partition_scope_key
or len(manifest_sha256) != 64
):
continue
groups.setdefault(manifest_sha256, []).append(dataset)
complete_groups: list[list[Dataset]] = []
for group in groups.values():
area_ids = {dataset.area_id for dataset in group}
expected_data_count = max(
int((dataset.source_metadata or {}).get("data_partition_count") or 0)
for dataset in group
)
if expected_data_count > 0 and len(group) == expected_data_count and len(area_ids) == len(group):
complete_groups.append(group)
if not complete_groups:
return []
def manifest_priority(group: list[Dataset]) -> tuple[str, int, str]:
observed_at = max(
str((dataset.source_metadata or {}).get("partition_manifest_observed_at") or "")
for dataset in group
)
manifest_sha256 = str((group[0].source_metadata or {}).get("partition_manifest_sha256") or "")
return observed_at, len(group), manifest_sha256
selected = max(complete_groups, key=manifest_priority)
return sorted(selected, key=lambda dataset: (str(dataset.area_id), str(dataset.id)))
@staticmethod
def select_partitioned_features_by_bbox(
db,
*,
project_id: UUID,
source_name: str,
partition_scope_key: str,
bbox: dict[str, Any],
limit: int = 100,
selection_geometry: Any | None = None,
selection_area_id: UUID | None = None,
partition_area_id: UUID | None = None,
) -> dict[str, Any]:
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
safe_limit = max(1, min(int(limit), 1000))
project_datasets = (
db.query(Dataset)
.filter(
Dataset.project_id == project_id,
Dataset.source_name == source_name,
Dataset.status == "ready",
)
.all()
)
manifest_datasets = VectorFeatureService._latest_complete_partition_manifest(
project_datasets,
source_name=source_name,
partition_scope_key=partition_scope_key,
)
if not manifest_datasets:
raise AppError(
code="VECTOR_PARTITIONS_NOT_READY",
message="No complete persisted vector partition manifest is available",
details={"source_name": source_name, "partition_scope_key": partition_scope_key},
status_code=409,
)
if len(manifest_datasets) > VectorFeatureService.MAX_VECTOR_PARTITIONS:
raise AppError(
code="VECTOR_PARTITION_LIMIT_EXCEEDED",
message="The complete vector partition manifest exceeds the safety limit",
details={
"partition_count": len(manifest_datasets),
"max_partitions": VectorFeatureService.MAX_VECTOR_PARTITIONS,
},
status_code=422,
)
scoped_datasets = [
dataset
for dataset in manifest_datasets
if (partition_area_id is None or dataset.area_id == partition_area_id)
and VectorFeatureService._dataset_bbox_intersects(dataset, normalized_bbox)
]
dataset_ids = [dataset.id for dataset in scoped_datasets]
representative = scoped_datasets[0] if scoped_datasets else manifest_datasets[0]
selection_shape = selection_geometry
if selection_shape is None:
selection_shape = ST_MakeEnvelope(
normalized_bbox["min_x"],
normalized_bbox["min_y"],
normalized_bbox["max_x"],
normalized_bbox["max_y"],
4326,
)
query = db.query(VectorFeature).filter(
VectorFeature.dataset_id.in_(dataset_ids),
ST_Intersects(VectorFeature.geometry, selection_shape),
)
total_feature_count = int(query.count())
rows = (
query.order_by(VectorFeature.created_at.asc(), VectorFeature.id.asc())
.limit(safe_limit + 1)
.all()
)
features = [
VectorFeatureService._row_to_geojson_feature(row)
for row in rows[:safe_limit]
]
result = {
"selection_bbox": normalized_bbox,
"feature_count": len(features),
"total_feature_count": total_feature_count,
"limit": safe_limit,
"truncated": total_feature_count > safe_limit,
"geojson": {"type": "FeatureCollection", "features": features},
"partition_count": len(scoped_datasets),
"available_partition_count": len(manifest_datasets),
"partition_scope_key": partition_scope_key,
"source_name": source_name,
"dataset_ids": dataset_ids,
}
if selection_area_id is not None:
result["selection_area_id"] = str(selection_area_id)
if VectorFeatureService.supports_selection_summary(representative):
result["summary"] = VectorFeatureService.summarize_features_by_bbox(
db,
dataset=representative,
dataset_ids=dataset_ids,
bbox=normalized_bbox,
total_feature_count=total_feature_count,
selection_geometry=selection_shape,
)
return result
@staticmethod @staticmethod
def _row_to_geojson_feature(row: VectorFeature) -> dict[str, Any]: def _row_to_geojson_feature(row: VectorFeature) -> dict[str, Any]:
geometry_value = row.geometry geometry_value = row.geometry
@@ -414,6 +588,7 @@ class VectorFeatureService:
*, *,
dataset: Dataset, dataset: Dataset,
bbox: dict[str, Any], bbox: dict[str, Any],
dataset_ids: list[UUID] | None = None,
total_feature_count: int | None = None, total_feature_count: int | None = None,
selection_geometry: Any | None = None, selection_geometry: Any | None = None,
full_dataset_area: bool = False, full_dataset_area: bool = False,
@@ -429,7 +604,11 @@ class VectorFeatureService:
normalized_bbox["max_y"], normalized_bbox["max_y"],
4326, 4326,
) )
selection_filter = (VectorFeature.dataset_id == dataset.id,) selection_filter = (
(VectorFeature.dataset_id.in_(dataset_ids),)
if dataset_ids is not None
else (VectorFeature.dataset_id == dataset.id,)
)
if preclipped_partition_filter is not None: if preclipped_partition_filter is not None:
partition_property, partition_value = preclipped_partition_filter partition_property, partition_value = preclipped_partition_filter
selection_filter += ( selection_filter += (
@@ -106,6 +106,56 @@ def test_current_vector_map_result_uses_authoritative_selection_export(monkeypat
assert captured["limit"] == 1000 assert captured["limit"] == 1000
def test_partitioned_vector_map_result_uses_governed_partition_export(monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
dataset = Dataset(
id=dataset_id,
project_id=project_id,
name="vha-municipality.geojson",
dataset_type="vector",
source="VHA",
source_name="vmm_vha_bathymetry_profiles",
status="ready",
)
db = FakeSession({(Dataset, dataset_id): dataset})
expected = ExportCreateResponse(
export_id=uuid4(),
path="storage/exports/bathymetry-profile-selection.geojson",
status="ready",
export_type="partitioned_vector_selection_geojson",
)
captured: dict = {}
def fake_partition_export(*args, **kwargs):
captured["dataset"] = args[1]
captured.update(kwargs)
return expected
monkeypatch.setattr(
ExportService,
"export_partitioned_vector_selection_geojson",
fake_partition_export,
)
response = ExportService.export_map_result(
db,
MapResultExportRequest(
project_id=project_id,
mode="current",
dataset_id=dataset_id,
bbox=bbox_payload(),
partitioned=True,
partition_scope_key="flanders",
theme_id="bathymetry",
),
)
assert response is expected
assert captured["dataset"] is dataset
assert captured["partition_scope_key"] == "flanders"
assert captured["limit"] == 1000
def test_raster_map_result_is_recomputed_and_persisted(tmp_path, monkeypatch) -> None: def test_raster_map_result_is_recomputed_and_persisted(tmp_path, monkeypatch) -> None:
project_id = uuid4() project_id = uuid4()
dataset_id = uuid4() dataset_id = uuid4()
@@ -10,7 +10,9 @@ from urllib.error import URLError
from uuid import uuid4 from uuid import uuid4
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from geoalchemy2.shape import from_shape
import pytest import pytest
from shapely.geometry import MultiPolygon, Polygon
from app.core.config import Settings from app.core.config import Settings
from app.core.errors import AppError from app.core.errors import AppError
@@ -20,6 +22,7 @@ from app.models import Area, Dataset, DatasetVersion, Project
from app.schemas.bathymetry import BathymetryPartitionFinalizeRequest from app.schemas.bathymetry import BathymetryPartitionFinalizeRequest
from app.services.bathymetry_profile_acquisition_service import BathymetryProfileAcquisitionService from app.services.bathymetry_profile_acquisition_service import BathymetryProfileAcquisitionService
from app.services.mdk_bathymetry_probe_service import MdkBathymetryProbeService from app.services.mdk_bathymetry_probe_service import MdkBathymetryProbeService
from app.services.vector_feature_service import VectorFeatureService
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
@@ -247,6 +250,117 @@ def test_partition_finalization_requires_complete_area_accounting_and_updates_ve
assert exc_info.value.code == "BATHYMETRY_PARTITION_MANIFEST_INCOMPLETE" assert exc_info.value.code == "BATHYMETRY_PARTITION_MANIFEST_INCOMPLETE"
def test_partition_selection_uses_latest_complete_manifest_without_duplicates() -> None:
project_id = uuid4()
source_name = BathymetryProfileAcquisitionService.PROVIDER
def partition(area_id, manifest, observed_at, data_partition_count):
return Dataset(
id=uuid4(),
project_id=project_id,
area_id=area_id,
name="vha.geojson",
dataset_type="vector",
source="VHA",
source_name=source_name,
source_metadata={
"regional_partitions_complete": True,
"partition_scope_key": "flanders",
"partition_manifest_sha256": manifest,
"partition_manifest_observed_at": observed_at,
"data_partition_count": data_partition_count,
},
status="ready",
)
old = [partition(uuid4(), "a" * 64, "2026-07-16T00:00:00+00:00", 1)]
new_area_ids = [uuid4(), uuid4()]
new = [
partition(area_id, "b" * 64, "2026-07-17T00:00:00+00:00", 2)
for area_id in new_area_ids
]
incomplete = [partition(uuid4(), "c" * 64, "2026-07-18T00:00:00+00:00", 2)]
selected = VectorFeatureService._latest_complete_partition_manifest(
[*old, *new, *incomplete],
source_name=source_name,
partition_scope_key="flanders",
)
assert {dataset.area_id for dataset in selected} == set(new_area_ids)
assert all(dataset.source_metadata["partition_manifest_sha256"] == "b" * 64 for dataset in selected)
def test_partition_selection_route_uses_exact_municipality_and_canonical_envelope(monkeypatch) -> None:
project_id, area_id, dataset_id = uuid4(), uuid4(), uuid4()
area_geometry = MultiPolygon(
[
Polygon(
[
(5.0, 51.0),
(5.2, 51.0),
(5.2, 51.2),
(5.0, 51.2),
(5.0, 51.0),
]
)
]
)
area = Area(
id=area_id,
project_id=project_id,
name="Gemeente Mol - officiele grens",
geometry=from_shape(area_geometry, srid=4326),
)
db = SimpleNamespace(get=lambda model, row_id: area if model is Area and row_id == area_id else None)
captured = {}
def select_partitions(_db, **kwargs):
captured.update(kwargs)
return {
"selection_bbox": kwargs["bbox"],
"selection_area_id": area_id,
"feature_count": 1,
"total_feature_count": 1,
"limit": kwargs["limit"],
"truncated": False,
"geojson": {"type": "FeatureCollection", "features": []},
"partition_count": 1,
"available_partition_count": 269,
"partition_scope_key": "flanders",
"source_name": BathymetryProfileAcquisitionService.PROVIDER,
"dataset_ids": [dataset_id],
}
monkeypatch.setattr(VectorFeatureService, "select_partitioned_features_by_bbox", select_partitions)
app.dependency_overrides[get_db] = lambda: db
try:
response = TestClient(app).post(
f"/api/v1/projects/{project_id}/datasets/bathymetry/profiles/partitions/select",
json={
"bbox": {
"min_x": 5.0,
"min_y": 51.0,
"max_x": 5.2,
"max_y": 51.2,
"crs": "EPSG:4326",
},
"area_id": str(area_id),
"limit": 1000,
},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert set(response.json()) == {"data"}
assert response.json()["data"]["partition_count"] == 1
assert response.json()["data"]["available_partition_count"] == 269
assert captured["partition_area_id"] == area_id
assert captured["source_name"] == BathymetryProfileAcquisitionService.PROVIDER
assert captured["partition_scope_key"] == "flanders"
def test_flanders_scope_discovery_uses_complete_unique_vrbg_inventory() -> None: def test_flanders_scope_discovery_uses_complete_unique_vrbg_inventory() -> None:
features = [ features = [
{ {
@@ -333,3 +447,22 @@ def test_frontend_labels_flanders_scope_without_kempen_mislabeling() -> None:
assert "'Grens Vlaanderen'" in dataset_display assert "'Grens Vlaanderen'" in dataset_display
assert "coverageScope === 'flanders' && layer === 'municipality_boundaries'" in dataset_display assert "coverageScope === 'flanders' && layer === 'municipality_boundaries'" in dataset_display
assert "'Gemeentegrenzen Vlaanderen'" in dataset_display assert "'Gemeentegrenzen Vlaanderen'" in dataset_display
def test_frontend_uses_partitioned_bathymetry_selection_for_regional_scope() -> None:
map_workspace = (
ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx"
).read_text(encoding="utf-8")
theme_hook = (
ROOT / "frontend" / "src" / "hooks" / "useMapThemeSelectionInsights.ts"
).read_text(encoding="utf-8")
dataset_api = (
ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts"
).read_text(encoding="utf-8")
assert "isPartitionedBathymetry" in map_workspace
assert "regionalPartitionedThemeActive" in map_workspace
assert "datasetAvailabilityLabel(dataset, partitions)" in map_workspace
assert "activeSelectionResult?.geojson ?? null" in map_workspace
assert "selectBathymetryProfilePartitions" in theme_hook
assert "datasets/bathymetry/profiles/partitions/select" in dataset_api
+20
View File
@@ -2111,6 +2111,26 @@ accounting sets `partitioned_source_audit=true` and
`regional_partitions_complete=true`; partial operator runs remain hidden at `regional_partitions_complete=true`; partial operator runs remain hidden at
regional scope. regional scope.
### POST `/api/v1/projects/{project_id}/datasets/bathymetry/profiles/partitions/select`
Runs one bounded spatial selection across the latest complete `flanders`
VHA manifest. The request reuses `VectorSelectionRequest`: EPSG:4326 bbox,
optional persisted Area and a result limit of at most 1,000 features.
The backend first rejects incomplete or inconsistent manifests, selects only
municipality Dataset partitions whose recorded bounds overlap the request and
then queries their persisted `vector_features` with one PostGIS intersection.
When the Area is a municipality, only that exact Area partition is eligible.
A regional Area can combine all intersecting partitions without treating one
municipality Dataset as representative regional data.
The canonical response extends the ordinary vector-selection result with
`partition_count`, `available_partition_count`, `partition_scope_key`,
`source_name` and the contributing `dataset_ids`. GeoJSON is capped by the
request limit, while `total_feature_count` and all configured depth/width
metrics are calculated across the complete spatial result. Empty municipalities
return an empty, honest result. No provider request occurs during analysis.
Future provider output continues to use DatasetService and, for vectors, Future provider output continues to use DatasetService and, for vectors,
VectorFeatureService. Arbitrary service URLs, browser-side fetches, insecure VectorFeatureService. Arbitrary service URLs, browser-side fetches, insecure
TLS bypasses and startup downloads remain forbidden. TLS bypasses and startup downloads remain forbidden.
+14 -7
View File
@@ -50,14 +50,19 @@ the provider and the application. A regional logical layer may group complete
partitions, but each Dataset retains its Area id, query URLs, checksums, exact partitions, but each Dataset retains its Area id, query URLs, checksums, exact
count and measurement-date range. count and measurement-date range.
Before regional activation: Regional activation now uses the complete checksum-bound municipality
manifest. The Map flow chooses the exact Area partition for a municipality and
uses a bounded multi-Dataset PostGIS query for regional rectangles or the
complete Flanders Area. GeoJSON remains limited to 1,000 rendered features;
counts and configured metrics cover the complete spatial result. Individual
profile dates remain authoritative and no Dataset-level survey date is
fabricated.
Remaining operational follow-up:
- add a governed Flanders boundary manifest and partition coordinator;
- prove idempotent resume and no duplicate VHA `OBJECTID` within a partition;
- benchmark PostGIS point selection and viewport delivery;
- add freshness/version probing for the VHA MapServer; - add freshness/version probing for the VHA MapServer;
- keep individual profile dates instead of fabricating one Dataset - keep selection-performance evidence as the profile inventory grows;
`observed_at`. - retain explicit no-profile municipalities in every refreshed manifest.
## Tier 3: Belgium ## Tier 3: Belgium
@@ -123,7 +128,9 @@ bed evolution.
1. Operate and validate the Mol VHA profile Dataset and map flow. **Done.** 1. Operate and validate the Mol VHA profile Dataset and map flow. **Done.**
2. Add VHA municipal partition orchestration for Flanders. **Done and live: 2. Add VHA municipal partition orchestration for Flanders. **Done and live:
285/285 municipality partitions accounted for, 269 with data, 16 explicit 285/285 municipality partitions accounted for, 269 with data, 16 explicit
no-profile results, 128,913 profile points and zero failures.** no-profile results, 128,913 profile points and zero failures. Regional
selection and export now aggregate the latest complete manifest in
PostGIS.**
3. Implement a bounded MDK WCS probe, then acquisition behind live evidence. 3. Implement a bounded MDK WCS probe, then acquisition behind live evidence.
**Probe implemented. Acquisition blocked because the live endpoint fails **Probe implemented. Acquisition blocked because the live endpoint fails
strict hostname validation and does not expose usable capabilities.** strict hostname validation and does not expose usable capabilities.**
+22
View File
@@ -10119,3 +10119,25 @@ Final live acceptance:
searchable Area/Dataset pages and mounts collapsed Area/history catalogs searchable Area/Dataset pages and mounts collapsed Area/history catalogs
only when opened. This preserves the complete regional inventory without only when opened. This preserves the complete regional inventory without
overwhelming the browser DOM. overwhelming the browser DOM.
## Sprint 236 follow-up - manifest-aware regional VHA analysis (2026-07-17)
Implemented:
- Corrected the Map explorer so a complete Flemish VHA manifest is never
represented or queried as one arbitrary municipality Dataset.
- Added one canonical bounded partition-selection endpoint. It selects the
latest internally complete manifest generation, prefilters Dataset bounds
and runs a single PostGIS intersection across the contributing persisted
`vector_features`.
- Kept exact municipality selection on its own Area partition and added
manifest-aware server-side GeoJSON export with complete contributing
Dataset provenance.
- Made the regional Waterbodem card report the sum of all 269 data-bearing
partitions: 128,913 profiles and 79,398 source documents. The rendered
GeoJSON remains capped at 1,000 objects while counts and configured metrics
cover the complete spatial result.
Validation:
- The complete readiness gate passed: 927 backend tests, backend compilation,
116 documented API routes, Alembic head `202607160001`, frontend TypeScript
typecheck and the production Vite build.
+7
View File
@@ -657,3 +657,10 @@ Municipality partitions remain hidden for a regional Area until their backend
manifest reports complete coverage. The Sources workspace lists MDK North Sea manifest reports complete coverage. The Sources workspace lists MDK North Sea
as read-only probe-only and SPW Walloon bathymetry as planned until bounded as read-only probe-only and SPW Walloon bathymetry as planned until bounded
raster/download acquisition is operational. raster/download acquisition is operational.
For a complete regional manifest, the theme card totals all data-bearing
municipality partitions instead of displaying one representative partition.
A regional rectangle or full-Area analysis calls the partitioned backend
selection and draws only its bounded GeoJSON result. Switching to a
municipality automatically returns to the exact single-Area Dataset. Regional
downloads are recomputed server-side through the same manifest-aware path.
+67 -19
View File
@@ -186,7 +186,11 @@ const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }>
parcels: { fill: '#a7792f', line: '#7d571f' }, parcels: { fill: '#a7792f', line: '#7d571f' },
} }
function datasetAvailabilityLabel(dataset: DatasetCreateResponse, partitionCount = 1): string { function datasetAvailabilityLabel(
dataset: DatasetCreateResponse,
partitions: DatasetCreateResponse[] = [dataset],
): string {
const partitionCount = partitions.length
const regionalSuffix = partitionCount > 1 ? ` · ${partitionCount} gemeenten` : '' const regionalSuffix = partitionCount > 1 ? ` · ${partitionCount} gemeenten` : ''
if (dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv') { if (dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m']) const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
@@ -197,9 +201,15 @@ function datasetAvailabilityLabel(dataset: DatasetCreateResponse, partitionCount
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} overstromingsscenario${regionalSuffix}` return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} overstromingsscenario${regionalSuffix}`
} }
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') { if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
const profiles = dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0 const profiles = partitions.reduce(
const documents = Number(dataset.source_metadata?.['document_count'] ?? 0) (total, item) => total + (item.feature_count ?? item.vector_summary?.feature_count ?? 0),
return `${profiles.toLocaleString('nl-BE')} profielen · ${documents.toLocaleString('nl-BE')} bronbladen` 0,
)
const documents = partitions.reduce(
(total, item) => total + Number(item.source_metadata?.['document_count'] ?? 0),
0,
)
return `${profiles.toLocaleString('nl-BE')} profielen · ${documents.toLocaleString('nl-BE')} bronbladen${regionalSuffix}`
} }
if (dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster') { if (dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m']) const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
@@ -261,6 +271,13 @@ function isPartitionedRaster(dataset: DatasetCreateResponse | null | undefined):
) )
} }
function isPartitionedBathymetry(dataset: DatasetCreateResponse | null | undefined): boolean {
return Boolean(
dataset?.source_name === 'vmm_vha_bathymetry_profiles'
&& dataset.source_metadata?.['regional_partitions_complete'] === true,
)
}
function datasetProductKey(dataset: DatasetCreateResponse): string { function datasetProductKey(dataset: DatasetCreateResponse): string {
return String(dataset.source_metadata?.['product_key'] ?? '') return String(dataset.source_metadata?.['product_key'] ?? '')
} }
@@ -271,12 +288,14 @@ function datasetCoversSelectedArea(
regionalScope = false, regionalScope = false,
): boolean { ): boolean {
const coverageScope = String(dataset.source_metadata?.['coverage_scope'] ?? '') const coverageScope = String(dataset.source_metadata?.['coverage_scope'] ?? '')
if (isPartitionedBathymetry(dataset)) {
return regionalScope
? true
: Boolean(selectedAreaId) && dataset.area_id === selectedAreaId
}
if (coverageScope !== 'municipality' || !dataset.area_id) { if (coverageScope !== 'municipality' || !dataset.area_id) {
return true return true
} }
if (regionalScope && dataset.source_name === 'vmm_vha_bathymetry_profiles') {
return dataset.source_metadata?.['regional_partitions_complete'] === true
}
if (regionalScope) { if (regionalScope) {
return true return true
} }
@@ -292,15 +311,20 @@ function rasterPartitionsForDataset(
if (!representative) { if (!representative) {
return [] return []
} }
if (!regionalScope || !isPartitionedRaster(representative)) { if (!regionalScope || (!isPartitionedRaster(representative) && !isPartitionedBathymetry(representative))) {
return [representative] return [representative]
} }
const productKey = datasetProductKey(representative) const productKey = datasetProductKey(representative)
const manifestSha256 = String(representative.source_metadata?.['partition_manifest_sha256'] ?? '')
return datasets return datasets
.filter( .filter(
(dataset) => (dataset) =>
dataset.source_name === representative.source_name dataset.source_name === representative.source_name
&& datasetProductKey(dataset) === productKey && (
isPartitionedBathymetry(representative)
? String(dataset.source_metadata?.['partition_manifest_sha256'] ?? '') === manifestSha256
: datasetProductKey(dataset) === productKey
)
&& datasetCoversSelectedArea(dataset, selectedAreaId, true), && datasetCoversSelectedArea(dataset, selectedAreaId, true),
) )
.sort((left, right) => String(left.area_id ?? '').localeCompare(String(right.area_id ?? ''))) .sort((left, right) => String(left.area_id ?? '').localeCompare(String(right.area_id ?? '')))
@@ -740,6 +764,8 @@ export function MapWorkspace({
const activeThemeDataset = themeDatasetMap[activeTheme.id] const activeThemeDataset = themeDatasetMap[activeTheme.id]
const activeThemePartitions = themePartitionMap[activeTheme.id] const activeThemePartitions = themePartitionMap[activeTheme.id]
const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset) const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset)
const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset)
const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive
const terrainImageOverlays = useMemo( const terrainImageOverlays = useMemo(
() => () =>
activeTheme.id === 'elevation' && selectedProjectId activeTheme.id === 'elevation' && selectedProjectId
@@ -842,7 +868,15 @@ export function MapWorkspace({
[themeInsights], [themeInsights],
) )
const activeSelectionResult = themeResults.find((item) => item.theme.id === activeThemeId)?.result const activeSelectionResult = themeResults.find((item) => item.theme.id === activeThemeId)?.result
?? (!regionalRasterThemeActive && selectedMapDataset?.id === activeThemeDataset?.id ? mapSelectionResult : null) ?? (!regionalPartitionedThemeActive && selectedMapDataset?.id === activeThemeDataset?.id ? mapSelectionResult : null)
const explorerMapFeatureCollection = regionalBathymetryThemeActive
? null
: analysisMode === 'evolution' && temporalComparison?.geojson.features.length
? temporalComparison.geojson
: mapFeatureCollection
const explorerSelectionFeatureCollection = analysisMode === 'current'
? activeSelectionResult?.geojson ?? null
: null
const selectedAreaSquareMetres = useMemo( const selectedAreaSquareMetres = useMemo(
() => () =>
bboxesEqual(mapSelectionBbox, selectedAreaBbox) && selectedMapArea?.area_m2 bboxesEqual(mapSelectionBbox, selectedAreaBbox) && selectedMapArea?.area_m2
@@ -1083,8 +1117,13 @@ export function MapWorkspace({
bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' }, bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' },
dataset_id: activeThemeDataset.id, dataset_id: activeThemeDataset.id,
area_id: areaId, area_id: areaId,
partitioned: regionalRasterThemeActive, partitioned: regionalPartitionedThemeActive,
product_key: String(activeThemeDataset.source_metadata?.['product_key'] ?? '') || undefined, product_key: regionalRasterThemeActive
? String(activeThemeDataset.source_metadata?.['product_key'] ?? '') || undefined
: undefined,
partition_scope_key: regionalBathymetryThemeActive
? String(activeThemeDataset.source_metadata?.['partition_scope_key'] ?? 'flanders')
: undefined,
theme_id: activeTheme.id, theme_id: activeTheme.id,
name: `${activeTheme.id}-analysis`, name: `${activeTheme.id}-analysis`,
} }
@@ -1157,7 +1196,8 @@ export function MapWorkspace({
? [{ ? [{
themeId: theme.id, themeId: theme.id,
dataset, dataset,
partitioned: regionalScopeSelected && isPartitionedRaster(dataset), partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
}] }]
: [] : []
}) })
@@ -1167,7 +1207,7 @@ export function MapWorkspace({
const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => { const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => {
setSelectionBbox(bbox) setSelectionBbox(bbox)
const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)] const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)]
if (!regionalRasterThemeActive) { if (!regionalPartitionedThemeActive) {
tasks.push(onRunMapSelectionExtract(bbox, areaId)) tasks.push(onRunMapSelectionExtract(bbox, areaId))
} }
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) { if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
@@ -1328,7 +1368,7 @@ export function MapWorkspace({
<div className="geo-theme-list"> <div className="geo-theme-list">
{DATA_THEMES.map((theme) => { {DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id] const dataset = themeDatasetMap[theme.id]
const partitionCount = themePartitionMap[theme.id].length const partitions = themePartitionMap[theme.id]
const temporalGroups = themeTemporalSeriesMap[theme.id] const temporalGroups = themeTemporalSeriesMap[theme.id]
const temporalGroup = temporalGroups[0] const temporalGroup = temporalGroups[0]
const evolutionAvailable = temporalGroups.some((group) => group.items.length >= 2) const evolutionAvailable = temporalGroups.some((group) => group.items.length >= 2)
@@ -1355,7 +1395,7 @@ export function MapWorkspace({
? 'Alleen huidige toestand' ? 'Alleen huidige toestand'
: 'Bron nog niet ingeladen' : 'Bron nog niet ingeladen'
: dataset : dataset
? datasetAvailabilityLabel(dataset, partitionCount) ? datasetAvailabilityLabel(dataset, partitions)
: 'Bron nog niet ingeladen'} : 'Bron nog niet ingeladen'}
</small> </small>
</span> </span>
@@ -1376,7 +1416,9 @@ export function MapWorkspace({
? mapLayerLabel ? mapLayerLabel
: analysisMode === 'evolution' : analysisMode === 'evolution'
? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen' ? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen'
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'} : regionalBathymetryThemeActive
? 'VHA-dwarsprofielen Vlaanderen'
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
</strong> </strong>
<small> <small>
{analysisOverlayActive {analysisOverlayActive
@@ -1385,6 +1427,8 @@ export function MapWorkspace({
? activeTemporalSeries.length >= 2 ? activeTemporalSeries.length >= 2
? `${activeTemporalSeries.length} officiële meetmomenten · ${formatObservationDate(activeTemporalSeries[0].observed_at)} tot ${formatObservationDate(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at)}` ? `${activeTemporalSeries.length} officiële meetmomenten · ${formatObservationDate(activeTemporalSeries[0].observed_at)} tot ${formatObservationDate(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at)}`
: 'Voor dit thema is nog geen tweede officieel meetmoment beschikbaar.' : 'Voor dit thema is nog geen tweede officieel meetmoment beschikbaar.'
: regionalBathymetryThemeActive
? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd`
: activeThemeDataset : activeThemeDataset
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}` ? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}`
: activeTheme.description} : activeTheme.description}
@@ -1487,6 +1531,8 @@ export function MapWorkspace({
? 'Sleep nu een rechthoek op de kaart.' ? 'Sleep nu een rechthoek op de kaart.'
: regionalRasterThemeActive : regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.' ? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: regionalBathymetryThemeActive
? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.'
: 'Sleep een rechthoek of analyseer het volledige werkgebied.'} : 'Sleep een rechthoek of analyseer het volledige werkgebied.'}
</p> </p>
</div> </div>
@@ -1517,12 +1563,12 @@ export function MapWorkspace({
<div className={bboxSelectionMode ? 'geo-map-canvas geo-map-canvas-drawing' : 'geo-map-canvas'}> <div className={bboxSelectionMode ? 'geo-map-canvas geo-map-canvas-drawing' : 'geo-map-canvas'}>
<GeoMap <GeoMap
data={analysisMode === 'evolution' && temporalComparison?.geojson.features.length ? temporalComparison.geojson : mapFeatureCollection} data={explorerMapFeatureCollection}
dataFillColor={activeThemeMapStyle.fill} dataFillColor={activeThemeMapStyle.fill}
dataLineColor={activeThemeMapStyle.line} dataLineColor={activeThemeMapStyle.line}
areaData={areaFeatureCollection} areaData={areaFeatureCollection}
selectedFeature={selectedFeature} selectedFeature={selectedFeature}
selectionData={analysisMode === 'current' ? mapSelectionResult?.geojson ?? null : null} selectionData={explorerSelectionFeatureCollection}
imageOverlays={activeImageOverlays} imageOverlays={activeImageOverlays}
selectionBbox={mapSelectionBbox} selectionBbox={mapSelectionBbox}
bboxSelectionMode={bboxSelectionMode} bboxSelectionMode={bboxSelectionMode}
@@ -1872,6 +1918,8 @@ export function MapWorkspace({
? `${mapLayerLabel} · ${mapLayerSourceLabel}` ? `${mapLayerLabel} · ${mapLayerSourceLabel}`
: analysisMode === 'evolution' : analysisMode === 'evolution'
? activeTemporalSeriesGroup?.label ?? 'geen vergelijkbare tijdreeks' ? activeTemporalSeriesGroup?.label ?? 'geen vergelijkbare tijdreeks'
: regionalBathymetryThemeActive
? `VHA-dwarsprofielen Vlaanderen · ${activeThemePartitions.length} gemeentepartities`
: activeThemeDataset : activeThemeDataset
? getDatasetDisplayName(activeThemeDataset) ? getDatasetDisplayName(activeThemeDataset)
: 'niet beschikbaar'} : 'niet beschikbaar'}
@@ -89,6 +89,12 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
bbox, bbox,
area_id: areaId, area_id: areaId,
})) }))
: dataset.source_name === 'vmm_vha_bathymetry_profiles' && partitioned
? await datasetsApi.selectBathymetryProfilePartitions(selectedProjectId, {
bbox,
area_id: areaId,
limit: 1000,
})
: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, { : await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
bbox, bbox,
area_id: areaId, area_id: areaId,
+8
View File
@@ -180,6 +180,14 @@ export const datasetsApi = {
), ),
acquireBathymetryProfiles: (projectId: string, payload: BathymetryProfileAcquireRequest): Promise<JobRead> => acquireBathymetryProfiles: (projectId: string, payload: BathymetryProfileAcquireRequest): Promise<JobRead> =>
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/bathymetry/profiles/acquire`, payload), apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/bathymetry/profiles/acquire`, payload),
selectBathymetryProfilePartitions: (
projectId: string,
payload: VectorSelectionRequest,
): Promise<VectorSelectionResponse> =>
apiPost<VectorSelectionResponse>(
`/api/v1/projects/${projectId}/datasets/bathymetry/profiles/partitions/select`,
payload,
),
acquireThematicRaster: (projectId: string, payload: ThematicRasterAcquireRequest): Promise<JobRead> => acquireThematicRaster: (projectId: string, payload: ThematicRasterAcquireRequest): Promise<JobRead> =>
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/thematic-raster/acquire`, payload), apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/thematic-raster/acquire`, payload),
listThematicRasterProducts: (projectId: string): Promise<{ items: ThematicRasterProductRead[]; total: number }> => listThematicRasterProducts: (projectId: string): Promise<{ items: ThematicRasterProductRead[]; total: number }> =>
+6
View File
@@ -575,6 +575,11 @@ export interface VectorSelectionResponse {
truncated: boolean truncated: boolean
geojson: GeoJSON.FeatureCollection geojson: GeoJSON.FeatureCollection
summary?: VectorSelectionSummary | null summary?: VectorSelectionSummary | null
partition_count?: number | null
available_partition_count?: number | null
partition_scope_key?: string | null
source_name?: string | null
dataset_ids?: string[]
} }
export interface VectorSelectionSummary { export interface VectorSelectionSummary {
@@ -1457,6 +1462,7 @@ export interface MapResultExportRequest {
area_id?: string area_id?: string
partitioned?: boolean partitioned?: boolean
product_key?: string product_key?: string
partition_scope_key?: string
theme_id?: string theme_id?: string
name?: string name?: string
} }