fix: query persisted work area geometry
This commit is contained in:
@@ -237,19 +237,32 @@ def select_vector_features(
|
|||||||
raise HTTPException(status_code=404, detail="Dataset not found")
|
raise HTTPException(status_code=404, detail="Dataset not found")
|
||||||
if dataset.dataset_type not in {"vector", "geojson"}:
|
if dataset.dataset_type not in {"vector", "geojson"}:
|
||||||
raise AppError(code="DATASET_NOT_VECTOR", message="Area selection requires a vector dataset", status_code=400)
|
raise AppError(code="DATASET_NOT_VECTOR", message="Area selection requires a vector dataset", status_code=400)
|
||||||
result = VectorFeatureService.select_features_by_bbox(
|
selection_area = None
|
||||||
db,
|
if payload.area_id is not None:
|
||||||
dataset_id=dataset_id,
|
selection_area = db.get(Area, payload.area_id)
|
||||||
bbox=payload.bbox.model_dump(),
|
if selection_area is None or selection_area.project_id != project_id:
|
||||||
limit=payload.limit,
|
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
|
||||||
)
|
|
||||||
if isinstance(dataset.source_metadata, dict) and dataset.source_metadata.get("selection_aggregation"):
|
selection_kwargs = {
|
||||||
result["summary"] = VectorFeatureService.summarize_features_by_bbox(
|
"dataset_id": dataset_id,
|
||||||
db,
|
"bbox": payload.bbox.model_dump(),
|
||||||
dataset=dataset,
|
"limit": payload.limit,
|
||||||
bbox=payload.bbox.model_dump(),
|
}
|
||||||
total_feature_count=result.get("total_feature_count"),
|
if selection_area is not None:
|
||||||
|
selection_kwargs.update(
|
||||||
|
selection_geometry=selection_area.geometry,
|
||||||
|
selection_area_id=selection_area.id,
|
||||||
)
|
)
|
||||||
|
result = VectorFeatureService.select_features_by_bbox(db, **selection_kwargs)
|
||||||
|
if isinstance(dataset.source_metadata, dict) and dataset.source_metadata.get("selection_aggregation"):
|
||||||
|
summary_kwargs = {
|
||||||
|
"dataset": dataset,
|
||||||
|
"bbox": payload.bbox.model_dump(),
|
||||||
|
"total_feature_count": result.get("total_feature_count"),
|
||||||
|
}
|
||||||
|
if selection_area is not None:
|
||||||
|
summary_kwargs["selection_geometry"] = selection_area.geometry
|
||||||
|
result["summary"] = VectorFeatureService.summarize_features_by_bbox(db, **summary_kwargs)
|
||||||
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
|
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
@@ -210,6 +212,7 @@ class VectorSelectionBBox(BaseModel):
|
|||||||
|
|
||||||
class VectorSelectionRequest(BaseModel):
|
class VectorSelectionRequest(BaseModel):
|
||||||
bbox: VectorSelectionBBox
|
bbox: VectorSelectionBBox
|
||||||
|
area_id: UUID | None = None
|
||||||
limit: int = Field(default=100, ge=1, le=1000)
|
limit: int = Field(default=100, ge=1, le=1000)
|
||||||
|
|
||||||
|
|
||||||
@@ -229,6 +232,7 @@ class VectorSelectionSummary(BaseModel):
|
|||||||
|
|
||||||
class VectorSelectionResponse(BaseModel):
|
class VectorSelectionResponse(BaseModel):
|
||||||
selection_bbox: VectorSelectionBBox
|
selection_bbox: VectorSelectionBBox
|
||||||
|
selection_area_id: UUID | None = None
|
||||||
feature_count: int
|
feature_count: int
|
||||||
total_feature_count: int | None = None
|
total_feature_count: int | None = None
|
||||||
limit: int
|
limit: int
|
||||||
|
|||||||
@@ -124,25 +124,25 @@ class VectorFeatureService:
|
|||||||
bbox: dict[str, Any],
|
bbox: dict[str, Any],
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
dataset: Dataset | None = None,
|
dataset: Dataset | None = None,
|
||||||
|
selection_geometry: Any | None = None,
|
||||||
|
selection_area_id: UUID | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||||
safe_limit = max(1, min(int(limit), 1000))
|
safe_limit = max(1, min(int(limit), 1000))
|
||||||
|
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 = (
|
query = (
|
||||||
db.query(VectorFeature)
|
db.query(VectorFeature)
|
||||||
.filter(VectorFeature.dataset_id == dataset_id)
|
.filter(VectorFeature.dataset_id == dataset_id)
|
||||||
.filter(
|
.filter(ST_Intersects(VectorFeature.geometry, selection_shape))
|
||||||
ST_Intersects(
|
|
||||||
VectorFeature.geometry,
|
|
||||||
ST_MakeEnvelope(
|
|
||||||
normalized_bbox["min_x"],
|
|
||||||
normalized_bbox["min_y"],
|
|
||||||
normalized_bbox["max_x"],
|
|
||||||
normalized_bbox["max_y"],
|
|
||||||
4326,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if hasattr(query, "count"):
|
if hasattr(query, "count"):
|
||||||
total_feature_count = int(query.count())
|
total_feature_count = int(query.count())
|
||||||
@@ -164,9 +164,10 @@ class VectorFeatureService:
|
|||||||
dataset=dataset,
|
dataset=dataset,
|
||||||
bbox=normalized_bbox,
|
bbox=normalized_bbox,
|
||||||
total_feature_count=total_feature_count,
|
total_feature_count=total_feature_count,
|
||||||
|
selection_geometry=selection_geometry,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
"selection_bbox": normalized_bbox,
|
"selection_bbox": normalized_bbox,
|
||||||
"feature_count": len(features),
|
"feature_count": len(features),
|
||||||
"total_feature_count": total_feature_count,
|
"total_feature_count": total_feature_count,
|
||||||
@@ -178,6 +179,9 @@ class VectorFeatureService:
|
|||||||
},
|
},
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
}
|
}
|
||||||
|
if selection_area_id is not None:
|
||||||
|
result["selection_area_id"] = str(selection_area_id)
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def summarize_features_by_bbox(
|
def summarize_features_by_bbox(
|
||||||
@@ -186,18 +190,21 @@ class VectorFeatureService:
|
|||||||
dataset: Dataset,
|
dataset: Dataset,
|
||||||
bbox: dict[str, Any],
|
bbox: dict[str, Any],
|
||||||
total_feature_count: int | None = None,
|
total_feature_count: int | None = None,
|
||||||
|
selection_geometry: Any | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||||
envelope = ST_MakeEnvelope(
|
selection_shape = selection_geometry
|
||||||
normalized_bbox["min_x"],
|
if selection_shape is None:
|
||||||
normalized_bbox["min_y"],
|
selection_shape = ST_MakeEnvelope(
|
||||||
normalized_bbox["max_x"],
|
normalized_bbox["min_x"],
|
||||||
normalized_bbox["max_y"],
|
normalized_bbox["min_y"],
|
||||||
4326,
|
normalized_bbox["max_x"],
|
||||||
)
|
normalized_bbox["max_y"],
|
||||||
|
4326,
|
||||||
|
)
|
||||||
selection_filter = (
|
selection_filter = (
|
||||||
VectorFeature.dataset_id == dataset.id,
|
VectorFeature.dataset_id == dataset.id,
|
||||||
ST_Intersects(VectorFeature.geometry, envelope),
|
ST_Intersects(VectorFeature.geometry, selection_shape),
|
||||||
)
|
)
|
||||||
feature_count = total_feature_count
|
feature_count = total_feature_count
|
||||||
if feature_count is None:
|
if feature_count is None:
|
||||||
@@ -215,13 +222,13 @@ class VectorFeatureService:
|
|||||||
|
|
||||||
metric_value = float(feature_count)
|
metric_value = float(feature_count)
|
||||||
if method == "intersection_area":
|
if method == "intersection_area":
|
||||||
intersection = func.ST_Intersection(VectorFeature.geometry, envelope)
|
intersection = func.ST_Intersection(VectorFeature.geometry, selection_shape)
|
||||||
area_expression = func.ST_Area(func.ST_Transform(intersection, 31370))
|
area_expression = func.ST_Area(func.ST_Transform(intersection, 31370))
|
||||||
area_m2 = db.query(func.coalesce(func.sum(area_expression), 0.0)).filter(*selection_filter).scalar()
|
area_m2 = db.query(func.coalesce(func.sum(area_expression), 0.0)).filter(*selection_filter).scalar()
|
||||||
divisor = 10_000.0 if unit == "ha" else 1.0
|
divisor = 10_000.0 if unit == "ha" else 1.0
|
||||||
metric_value = float(area_m2 or 0.0) / divisor
|
metric_value = float(area_m2 or 0.0) / divisor
|
||||||
elif method == "intersection_length":
|
elif method == "intersection_length":
|
||||||
intersection = func.ST_Intersection(VectorFeature.geometry, envelope)
|
intersection = func.ST_Intersection(VectorFeature.geometry, selection_shape)
|
||||||
length_expression = func.ST_Length(func.ST_Transform(intersection, 31370))
|
length_expression = func.ST_Length(func.ST_Transform(intersection, 31370))
|
||||||
length_m = db.query(func.coalesce(func.sum(length_expression), 0.0)).filter(*selection_filter).scalar()
|
length_m = db.query(func.coalesce(func.sum(length_expression), 0.0)).filter(*selection_filter).scalar()
|
||||||
divisor = 1_000.0 if unit == "km" else 1.0
|
divisor = 1_000.0 if unit == "km" else 1.0
|
||||||
@@ -240,7 +247,7 @@ class VectorFeatureService:
|
|||||||
if method == "area_weighted_sum":
|
if method == "area_weighted_sum":
|
||||||
source_area = func.ST_Area(func.ST_Transform(VectorFeature.geometry, 31370))
|
source_area = func.ST_Area(func.ST_Transform(VectorFeature.geometry, 31370))
|
||||||
intersection_area = func.ST_Area(
|
intersection_area = func.ST_Area(
|
||||||
func.ST_Transform(func.ST_Intersection(VectorFeature.geometry, envelope), 31370)
|
func.ST_Transform(func.ST_Intersection(VectorFeature.geometry, selection_shape), 31370)
|
||||||
)
|
)
|
||||||
coverage_ratio = intersection_area / func.nullif(source_area, 0.0)
|
coverage_ratio = intersection_area / func.nullif(source_area, 0.0)
|
||||||
value_expression = numeric_value * coverage_ratio
|
value_expression = numeric_value * coverage_ratio
|
||||||
|
|||||||
@@ -162,11 +162,108 @@ def test_vector_select_route_rejects_non_vector_dataset(monkeypatch) -> None:
|
|||||||
raise AssertionError("Expected DATASET_NOT_VECTOR")
|
raise AssertionError("Expected DATASET_NOT_VECTOR")
|
||||||
|
|
||||||
|
|
||||||
|
def test_vector_select_route_uses_persisted_area_geometry_when_requested(monkeypatch) -> None:
|
||||||
|
from app.api.routes import datasets as dataset_routes
|
||||||
|
|
||||||
|
project_id = uuid.uuid4()
|
||||||
|
dataset_id = uuid.uuid4()
|
||||||
|
area_id = uuid.uuid4()
|
||||||
|
dataset = Dataset(
|
||||||
|
id=dataset_id,
|
||||||
|
project_id=project_id,
|
||||||
|
dataset_type="vector",
|
||||||
|
source="fixture",
|
||||||
|
name="Regional vector",
|
||||||
|
source_metadata={"selection_aggregation": {"method": "feature_count"}},
|
||||||
|
)
|
||||||
|
area_geometry = object()
|
||||||
|
area = SimpleNamespace(id=area_id, project_id=project_id, geometry=area_geometry)
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
class _AreaSession:
|
||||||
|
@staticmethod
|
||||||
|
def get(model, selected_id): # noqa: ANN001
|
||||||
|
assert model is dataset_routes.Area
|
||||||
|
assert selected_id == area_id
|
||||||
|
return area
|
||||||
|
|
||||||
|
def select_features(db, **kwargs): # noqa: ANN001
|
||||||
|
captured["select"] = kwargs
|
||||||
|
return {
|
||||||
|
"selection_bbox": {"min_x": 4.9, "min_y": 50.9, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"},
|
||||||
|
"selection_area_id": str(area_id),
|
||||||
|
"feature_count": 1,
|
||||||
|
"total_feature_count": 1,
|
||||||
|
"limit": 100,
|
||||||
|
"truncated": False,
|
||||||
|
"geojson": {"type": "FeatureCollection", "features": []},
|
||||||
|
}
|
||||||
|
|
||||||
|
def summarize_features(db, **kwargs): # noqa: ANN001
|
||||||
|
captured["summary"] = kwargs
|
||||||
|
return {
|
||||||
|
"metric_label": "Gebouwen",
|
||||||
|
"metric_value": 1,
|
||||||
|
"metric_unit": "objecten",
|
||||||
|
"aggregation_method": "feature_count",
|
||||||
|
"feature_count": 1,
|
||||||
|
"is_estimate": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(dataset_routes.DatasetService, "get_dataset", lambda db, selected_id: dataset)
|
||||||
|
monkeypatch.setattr(dataset_routes.VectorFeatureService, "select_features_by_bbox", select_features)
|
||||||
|
monkeypatch.setattr(dataset_routes.VectorFeatureService, "summarize_features_by_bbox", summarize_features)
|
||||||
|
|
||||||
|
response = dataset_routes.select_vector_features(
|
||||||
|
project_id=project_id,
|
||||||
|
dataset_id=dataset_id,
|
||||||
|
payload=dataset_routes.VectorSelectionRequest(
|
||||||
|
bbox=dataset_routes.VectorSelectionBBox(min_x=4.9, min_y=50.9, max_x=5.2, max_y=51.2),
|
||||||
|
area_id=area_id,
|
||||||
|
limit=100,
|
||||||
|
),
|
||||||
|
db=_AreaSession(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert str(response["data"]["selection_area_id"]) == str(area_id)
|
||||||
|
assert captured["select"]["selection_geometry"] is area_geometry
|
||||||
|
assert captured["select"]["selection_area_id"] == area_id
|
||||||
|
assert captured["summary"]["selection_geometry"] is area_geometry
|
||||||
|
|
||||||
|
|
||||||
|
def test_vector_select_route_rejects_area_from_another_project(monkeypatch) -> None:
|
||||||
|
from app.api.routes import datasets as dataset_routes
|
||||||
|
|
||||||
|
project_id = uuid.uuid4()
|
||||||
|
dataset_id = uuid.uuid4()
|
||||||
|
area_id = uuid.uuid4()
|
||||||
|
dataset = Dataset(id=dataset_id, project_id=project_id, dataset_type="vector", source="fixture", name="Vector")
|
||||||
|
other_area = SimpleNamespace(id=area_id, project_id=uuid.uuid4(), geometry=object())
|
||||||
|
monkeypatch.setattr(dataset_routes.DatasetService, "get_dataset", lambda db, selected_id: dataset)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dataset_routes.select_vector_features(
|
||||||
|
project_id=project_id,
|
||||||
|
dataset_id=dataset_id,
|
||||||
|
payload=dataset_routes.VectorSelectionRequest(
|
||||||
|
bbox=dataset_routes.VectorSelectionBBox(min_x=4.9, min_y=50.9, max_x=5.2, max_y=51.2),
|
||||||
|
area_id=area_id,
|
||||||
|
),
|
||||||
|
db=SimpleNamespace(get=lambda model, selected_id: other_area),
|
||||||
|
)
|
||||||
|
except AppError as exc:
|
||||||
|
assert exc.code == "AREA_NOT_FOUND"
|
||||||
|
else: # pragma: no cover
|
||||||
|
raise AssertionError("Expected AREA_NOT_FOUND")
|
||||||
|
|
||||||
|
|
||||||
def test_frontend_exposes_map_bbox_selection_contracts() -> None:
|
def test_frontend_exposes_map_bbox_selection_contracts() -> None:
|
||||||
api_client = (ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8")
|
api_client = (ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8")
|
||||||
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
||||||
geomap = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
geomap = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
extract_hook = (ROOT / "frontend" / "src" / "hooks" / "useMapSelectionExtract.ts").read_text(encoding="utf-8")
|
||||||
|
theme_hook = (ROOT / "frontend" / "src" / "hooks" / "useMapThemeSelectionInsights.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "selectVectorFeatures" in api_client
|
assert "selectVectorFeatures" in api_client
|
||||||
assert "Area selection" in map_workspace
|
assert "Area selection" in map_workspace
|
||||||
@@ -177,3 +274,6 @@ def test_frontend_exposes_map_bbox_selection_contracts() -> None:
|
|||||||
assert "selection-bbox" in geomap
|
assert "selection-bbox" in geomap
|
||||||
assert "selection-result" in geomap
|
assert "selection-result" in geomap
|
||||||
assert "useMapSelectionExtract" in app
|
assert "useMapSelectionExtract" in app
|
||||||
|
assert "area_id: areaId" in extract_hook
|
||||||
|
assert "area_id: areaId" in theme_hook
|
||||||
|
assert "analyzeSelection(selectedAreaBbox, selectedMapArea?.id)" in map_workspace
|
||||||
|
|||||||
@@ -372,6 +372,7 @@ Request:
|
|||||||
"max_y": 51.1,
|
"max_y": 51.1,
|
||||||
"crs": "EPSG:4326"
|
"crs": "EPSG:4326"
|
||||||
},
|
},
|
||||||
|
"area_id": "optional persisted area UUID",
|
||||||
"limit": 250
|
"limit": 250
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -388,6 +389,7 @@ Response:
|
|||||||
"max_y": 51.1,
|
"max_y": 51.1,
|
||||||
"crs": "EPSG:4326"
|
"crs": "EPSG:4326"
|
||||||
},
|
},
|
||||||
|
"selection_area_id": "present when area_id was requested",
|
||||||
"feature_count": 2,
|
"feature_count": 2,
|
||||||
"total_feature_count": 2,
|
"total_feature_count": 2,
|
||||||
"limit": 250,
|
"limit": 250,
|
||||||
@@ -404,8 +406,9 @@ Rules:
|
|||||||
|
|
||||||
- Only vector/GeoJSON datasets are supported.
|
- Only vector/GeoJSON datasets are supported.
|
||||||
- Coordinates are EPSG:4326 longitude/latitude.
|
- Coordinates are EPSG:4326 longitude/latitude.
|
||||||
|
- `area_id` is optional and must belong to the route project. When present, the bbox remains the bounded preview extent but PostGIS filtering and configured aggregations use the persisted Area geometry exactly. This prevents a municipal or regional full-work-area query from counting objects in the surrounding bbox corners.
|
||||||
- Results are generated from persisted PostGIS `vector_features`, not from client-side map data.
|
- Results are generated from persisted PostGIS `vector_features`, not from client-side map data.
|
||||||
- `feature_count` is the number of GeoJSON features returned in the bounded preview. `total_feature_count` is the exact number of persisted rows intersecting the bbox.
|
- `feature_count` is the number of GeoJSON features returned in the bounded preview. `total_feature_count` is the exact number of persisted rows intersecting the requested bbox or persisted Area geometry.
|
||||||
- The response is capped by `limit` and returns `truncated=true` when `total_feature_count` exceeds the returned preview.
|
- The response is capped by `limit` and returns `truncated=true` when `total_feature_count` exceeds the returned preview.
|
||||||
- `limit` is bounded to `1..1000`. Municipality-scale clients must page spatially by viewport instead of requesting an unbounded municipality FeatureCollection.
|
- `limit` is bounded to `1..1000`. Municipality-scale clients must page spatially by viewport instead of requesting an unbounded municipality FeatureCollection.
|
||||||
- The Map workspace uses this existing endpoint for vector datasets above 5,000 features. It starts delivery at zoom level 14, debounces `moveend` requests and explicitly reports `truncated=true` as a request to zoom further in. This is a client delivery policy, not a second API or persistence path.
|
- The Map workspace uses this existing endpoint for vector datasets above 5,000 features. It starts delivery at zoom level 14, debounces `moveend` requests and explicitly reports `truncated=true` as a request to zoom further in. This is a client delivery policy, not a second API or persistence path.
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ interface MapWorkspaceProps {
|
|||||||
onSelectMapFeature: (feature: GeoJSON.Feature | null) => void
|
onSelectMapFeature: (feature: GeoJSON.Feature | null) => void
|
||||||
onMapViewportChange: (viewport: MapViewportState) => void
|
onMapViewportChange: (viewport: MapViewportState) => void
|
||||||
onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void
|
onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void
|
||||||
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => Promise<VectorSelectionResponse | null>
|
onRunMapSelectionExtract: (bbox: VectorSelectionBBox, areaId?: string) => Promise<VectorSelectionResponse | null>
|
||||||
onClearMapSelectionExtract: () => void
|
onClearMapSelectionExtract: () => void
|
||||||
onExportMapSelection: (bbox: VectorSelectionBBox) => Promise<unknown>
|
onExportMapSelection: (bbox: VectorSelectionBBox) => Promise<unknown>
|
||||||
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise<DatasetCreateResponse | null>
|
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise<DatasetCreateResponse | null>
|
||||||
@@ -804,17 +804,17 @@ export function MapWorkspace({
|
|||||||
clearTemporalComparison()
|
clearTemporalComparison()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadAllThemeResults = async (bbox: VectorSelectionBBox) => {
|
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
|
||||||
const availableThemes = DATA_THEMES.flatMap((theme) => {
|
const availableThemes = DATA_THEMES.flatMap((theme) => {
|
||||||
const dataset = themeDatasetMap[theme.id]
|
const dataset = themeDatasetMap[theme.id]
|
||||||
return dataset ? [{ themeId: theme.id, dataset }] : []
|
return dataset ? [{ themeId: theme.id, dataset }] : []
|
||||||
})
|
})
|
||||||
await loadThemeInsights(bbox, availableThemes)
|
await loadThemeInsights(bbox, availableThemes, areaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const analyzeSelection = async (bbox: VectorSelectionBBox) => {
|
const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => {
|
||||||
setSelectionBbox(bbox)
|
setSelectionBbox(bbox)
|
||||||
const tasks: Array<Promise<unknown>> = [onRunMapSelectionExtract(bbox), loadAllThemeResults(bbox)]
|
const tasks: Array<Promise<unknown>> = [onRunMapSelectionExtract(bbox, areaId), loadAllThemeResults(bbox, areaId)]
|
||||||
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
|
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
|
||||||
tasks.push(compareTemporalSnapshots(earlierDatasetId, laterDatasetId, bbox))
|
tasks.push(compareTemporalSnapshots(earlierDatasetId, laterDatasetId, bbox))
|
||||||
}
|
}
|
||||||
@@ -844,7 +844,7 @@ export function MapWorkspace({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSelectionBbox(bbox)
|
setSelectionBbox(bbox)
|
||||||
void analyzeSelection(bbox)
|
void analyzeSelection(bbox, selectedAreaBbox ? selectedMapArea?.id : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const runFullGisWorkflow = async () => {
|
const runFullGisWorkflow = async () => {
|
||||||
@@ -1061,7 +1061,7 @@ export function MapWorkspace({
|
|||||||
|
|
||||||
<label className="geo-scope-select">
|
<label className="geo-scope-select">
|
||||||
Werkgebied
|
Werkgebied
|
||||||
<select value={selectedMapAreaId} onChange={(event) => onSelectMapArea(event.target.value)} disabled={areas.length === 0}>
|
<select aria-label="Werkgebied" value={selectedMapAreaId} onChange={(event) => onSelectMapArea(event.target.value)} disabled={areas.length === 0}>
|
||||||
<option value="">Geen werkgebied</option>
|
<option value="">Geen werkgebied</option>
|
||||||
{areas.map((area) => (
|
{areas.map((area) => (
|
||||||
<option key={area.id} value={area.id}>{area.name}</option>
|
<option key={area.id} value={area.id}>{area.name}</option>
|
||||||
@@ -1092,7 +1092,7 @@ export function MapWorkspace({
|
|||||||
className="secondary-action"
|
className="secondary-action"
|
||||||
disabled={!activeThemeDataset || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
|
disabled={!activeThemeDataset || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox)}
|
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)}
|
||||||
>
|
>
|
||||||
Volledig werkgebied
|
Volledig werkgebied
|
||||||
</button>
|
</button>
|
||||||
@@ -1387,6 +1387,7 @@ export function MapWorkspace({
|
|||||||
<label>
|
<label>
|
||||||
Area
|
Area
|
||||||
<select
|
<select
|
||||||
|
aria-label="Werkgebied"
|
||||||
value={selectedMapAreaId}
|
value={selectedMapAreaId}
|
||||||
onChange={(event) => onSelectMapArea(event.target.value)}
|
onChange={(event) => onSelectMapArea(event.target.value)}
|
||||||
disabled={areas.length === 0}
|
disabled={areas.length === 0}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function useMapSelectionExtract({
|
|||||||
setMapSelectionError(null)
|
setMapSelectionError(null)
|
||||||
}, [selectedProjectId, selectedDataset?.id])
|
}, [selectedProjectId, selectedDataset?.id])
|
||||||
|
|
||||||
const runMapSelectionExtract = async (bbox: VectorSelectionBBox) => {
|
const runMapSelectionExtract = async (bbox: VectorSelectionBBox, areaId?: string) => {
|
||||||
if (!selectedProjectId || !selectedDataset) {
|
if (!selectedProjectId || !selectedDataset) {
|
||||||
setMapSelectionError('Open a vector dataset before extracting a map area.')
|
setMapSelectionError('Open a vector dataset before extracting a map area.')
|
||||||
return null
|
return null
|
||||||
@@ -44,6 +44,7 @@ export function useMapSelectionExtract({
|
|||||||
try {
|
try {
|
||||||
const response = await datasetsApi.selectVectorFeatures(selectedProjectId, selectedDataset.id, {
|
const response = await datasetsApi.selectVectorFeatures(selectedProjectId, selectedDataset.id, {
|
||||||
bbox: { ...bbox, crs: 'EPSG:4326' },
|
bbox: { ...bbox, crs: 'EPSG:4326' },
|
||||||
|
area_id: areaId,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
})
|
})
|
||||||
setMapSelectionResult(response)
|
setMapSelectionResult(response)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
const loadThemeInsights = async (
|
const loadThemeInsights = async (
|
||||||
bbox: VectorSelectionBBox,
|
bbox: VectorSelectionBBox,
|
||||||
queries: Array<MapThemeQuery<TThemeId>>,
|
queries: Array<MapThemeQuery<TThemeId>>,
|
||||||
|
areaId?: string,
|
||||||
): Promise<Array<MapThemeInsight<TThemeId>>> => {
|
): Promise<Array<MapThemeInsight<TThemeId>>> => {
|
||||||
if (!selectedProjectId) {
|
if (!selectedProjectId) {
|
||||||
setThemeInsights([])
|
setThemeInsights([])
|
||||||
@@ -48,6 +49,7 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
dataset,
|
dataset,
|
||||||
result: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
|
result: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
|
||||||
bbox,
|
bbox,
|
||||||
|
area_id: areaId,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ export interface MapViewportState {
|
|||||||
|
|
||||||
export interface VectorSelectionRequest {
|
export interface VectorSelectionRequest {
|
||||||
bbox: VectorSelectionBBox
|
bbox: VectorSelectionBBox
|
||||||
|
area_id?: string
|
||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +315,7 @@ export interface VectorSelectionDeriveRequest extends VectorSelectionRequest {
|
|||||||
|
|
||||||
export interface VectorSelectionResponse {
|
export interface VectorSelectionResponse {
|
||||||
selection_bbox: VectorSelectionBBox
|
selection_bbox: VectorSelectionBBox
|
||||||
|
selection_area_id?: string | null
|
||||||
feature_count: number
|
feature_count: number
|
||||||
total_feature_count?: number | null
|
total_feature_count?: number | null
|
||||||
limit: number
|
limit: number
|
||||||
|
|||||||
Reference in New Issue
Block a user