feat: add semantic GIS selection metrics
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-15 06:07:34 +02:00
parent 3ff2d07f3c
commit 0baa9b069c
16 changed files with 520 additions and 22 deletions
+8 -5
View File
@@ -29,11 +29,14 @@ them explicit. Forest therefore defaults to the official modern 2013-2025
10 m series, while the separate 1778-1969 historical map series remains
selectable and is never merged into the same trend.
Selection results use dataset-specific PostGIS summaries. Object layers show
intersecting counts, population shows inhabitants with partial-sector
estimates clearly marked and land-cover sources show intersected hectares. The
advanced workbench remains available but is not required for the primary
choose-theme, draw-area, read-result flow.
Selection results use dataset-specific PostGIS summaries with end-user units.
Building footprints, forest, water surfaces and parcels show intersected
hectares; roads and linear watercourses show kilometres; population shows
inhabitants with partial-sector estimates clearly marked. Intersecting object
counts remain visible as supporting source evidence instead of being the only
result. Water explicitly explains that volume cannot be derived without a
reliable depth or bathymetry source. The advanced workbench remains available
but is not required for the primary choose-theme, draw-area, read-result flow.
The primary workflow is deliberately short: choose a municipality or the complete region, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count.
+23 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import GeoMap from '../GeoMap'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapViewportState, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionResponse } from '../../types'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapViewportState, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { featureCollectionBounds } from '../../lib/geojsonBounds'
import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights'
import { useTemporalComparison } from '../../hooks/useTemporalComparison'
@@ -222,6 +222,11 @@ function resultMetricLabel(result: VectorSelectionResponse): string {
return `${result.summary.metric_value.toLocaleString('nl-BE', { maximumFractionDigits })} ${result.summary.metric_unit}`
}
function selectionMetricLabel(metric: VectorSelectionMetric): string {
const maximumFractionDigits = metric.metric_unit === 'inwoners' || metric.metric_unit === 'objecten' ? 0 : 2
return `${metric.metric_value.toLocaleString('nl-BE', { maximumFractionDigits })} ${metric.metric_unit}`
}
function formatTemporalMetric(value: number, unit: string): string {
const maximumFractionDigits = unit === 'inwoners' || unit === 'objecten' ? 0 : 2
return `${value.toLocaleString('nl-BE', { maximumFractionDigits })} ${unit}`
@@ -648,6 +653,9 @@ export function MapWorkspace({
const activeMetricValue = activeSelectionResult?.summary?.metric_value ?? selectedResultTotal
const activeMetricUnit = activeSelectionResult?.summary?.metric_unit ?? 'objecten'
const activeMetricLabel = activeSelectionResult?.summary?.metric_label ?? activeTheme.shortLabel
const activeSupportingMetrics = (activeSelectionResult?.summary?.metrics ?? []).filter(
(metric) => metric.metric_key !== activeSelectionResult?.summary?.primary_metric_key,
)
const activeSecondaryMetric = selectedAreaSquareMetres && selectedAreaSquareMetres > 0
? activeMetricUnit === 'ha'
? `${((activeMetricValue * 10_000) / selectedAreaSquareMetres * 100).toLocaleString('nl-BE', { maximumFractionDigits: 1 })}% dekking`
@@ -1350,6 +1358,17 @@ export function MapWorkspace({
</div>
</div>
{activeSupportingMetrics.length > 0 ? (
<div className="geo-supporting-metrics" aria-label="Aanvullende gebiedsmetingen">
{activeSupportingMetrics.map((metric) => (
<div key={metric.metric_key}>
<span>{metric.metric_label}</span>
<strong>{selectionMetricLabel(metric)}</strong>
</div>
))}
</div>
) : null}
<div className="geo-theme-results">
<div className="geo-results-title-row">
<h4>Alle beschikbare themas</h4>
@@ -1376,6 +1395,9 @@ export function MapWorkspace({
{analysisMode === 'current' && activeSelectionResult?.truncated ? (
<p className="geo-data-notice">De telling is volledig; op de kaart en in de tabel worden maximaal {activeSelectionResult.limit.toLocaleString('nl-BE')} objecten getoond.</p>
) : null}
{analysisMode === 'current' && activeSelectionResult?.summary?.warning ? (
<p className="geo-data-notice">{activeSelectionResult.summary.warning}</p>
) : null}
{mapSelectionError ? <p className="error">{mapSelectionError}</p> : null}
{themeResultsError ? <p className="error">{themeResultsError}</p> : null}
{temporalComparisonError ? <p className="error">{temporalComparisonError}</p> : null}
+27
View File
@@ -6135,6 +6135,33 @@ section {
white-space: nowrap;
}
.geo-supporting-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.35rem;
}
.geo-supporting-metrics > div {
display: flex;
gap: 0.4rem;
align-items: baseline;
justify-content: space-between;
min-width: 0;
border-bottom: 1px solid #e4ebe8;
padding: 0.3rem 0.1rem;
}
.geo-supporting-metrics span {
color: #687570;
font-size: 0.64rem;
}
.geo-supporting-metrics strong {
color: #263832;
font-size: 0.72rem;
white-space: nowrap;
}
.geo-temporal-metrics > div.positive {
border-color: #b8d8c5;
background: #f1faf4;
+12
View File
@@ -349,9 +349,21 @@ export interface VectorSelectionSummary {
metric_value: number
metric_unit: string
aggregation_method: string
primary_metric_key?: string | null
feature_count: number
is_estimate: boolean
warning?: string | null
metrics?: VectorSelectionMetric[]
}
export interface VectorSelectionMetric {
metric_key: string
metric_label: string
metric_value: number
metric_unit: string
aggregation_method: string
is_estimate: boolean
warning?: string | null
}
export interface DatasetTemporalUpdate {