feat(scope): make Belgium and North Sea operational default
This commit is contained in:
@@ -842,7 +842,7 @@ function App(): JSX.Element {
|
||||
>
|
||||
{activeWorkspace !== 'map' ? <div className="workspace-heading">
|
||||
<div className="workspace-heading-copy">
|
||||
<p className="eyebrow">{selectedProject?.region ?? 'Mol, Kempen'}</p>
|
||||
<p className="eyebrow">{selectedProject?.region ?? 'Belgie en Belgische Noordzee'}</p>
|
||||
<h2>{activeWorkspaceItem.label}</h2>
|
||||
</div>
|
||||
<div className="workspace-heading-actions">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import { PRIMARY_FOCUS_CENTER } from '../config/primaryFocus'
|
||||
import { NATIONAL_MAP_CENTER, NATIONAL_MAP_ZOOM } from '../config/primaryFocus'
|
||||
import { featureCollectionBounds } from '../lib/geojsonBounds'
|
||||
import type { MapImageOverlay, MapViewportState, VectorSelectionBBox } from '../types'
|
||||
|
||||
@@ -221,8 +221,8 @@ function GeoMap({
|
||||
const map = new maplibregl.Map({
|
||||
container: containerRef.current,
|
||||
style: defaultMapStyle(),
|
||||
center: PRIMARY_FOCUS_CENTER,
|
||||
zoom: 11,
|
||||
center: NATIONAL_MAP_CENTER,
|
||||
zoom: NATIONAL_MAP_ZOOM,
|
||||
attributionControl: false,
|
||||
})
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
|
||||
@@ -289,7 +289,7 @@ export function SourceCatalogPanel({
|
||||
{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
|
||||
</span>
|
||||
<p>Registerstatus en geaggregeerde koppelingen voor Mol; adreslabels en persoonsgegevens worden niet in de kaartlaag getoond.</p>
|
||||
<p>Registerstatus en geaggregeerde koppelingen binnen de werkelijk ingeladen dekking; adreslabels en persoonsgegevens worden niet in de kaartlaag getoond.</p>
|
||||
</article>
|
||||
) : null}
|
||||
{dhmvDatasets.length > 0 ? (
|
||||
|
||||
@@ -297,9 +297,9 @@ export function DetectionLab({
|
||||
<p>{yoloRuntimeReady ? `${yoloPreflight?.runtime.cuda_available ? 'GPU' : 'CPU'} · lokaal model gevonden` : 'Controleer de modelconfiguratie onder beheer.'}</p>
|
||||
</div>
|
||||
<div className="ai-user-summary-card">
|
||||
<span>Gevalideerde kwaliteit</span>
|
||||
<span>Validatiescope</span>
|
||||
<strong>{selectedOperatorProfile ? `F1 ${selectedOperatorProfile.f1.toFixed(3)}` : 'Nog niet gekoppeld'}</strong>
|
||||
<p>{selectedOperatorProfile ? `${selectedOperatorProfile.positiveSampleCount} testgebieden · resultaten blijven controleplichtig` : 'Kies het goedgekeurde lokale profiel.'}</p>
|
||||
<p>{selectedOperatorProfile ? selectedOperatorProfile.validationScope : 'Kies een modelprofiel met gedocumenteerd evaluatiebewijs.'}</p>
|
||||
</div>
|
||||
<div className={rasterDatasets.length > 0 ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}>
|
||||
<span>Beschikbare luchtbeelden</span>
|
||||
@@ -310,6 +310,15 @@ export function DetectionLab({
|
||||
<p className="ai-quality-guidance">
|
||||
{detectionQualityInterpretation(selectedOperatorProfile?.f1)}
|
||||
</p>
|
||||
{selectedOperatorProfile && !selectedOperatorProfile.nationallyValidated ? (
|
||||
<div className="result-state result-state-warning" role="status">
|
||||
<strong>Nog niet nationaal gevalideerd</strong>
|
||||
<p>
|
||||
Dit model is operationeel voor gecontroleerde beeldanalyse, maar de gemeten kwaliteit geldt alleen voor {selectedOperatorProfile.validationScope}.
|
||||
Resultaten elders in Belgie of op zee vereisen lokale referentiedata en QA voordat ze als betrouwbaar kunnen worden vrijgegeven.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DetectionModelManagement
|
||||
detectionModels={detectionModels}
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface DetectionOperatorProfile {
|
||||
f1: number
|
||||
positiveSampleCount: number
|
||||
maxBackgroundDetections: number
|
||||
validationScope: string
|
||||
nationallyValidated: boolean
|
||||
description: string
|
||||
limitationMessage: string
|
||||
}
|
||||
@@ -27,6 +29,8 @@ export const DETECTION_OPERATOR_PROFILES: DetectionOperatorProfile[] = [
|
||||
f1: 0.6068607646002744,
|
||||
positiveSampleCount: 7,
|
||||
maxBackgroundDetections: 0,
|
||||
validationScope: '7 onafhankelijke testgebieden in Mol en de Kempen',
|
||||
nationallyValidated: false,
|
||||
description:
|
||||
'Aanbevolen profiel met een evenwicht tussen gevonden en gemiste kleine gebouwen, opnieuw gemeten over zeven onafhankelijke testgebieden in Mol en de Kempen.',
|
||||
limitationMessage:
|
||||
@@ -44,6 +48,8 @@ export const DETECTION_OPERATOR_PROFILES: DetectionOperatorProfile[] = [
|
||||
f1: 0.5432865390636915,
|
||||
positiveSampleCount: 7,
|
||||
maxBackgroundDetections: 0,
|
||||
validationScope: '7 onafhankelijke testgebieden in Mol en de Kempen',
|
||||
nationallyValidated: false,
|
||||
description: 'Voorgaand profiel voor controles waarbij minder foutieve vondsten belangrijker zijn dan maximale dekking.',
|
||||
limitationMessage:
|
||||
'De lege-achtergrondtest is geslaagd. Dit profiel vindt minder onterechte objecten, maar mist meer kleine gebouwen dan het aanbevolen profiel.',
|
||||
@@ -60,6 +66,8 @@ export const DETECTION_OPERATOR_PROFILES: DetectionOperatorProfile[] = [
|
||||
f1: 0.32086574003576274,
|
||||
positiveSampleCount: 7,
|
||||
maxBackgroundDetections: 0,
|
||||
validationScope: '7 onafhankelijke testgebieden in Mol en de Kempen',
|
||||
nationallyValidated: false,
|
||||
description: 'Profiel met hoge precisie voor controles waarbij zo weinig mogelijk foutieve vondsten zwaarder wegen dan volledige dekking.',
|
||||
limitationMessage:
|
||||
'Goedgekeurd na de lege-achtergrondtest. Resultaten in dun bebouwde context blijven altijd controlebewijs en geen automatische waarheid.',
|
||||
|
||||
@@ -178,7 +178,7 @@ const DATA_THEMES: DataTheme[] = [
|
||||
id: 'soil',
|
||||
label: 'Bodem',
|
||||
shortLabel: 'Bodemkaart',
|
||||
description: 'Historische DOV-bodemkartering met bodemtype, textuur en drainageklasse voor Mol.',
|
||||
description: 'Officiele bodemkartering met bodemtype, textuur en drainageklasse waar de geselecteerde zone door een gekoppelde bron wordt gedekt.',
|
||||
tokens: ['soil', 'bodem', 'bodemkaart', 'bodemtype', 'dov_soil_map'],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,9 @@ import type { AreaRead, ProjectRead } from '../../types'
|
||||
const AREA_CATALOG_PAGE_SIZE = 12
|
||||
|
||||
function projectDisplayName(project: ProjectRead | null): string {
|
||||
if (project?.name === 'Belgium and North Sea Workbench') {
|
||||
return 'Belgie en Belgische Noordzee'
|
||||
}
|
||||
if (project?.name === 'Kempen Regional Workbench') {
|
||||
return 'Kempen · volledige regio'
|
||||
}
|
||||
@@ -81,7 +84,7 @@ export function AreaPanel({
|
||||
<div className="data-selection-summary data-selection-summary-area">
|
||||
<span>Getoond op de kaart</span>
|
||||
<strong>{selectedArea?.name ?? 'Geen gebied geselecteerd'}</strong>
|
||||
<small>{selectedArea?.area_m2 ? `${(selectedArea.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Kies een gemeente of de volledige regio op de kaart.'}</small>
|
||||
<small>{selectedArea?.area_m2 ? `${(selectedArea.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Kies een gemeente, gewest, zeezone of teken een eigen selectie.'}</small>
|
||||
</div>
|
||||
|
||||
<details className="data-panel-form-block">
|
||||
@@ -136,7 +139,7 @@ export function AreaPanel({
|
||||
setSearchQuery(event.target.value)
|
||||
setPage(1)
|
||||
}}
|
||||
placeholder="Bijvoorbeeld Mol"
|
||||
placeholder="Bijvoorbeeld Brussel, Namen of Noordzee"
|
||||
/>
|
||||
</label>
|
||||
<div className="catalog-pagination" aria-label="Paginering van gebieden">
|
||||
|
||||
@@ -2,19 +2,23 @@ import type { FormEvent } from 'react'
|
||||
import type { ProjectCreate, ProjectRead } from '../../types'
|
||||
|
||||
const TECHNICAL_PROJECT_PATTERN = /^(GeoIntel Detection Quality Matrix|GeoIntel hard-negative|GeoIntel training|Mol Building QA)/i
|
||||
const NATIONAL_PROJECT_NAME = 'Belgium and North Sea Workbench'
|
||||
const REGIONAL_PROJECT_NAME = 'Kempen Regional Workbench'
|
||||
const LEGACY_MOL_PROJECT_NAME = 'Mol Municipality Workbench'
|
||||
const PROTECTED_PROJECT_NAMES = new Set([REGIONAL_PROJECT_NAME, LEGACY_MOL_PROJECT_NAME])
|
||||
const PROTECTED_PROJECT_NAMES = new Set([NATIONAL_PROJECT_NAME, REGIONAL_PROJECT_NAME, LEGACY_MOL_PROJECT_NAME])
|
||||
|
||||
function isTechnicalProject(project: ProjectRead): boolean {
|
||||
return TECHNICAL_PROJECT_PATTERN.test(project.name)
|
||||
}
|
||||
|
||||
function isAdvancedProject(project: ProjectRead): boolean {
|
||||
return isTechnicalProject(project) || project.name === LEGACY_MOL_PROJECT_NAME
|
||||
return isTechnicalProject(project) || project.name === LEGACY_MOL_PROJECT_NAME || project.name === REGIONAL_PROJECT_NAME
|
||||
}
|
||||
|
||||
function projectDisplayName(project: ProjectRead): string {
|
||||
if (project.name === NATIONAL_PROJECT_NAME) {
|
||||
return 'Belgie en Belgische Noordzee'
|
||||
}
|
||||
if (project.name === REGIONAL_PROJECT_NAME) {
|
||||
return 'Kempen · volledige regionale werkruimte'
|
||||
}
|
||||
@@ -126,7 +130,7 @@ export function ProjectPanel({
|
||||
<label>
|
||||
Regio
|
||||
<input
|
||||
value={projectForm.region ?? 'Kempen'}
|
||||
value={projectForm.region ?? 'Belgie en Belgische Noordzee'}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, region: event.target.value })}
|
||||
placeholder="Regio"
|
||||
/>
|
||||
|
||||
@@ -70,6 +70,9 @@ function persistedSegmentationModelLabel(modelName: string | null | undefined):
|
||||
if (modelName === 'fixture-segmenter') return 'Testsegmentatie'
|
||||
if (modelName === 'sam-placeholder') return 'SAM-model niet geconfigureerd'
|
||||
if (modelName === 'yolo-seg-placeholder') return 'YOLO-segmentatie niet geconfigureerd'
|
||||
if (modelName === 'segmentation-placeholder') return 'Segmentatiemodel niet geconfigureerd'
|
||||
if (modelName === 'yolo-seg-configured') return 'Lokaal YOLO-segmentatiemodel'
|
||||
if (modelName === 'sam-configured') return 'Lokaal SAM-model'
|
||||
return modelName
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ export const PRIMARY_FOCUS_LABEL = 'Mol'
|
||||
export const PRIMARY_FOCUS_REGION = 'Mol, Kempen'
|
||||
export const NATIONAL_WORKSPACE_PROJECT_NAME = 'Belgium and North Sea Workbench'
|
||||
export const NATIONAL_WORKSPACE_LABEL = 'Belgie en Belgische Noordzee'
|
||||
export const NATIONAL_WORKSPACE_REGION = 'Belgie en Belgische Noordzee'
|
||||
export const NATIONAL_MAP_CENTER: [number, number] = [4.62, 50.72]
|
||||
export const NATIONAL_MAP_ZOOM = 7.25
|
||||
export const REGIONAL_WORKSPACE_PROJECT_NAME = 'Kempen Regional Workbench'
|
||||
export const REGIONAL_WORKSPACE_LABEL = 'Kempen (28 gemeenten)'
|
||||
export const FLANDERS_WORKSPACE_PROJECT_NAME = 'Flanders Regional Workbench'
|
||||
|
||||
@@ -54,7 +54,7 @@ function formatOrthophotoError(caught: unknown): string {
|
||||
return 'Maak de rechthoek maximaal 1.024 bij 1.024 meter groot.'
|
||||
}
|
||||
if (code === 'ORTHOPHOTO_SELECTION_OUTSIDE_AREA') {
|
||||
return 'Teken de rechthoek binnen de ingeladen regio Kempen.'
|
||||
return 'Teken de rechthoek binnen het ingeladen Belgische land- of zeegebied.'
|
||||
}
|
||||
if (code === 'ORTHOPHOTO_PROVIDER_UNAVAILABLE' || code === 'ORTHOPHOTO_PROVIDER_INVALID_RESPONSE') {
|
||||
return 'De officiële luchtbeeldbron is tijdelijk niet bereikbaar. Probeer later opnieuw.'
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { FormEvent, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
PRIMARY_FOCUS_AREA_GEOJSON,
|
||||
PRIMARY_FOCUS_AREA_NAME,
|
||||
PRIMARY_FOCUS_REGION,
|
||||
NATIONAL_WORKSPACE_PROJECT_NAME,
|
||||
NATIONAL_WORKSPACE_REGION,
|
||||
REGIONAL_WORKSPACE_PROJECT_NAME,
|
||||
isPrimaryFocusMunicipalityBoundaryDataset,
|
||||
isPrimaryFocusMunicipalityProject,
|
||||
isPrimaryFocusProject,
|
||||
isPrimaryFocusProjectData,
|
||||
} from '../config/primaryFocus'
|
||||
import { areasApi } from '../services/api/areas'
|
||||
import { datasetsApi } from '../services/api/datasets'
|
||||
@@ -54,11 +48,11 @@ export function useProjectWorkspace() {
|
||||
const [projectForm, setProjectForm] = useState<ProjectCreate>({
|
||||
name: '',
|
||||
description: '',
|
||||
region: PRIMARY_FOCUS_REGION,
|
||||
region: NATIONAL_WORKSPACE_REGION,
|
||||
})
|
||||
const [areaForm, setAreaForm] = useState({
|
||||
name: PRIMARY_FOCUS_AREA_NAME,
|
||||
geometry: PRIMARY_FOCUS_AREA_GEOJSON,
|
||||
name: '',
|
||||
geometry: '',
|
||||
crs: 'EPSG:4326',
|
||||
})
|
||||
|
||||
@@ -84,14 +78,7 @@ export function useProjectWorkspace() {
|
||||
}
|
||||
const nationalProject = items.find((project) => project.name === NATIONAL_WORKSPACE_PROJECT_NAME)
|
||||
if (nationalProject) {
|
||||
try {
|
||||
const data = await fetchProjectData(nationalProject.id)
|
||||
if (data.areas.length > 0 && data.datasets.some((dataset) => dataset.status === 'ready')) {
|
||||
return nationalProject.id
|
||||
}
|
||||
} catch {
|
||||
// Continue with regional and municipality fallbacks while national data is unavailable.
|
||||
}
|
||||
return nationalProject.id
|
||||
}
|
||||
const regionalProject = items.find((project) => project.name === REGIONAL_WORKSPACE_PROJECT_NAME)
|
||||
if (regionalProject) {
|
||||
@@ -104,22 +91,10 @@ export function useProjectWorkspace() {
|
||||
// Continue with the municipality and completeness fallbacks when regional data is unavailable.
|
||||
}
|
||||
}
|
||||
const municipalityProject = items.find(isPrimaryFocusMunicipalityProject)
|
||||
if (municipalityProject) {
|
||||
try {
|
||||
const data = await fetchProjectData(municipalityProject.id)
|
||||
if (data.areas.length > 0 && data.datasets.some(isPrimaryFocusMunicipalityBoundaryDataset)) {
|
||||
return municipalityProject.id
|
||||
}
|
||||
} catch {
|
||||
// Continue with the normal completeness ranking if the canonical workspace is temporarily unavailable.
|
||||
}
|
||||
}
|
||||
const primaryProjects = items.filter(isPrimaryFocusProject)
|
||||
const demoProjects = items.filter(isDemoProject)
|
||||
const candidates = Array.from(
|
||||
new Map(
|
||||
[...primaryProjects, ...demoProjects, ...items].map((project) => [project.id, project]),
|
||||
[...items, ...demoProjects].map((project) => [project.id, project]),
|
||||
).values(),
|
||||
).slice(0, 12)
|
||||
const inspectedCandidates: Array<{
|
||||
@@ -130,27 +105,17 @@ export function useProjectWorkspace() {
|
||||
try {
|
||||
const data = await fetchProjectData(project.id)
|
||||
inspectedCandidates.push({ project, data })
|
||||
if (
|
||||
isPrimaryFocusProjectData(project, data.datasets) &&
|
||||
hasMappedAnalysisContext(data)
|
||||
) {
|
||||
if (hasMappedAnalysisContext(data)) {
|
||||
return project.id
|
||||
}
|
||||
} catch {
|
||||
// Project list should still render if a candidate's detail endpoints are temporarily unavailable.
|
||||
}
|
||||
}
|
||||
const primaryContext = inspectedCandidates.find(
|
||||
({ project, data }) =>
|
||||
data.datasets.length > 0 && isPrimaryFocusProjectData(project, data.datasets),
|
||||
)
|
||||
if (primaryContext) {
|
||||
return primaryContext.project.id
|
||||
}
|
||||
const completeContext = inspectedCandidates.find(
|
||||
({ data }) => data.areas.length > 0 && data.datasets.length > 0,
|
||||
)
|
||||
return completeContext?.project.id ?? primaryProjects[0]?.id ?? demoProjects[0]?.id ?? items[0]?.id ?? null
|
||||
return completeContext?.project.id ?? demoProjects[0]?.id ?? items[0]?.id ?? null
|
||||
}
|
||||
|
||||
const loadProjects = async (preferredProjectId?: string | null) => {
|
||||
@@ -216,7 +181,7 @@ export function useProjectWorkspace() {
|
||||
const createdProject = await projectsApi.create({
|
||||
name: projectForm.name.trim(),
|
||||
description: projectForm.description?.trim() || undefined,
|
||||
region: projectForm.region?.trim() || PRIMARY_FOCUS_REGION,
|
||||
region: projectForm.region?.trim() || NATIONAL_WORKSPACE_REGION,
|
||||
})
|
||||
setProjectForm((previous) => ({ ...previous, name: '', description: '' }))
|
||||
setSelectedProjectId(createdProject.id)
|
||||
|
||||
@@ -59,8 +59,17 @@ export function useSegmentationWorkflow({
|
||||
try {
|
||||
const response = await segmentationApi.listModels()
|
||||
setSegmentationModels(response.models)
|
||||
if (!response.models.some((model) => model.model_id === selectedSegmentationModelId) && response.models.length > 0) {
|
||||
setSelectedSegmentationModelId(response.models[0].model_id)
|
||||
const selectionStillAvailable = response.models.some((model) => model.model_id === selectedSegmentationModelId)
|
||||
const selectionConfigured = response.models.some(
|
||||
(model) => model.model_id === selectedSegmentationModelId && model.configured,
|
||||
)
|
||||
if ((!selectionStillAvailable || !selectionConfigured) && response.models.length > 0) {
|
||||
const configuredModel = response.models.find(
|
||||
(model) => model.configured && model.model_id !== 'fixture-segmenter',
|
||||
)
|
||||
setSelectedSegmentationModelId(
|
||||
configuredModel?.model_id ?? (selectionStillAvailable ? selectedSegmentationModelId : response.models[0].model_id),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
setSegmentationModelError(formatError(error, 'Failed to load segmentation models'))
|
||||
|
||||
@@ -318,7 +318,7 @@ export const OFFICIAL_SOURCE_PORTFOLIO: OfficialSourceDefinition[] = [
|
||||
name: 'Gemeente in cijfers',
|
||||
owner: 'Vlaamse Milieumaatschappij',
|
||||
coverage: 'Gemeentelijke klimaat- en leefomgevingsindicatoren',
|
||||
value: 'Contextcijfers voor Mol en vergelijking met andere gemeenten.',
|
||||
value: 'Contextcijfers en vergelijking tussen gemeenten binnen de beschikbare brondekking.',
|
||||
metricExamples: 'hittegolfdagen, temperatuur en luchtkwaliteitsindex',
|
||||
priority: 'later',
|
||||
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/gemeente-in-cijfers',
|
||||
|
||||
Reference in New Issue
Block a user