Broaden official source portfolio
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-16 02:04:31 +02:00
parent 8628582caa
commit fdf3881592
10 changed files with 970 additions and 197 deletions
@@ -1,5 +1,12 @@
import type { DatasetCreateResponse } from '../../types'
import { getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
import {
OFFICIAL_SOURCE_PORTFOLIO,
SOURCE_DOMAINS,
operationalSources,
sourcesForDomain,
type SourcePriority,
} from '../../lib/sourcePortfolio'
interface SourceCatalogPanelProps {
datasets: DatasetCreateResponse[]
@@ -18,65 +25,6 @@ const THEME_LABELS: Record<string, string> = {
parcels: 'Percelen',
}
const AVAILABLE_SOURCES = [
{
key: 'historical_orthophoto',
name: 'Historische orthofotos',
owner: 'Digitaal Vlaanderen',
coverage: '1971, 1979-1990, 2000-2025',
value: 'Visuele evolutie via begrensde officiële luchtbeelden',
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/orthofotomozaiek-kleinschalig-zomeropnamen',
},
{
key: 'bwk',
name: 'Biologische Waarderingskaart / Natura 2000',
owner: 'INBO',
coverage: 'Toestand 2025',
value: 'Natuurwaarde, biotopen en habitattypen',
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/biologische-waarderingskaart-en-natura-2000-habitatkaart-toestand-2025',
},
{
key: 'agriculture',
name: 'Landbouwgebruikspercelen',
owner: 'Agentschap Landbouw en Zeevisserij',
coverage: 'Jaarlijkse bestanden',
value: 'Landbouwoppervlakte, teelten en perceelevolutie',
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/open-geodata-landbouwgebruikspercelen',
},
{
key: 'buildings_register',
name: 'Gebouwen- en adressenregister',
owner: 'Digitaal Vlaanderen',
coverage: 'Continu geactualiseerd',
value: 'Gebouwstatus, levensloop en adressen als aanvulling op GRB',
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/gebouwen-en-adressenregister',
},
{
key: 'elevation',
name: 'Digitaal Hoogtemodel Vlaanderen II',
owner: 'Digitaal Vlaanderen',
coverage: 'LiDAR-opname 2013-2015, DTM/DSM 1 m en 5 m',
value: 'Hoogte, reliëf en helling; afstroming is afgeleid, geen waterdiepte',
url: 'https://www.vlaanderen.be/digitaal-vlaanderen/onze-diensten-en-platformen/earth-observation-data-science-eodas/het-digitaal-hoogtemodel/digitaal-hoogtemodel-vlaanderen-ii',
},
{
key: 'waterinfo',
name: 'Waterinfo en VMM-metingen',
owner: 'Vlaamse Milieumaatschappij',
coverage: 'Meetpunten en tijdreeksen voor waterstand, debiet en neerslag',
value: 'Hydrologische toestand; geen gebiedsdekkend watervolume zonder bodemprofiel',
url: 'https://waterinfo.vlaanderen.be/',
},
{
key: 'flood_hazard',
name: 'Overstromingsgevaarkaarten waterdiepte',
owner: 'Vlaamse Milieumaatschappij',
coverage: 'Fluviaal/pluviaal, T10/T100/T1000, huidig klimaat en projectie 2050',
value: 'Gemodelleerd overstroomd oppervlak en maximale waterdiepte per vast scenario; geen bathymetrie',
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/publieke-inspire-coverage-service-van-ogrk',
},
]
function datasetTheme(dataset: DatasetCreateResponse): string | null {
const configured = String(dataset.source_metadata?.['theme'] ?? dataset.reference_layer_name ?? '').toLowerCase()
if (configured === 'built' || configured === 'building') return 'buildings'
@@ -85,6 +33,12 @@ function datasetTheme(dataset: DatasetCreateResponse): string | null {
return null
}
function priorityLabel(priority: SourcePriority): string {
if (priority === 'next') return 'Eerstvolgend'
if (priority === 'planned') return 'Daarna'
return 'Later'
}
function timelineSummary(
temporalCount: number,
temporalSeriesCount: number,
@@ -132,15 +86,23 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
)[0]
const agricultureYears = agricultureDatasets
.flatMap((dataset) => dataset.observed_at ? [new Date(dataset.observed_at).getUTCFullYear()] : [])
const pendingSources = AVAILABLE_SOURCES.filter((source) => {
if (source.key === 'waterinfo') return waterinfoDatasets.length === 0
if (source.key === 'historical_orthophoto') return historicalOrthophotos.length === 0
const availableSources = operationalSources(ready)
const availableSourceKeys = new Set(availableSources.map((source) => source.key))
const pendingSources = OFFICIAL_SOURCE_PORTFOLIO.filter((source) => {
if (source.key === 'bwk') return bwkDatasets.length === 0
if (source.key === 'agriculture') return agricultureDatasets.length === 0
if (source.key === 'buildings_register') return buildingsRegisterDatasets.length === 0
if (source.key === 'elevation') return dhmvDatasets.length === 0
if (source.key === 'flood_hazard') return floodHazardDatasets.length === 0
return true
return !availableSourceKeys.has(source.key)
})
const domains = SOURCE_DOMAINS.map((domain) => {
const sources = sourcesForDomain(domain.key)
const loaded = sources.filter((source) => availableSourceKeys.has(source.key))
const next = sources.filter((source) => source.priority === 'next' && !availableSourceKeys.has(source.key))
return {
...domain,
sourceCount: sources.length,
loadedCount: loaded.length,
nextCount: next.length,
loadedNames: loaded.map((source) => source.name),
}
})
const themes = Object.keys(THEME_LABELS).map((theme) => {
const matches = ready.filter((dataset) => datasetTheme(dataset) === theme)
@@ -174,33 +136,58 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
<div className="panel-title-row">
<div>
<span className="section-kicker">Broninventaris</span>
<h2>Wat is werkelijk beschikbaar?</h2>
<p className="muted">Ingeladen bronnen staan direct klaar voor de kaart. Andere officiële bronnen worden pas gebruikt na een gecontroleerde import.</p>
<h2>Welke vragen kan GeoIntel beantwoorden?</h2>
<p className="muted">Zes domeinen vormen samen het platform. Alleen werkelijk ingeladen bronnen krijgen de status operationeel; de rest blijft een transparante aansluitplanning.</p>
</div>
<span className="status-badge status-badge-ready">{ready.length} datasets klaar</span>
<span className="status-badge status-badge-ready">{availableSources.length} officiële bronnen operationeel</span>
</div>
<div className="source-catalog-grid">
{themes.map((theme) => (
<article className="source-catalog-card" key={theme.theme}>
<div className="source-domain-grid" aria-label="Platformbrede brondomeinen">
{domains.map((domain) => (
<article className="source-domain-card" key={domain.key}>
<div>
<strong>{theme.label}</strong>
<span>{theme.source ?? 'Nog niet ingeladen'}</span>
<strong>{domain.label}</strong>
<span>{domain.loadedCount} van {domain.sourceCount} bronnen operationeel</span>
</div>
<b>{theme.datasetCount > 0 ? 'Beschikbaar' : 'Ontbreekt'}</b>
<p>{timelineSummary(
theme.temporalCount,
theme.temporalSeriesCount,
theme.otherMethodCount,
theme.firstYear,
theme.lastYear,
)}</p>
<b className={domain.loadedCount > 0 ? 'source-status-operational' : 'source-status-planned'}>
{domain.loadedCount > 0 ? 'Operationeel' : 'Aansluiting nodig'}
</b>
<p>{domain.question}</p>
<small>
{domain.loadedNames.length > 0
? domain.loadedNames.slice(0, 2).join(' · ')
: `${domain.nextCount} geverifieerde bron${domain.nextCount === 1 ? '' : 'nen'} eerstvolgend`}
</small>
</article>
))}
</div>
<details className="source-theme-coverage">
<summary>Dekking per kaartthema en tijdreeks</summary>
<div className="source-catalog-grid">
{themes.map((theme) => (
<article className="source-catalog-card" key={theme.theme}>
<div>
<strong>{theme.label}</strong>
<span>{theme.source ?? 'Nog niet ingeladen'}</span>
</div>
<b>{theme.datasetCount > 0 ? 'Beschikbaar' : 'Ontbreekt'}</b>
<p>{timelineSummary(
theme.temporalCount,
theme.temporalSeriesCount,
theme.otherMethodCount,
theme.firstYear,
theme.lastYear,
)}</p>
</article>
))}
</div>
</details>
{waterinfoDatasets.length > 0 || historicalOrthophotos.length > 0 || bwkDatasets.length > 0 || agricultureDatasets.length > 0 || buildingsRegisterDatasets.length > 0 || dhmvDatasets.length > 0 || floodHazardDatasets.length > 0 ? (
<div className="source-catalog-loaded" aria-label="Aanvullende ingeladen bronnen">
<details className="source-loaded-disclosure">
<summary>Actieve broncollecties en hun beperkingen</summary>
<div className="source-catalog-loaded" aria-label="Aanvullende ingeladen bronnen">
{waterinfoDatasets.length > 0 ? (
<article>
<strong>Waterinfo meetreeksen</strong>
@@ -257,22 +244,39 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
<p>Gemodelleerde maximumdiepte per kansscenario. Geen actuele waterstand, bathymetrie of permanent watervolume.</p>
</article>
) : null}
</div>
</div>
</details>
) : null}
<details className="source-opportunity-list">
<summary>Officiële bronnen die hierna kunnen worden ingeladen</summary>
<div>
{pendingSources.map((source) => (
<article key={source.name}>
<div>
<strong>{source.name}</strong>
<span>{source.owner} · {source.coverage}</span>
</div>
<p>{source.value}</p>
<a href={source.url} target="_blank" rel="noreferrer">Bekijk officiële bron</a>
</article>
))}
<div className="source-opportunity-domains">
{SOURCE_DOMAINS.map((domain) => {
const sources = pendingSources.filter((source) => source.domain === domain.key)
if (sources.length === 0) return null
return (
<section className="source-opportunity-domain" key={domain.key}>
<header>
<strong>{domain.label}</strong>
<span>{sources.length} gecontroleerde kandidaatbron{sources.length === 1 ? '' : 'nen'}</span>
</header>
<div>
{sources.map((source) => (
<article key={source.name}>
<div>
<strong>{source.name}</strong>
<span>{source.owner} · {source.coverage}</span>
</div>
<span className={`source-priority source-priority-${source.priority}`}>{priorityLabel(source.priority)}</span>
<p>{source.value}</p>
<small>Meetbaar als: {source.metricExamples}</small>
<a href={source.url} target="_blank" rel="noreferrer">Bekijk officiële bron</a>
</article>
))}
</div>
</section>
)
})}
</div>
</details>
</section>