Add governed DHMV terrain analysis
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-15 18:13:05 +02:00
parent 0e36d750c0
commit 5d8b46ed60
36 changed files with 1936 additions and 44 deletions
+8
View File
@@ -492,6 +492,14 @@ another municipality or the complete Kempen scope falls back to the complete
regional GRB building layer. This avoids presenting a Mol-only snapshot as
regional coverage.
When a governed DHMV Dataset is loaded, the Map explorer adds `Hoogte &
reliëf`. The active 5 m analysis raster is rendered as a colour-relief
MapLibre image over the ordinary OpenStreetMap context. A drawn rectangle or
the exact selected Area returns measured DTM/DSM height, relief and slope with
TAW and the 2013-2015 source period visible. Raster cells are not presented as
objects. GeoJSON export is disabled for this raster-only result, and the UI
explicitly states that water depth and volume cannot be derived from DHMV.
## Useful repository scripts
- `bash scripts/frontend_install.sh`
+8 -2
View File
@@ -182,7 +182,13 @@ function App(): JSX.Element {
)
const availableVectorDatasets = useMemo(() => datasets.filter((item) => isVectorDatasetType(item.dataset_type)), [datasets])
const availableMapDatasets = useMemo(
() => datasets.filter((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready'),
() => {
const vectors = datasets.filter((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready')
const terrain = datasets.filter(
(dataset) => dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv' && dataset.status === 'ready',
)
return [...vectors, ...terrain]
},
[datasets],
)
const referenceDatasets = useMemo(
@@ -1023,7 +1029,7 @@ function App(): JSX.Element {
orthophotoResult={mapOrthophotoAnalysis.lastResult}
orthophotoImageUrl={mapOrthophotoAnalysis.imageUrl}
availableMapDatasets={availableMapDatasets}
selectedMapDatasetId={selectedDataset && isVectorDatasetType(selectedDataset.dataset_type) ? selectedDataset.id : ''}
selectedMapDatasetId={selectedDataset && availableMapDatasets.some((dataset) => dataset.id === selectedDataset.id) ? selectedDataset.id : ''}
selectedFeature={selectedMapFeature}
onSelectMapArea={setSelectedMapAreaId}
onOpenDatasetInMap={openDatasetInMap}
@@ -12,6 +12,7 @@ const THEME_LABELS: Record<string, string> = {
nature_value: 'Natuurwaarde',
agriculture: 'Landbouw',
water: 'Water',
elevation: 'Hoogte en reliëf',
roads: 'Wegen en transport',
parcels: 'Percelen',
}
@@ -54,7 +55,7 @@ const AVAILABLE_SOURCES = [
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',
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',
},
{
@@ -111,6 +112,7 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
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 latestBuildingsRegister = [...buildingsRegisterDatasets].sort(
(left, right) => new Date(right.observed_at ?? 0).getTime() - new Date(left.observed_at ?? 0).getTime(),
)[0]
@@ -122,6 +124,7 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
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
return true
})
const themes = Object.keys(THEME_LABELS).map((theme) => {
@@ -181,7 +184,7 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
))}
</div>
{waterinfoDatasets.length > 0 || historicalOrthophotos.length > 0 || bwkDatasets.length > 0 || agricultureDatasets.length > 0 || buildingsRegisterDatasets.length > 0 ? (
{waterinfoDatasets.length > 0 || historicalOrthophotos.length > 0 || bwkDatasets.length > 0 || agricultureDatasets.length > 0 || buildingsRegisterDatasets.length > 0 || dhmvDatasets.length > 0 ? (
<div className="source-catalog-loaded" aria-label="Aanvullende ingeladen bronnen">
{waterinfoDatasets.length > 0 ? (
<article>
@@ -222,6 +225,13 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
<p>Registerstatus en geaggregeerde koppelingen voor Mol; adreslabels en persoonsgegevens worden niet in de kaartlaag getoond.</p>
</article>
) : null}
{dhmvDatasets.length > 0 ? (
<article>
<strong>Digitaal Hoogtemodel Vlaanderen II</strong>
<span>{dhmvDatasets.length} rasterproducten · DTM/DSM · analyse op 5 m</span>
<p>Hoogte in TAW, reliëf en helling uit opnameperiode 2013-2015. Geen waterdiepte of watervolume.</p>
</article>
) : null}
</div>
) : null}
+52 -10
View File
@@ -6,6 +6,7 @@ import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionIn
import { useTemporalComparison } from '../../hooks/useTemporalComparison'
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
import { TemporalTrendChart } from './TemporalTrendChart'
import { terrainImageUrl } from '../../lib/terrainImage'
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
@@ -13,7 +14,7 @@ const EMPTY_TEMPORAL_SERIES: DatasetCreateResponse[] = []
const MOL_PROJECT_NAME = 'Mol Municipality Workbench'
const KEMPEN_PROJECT_NAME = 'Kempen Regional Workbench'
type DataThemeId = 'buildings' | 'population' | 'forest' | 'nature_value' | 'agriculture' | 'water' | 'roads' | 'parcels'
type DataThemeId = 'buildings' | 'population' | 'forest' | 'nature_value' | 'agriculture' | 'water' | 'elevation' | 'roads' | 'parcels'
interface DataTheme {
id: DataThemeId
@@ -72,6 +73,13 @@ const DATA_THEMES: DataTheme[] = [
description: 'Waterlopen, grachten, kanalen en wateroppervlakken.',
tokens: ['waterways', 'waterway', 'water', 'hydro', 'river', 'stream', 'canal', 'waterloop'],
},
{
id: 'elevation',
label: 'Hoogte & reliëf',
shortLabel: 'Hoogte',
description: 'Maaiveld- of oppervlaktehoogte, reliëf en helling uit DHMV II.',
tokens: ['dhmv', 'elevation', 'height', 'hoogte', 'terrain', 'surface', 'dtm', 'dsm', 'reliëf'],
},
{
id: 'roads',
label: 'Wegen',
@@ -95,10 +103,19 @@ const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }>
nature_value: { fill: '#9a4f64', line: '#74364a' },
agriculture: { fill: '#7b8f32', line: '#53671d' },
water: { fill: '#2676a8', line: '#155b85' },
elevation: { fill: '#a57a4b', line: '#315f59' },
roads: { fill: '#6b7280', line: '#4b5563' },
parcels: { fill: '#a7792f', line: '#7d571f' },
}
function datasetAvailabilityLabel(dataset: DatasetCreateResponse): string {
if (dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} hoogtegrid beschikbaar`
}
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar`
}
function datasetSearchText(dataset: DatasetCreateResponse): string {
return [
dataset.name,
@@ -144,6 +161,8 @@ function pickThemeDataset(
(dataset.source_name === 'inbo_bwk_natura2000' ? 95_000 : 0) +
(dataset.source_name === 'agentschap_landbouw_zeevisserij_agricultural_parcels' ? 98_000 : 0) +
(dataset.source_name === 'digitaal_vlaanderen_buildings_addresses_register' ? 5_000_000 : 0) +
(dataset.source_name === 'digitaal_vlaanderen_dhmv' ? 5_000_000 : 0) +
(dataset.source_metadata?.['product_key'] === 'dtm_1m' ? 1_000_000 : 0) +
(dataset.dataset_role === 'reference' ? 10_000 : 0) +
(dataset.observed_at ? new Date(dataset.observed_at).getTime() / 100_000_000 : 0) +
(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0)
@@ -200,6 +219,14 @@ function formatObservationDate(value: string | null | undefined): string {
return new Intl.DateTimeFormat('nl-BE', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(value))
}
function formatDatasetObservation(dataset: DatasetCreateResponse): string {
const period = dataset.source_metadata?.['acquisition_period']
if (typeof period === 'string' && period.trim()) {
return `opnameperiode ${period}`
}
return formatObservationDate(dataset.observed_at)
}
function operationalScopeProjectLabel(project: ProjectRead): string {
if (project.name === MOL_PROJECT_NAME) {
return 'Mol'
@@ -668,6 +695,18 @@ export function MapWorkspace({
}
: null
const activeThemeDataset = themeDatasetMap[activeTheme.id]
const terrainBounds = activeThemeDataset?.source_name === 'digitaal_vlaanderen_dhmv'
? activeThemeDataset.source_metadata?.['bbox_epsg4326']
: null
const terrainImageOverlay = activeTheme.id === 'elevation' && activeThemeDataset && selectedProjectId && Array.isArray(terrainBounds) && terrainBounds.length === 4
? {
url: terrainImageUrl(selectedProjectId, activeThemeDataset.id),
bbox: terrainBounds.map(Number) as [number, number, number, number],
label: getDatasetDisplayName(activeThemeDataset),
opacity: 0.82,
}
: null
const activeImageOverlay = terrainImageOverlay ?? orthophotoImageOverlay
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
@@ -709,11 +748,14 @@ export function MapWorkspace({
const activeSupportingMetrics = (activeSelectionResult?.summary?.metrics ?? []).filter(
(metric) => metric.metric_key !== activeSelectionResult?.summary?.primary_metric_key,
)
const activeSecondaryMetric = selectedAreaSquareMetres && selectedAreaSquareMetres > 0
? activeMetricUnit === 'ha'
const terrainReliefMetric = activeSupportingMetrics.find((metric) => metric.metric_key === 'relief_m')
const activeSecondaryMetric = activeMetricUnit === 'm TAW'
? terrainReliefMetric ? `${terrainReliefMetric.metric_value.toLocaleString('nl-BE', { maximumFractionDigits: 2 })} m reliëf` : null
: selectedAreaSquareMetres && selectedAreaSquareMetres > 0
? activeMetricUnit === 'ha'
? `${((activeMetricValue * 10_000) / selectedAreaSquareMetres * 100).toLocaleString('nl-BE', { maximumFractionDigits: 1 })}% dekking`
: `${(activeMetricValue / (selectedAreaSquareMetres / 1_000_000)).toLocaleString('nl-BE', { maximumFractionDigits: 1 })} ${activeMetricUnit} / km2`
: null
: null
const selectedResultProperties = useMemo(() => {
const keys = new Map<string, Set<string>>()
for (const feature of activeSelectionResult?.geojson.features ?? []) {
@@ -1094,7 +1136,7 @@ export function MapWorkspace({
? 'Alleen huidige toestand'
: 'Bron nog niet ingeladen'
: dataset
? `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar`
? datasetAvailabilityLabel(dataset)
: 'Bron nog niet ingeladen'}
</small>
</span>
@@ -1125,7 +1167,7 @@ export function MapWorkspace({
? `${activeTemporalSeries.length} officiële meetmomenten · ${formatObservationDate(activeTemporalSeries[0].observed_at)} tot ${formatObservationDate(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at)}`
: 'Voor dit thema is nog geen tweede officieel meetmoment beschikbaar.'
: activeThemeDataset
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatObservationDate(activeThemeDataset.observed_at)}`
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}`
: activeTheme.description}
</small>
</div>
@@ -1225,7 +1267,7 @@ export function MapWorkspace({
areaData={areaFeatureCollection}
selectedFeature={selectedFeature}
selectionData={analysisMode === 'current' ? mapSelectionResult?.geojson ?? null : null}
imageOverlay={orthophotoImageOverlay}
imageOverlay={activeImageOverlay}
selectionBbox={mapSelectionBbox}
bboxSelectionMode={bboxSelectionMode}
visible={mapLayerVisible}
@@ -1241,7 +1283,7 @@ export function MapWorkspace({
/>
<div className="geo-map-legend" aria-label="Kaartlegende">
<span><i className="geo-legend-area" /> Werkgebied</span>
{orthophotoImageOverlay ? <span><i className="geo-legend-imagery" /> {orthophotoImageOverlay.label}</span> : null}
{activeImageOverlay ? <span><i className="geo-legend-imagery" /> {activeImageOverlay.label}</span> : null}
{analysisOverlayActive ? (
<>
<span><i className="geo-legend-layer geo-legend-layer-buildings" /> AI-kandidaten</span>
@@ -1444,7 +1486,7 @@ export function MapWorkspace({
<strong>{activeSelectionResult ? resultMetricLabel(activeSelectionResult) : 'Geen resultaat'}</strong>
</div>
<div>
<span>{activeMetricUnit === 'ha' ? 'Aandeel selectie' : 'Dichtheid'}</span>
<span>{activeMetricUnit === 'ha' ? 'Aandeel selectie' : activeMetricUnit === 'm TAW' ? 'Reliëf' : 'Dichtheid'}</span>
<strong>{activeSecondaryMetric ?? (selectedDensity === null ? 'n.v.t.' : `${selectedDensity.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} / km2`)}</strong>
</div>
</div>
@@ -1517,7 +1559,7 @@ export function MapWorkspace({
{analysisMode === 'current' ? (
<div className="geo-result-actions">
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeSelection}>Download GeoJSON</button>
<button className="secondary-action" disabled={!activeSelectionResult || activeTheme.id === 'elevation'} type="button" onClick={downloadActiveThemeSelection}>Download GeoJSON</button>
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeSelection}>Kopieer gegevens</button>
</div>
) : null}
+14 -7
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { datasetsApi } from '../services/api'
import { formatError } from '../lib/formatError'
import type { DatasetCreateResponse, VectorSelectionBBox, VectorSelectionResponse } from '../types'
import { terrainSelectionToMapSelection } from '../lib/terrainSelection'
interface MapSelectionExtractOptions {
selectedProjectId: string | null
@@ -38,8 +39,9 @@ export function useMapSelectionExtract({
setMapSelectionError('Open a vector dataset before extracting a map area.')
return null
}
if (!isVectorDatasetType(selectedDataset.dataset_type)) {
setMapSelectionError('Area extraction requires an active vector dataset.')
const terrainDataset = selectedDataset.dataset_type === 'raster' && selectedDataset.source_name === 'digitaal_vlaanderen_dhmv'
if (!isVectorDatasetType(selectedDataset.dataset_type) && !terrainDataset) {
setMapSelectionError('Gebiedsanalyse ondersteunt een vectorlaag of een beheerd DHMV-hoogtemodel.')
return null
}
@@ -49,11 +51,16 @@ export function useMapSelectionExtract({
setMapSelectionError(null)
setMapSelectionBbox(bbox)
try {
const response = await datasetsApi.selectVectorFeatures(selectedProjectId, selectedDataset.id, {
bbox: { ...bbox, crs: 'EPSG:4326' },
area_id: areaId,
limit: 1000,
})
const response = terrainDataset
? terrainSelectionToMapSelection(await datasetsApi.selectTerrain(selectedProjectId, selectedDataset.id, {
bbox: { ...bbox, crs: 'EPSG:4326' },
area_id: areaId,
}))
: await datasetsApi.selectVectorFeatures(selectedProjectId, selectedDataset.id, {
bbox: { ...bbox, crs: 'EPSG:4326' },
area_id: areaId,
limit: 1000,
})
if (requestSequence.current !== sequence) {
return null
}
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { formatError } from '../lib/formatError'
import { datasetsApi } from '../services/api/datasets'
import type { DatasetCreateResponse, VectorSelectionBBox, VectorSelectionResponse } from '../types'
import { terrainSelectionToMapSelection } from '../lib/terrainSelection'
export interface MapThemeQuery<TThemeId extends string> {
themeId: TThemeId
@@ -54,11 +55,16 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
queries.map(async ({ themeId, dataset }) => ({
themeId,
dataset,
result: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
limit: 1000,
}),
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
? terrainSelectionToMapSelection(await datasetsApi.selectTerrain(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
}))
: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
limit: 1000,
}),
})),
)
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
+6
View File
@@ -9,6 +9,7 @@ const DATASET_LABEL_BY_LAYER: Record<string, string> = {
forest: 'Bos en groen',
nature_value: 'Natuurwaarde',
agriculture: 'Landbouwgebruikspercelen',
elevation: 'Hoogte en reliëf',
building_registry: 'Gebouwenregister',
regional_boundary: 'Grens vervoerregio Kempen',
municipality_boundaries: 'Gemeentegrenzen Kempen',
@@ -19,6 +20,7 @@ const DATASET_SOURCE_LABELS: Record<string, string> = {
agentschap_landbouw_zeevisserij_agricultural_parcels: 'Agentschap Landbouw en Zeevisserij',
digitaal_vlaanderen_orthophoto: 'Digitaal Vlaanderen',
digitaal_vlaanderen_buildings_addresses_register: 'Digitaal Vlaanderen',
digitaal_vlaanderen_dhmv: 'Digitaal Vlaanderen',
grb: 'GRB',
historical_landuse: 'Digitaal Vlaanderen',
inbo_bwk_natura2000: 'INBO',
@@ -33,6 +35,10 @@ export function getDatasetSourceDisplayName(dataset: DatasetCreateResponse): str
}
export function getDatasetDisplayName(dataset: DatasetCreateResponse): string {
if (dataset.source_name === 'digitaal_vlaanderen_dhmv') {
const productName = dataset.source_metadata?.['product_display_name']
return typeof productName === 'string' && productName.trim() ? productName : 'DHMV II hoogtemodel'
}
const layer = (dataset.reference_layer_name ?? dataset.source_metadata?.layer_name ?? dataset.source_metadata?.layer_type ?? '')
.toString()
.toLowerCase()
+3
View File
@@ -0,0 +1,3 @@
export function terrainImageUrl(projectId: string, datasetId: string): string {
return `/api/v1/projects/${projectId}/datasets/${datasetId}/raster/terrain/image`
}
+23
View File
@@ -0,0 +1,23 @@
import type { TerrainSelectionResponse, VectorSelectionResponse } from '../types'
export function terrainSelectionToMapSelection(result: TerrainSelectionResponse): VectorSelectionResponse {
return {
selection_bbox: result.selection_bbox,
selection_area_id: result.selection_area_id,
feature_count: 0,
total_feature_count: 0,
limit: 0,
truncated: false,
geojson: { type: 'FeatureCollection', features: [] },
summary: {
...result.summary,
feature_count: result.sample_count,
is_estimate: false,
warning: result.limitation_message,
metrics: result.summary.metrics.map((metric) => ({
...metric,
is_estimate: false,
})),
},
}
}
+13
View File
@@ -18,6 +18,9 @@ import type {
RasterNdbiRequest,
OrthophotoAcquireRequest,
OrthophotoProductRead,
DhmvAcquireRequest,
DhmvProductRead,
TerrainSelectionResponse,
} from '../../types'
const DATASET_PAGE_SIZE = 200
@@ -115,6 +118,16 @@ export const datasetsApi = {
apiGet<{ items: OrthophotoProductRead[]; total: number }>(`/api/v1/projects/${projectId}/datasets/orthophoto/products`),
orthophotoImageUrl: (projectId: string, datasetId: string): string =>
`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/image`,
acquireDhmv: (projectId: string, payload: DhmvAcquireRequest): Promise<JobRead> =>
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/dhmv/acquire`, payload),
listDhmvProducts: (projectId: string): Promise<{ items: DhmvProductRead[]; total: number }> =>
apiGet<{ items: DhmvProductRead[]; total: number }>(`/api/v1/projects/${projectId}/datasets/dhmv/products`),
selectTerrain: (
projectId: string,
datasetId: string,
payload: { bbox: VectorSelectionRequest['bbox']; area_id?: string },
): Promise<TerrainSelectionResponse> =>
apiPost<TerrainSelectionResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/terrain/select`, payload),
refreshMetadata: (projectId: string, datasetId: string): Promise<DatasetCreateResponse> =>
apiPost<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/metadata/refresh`, {}),
inspectRaster: (projectId: string, datasetId: string): Promise<RasterInspectResponse> =>
+6
View File
@@ -5756,6 +5756,7 @@ section {
.geo-theme-symbol-nature_value { background: #9a4f64; }
.geo-theme-symbol-agriculture { background: #7b8f32; }
.geo-theme-symbol-water { background: #2676a8; }
.geo-theme-symbol-elevation { background: #a57a4b; }
.geo-theme-symbol-roads { background: #6b7280; }
.geo-theme-symbol-parcels { background: #a7792f; }
@@ -5940,6 +5941,11 @@ section {
background: rgba(38, 118, 168, 0.24);
}
.geo-map-legend .geo-legend-layer-elevation {
border-color: #315f59;
background: rgba(165, 122, 75, 0.26);
}
.geo-map-legend .geo-legend-layer-roads {
border-color: #4b5563;
background: rgba(107, 114, 128, 0.24);
+46
View File
@@ -336,6 +336,52 @@ export interface OrthophotoAcquisitionResult {
limitation_message: string
}
export interface DhmvAcquireRequest {
bbox: VectorSelectionBBox
area_id?: string | null
product_key?: 'dtm_1m' | 'dsm_1m'
resolution_m?: number | null
force_refresh?: boolean
}
export interface DhmvProductRead {
key: 'dtm_1m' | 'dsm_1m'
display_name: string
surface_model: 'terrain' | 'surface'
coverage_id: string
native_resolution_m: number
source_crs: string
vertical_reference: string
acquisition_period: string
catalog_url: string
attribution: string
limitation_message: string
}
export interface TerrainSelectionResponse {
dataset_id: string
product_key: string
surface_model: 'terrain' | 'surface'
selection_bbox: VectorSelectionBBox
selection_area_id?: string | null
sample_count: number
slope_sample_count: number
coverage_ratio: number
resolution_m: number
vertical_reference: string
summary: {
metric_label: string
metric_value: number
metric_unit: string
aggregation_method: string
primary_metric_key: string
metrics: VectorSelectionMetric[]
}
unsupported_metrics: string[]
limitation_message: string
generated_at: string
}
export interface MapImageOverlay {
url: string
bbox: [number, number, number, number]