feat: add official modern Mol land-use series
This commit is contained in:
@@ -10,6 +10,10 @@ current, while `Evolution` lets the operator compare an earlier and later
|
||||
snapshot from the same series over a drawn rectangle. Results show units,
|
||||
absolute/percentage change, estimate status and source limitations.
|
||||
Added/removed/modified overlays only appear for stable source identities.
|
||||
When a theme has multiple valid methodologies, a compact series selector keeps
|
||||
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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user