feat: make Mol explorer map-first
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user