feat: make Mol explorer map-first
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 13:21:53 +02:00
parent e340b7f1ec
commit 2958c25b6f
24 changed files with 1901 additions and 14 deletions
+2
View File
@@ -2,6 +2,8 @@
FastAPI backend for GeoIntel Kempen Foundation Sprints.
The map-first explorer uses the existing persisted vector selection endpoint. Its bounded GeoJSON preview reports `feature_count`, while `total_feature_count` reports the exact PostGIS intersection count before the 1,000-feature response cap. This keeps municipality-scale analysis honest without sending unbounded geometry to the browser.
## Scope implemented
- Project CRUD
- Area CRUD with PostGIS geometry
+1 -1
View File
@@ -203,7 +203,7 @@ def select_vector_features(
bbox=payload.bbox.model_dump(),
limit=payload.limit,
)
return envelope(VectorSelectionResponse(**result).model_dump())
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
@router.post("/datasets/{dataset_id}/vector/select/derive", status_code=201, response_model=dict)
+1
View File
@@ -220,6 +220,7 @@ class VectorSelectionDeriveRequest(VectorSelectionRequest):
class VectorSelectionResponse(BaseModel):
selection_bbox: VectorSelectionBBox
feature_count: int
total_feature_count: int | None = None
limit: int
truncated: bool
geojson: dict
+11 -3
View File
@@ -92,7 +92,7 @@ class VectorFeatureService:
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
safe_limit = max(1, min(int(limit), 1000))
rows = (
query = (
db.query(VectorFeature)
.filter(VectorFeature.dataset_id == dataset_id)
.filter(
@@ -107,17 +107,25 @@ class VectorFeatureService:
),
)
)
.order_by(VectorFeature.created_at.asc())
)
if hasattr(query, "count"):
total_feature_count = int(query.count())
else: # Lightweight unit-test sessions do not always implement Query.count().
total_feature_count = len(query.all())
rows = (
query.order_by(VectorFeature.created_at.asc())
.limit(safe_limit + 1)
.all()
)
truncated = len(rows) > safe_limit
truncated = total_feature_count > safe_limit
selected_rows = rows[:safe_limit]
features = [VectorFeatureService._row_to_geojson_feature(row) for row in selected_rows]
return {
"selection_bbox": normalized_bbox,
"feature_count": len(features),
"total_feature_count": total_feature_count,
"limit": safe_limit,
"truncated": truncated,
"geojson": {
@@ -79,6 +79,7 @@ def test_vector_feature_service_extracts_bbox_geojson_from_persisted_rows() -> N
)
assert result["feature_count"] == 1
assert result["total_feature_count"] == 1
assert result["truncated"] is False
assert result["geojson"]["type"] == "FeatureCollection"
feature = result["geojson"]["features"][0]
@@ -0,0 +1,63 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def read(path: str) -> str:
return (ROOT / path).read_text(encoding="utf-8")
def test_map_first_explorer_is_the_default_product_flow() -> None:
app = read("frontend/src/App.tsx")
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
assert "useState<WorkspaceKey>('map')" in app
assert "Wat bevindt zich in dit gebied?" in workspace
assert "Kies een datathema" in workspace
assert "Teken rechthoek" in workspace
assert "Volledige gemeente" in workspace
assert "Alle beschikbare thema" in workspace
assert "Bron nog niet ingeladen" in workspace
assert "useMapThemeSelectionInsights" in workspace
assert "datasetsApi.selectVectorFeatures" in read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
def test_map_rectangle_drag_is_wired_to_automatic_analysis() -> None:
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
geomap = read("frontend/src/components/GeoMap.tsx")
assert "onMapBboxPreview={handleMapBboxPreview}" in workspace
assert "onMapBboxSelect={handleMapBboxSelect}" in workspace
assert "void analyzeSelection(bbox)" in workspace
assert "map.on('mousedown'" in geomap
assert "map.on('mousemove'" in geomap
assert "map.on('mouseup'" in geomap
assert "onMapBboxSelectRef.current?.(bbox)" in geomap
def test_selection_contract_reports_total_intersections_separately_from_preview() -> None:
schema = read("backend/app/schemas/operations.py")
service = read("backend/app/services/vector_feature_service.py")
frontend_types = read("frontend/src/types.ts")
assert "total_feature_count: int | None = None" in schema
assert '"total_feature_count": total_feature_count' in service
assert "total_feature_count?: number | null" in frontend_types
def test_official_mol_context_provisioner_uses_existing_dataset_flow() -> None:
script = read("scripts/provision_mol_context_layers.py")
dockerfile = read("deploy/unraid/Dockerfile.all-in-one")
readiness = read("scripts/run_readiness_check.sh")
assert '("Wegsegment",)' in script
assert '("WTZ", "WLAS", "WGR")' in script
assert '("ADP",)' in script
assert '"dataset_role": "reference"' in script
assert '"source_name": "grb"' in script
assert "/datasets/upload" in script
assert "provision_mol_context_layers.py" in dockerfile
assert "py_compile scripts/provision_mol_context_layers.py" in readiness
@@ -39,7 +39,7 @@ def test_app_entrypoint_has_clean_encoding_and_react_imports() -> None:
assert "FormEvent" not in app
assert app.count("useEffect(") == 1
assert "window.scrollTo({ top: 0, left: 0 })" in app
assert "const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview')" in app
assert "const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('map')" in app
def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None: