Broaden official source portfolio
This commit is contained in:
@@ -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 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 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>
|
||||
|
||||
Reference in New Issue
Block a user