import { useState } from 'react' import { datasetsApi } from '../../services/api' import type { BathymetrySourceProbeRead, 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[] projectId?: string } const THEME_LABELS: Record = { buildings: 'Bebouwing', population: 'Bevolking', forest: 'Bos', nature_value: 'Natuurwaarde', agriculture: 'Landbouw', water: 'Water', bathymetry: 'Waterbodem en dwarsprofielen', flood_hazard: 'Overstromingsgevaar', elevation: 'Hoogte en reliëf', roads: 'Wegen en transport', parcels: 'Percelen', } 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' if (configured === 'transport' || configured === 'road') return 'roads' if (configured in THEME_LABELS) return configured 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, otherMethodCount: number, firstYear: number | null, lastYear: number | null, ): string { if (temporalCount < 2 || !firstYear || !lastYear) { return 'Alleen de huidige toestand is vergelijkbaar beschikbaar.' } const seriesNote = temporalSeriesCount > 1 ? ` in ${temporalSeriesCount} afzonderlijke reeksen` : '' const methodNote = otherMethodCount > 0 ? `plus ${otherMethodCount} andere bronmethode${otherMethodCount === 1 ? '' : 'n'}` : null return [ `${temporalCount} vergelijkbare meetmomenten${seriesNote}`, `${firstYear}-${lastYear}`, methodNote, ].filter(Boolean).join(' · ') } function mdkStatusLabel(status: BathymetrySourceProbeRead['status']): string { if (status === 'reachable') return 'Service bereikbaar' if (status === 'tls_error') return 'Geblokkeerd: certificaat' if (status === 'endpoint_unavailable') return 'Service niet bereikbaar' if (status === 'invalid_capabilities') return 'Ongeldige service-informatie' if (status === 'disabled') return 'Controle uitgeschakeld' return 'Configuratie controleren' } export function SourceCatalogPanel({ datasets, projectId }: SourceCatalogPanelProps): JSX.Element { const [mdkProbe, setMdkProbe] = useState(null) const [mdkProbeLoading, setMdkProbeLoading] = useState(false) const [mdkProbeError, setMdkProbeError] = useState(null) const ready = datasets.filter((dataset) => dataset.status === 'ready') const waterinfoDatasets = ready.filter((dataset) => dataset.source_name === 'waterinfo') const historicalOrthophotos = ready.filter( (dataset) => dataset.source_name === 'digitaal_vlaanderen_orthophoto' && String(dataset.source_metadata?.['product_key'] ?? 'most_recent') !== 'most_recent', ) const bwkDatasets = ready.filter((dataset) => dataset.source_name === 'inbo_bwk_natura2000') const largestBwkSnapshot = bwkDatasets.reduce( (largest, dataset) => !largest || (dataset.feature_count ?? 0) > (largest.feature_count ?? 0) ? dataset : largest, null, ) const agricultureDatasets = ready.filter((dataset) => dataset.source_name === 'agentschap_landbouw_zeevisserij_agricultural_parcels') const buildingsRegisterDatasets = ready.filter( (dataset) => dataset.source_name === 'digitaal_vlaanderen_buildings_addresses_register', ) const dhmvDatasets = ready.filter((dataset) => dataset.source_name === 'digitaal_vlaanderen_dhmv') const floodHazardDatasets = ready.filter((dataset) => dataset.source_name === 'vmm_flood_hazard') const bathymetryProfileDatasets = ready.filter( (dataset) => dataset.source_name === 'vmm_vha_bathymetry_profiles', ) const latestBuildingsRegister = [...buildingsRegisterDatasets].sort( (left, right) => new Date(right.observed_at ?? 0).getTime() - new Date(left.observed_at ?? 0).getTime(), )[0] const agricultureYears = agricultureDatasets .flatMap((dataset) => dataset.observed_at ? [new Date(dataset.observed_at).getUTCFullYear()] : []) 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 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) const temporal = matches.filter((dataset) => dataset.temporal_series_key && dataset.observed_at) const seriesGroups = new Map() temporal.forEach((dataset) => { const seriesKey = dataset.temporal_series_key as string seriesGroups.set(seriesKey, [...(seriesGroups.get(seriesKey) ?? []), dataset]) }) const comparableSeries = [...seriesGroups.values()].filter((series) => series.length >= 2) const comparableObservations = comparableSeries.flat() const years = comparableObservations.map((dataset) => new Date(dataset.observed_at as string).getUTCFullYear()) const latest = [...matches].sort( (left, right) => new Date(right.observed_at ?? right.imported_at ?? 0).getTime() - new Date(left.observed_at ?? left.imported_at ?? 0).getTime(), )[0] return { theme, label: THEME_LABELS[theme], datasetCount: matches.length, temporalCount: comparableObservations.length, temporalSeriesCount: comparableSeries.length, otherMethodCount: matches.length - comparableObservations.length, firstYear: years.length ? Math.min(...years) : null, lastYear: years.length ? Math.max(...years) : null, source: latest ? getDatasetSourceDisplayName(latest) : null, } }) const checkMdkReadiness = async (): Promise => { if (!projectId || mdkProbeLoading) return setMdkProbeLoading(true) setMdkProbeError(null) try { setMdkProbe(await datasetsApi.probeMdkBathymetry(projectId)) } catch (error) { setMdkProbeError(error instanceof Error ? error.message : 'De Noordzee-bron kon niet worden gecontroleerd.') } finally { setMdkProbeLoading(false) } } return (
Broninventaris

Welke vragen kan GeoIntel beantwoorden?

Zes domeinen vormen samen het platform. Alleen werkelijk ingeladen bronnen krijgen de status operationeel; de rest blijft een transparante aansluitplanning.

