fix: query persisted work area geometry
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 19:38:41 +02:00
parent a879c74b12
commit 616424e2a2
9 changed files with 179 additions and 46 deletions
+25 -12
View File
@@ -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))
+4
View File
@@ -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
+31 -24
View File
@@ -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
+4 -1
View File
@@ -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.
+9 -8
View File
@@ -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}
+2 -1
View File
@@ -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,
}), }),
})), })),
+2
View File
@@ -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