feat: add semantic GIS selection metrics
This commit is contained in:
+8
-5
@@ -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.
|
||||
|
||||
|
||||
@@ -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 thema’s</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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user