{availableSources.length} officiële bronnen operationeel
{domains.map((domain) => (
{domain.label} {domain.loadedCount} van {domain.sourceCount} bronnen operationeel
0 ? 'source-status-operational' : 'source-status-planned'}> {domain.loadedCount > 0 ? 'Operationeel' : 'Aansluiting nodig'}

{domain.question}

{domain.loadedNames.length > 0 ? domain.loadedNames.slice(0, 2).join(' · ') : `${domain.nextCount} geverifieerde bron${domain.nextCount === 1 ? '' : 'nen'} eerstvolgend`}
))}
Dekking per kaartthema en tijdreeks
{themes.map((theme) => (
{theme.label} {theme.source ?? 'Nog niet ingeladen'}
{theme.datasetCount > 0 ? 'Beschikbaar' : 'Ontbreekt'}

{timelineSummary( theme.temporalCount, theme.temporalSeriesCount, theme.otherMethodCount, theme.firstYear, theme.lastYear, )}

))}
{waterinfoDatasets.length > 0 || historicalOrthophotos.length > 0 || bwkDatasets.length > 0 || agricultureDatasets.length > 0 || buildingsRegisterDatasets.length > 0 || dhmvDatasets.length > 0 || floodHazardDatasets.length > 0 || bathymetryProfileDatasets.length > 0 ? (
Actieve broncollecties en hun beperkingen
{waterinfoDatasets.length > 0 ? (
Waterinfo meetreeksen {new Set(waterinfoDatasets.map((dataset) => dataset.temporal_series_key).filter(Boolean)).size} stationsreeksen · {waterinfoDatasets.length} jaarmetingen

Puntmetingen blijven afzonderlijk per station en worden niet als gebiedsgemiddelde of watervolume voorgesteld.

) : null} {historicalOrthophotos.length > 0 ? (
Historische luchtbeelden {historicalOrthophotos.length} begrensde kaartselecties bewaard

Officiële jaargangen en periodes zijn via de kaart beschikbaar zonder actuele GRB-validatie.

) : null} {bwkDatasets.length > 0 ? (
BWK / Natura 2000 Toestand 2025 · {bwkDatasets.length} gebiedsdekking{bwkDatasets.length === 1 ? '' : 'en'} ·{' '} {(largestBwkSnapshot?.feature_count ?? 0).toLocaleString('nl-BE')} kaartvlakken in de grootste dekking

Biologische waardering en habitataandelen blijven afzonderlijke, brongetrouwe metingen met herkomstinformatie.

) : null} {agricultureDatasets.length > 0 ? (
Landbouwgebruikspercelen {agricultureDatasets.length} definitieve jaargangen{agricultureYears.length ? ` · ${Math.min(...agricultureYears)}-${Math.max(...agricultureYears)}` : ''}

Oppervlakte en officiële hoofdteeltgroepen zijn historisch vergelijkbaar; perceelidentiteiten blijven bewust niet gekoppeld tussen jaren.

) : null} {latestBuildingsRegister ? (
Gebouwen- en Adressenregister {(latestBuildingsRegister.feature_count ?? 0).toLocaleString('nl-BE')} gebouwen · {' '} {Number(latestBuildingsRegister.source_metadata?.['building_unit_count'] ?? 0).toLocaleString('nl-BE')} eenheden · {' '} {Number(latestBuildingsRegister.source_metadata?.['linked_address_count'] ?? 0).toLocaleString('nl-BE')} gekoppelde adressen

Registerstatus en geaggregeerde koppelingen voor Mol; adreslabels en persoonsgegevens worden niet in de kaartlaag getoond.

) : null} {dhmvDatasets.length > 0 ? (
Digitaal Hoogtemodel Vlaanderen II {dhmvDatasets.length} rasterproducten · DTM/DSM · analyse op 5 m

Hoogte in TAW, reliëf en helling uit opnameperiode 2013-2015. Geen waterdiepte of watervolume.

) : null} {floodHazardDatasets.length > 0 ? (
VMM-overstromingsgevaarkaarten {floodHazardDatasets.length} scenario's · fluviaal en pluviaal · huidig en 2050

Gemodelleerde maximumdiepte per kansscenario. Geen actuele waterstand, bathymetrie of permanent watervolume.

) : null} {bathymetryProfileDatasets.length > 0 ? (
VHA-dwarsprofielen waterbodem {bathymetryProfileDatasets.reduce( (total, dataset) => total + Number(dataset.feature_count ?? 0), 0, ).toLocaleString('nl-BE')} profielpunten · historische meetcampagnes

Meetvelden en officiële profielbladen per punt. Geen continue actuele bathymetrie of volume zonder een gelijktijdig waterpeil.

) : null}
) : null}
Maritieme bronstatus
MDK-dieptemodel Belgische Noordzee {mdkProbe ? mdkStatusLabel(mdkProbe.status) : 'Nog niet live gecontroleerd'}

{mdkProbe?.message ?? 'Controleert uitsluitend de officiële service-informatie met geldige TLS. Er wordt geen raster gedownload.'}

{mdkProbeError ? {mdkProbeError} : null}
Officiële bronnen die hierna kunnen worden ingeladen
{SOURCE_DOMAINS.map((domain) => { const sources = pendingSources.filter((source) => source.domain === domain.key) if (sources.length === 0) return null return (
{domain.label} {sources.length} gecontroleerde kandidaatbron{sources.length === 1 ? '' : 'nen'}
{sources.map((source) => (
{source.name} {source.owner} · {source.coverage}
{priorityLabel(source.priority)}

{source.value}

Meetbaar als: {source.metricExamples} Bekijk officiële bron
))}
) })}
) }