feat: add official modern Mol land-use series
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 17:51:24 +02:00
parent 8f27c5cc6f
commit b35f135fea
14 changed files with 1199 additions and 20 deletions
+67 -12
View File
@@ -7,6 +7,7 @@ import { useTemporalComparison } from '../../hooks/useTemporalComparison'
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
const EMPTY_TEMPORAL_SERIES: DatasetCreateResponse[] = []
type DataThemeId = 'buildings' | 'population' | 'forest' | 'water' | 'roads' | 'parcels'
@@ -18,6 +19,12 @@ interface DataTheme {
tokens: string[]
}
interface TemporalSeriesGroup {
key: string
label: string
items: DatasetCreateResponse[]
}
const DATA_THEMES: DataTheme[] = [
{
id: 'buildings',
@@ -90,6 +97,7 @@ function pickThemeDataset(datasets: DatasetCreateResponse[], theme: DataTheme):
const score = (dataset: DatasetCreateResponse) =>
(dataset.reference_layer_name && theme.tokens.includes(dataset.reference_layer_name.toLowerCase()) ? 1_000_000 : 0) +
(dataset.source_name === 'grb' ? 100_000 : 0) +
(dataset.source_name === 'department_omgeving_land_use' ? 90_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)
@@ -98,7 +106,21 @@ function pickThemeDataset(datasets: DatasetCreateResponse[], theme: DataTheme):
return candidates[0] ?? null
}
function pickThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataTheme): DatasetCreateResponse[] {
function temporalSeriesLabel(items: DatasetCreateResponse[]): string {
const configuredLabel = items.find((item) => typeof item.source_metadata?.['temporal_series_label'] === 'string')
?.source_metadata?.['temporal_series_label']
if (typeof configuredLabel === 'string' && configuredLabel.trim()) {
return configuredLabel
}
const first = items[0]
const source = first?.source_name ?? first?.source ?? 'Tijdreeks'
const firstYear = first?.observed_at ? new Date(first.observed_at).getUTCFullYear() : null
const last = items[items.length - 1]
const lastYear = last?.observed_at ? new Date(last.observed_at).getUTCFullYear() : null
return firstYear && lastYear ? `${source} (${firstYear}-${lastYear})` : source
}
function listThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataTheme): TemporalSeriesGroup[] {
const groups = new Map<string, DatasetCreateResponse[]>()
for (const dataset of datasets) {
if (!datasetMatchesTheme(dataset, theme) || !dataset.temporal_series_key || !dataset.observed_at) {
@@ -108,16 +130,21 @@ function pickThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataT
items.push(dataset)
groups.set(dataset.temporal_series_key, items)
}
return Array.from(groups.values())
.filter((items) => items.length >= 2)
return Array.from(groups.entries())
.filter(([, items]) => items.length >= 2)
.map(([key, items]) => {
const ordered = [...items].sort(
(left, right) => new Date(left.observed_at ?? 0).getTime() - new Date(right.observed_at ?? 0).getTime(),
)
return { key, label: temporalSeriesLabel(ordered), items: ordered }
})
.sort((left, right) => {
if (right.length !== left.length) {
return right.length - left.length
if (right.items.length !== left.items.length) {
return right.items.length - left.items.length
}
const latest = (items: DatasetCreateResponse[]) => Math.max(...items.map((item) => new Date(item.observed_at ?? 0).getTime()))
const latest = (group: TemporalSeriesGroup) => Math.max(...group.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 {
@@ -496,6 +523,7 @@ export function MapWorkspace({
clearTemporalComparison,
} = useTemporalComparison(selectedProjectId)
const [analysisMode, setAnalysisMode] = useState<'current' | 'evolution'>('current')
const [selectedTemporalSeriesKey, setSelectedTemporalSeriesKey] = useState('')
const [earlierDatasetId, setEarlierDatasetId] = useState('')
const [laterDatasetId, setLaterDatasetId] = useState('')
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
@@ -536,10 +564,13 @@ 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),
const activeTemporalSeriesGroups = useMemo(
() => listThemeTemporalSeries(availableMapDatasets, activeTheme),
[activeTheme, availableMapDatasets],
)
const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey)
?? activeTemporalSeriesGroups[0]
const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES
const themeResults = useMemo(
() =>
themeInsights.flatMap((insight) => {
@@ -592,6 +623,14 @@ export function MapWorkspace({
setBboxInput(bboxToInputState(mapSelectionBbox))
}, [mapSelectionBbox])
useEffect(() => {
setSelectedTemporalSeriesKey((current) =>
activeTemporalSeriesGroups.some((group) => group.key === current)
? current
: activeTemporalSeriesGroups[0]?.key ?? '',
)
}, [activeTemporalSeriesGroups])
useEffect(() => {
const first = activeTemporalSeries[0]
const last = activeTemporalSeries[activeTemporalSeries.length - 1]
@@ -912,7 +951,7 @@ export function MapWorkspace({
<span>{analysisMode === 'evolution' ? 'Tijdreeks' : 'Actieve bron'}</span>
<strong>
{analysisMode === 'evolution'
? activeTemporalSeries[0]?.temporal_series_key ?? 'Geen tijdreeks beschikbaar'
? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar'
: activeThemeDataset?.name ?? 'Geen databron beschikbaar'}
</strong>
<small>
@@ -928,6 +967,22 @@ export function MapWorkspace({
{analysisMode === 'evolution' ? (
<div className="geo-time-controls" aria-label="Meetmomenten vergelijken">
{activeTemporalSeriesGroups.length > 1 ? (
<label className="geo-series-control">
Reeks
<select
value={activeTemporalSeriesGroup?.key ?? ''}
onChange={(event) => {
setSelectedTemporalSeriesKey(event.target.value)
clearTemporalComparison()
}}
>
{activeTemporalSeriesGroups.map((group) => (
<option key={group.key} value={group.key}>{group.label}</option>
))}
</select>
</label>
) : null}
<label>
Van
<select value={earlierDatasetId} onChange={(event) => { setEarlierDatasetId(event.target.value); clearTemporalComparison() }} disabled={activeTemporalSeries.length < 2}>
@@ -1208,7 +1263,7 @@ export function MapWorkspace({
<span>
<strong>Bron:</strong>{' '}
{analysisMode === 'evolution'
? activeTemporalSeries[0]?.temporal_series_key ?? 'geen vergelijkbare tijdreeks'
? activeTemporalSeriesGroup?.label ?? 'geen vergelijkbare tijdreeks'
: activeThemeDataset
? `${activeThemeDataset.source_name ?? activeThemeDataset.source} · ${activeThemeDataset.name}`
: 'niet beschikbaar'}
+4
View File
@@ -5675,6 +5675,10 @@ section {
font-size: 0.68rem;
}
.geo-time-controls .geo-series-control {
grid-column: 1 / -1;
}
.geo-time-controls button {
grid-column: 1 / -1;
min-height: 2.25rem;