fix: make regional time series discoverable
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 05:55:12 +02:00
parent ecc9f77732
commit 3ff2d07f3c
6 changed files with 147 additions and 10 deletions
+5 -1
View File
@@ -6,7 +6,7 @@ The persisted `Kempen Regional Workbench` is the automatic operational data cont
The user-facing shell is task based: `Kaart`, `Bronnen`, `Kwaliteit`, `Beeldanalyse`, `Downloads`, `Status` and `Beheer`. Internal benchmark projects, raw dataset metadata, provider capabilities, model registry details and QA evidence remain accessible through labelled advanced disclosures instead of competing with the normal workflow.
Detection defaults to the configured local YOLO asset and automatically selects an available raster and active model asset where possible. The model registry and preflight remain honest when PyTorch, Ultralytics, a local model file or a tile manifest is unavailable. The active building profile is operational but remains review-required: its recorded coverage-aware benchmark is approximately precision 0.590, recall 0.577 and F1 0.582 over seven positive AOIs, with zero detections in the empty-background control. Another training pass is intentionally blocked until the generated false-positive and false-negative review decisions are completed.
Detection defaults to the configured local YOLO asset and automatically selects an available raster and active model asset where possible. The model registry and preflight remain honest when PyTorch, Ultralytics, a local model file or a tile manifest is unavailable. The active building profile is operational but remains review-required: its current coverage-aligned benchmark is approximately precision 0.614, recall 0.606 and F1 0.607 over seven positive AOIs, with zero detections in all three pure-empty controls. A reviewed challenger remains inactive because it produced two detections in empty Postel forest.
Map-driven building analysis uses the documented footprint-IoU `0.25` and
distinguishes model candidates from verified buildings. It shows persisted
@@ -20,6 +20,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 Evolution is opened from a current-only theme, the explorer automatically
switches to the first available persisted series. Theme cards then show either
the number and range of historical observations or `Alleen huidige toestand`,
so a single GRB snapshot can no longer strand the workflow in an empty state.
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
+49 -9
View File
@@ -613,10 +613,15 @@ export function MapWorkspace({
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
const activeTemporalSeriesGroups = useMemo(
() => listThemeTemporalSeries(availableMapDatasets, activeTheme),
[activeTheme, availableMapDatasets],
const themeTemporalSeriesMap = useMemo(
() =>
Object.fromEntries(
DATA_THEMES.map((theme) => [theme.id, listThemeTemporalSeries(availableMapDatasets, theme)]),
) as Record<DataThemeId, TemporalSeriesGroup[]>,
[availableMapDatasets],
)
const activeTemporalSeriesGroups = themeTemporalSeriesMap[activeTheme.id]
const availableEvolutionThemes = DATA_THEMES.filter((theme) => themeTemporalSeriesMap[theme.id].length > 0)
const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey)
?? activeTemporalSeriesGroups[0]
const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES
@@ -808,7 +813,10 @@ export function MapWorkspace({
}
const selectDataTheme = (theme: DataTheme) => {
const dataset = themeDatasetMap[theme.id]
const temporalGroup = themeTemporalSeriesMap[theme.id][0]
const dataset = analysisMode === 'evolution'
? temporalGroup?.items[temporalGroup.items.length - 1] ?? null
: themeDatasetMap[theme.id]
if (!dataset) {
return
}
@@ -820,6 +828,16 @@ export function MapWorkspace({
const setExplorerMode = (mode: 'current' | 'evolution') => {
setAnalysisMode(mode)
clearTemporalComparison()
if (mode !== 'evolution' || activeTemporalSeriesGroups.length > 0) {
return
}
const fallbackTheme = availableEvolutionThemes[0]
const fallbackGroup = fallbackTheme ? themeTemporalSeriesMap[fallbackTheme.id][0] : null
const fallbackDataset = fallbackGroup?.items[fallbackGroup.items.length - 1]
if (fallbackTheme && fallbackDataset) {
setActiveThemeId(fallbackTheme.id)
onOpenDatasetInMap(fallbackDataset)
}
}
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
@@ -985,11 +1003,19 @@ export function MapWorkspace({
<div className="geo-theme-list">
{DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id]
const temporalGroups = themeTemporalSeriesMap[theme.id]
const temporalGroup = temporalGroups[0]
const evolutionAvailable = temporalGroups.length > 0
const available = Boolean(dataset) && (analysisMode === 'current' || evolutionAvailable)
const active = activeThemeId === theme.id
const firstObservation = temporalGroup?.items[0]?.observed_at
const lastObservation = temporalGroup?.items[temporalGroup.items.length - 1]?.observed_at
const firstYear = firstObservation ? new Date(firstObservation).getUTCFullYear() : null
const lastYear = lastObservation ? new Date(lastObservation).getUTCFullYear() : null
return (
<button
className={active ? 'geo-theme-option geo-theme-option-active' : 'geo-theme-option'}
disabled={!dataset}
disabled={!available}
key={theme.id}
type="button"
onClick={() => selectDataTheme(theme)}
@@ -998,9 +1024,23 @@ export function MapWorkspace({
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
<span>
<strong>{theme.label}</strong>
<small>{dataset ? `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar` : 'Bron nog niet ingeladen'}</small>
<small>
{analysisMode === 'evolution'
? evolutionAvailable
? `${temporalGroup.items.length} meetmomenten${firstYear && lastYear ? ` · ${firstYear}-${lastYear}` : ''}`
: dataset
? 'Alleen huidige toestand'
: 'Bron nog niet ingeladen'
: dataset
? `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar`
: 'Bron nog niet ingeladen'}
</small>
</span>
<i>{dataset ? 'Beschikbaar' : 'Ontbreekt'}</i>
<i>
{analysisMode === 'evolution'
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
: dataset ? 'Beschikbaar' : 'Ontbreekt'}
</i>
</button>
)
})}
@@ -1012,7 +1052,7 @@ export function MapWorkspace({
{analysisOverlayActive
? mapLayerLabel
: analysisMode === 'evolution'
? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar'
? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen'
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
</strong>
<small>
@@ -1021,7 +1061,7 @@ export function MapWorkspace({
: 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.'
: 'Voor dit thema is nog geen tweede officieel meetmoment beschikbaar.'
: activeThemeDataset
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatObservationDate(activeThemeDataset.observed_at)}`
: activeTheme.description}