205 lines
8.4 KiB
TypeScript
205 lines
8.4 KiB
TypeScript
import type { DatasetCreateResponse } from '../../types'
|
||
|
||
interface SourceCatalogPanelProps {
|
||
datasets: DatasetCreateResponse[]
|
||
}
|
||
|
||
const THEME_LABELS: Record<string, string> = {
|
||
buildings: 'Bebouwing',
|
||
population: 'Bevolking',
|
||
forest: 'Bos',
|
||
water: 'Water',
|
||
roads: 'Wegen en transport',
|
||
parcels: 'Percelen',
|
||
}
|
||
|
||
const AVAILABLE_SOURCES = [
|
||
{
|
||
key: 'historical_orthophoto',
|
||
name: 'Historische orthofoto’s',
|
||
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, helling en afstroming; 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/',
|
||
},
|
||
]
|
||
|
||
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 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(' · ')
|
||
}
|
||
|
||
export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.Element {
|
||
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 pendingSources = AVAILABLE_SOURCES.filter((source) => {
|
||
if (source.key === 'waterinfo') return waterinfoDatasets.length === 0
|
||
if (source.key === 'historical_orthophoto') return historicalOrthophotos.length === 0
|
||
return true
|
||
})
|
||
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<string, DatasetCreateResponse[]>()
|
||
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?.source_name ?? latest?.source ?? null,
|
||
}
|
||
})
|
||
|
||
return (
|
||
<section className="workspace-panel source-catalog-panel" aria-label="Beschikbare databronnen">
|
||
<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>
|
||
</div>
|
||
<span className="status-badge status-badge-ready">{ready.length} datasets klaar</span>
|
||
</div>
|
||
|
||
<div className="source-catalog-grid">
|
||
{themes.map((theme) => (
|
||
<article className="source-catalog-card" key={theme.theme}>
|
||
<div>
|
||
<strong>{theme.label}</strong>
|
||
<span>{theme.source ? theme.source.replaceAll('_', ' ') : '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>
|
||
|
||
{waterinfoDatasets.length > 0 || historicalOrthophotos.length > 0 ? (
|
||
<div className="source-catalog-loaded" aria-label="Aanvullende ingeladen bronnen">
|
||
{waterinfoDatasets.length > 0 ? (
|
||
<article>
|
||
<strong>Waterinfo meetreeksen</strong>
|
||
<span>{new Set(waterinfoDatasets.map((dataset) => dataset.temporal_series_key).filter(Boolean)).size} stationsreeksen · {waterinfoDatasets.length} jaarmetingen</span>
|
||
<p>Puntmetingen blijven afzonderlijk per station en worden niet als gebiedsgemiddelde of watervolume voorgesteld.</p>
|
||
</article>
|
||
) : null}
|
||
{historicalOrthophotos.length > 0 ? (
|
||
<article>
|
||
<strong>Historische luchtbeelden</strong>
|
||
<span>{historicalOrthophotos.length} begrensde kaartselecties bewaard</span>
|
||
<p>Officiële jaargangen en periodes zijn via de kaart beschikbaar zonder actuele GRB-validatie.</p>
|
||
</article>
|
||
) : null}
|
||
</div>
|
||
) : 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>
|
||
</details>
|
||
</section>
|
||
)
|
||
}
|