feat: add temporal Mol explorer
This commit is contained in:
@@ -3,6 +3,7 @@ import GeoMap from '../GeoMap'
|
||||
import type { AreaRead, DatasetCreateResponse, MapViewportState, QaComparisonResult, VectorSelectionBBox, VectorSelectionResponse } from '../../types'
|
||||
import { featureCollectionBounds } from '../../lib/geojsonBounds'
|
||||
import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights'
|
||||
import { useTemporalComparison } from '../../hooks/useTemporalComparison'
|
||||
|
||||
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
|
||||
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
|
||||
@@ -90,12 +91,42 @@ function pickThemeDataset(datasets: DatasetCreateResponse[], theme: DataTheme):
|
||||
(dataset.reference_layer_name && theme.tokens.includes(dataset.reference_layer_name.toLowerCase()) ? 1_000_000 : 0) +
|
||||
(dataset.source_name === 'grb' ? 100_000 : 0) +
|
||||
(dataset.dataset_role === 'reference' ? 10_000 : 0) +
|
||||
(dataset.observed_at ? new Date(dataset.observed_at).getTime() / 100_000_000 : 0) +
|
||||
(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0)
|
||||
return score(right) - score(left)
|
||||
})
|
||||
return candidates[0] ?? null
|
||||
}
|
||||
|
||||
function pickThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataTheme): DatasetCreateResponse[] {
|
||||
const groups = new Map<string, DatasetCreateResponse[]>()
|
||||
for (const dataset of datasets) {
|
||||
if (!datasetMatchesTheme(dataset, theme) || !dataset.temporal_series_key || !dataset.observed_at) {
|
||||
continue
|
||||
}
|
||||
const items = groups.get(dataset.temporal_series_key) ?? []
|
||||
items.push(dataset)
|
||||
groups.set(dataset.temporal_series_key, items)
|
||||
}
|
||||
return Array.from(groups.values())
|
||||
.filter((items) => items.length >= 2)
|
||||
.sort((left, right) => {
|
||||
if (right.length !== left.length) {
|
||||
return right.length - left.length
|
||||
}
|
||||
const latest = (items: DatasetCreateResponse[]) => Math.max(...items.map((item) => new Date(item.observed_at ?? 0).getTime()))
|
||||
return latest(right) - latest(left)
|
||||
})[0]
|
||||
?.sort((left, right) => new Date(left.observed_at ?? 0).getTime() - new Date(right.observed_at ?? 0).getTime()) ?? []
|
||||
}
|
||||
|
||||
function formatObservationDate(value: string | null | undefined): string {
|
||||
if (!value) {
|
||||
return 'Geen peildatum'
|
||||
}
|
||||
return new Intl.DateTimeFormat('nl-BE', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(value))
|
||||
}
|
||||
|
||||
function selectionAreaSquareMetres(bbox: VectorSelectionBBox | null): number | null {
|
||||
if (!bbox) {
|
||||
return null
|
||||
@@ -134,6 +165,19 @@ function resultCountLabel(result: VectorSelectionResponse): string {
|
||||
return result.truncated && result.total_feature_count == null ? `${result.feature_count.toLocaleString('nl-BE')}+` : total.toLocaleString('nl-BE')
|
||||
}
|
||||
|
||||
function resultMetricLabel(result: VectorSelectionResponse): string {
|
||||
if (!result.summary) {
|
||||
return resultCountLabel(result)
|
||||
}
|
||||
const maximumFractionDigits = result.summary.metric_unit === 'inwoners' ? 0 : 2
|
||||
return `${result.summary.metric_value.toLocaleString('nl-BE', { maximumFractionDigits })} ${result.summary.metric_unit}`
|
||||
}
|
||||
|
||||
function formatTemporalMetric(value: number, unit: string): string {
|
||||
const maximumFractionDigits = unit === 'inwoners' || unit === 'objecten' ? 0 : 2
|
||||
return `${value.toLocaleString('nl-BE', { maximumFractionDigits })} ${unit}`
|
||||
}
|
||||
|
||||
function readablePropertyName(value: string): string {
|
||||
return value.replace(/_/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase())
|
||||
}
|
||||
@@ -444,6 +488,16 @@ export function MapWorkspace({
|
||||
loadThemeInsights,
|
||||
clearThemeInsights,
|
||||
} = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId)
|
||||
const {
|
||||
temporalComparison,
|
||||
temporalComparisonLoading,
|
||||
temporalComparisonError,
|
||||
compareTemporalSnapshots,
|
||||
clearTemporalComparison,
|
||||
} = useTemporalComparison(selectedProjectId)
|
||||
const [analysisMode, setAnalysisMode] = useState<'current' | 'evolution'>('current')
|
||||
const [earlierDatasetId, setEarlierDatasetId] = useState('')
|
||||
const [laterDatasetId, setLaterDatasetId] = useState('')
|
||||
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
|
||||
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
|
||||
const [bboxInput, setBboxInput] = useState(bboxToInputState(mapSelectionBbox))
|
||||
@@ -482,6 +536,10 @@ export function MapWorkspace({
|
||||
)
|
||||
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
|
||||
const activeThemeDataset = themeDatasetMap[activeTheme.id]
|
||||
const activeTemporalSeries = useMemo(
|
||||
() => pickThemeTemporalSeries(availableMapDatasets, activeTheme),
|
||||
[activeTheme, availableMapDatasets],
|
||||
)
|
||||
const themeResults = useMemo(
|
||||
() =>
|
||||
themeInsights.flatMap((insight) => {
|
||||
@@ -502,6 +560,14 @@ export function MapWorkspace({
|
||||
const selectedDensity = selectedAreaSquareMetres && selectedAreaSquareMetres > 0
|
||||
? selectedResultTotal / (selectedAreaSquareMetres / 1_000_000)
|
||||
: null
|
||||
const activeMetricValue = activeSelectionResult?.summary?.metric_value ?? selectedResultTotal
|
||||
const activeMetricUnit = activeSelectionResult?.summary?.metric_unit ?? 'objecten'
|
||||
const activeMetricLabel = activeSelectionResult?.summary?.metric_label ?? activeTheme.shortLabel
|
||||
const activeSecondaryMetric = selectedAreaSquareMetres && selectedAreaSquareMetres > 0
|
||||
? activeMetricUnit === 'ha'
|
||||
? `${((activeMetricValue * 10_000) / selectedAreaSquareMetres * 100).toLocaleString('nl-BE', { maximumFractionDigits: 1 })}% dekking`
|
||||
: `${(activeMetricValue / (selectedAreaSquareMetres / 1_000_000)).toLocaleString('nl-BE', { maximumFractionDigits: 1 })} ${activeMetricUnit} / km2`
|
||||
: null
|
||||
const selectedResultProperties = useMemo(() => {
|
||||
const keys = new Map<string, Set<string>>()
|
||||
for (const feature of activeSelectionResult?.geojson.features ?? []) {
|
||||
@@ -526,6 +592,14 @@ export function MapWorkspace({
|
||||
setBboxInput(bboxToInputState(mapSelectionBbox))
|
||||
}, [mapSelectionBbox])
|
||||
|
||||
useEffect(() => {
|
||||
const first = activeTemporalSeries[0]
|
||||
const last = activeTemporalSeries[activeTemporalSeries.length - 1]
|
||||
setEarlierDatasetId(first?.id ?? '')
|
||||
setLaterDatasetId(last?.id ?? '')
|
||||
clearTemporalComparison()
|
||||
}, [activeTemporalSeries])
|
||||
|
||||
useEffect(() => {
|
||||
if (advancedMode || !activeThemeDataset || (selectedMapDataset && datasetMatchesTheme(selectedMapDataset, activeTheme))) {
|
||||
return
|
||||
@@ -562,6 +636,7 @@ export function MapWorkspace({
|
||||
const startBboxSelection = () => {
|
||||
setFirstSelectionCorner(null)
|
||||
clearThemeInsights()
|
||||
clearTemporalComparison()
|
||||
setBboxSelectionMode(true)
|
||||
}
|
||||
|
||||
@@ -590,6 +665,7 @@ export function MapWorkspace({
|
||||
setFirstSelectionCorner(null)
|
||||
setBboxInput(bboxToInputState(null))
|
||||
clearThemeInsights()
|
||||
clearTemporalComparison()
|
||||
onClearMapSelectionExtract()
|
||||
}
|
||||
|
||||
@@ -644,9 +720,15 @@ export function MapWorkspace({
|
||||
return
|
||||
}
|
||||
setActiveThemeId(theme.id)
|
||||
clearTemporalComparison()
|
||||
onOpenDatasetInMap(dataset)
|
||||
}
|
||||
|
||||
const setExplorerMode = (mode: 'current' | 'evolution') => {
|
||||
setAnalysisMode(mode)
|
||||
clearTemporalComparison()
|
||||
}
|
||||
|
||||
const loadAllThemeResults = async (bbox: VectorSelectionBBox) => {
|
||||
const availableThemes = DATA_THEMES.flatMap((theme) => {
|
||||
const dataset = themeDatasetMap[theme.id]
|
||||
@@ -657,7 +739,18 @@ export function MapWorkspace({
|
||||
|
||||
const analyzeSelection = async (bbox: VectorSelectionBBox) => {
|
||||
setSelectionBbox(bbox)
|
||||
await Promise.all([onRunMapSelectionExtract(bbox), loadAllThemeResults(bbox)])
|
||||
const tasks: Array<Promise<unknown>> = [onRunMapSelectionExtract(bbox), loadAllThemeResults(bbox)]
|
||||
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
|
||||
tasks.push(compareTemporalSnapshots(earlierDatasetId, laterDatasetId, bbox))
|
||||
}
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
const runTemporalComparison = () => {
|
||||
if (!mapSelectionBbox || !earlierDatasetId || !laterDatasetId) {
|
||||
return
|
||||
}
|
||||
void compareTemporalSnapshots(earlierDatasetId, laterDatasetId, mapSelectionBbox)
|
||||
}
|
||||
|
||||
const handleMapBboxPreview = (bbox: VectorSelectionBBox) => {
|
||||
@@ -757,6 +850,26 @@ export function MapWorkspace({
|
||||
<h2>Wat bevindt zich in dit gebied?</h2>
|
||||
<p>Kies een datathema, teken een rechthoek en lees de beschikbare gegevens meteen uit.</p>
|
||||
</div>
|
||||
<div className="geo-analysis-mode" role="tablist" aria-label="Analyseperiode">
|
||||
<button
|
||||
className={analysisMode === 'current' ? 'active' : ''}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={analysisMode === 'current'}
|
||||
onClick={() => setExplorerMode('current')}
|
||||
>
|
||||
Laatste toestand
|
||||
</button>
|
||||
<button
|
||||
className={analysisMode === 'evolution' ? 'active' : ''}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={analysisMode === 'evolution'}
|
||||
onClick={() => setExplorerMode('evolution')}
|
||||
>
|
||||
Evolutie
|
||||
</button>
|
||||
</div>
|
||||
<button className="secondary-action geo-explorer-advanced" type="button" onClick={() => setAdvancedMode(true)}>
|
||||
Geavanceerde werkbank
|
||||
</button>
|
||||
@@ -796,15 +909,52 @@ export function MapWorkspace({
|
||||
</div>
|
||||
|
||||
<div className="geo-source-summary">
|
||||
<span>Actieve bron</span>
|
||||
<strong>{activeThemeDataset?.name ?? 'Geen databron beschikbaar'}</strong>
|
||||
<span>{analysisMode === 'evolution' ? 'Tijdreeks' : 'Actieve bron'}</span>
|
||||
<strong>
|
||||
{analysisMode === 'evolution'
|
||||
? activeTemporalSeries[0]?.temporal_series_key ?? 'Geen tijdreeks beschikbaar'
|
||||
: activeThemeDataset?.name ?? 'Geen databron beschikbaar'}
|
||||
</strong>
|
||||
<small>
|
||||
{activeThemeDataset
|
||||
? `${activeThemeDataset.source_name ?? activeThemeDataset.source} · ${activeThemeDataset.dataset_role ?? 'source'} · EPSG:4326`
|
||||
: activeTheme.description}
|
||||
{analysisMode === 'evolution'
|
||||
? activeTemporalSeries.length >= 2
|
||||
? `${activeTemporalSeries.length} officiële meetmomenten · ${formatObservationDate(activeTemporalSeries[0].observed_at)} tot ${formatObservationDate(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at)}`
|
||||
: 'Minstens twee expliciet gedateerde snapshots zijn vereist.'
|
||||
: activeThemeDataset
|
||||
? `${activeThemeDataset.source_name ?? activeThemeDataset.source} · ${activeThemeDataset.dataset_role ?? 'source'} · ${formatObservationDate(activeThemeDataset.observed_at)}`
|
||||
: activeTheme.description}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{analysisMode === 'evolution' ? (
|
||||
<div className="geo-time-controls" aria-label="Meetmomenten vergelijken">
|
||||
<label>
|
||||
Van
|
||||
<select value={earlierDatasetId} onChange={(event) => { setEarlierDatasetId(event.target.value); clearTemporalComparison() }} disabled={activeTemporalSeries.length < 2}>
|
||||
{activeTemporalSeries.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>{formatObservationDate(dataset.observed_at)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Naar
|
||||
<select value={laterDatasetId} onChange={(event) => { setLaterDatasetId(event.target.value); clearTemporalComparison() }} disabled={activeTemporalSeries.length < 2}>
|
||||
{activeTemporalSeries.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>{formatObservationDate(dataset.observed_at)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
className="primary-action"
|
||||
type="button"
|
||||
disabled={!mapSelectionBbox || !earlierDatasetId || !laterDatasetId || temporalComparisonLoading}
|
||||
onClick={runTemporalComparison}
|
||||
>
|
||||
{temporalComparisonLoading ? 'Vergelijken…' : 'Vergelijk periode'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<label className="geo-scope-select">
|
||||
Werkgebied
|
||||
<select value={selectedMapAreaId} onChange={(event) => onSelectMapArea(event.target.value)} disabled={areas.length === 0}>
|
||||
@@ -828,7 +978,7 @@ export function MapWorkspace({
|
||||
<div className="geo-map-actions">
|
||||
<button
|
||||
className={bboxSelectionMode ? 'primary-action geo-draw-active' : 'primary-action'}
|
||||
disabled={!activeThemeDataset || mapSelectionLoading || themeResultsLoading}
|
||||
disabled={!activeThemeDataset || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading}
|
||||
type="button"
|
||||
onClick={startBboxSelection}
|
||||
>
|
||||
@@ -836,7 +986,7 @@ export function MapWorkspace({
|
||||
</button>
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!activeThemeDataset || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
|
||||
disabled={!activeThemeDataset || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
|
||||
type="button"
|
||||
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox)}
|
||||
>
|
||||
@@ -850,10 +1000,10 @@ export function MapWorkspace({
|
||||
|
||||
<div className={bboxSelectionMode ? 'geo-map-canvas geo-map-canvas-drawing' : 'geo-map-canvas'}>
|
||||
<GeoMap
|
||||
data={mapFeatureCollection}
|
||||
data={analysisMode === 'evolution' && temporalComparison?.geojson.features.length ? temporalComparison.geojson : mapFeatureCollection}
|
||||
areaData={areaFeatureCollection}
|
||||
selectedFeature={selectedFeature}
|
||||
selectionData={mapSelectionResult?.geojson ?? null}
|
||||
selectionData={analysisMode === 'current' ? mapSelectionResult?.geojson ?? null : null}
|
||||
selectionBbox={mapSelectionBbox}
|
||||
bboxSelectionMode={bboxSelectionMode}
|
||||
visible={mapLayerVisible}
|
||||
@@ -869,8 +1019,18 @@ export function MapWorkspace({
|
||||
/>
|
||||
<div className="geo-map-legend" aria-label="Kaartlegende">
|
||||
<span><i className="geo-legend-area" /> Gemeentegrens</span>
|
||||
<span><i className="geo-legend-layer" /> {activeTheme.shortLabel}</span>
|
||||
<span><i className="geo-legend-selection" /> Selectie</span>
|
||||
{analysisMode === 'evolution' && temporalComparison?.object_changes.available ? (
|
||||
<>
|
||||
<span><i className="geo-legend-added" /> Nieuw</span>
|
||||
<span><i className="geo-legend-removed" /> Verdwenen</span>
|
||||
<span><i className="geo-legend-modified" /> Gewijzigd</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span><i className="geo-legend-layer" /> {activeTheme.shortLabel}</span>
|
||||
<span><i className="geo-legend-selection" /> Selectie</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{bboxSelectionMode ? (
|
||||
<div className="geo-draw-instruction" role="status">
|
||||
@@ -900,56 +1060,117 @@ export function MapWorkspace({
|
||||
<strong>Nog geen gebied geselecteerd</strong>
|
||||
<p>Teken een rechthoek op de kaart. De analyse start automatisch zodra je loslaat.</p>
|
||||
</div>
|
||||
) : mapSelectionLoading || themeResultsLoading ? (
|
||||
) : mapSelectionLoading || themeResultsLoading || temporalComparisonLoading ? (
|
||||
<div className="geo-results-loading" role="status">
|
||||
<span />
|
||||
<strong>Gegevens worden uit PostGIS gelezen…</strong>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="geo-primary-metrics">
|
||||
<div>
|
||||
<span>Oppervlakte selectie</span>
|
||||
<strong>{formatArea(selectedAreaSquareMetres)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{activeTheme.shortLabel}</span>
|
||||
<strong>{activeSelectionResult ? resultCountLabel(activeSelectionResult) : 'Geen resultaat'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Dichtheid</span>
|
||||
<strong>{selectedDensity === null ? 'n.v.t.' : `${selectedDensity.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} / km2`}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="geo-theme-results">
|
||||
<div className="geo-results-title-row">
|
||||
<h4>Alle beschikbare thema’s</h4>
|
||||
<span>{themeResults.length} bevraagd</span>
|
||||
</div>
|
||||
{DATA_THEMES.map((theme) => {
|
||||
const dataset = themeDatasetMap[theme.id]
|
||||
const item = themeResults.find((result) => result.theme.id === theme.id)
|
||||
return (
|
||||
<div className="geo-theme-result-row" key={theme.id}>
|
||||
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>{theme.label}</strong>
|
||||
<small>{dataset?.source_name ?? dataset?.source ?? 'Geen bron gekoppeld'}</small>
|
||||
</span>
|
||||
<b>{item ? resultCountLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'}</b>
|
||||
{analysisMode === 'evolution' ? (
|
||||
temporalComparison ? (
|
||||
<>
|
||||
<div className="geo-primary-metrics geo-temporal-metrics">
|
||||
<div>
|
||||
<span>{formatObservationDate(temporalComparison.earlier.observed_at)}</span>
|
||||
<strong>{formatTemporalMetric(temporalComparison.metric.earlier_value, temporalComparison.metric.unit)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{formatObservationDate(temporalComparison.later.observed_at)}</span>
|
||||
<strong>{formatTemporalMetric(temporalComparison.metric.later_value, temporalComparison.metric.unit)}</strong>
|
||||
</div>
|
||||
<div className={temporalComparison.metric.absolute_change >= 0 ? 'positive' : 'negative'}>
|
||||
<span>Verschil</span>
|
||||
<strong>
|
||||
{temporalComparison.metric.absolute_change >= 0 ? '+' : ''}
|
||||
{formatTemporalMetric(temporalComparison.metric.absolute_change, temporalComparison.metric.unit)}
|
||||
</strong>
|
||||
<small>
|
||||
{temporalComparison.metric.percent_change == null
|
||||
? 'geen percentage bij nulwaarde'
|
||||
: `${temporalComparison.metric.percent_change >= 0 ? '+' : ''}${temporalComparison.metric.percent_change.toLocaleString('nl-BE', { maximumFractionDigits: 1 })}%`}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="geo-temporal-summary">
|
||||
<div>
|
||||
<span>Gebied</span>
|
||||
<strong>{formatArea(selectedAreaSquareMetres)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Meting</span>
|
||||
<strong>{temporalComparison.metric.label}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Methode</span>
|
||||
<strong>{temporalComparison.metric.is_estimate ? 'Ruimtelijke schatting' : 'Exact'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
{temporalComparison.object_changes.available ? (
|
||||
<div className="geo-change-counts" aria-label="Objectwijzigingen">
|
||||
<span><strong>{temporalComparison.object_changes.added_count ?? 0}</strong> nieuw</span>
|
||||
<span><strong>{temporalComparison.object_changes.removed_count ?? 0}</strong> verdwenen</span>
|
||||
<span><strong>{temporalComparison.object_changes.modified_count ?? 0}</strong> gewijzigd</span>
|
||||
</div>
|
||||
) : null}
|
||||
{temporalComparison.warnings.map((warning) => (
|
||||
<p className="geo-data-notice" key={warning}>{warning}</p>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="geo-results-empty">
|
||||
<strong>Klaar om te vergelijken</strong>
|
||||
<p>Kies twee meetmomenten en gebruik “Vergelijk periode”. Bij een nieuwe rechthoek wordt de vergelijking automatisch herhaald.</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="geo-primary-metrics">
|
||||
<div>
|
||||
<span>Oppervlakte selectie</span>
|
||||
<strong>{formatArea(selectedAreaSquareMetres)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{activeMetricLabel}</span>
|
||||
<strong>{activeSelectionResult ? resultMetricLabel(activeSelectionResult) : 'Geen resultaat'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{activeMetricUnit === 'ha' ? 'Aandeel selectie' : 'Dichtheid'}</span>
|
||||
<strong>{activeSecondaryMetric ?? (selectedDensity === null ? 'n.v.t.' : `${selectedDensity.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} / km2`)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeSelectionResult?.truncated ? (
|
||||
<div className="geo-theme-results">
|
||||
<div className="geo-results-title-row">
|
||||
<h4>Alle beschikbare thema’s</h4>
|
||||
<span>{themeResults.length} bevraagd</span>
|
||||
</div>
|
||||
{DATA_THEMES.map((theme) => {
|
||||
const dataset = themeDatasetMap[theme.id]
|
||||
const item = themeResults.find((result) => result.theme.id === theme.id)
|
||||
return (
|
||||
<div className="geo-theme-result-row" key={theme.id}>
|
||||
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>{theme.label}</strong>
|
||||
<small>{dataset?.source_name ?? dataset?.source ?? 'Geen bron gekoppeld'}</small>
|
||||
</span>
|
||||
<b>{item ? resultMetricLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'}</b>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{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}
|
||||
{mapSelectionError ? <p className="error">{mapSelectionError}</p> : null}
|
||||
{themeResultsError ? <p className="error">{themeResultsError}</p> : null}
|
||||
{temporalComparisonError ? <p className="error">{temporalComparisonError}</p> : null}
|
||||
|
||||
{selectedResultProperties.length > 0 ? (
|
||||
{analysisMode === 'current' && selectedResultProperties.length > 0 ? (
|
||||
<details className="geo-result-details">
|
||||
<summary>Kenmerken van de gevonden objecten</summary>
|
||||
<dl>
|
||||
@@ -963,7 +1184,7 @@ export function MapWorkspace({
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
{selectedMapFeature ? (
|
||||
{analysisMode === 'current' && selectedMapFeature ? (
|
||||
<div className="geo-selected-feature">
|
||||
<span>Geselecteerd object</span>
|
||||
<strong>{String(selectedMapFeature.properties?.['name'] ?? selectedMapFeature.properties?.['source_feature_id'] ?? selectedMapFeature.id ?? 'Object')}</strong>
|
||||
@@ -971,10 +1192,12 @@ export function MapWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="geo-result-actions">
|
||||
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeSelection}>Download GeoJSON</button>
|
||||
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeSelection}>Kopieer gegevens</button>
|
||||
</div>
|
||||
{analysisMode === 'current' ? (
|
||||
<div className="geo-result-actions">
|
||||
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeSelection}>Download GeoJSON</button>
|
||||
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeSelection}>Kopieer gegevens</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
@@ -982,7 +1205,14 @@ export function MapWorkspace({
|
||||
|
||||
<footer className="geo-explorer-footer">
|
||||
<span><strong>Werkgebied:</strong> {selectedMapArea?.name ?? 'Geen gemeentegrens geselecteerd'}</span>
|
||||
<span><strong>Bron:</strong> {activeThemeDataset ? `${activeThemeDataset.source_name ?? activeThemeDataset.source} · ${activeThemeDataset.name}` : 'niet beschikbaar'}</span>
|
||||
<span>
|
||||
<strong>Bron:</strong>{' '}
|
||||
{analysisMode === 'evolution'
|
||||
? activeTemporalSeries[0]?.temporal_series_key ?? 'geen vergelijkbare tijdreeks'
|
||||
: activeThemeDataset
|
||||
? `${activeThemeDataset.source_name ?? activeThemeDataset.source} · ${activeThemeDataset.name}`
|
||||
: 'niet beschikbaar'}
|
||||
</span>
|
||||
{usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null}
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user