feat: add map-driven orthophoto analysis
This commit is contained in:
@@ -27,6 +27,13 @@ choose-theme, draw-area, read-result flow.
|
||||
|
||||
The primary workflow is deliberately short: choose a municipality or the complete region, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count.
|
||||
|
||||
For a rectangle in `Laatste toestand`, `Herken gebouwen` runs the complete
|
||||
operational image path without opening the technical AI screen: bounded
|
||||
official orthophoto acquisition, raster persistence, safe tiling, the active
|
||||
local YOLO model, Detection persistence and automatic QA against ready GRB
|
||||
buildings. The panel shows all stages and errors; successful detections open as
|
||||
an explicit AI-result overlay. Rectangles must be 128-1,024 m per side.
|
||||
|
||||
The theme catalog currently recognizes buildings, population, forest/green, water, roads and parcels from dataset names and canonical `reference_layer_name` metadata. A theme is enabled only when a ready persisted vector dataset exists; otherwise it states `Bron nog niet ingeladen`. This prevents missing population or land-cover sources from appearing as zero-valued observations. The previous technical Map workspace remains available through `Geavanceerde werkbank` for derived datasets, QA/QC evidence and export operations.
|
||||
|
||||
The workbench uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but Map is the default product entry and Overview, Data, QA/QC, AI Labs, Exports and System remain secondary workspaces with a persistent top context bar and an optional selection-detail drawer.
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useMapSelectionDataset } from './hooks/useMapSelectionDataset'
|
||||
import { useMapSelectionQa } from './hooks/useMapSelectionQa'
|
||||
import { useMapWorkspaceState } from './hooks/useMapWorkspaceState'
|
||||
import { useMapSelectionExtract } from './hooks/useMapSelectionExtract'
|
||||
import { useMapOrthophotoAnalysis } from './hooks/useMapOrthophotoAnalysis'
|
||||
import { useProviderCapabilities } from './hooks/useProviderCapabilities'
|
||||
import { useProjectWorkspace } from './hooks/useProjectWorkspace'
|
||||
import { useQualityWorkflow } from './hooks/useQualityWorkflow'
|
||||
@@ -278,6 +279,7 @@ function App(): JSX.Element {
|
||||
runDetection,
|
||||
uploadDetectionRaster,
|
||||
prepareAndRunDetection,
|
||||
compareDetectionRunWithReference,
|
||||
runDetectionQa,
|
||||
runDetectionCalibration,
|
||||
applyDetectionOperatorProfile,
|
||||
@@ -468,6 +470,20 @@ function App(): JSX.Element {
|
||||
loadQualityChecks,
|
||||
loadProjectData,
|
||||
})
|
||||
const mapOrthophotoAnalysis = useMapOrthophotoAnalysis({
|
||||
selectedProjectId,
|
||||
selectedAreaId: selectedMapAreaId,
|
||||
datasets,
|
||||
loadProjectData,
|
||||
prepareAndRunDetection: (datasetId) => prepareAndRunDetection(datasetId, 'yolo-configured'),
|
||||
compareDetectionRunWithReference: (analysisRunId, referenceDatasetId) =>
|
||||
compareDetectionRunWithReference(analysisRunId, referenceDatasetId, false),
|
||||
onAnalysisReady: () => {
|
||||
setMapContentMode('analysis')
|
||||
setMapLayerVisible(true)
|
||||
setActiveWorkspace('map')
|
||||
},
|
||||
})
|
||||
const openMapSelectionQualityEvidence = () => {
|
||||
setActiveWorkspace('analysis')
|
||||
}
|
||||
@@ -990,6 +1006,10 @@ function App(): JSX.Element {
|
||||
mapSelectionQaError={mapSelectionQaError}
|
||||
mapSelectionQaResult={mapSelectionQaResult}
|
||||
latestMapSelectionQualityCheckId={latestMapSelectionQualityCheckId}
|
||||
orthophotoAnalysisStage={mapOrthophotoAnalysis.stage}
|
||||
orthophotoAnalysisStatus={mapOrthophotoAnalysis.status}
|
||||
orthophotoAnalysisError={mapOrthophotoAnalysis.error}
|
||||
orthophotoAnalysisRunning={mapOrthophotoAnalysis.running}
|
||||
availableMapDatasets={availableMapDatasets}
|
||||
selectedMapDatasetId={selectedDataset && isVectorDatasetType(selectedDataset.dataset_type) ? selectedDataset.id : ''}
|
||||
selectedFeature={selectedMapFeature}
|
||||
@@ -1010,6 +1030,7 @@ function App(): JSX.Element {
|
||||
onSelectMapQaReferenceDataset={setSelectedMapQaReferenceDatasetId}
|
||||
onRunMapSelectionQa={runMapSelectionQa}
|
||||
onOpenMapSelectionQualityEvidence={openMapSelectionQualityEvidence}
|
||||
onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run}
|
||||
onClearQualityEvidence={clearQualityEvidenceGeoJson}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -441,6 +441,10 @@ interface MapWorkspaceProps {
|
||||
mapSelectionQaError: string | null
|
||||
mapSelectionQaResult: QaComparisonResult | null
|
||||
latestMapSelectionQualityCheckId: string | null
|
||||
orthophotoAnalysisStage: 'idle' | 'acquiring' | 'detecting' | 'validating' | 'complete' | 'failed'
|
||||
orthophotoAnalysisStatus: string
|
||||
orthophotoAnalysisError: string | null
|
||||
orthophotoAnalysisRunning: boolean
|
||||
availableMapDatasets: DatasetCreateResponse[]
|
||||
selectedMapDatasetId: string
|
||||
onSelectMapArea: (areaId: string) => void
|
||||
@@ -460,6 +464,7 @@ interface MapWorkspaceProps {
|
||||
onSelectMapQaReferenceDataset: (datasetId: string) => void
|
||||
onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise<QaComparisonResult | null>
|
||||
onOpenMapSelectionQualityEvidence: () => void
|
||||
onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise<boolean>
|
||||
onClearQualityEvidence?: () => void
|
||||
}
|
||||
|
||||
@@ -509,6 +514,10 @@ export function MapWorkspace({
|
||||
mapSelectionQaError,
|
||||
mapSelectionQaResult,
|
||||
latestMapSelectionQualityCheckId,
|
||||
orthophotoAnalysisStage,
|
||||
orthophotoAnalysisStatus,
|
||||
orthophotoAnalysisError,
|
||||
orthophotoAnalysisRunning,
|
||||
availableMapDatasets,
|
||||
selectedMapDatasetId,
|
||||
onSelectMapArea,
|
||||
@@ -528,6 +537,7 @@ export function MapWorkspace({
|
||||
onSelectMapQaReferenceDataset,
|
||||
onRunMapSelectionQa,
|
||||
onOpenMapSelectionQualityEvidence,
|
||||
onRunOrthophotoAnalysis,
|
||||
onClearQualityEvidence,
|
||||
}: MapWorkspaceProps): JSX.Element {
|
||||
const [advancedMode, setAdvancedMode] = useState(false)
|
||||
@@ -1159,6 +1169,34 @@ export function MapWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{analysisMode === 'current' && mapSelectionBbox ? (
|
||||
<div className={`geo-image-analysis geo-image-analysis-${orthophotoAnalysisStage}`}>
|
||||
<div>
|
||||
<span>Beeldanalyse</span>
|
||||
<strong>Gebouwen herkennen op luchtbeeld</strong>
|
||||
<small>Officieel luchtbeeld, lokaal AI-model en automatische controle met GRB.</small>
|
||||
</div>
|
||||
<button
|
||||
className="primary-action"
|
||||
disabled={orthophotoAnalysisRunning}
|
||||
type="button"
|
||||
onClick={() => void onRunOrthophotoAnalysis(mapSelectionBbox)}
|
||||
>
|
||||
{orthophotoAnalysisStage === 'acquiring'
|
||||
? 'Luchtbeeld ophalen...'
|
||||
: orthophotoAnalysisStage === 'detecting'
|
||||
? 'Gebouwen herkennen...'
|
||||
: orthophotoAnalysisStage === 'validating'
|
||||
? 'Controleren...'
|
||||
: orthophotoAnalysisStage === 'complete'
|
||||
? 'Opnieuw analyseren'
|
||||
: 'Herken gebouwen'}
|
||||
</button>
|
||||
{orthophotoAnalysisStatus ? <p role="status">{orthophotoAnalysisStatus}</p> : null}
|
||||
{orthophotoAnalysisError ? <p className="error" role="alert">{orthophotoAnalysisError}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!mapSelectionBbox ? (
|
||||
<div className="geo-results-empty">
|
||||
<strong>Nog geen gebied geselecteerd</strong>
|
||||
|
||||
@@ -229,12 +229,18 @@ export function useDetectionWorkflow({
|
||||
}
|
||||
}
|
||||
|
||||
const executeDetection = async (projectId: string, datasetId: string, manifestPath: string | null) => {
|
||||
const executeDetection = async (
|
||||
projectId: string,
|
||||
datasetId: string,
|
||||
manifestPath: string | null,
|
||||
modelId = selectedDetectionModelId,
|
||||
modelAssetId = selectedModelAssetId,
|
||||
) => {
|
||||
const result = await detectionApi.run({
|
||||
project_id: projectId,
|
||||
dataset_id: datasetId,
|
||||
model_id: selectedDetectionModelId,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
model_id: modelId,
|
||||
model_asset_id: modelAssetId || null,
|
||||
confidence_threshold: detectionConfidenceThreshold,
|
||||
tile_manifest_path: manifestPath,
|
||||
parameters_json: {},
|
||||
@@ -303,24 +309,31 @@ export function useDetectionWorkflow({
|
||||
}
|
||||
}
|
||||
|
||||
const prepareAndRunDetection = async (): Promise<boolean> => {
|
||||
const prepareAndRunDetection = async (
|
||||
datasetIdOverride?: string,
|
||||
modelIdOverride?: string,
|
||||
): Promise<DetectionRunResponse | null> => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionRunError('De regionale werkruimte is nog niet geladen')
|
||||
return false
|
||||
return null
|
||||
}
|
||||
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
|
||||
const datasetId = datasetIdOverride || selectedDetectionDatasetId || rasterDatasets[0]?.id
|
||||
if (!datasetId) {
|
||||
setDetectionRunError('Kies of voeg eerst een gegeorefereerd luchtbeeld toe')
|
||||
return false
|
||||
return null
|
||||
}
|
||||
const selectedModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId)
|
||||
if (!selectedModel?.configured || selectedDetectionModelId === 'manual-fixture-detector') {
|
||||
const effectiveModelId = modelIdOverride || selectedDetectionModelId
|
||||
const effectiveModelAssetId = effectiveModelId === 'yolo-configured'
|
||||
? modelAssets.find((asset) => asset.active)?.model_asset_id ?? selectedModelAssetId
|
||||
: selectedModelAssetId
|
||||
const selectedModel = detectionModels.find((model) => model.model_id === effectiveModelId)
|
||||
if (!selectedModel?.configured || effectiveModelId === 'manual-fixture-detector') {
|
||||
setDetectionRunError(selectedModel?.limitation_message ?? 'Het gekozen analysemodel is niet beschikbaar')
|
||||
return false
|
||||
return null
|
||||
}
|
||||
if (selectedDetectionModelId === 'yolo-configured' && modelAssets.length > 0 && !selectedModelAssetId) {
|
||||
if (effectiveModelId === 'yolo-configured' && modelAssets.length > 0 && !effectiveModelAssetId) {
|
||||
setDetectionRunError('Kies eerst een lokaal modelbestand')
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
setDetectionRunError(null)
|
||||
@@ -355,7 +368,7 @@ export function useDetectionWorkflow({
|
||||
setDetectionWorkflowStage('validating')
|
||||
const preflight = await detectionApi.getYoloPreflight({
|
||||
tile_manifest_path: manifestPath,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
model_asset_id: effectiveModelAssetId || null,
|
||||
})
|
||||
setYoloPreflight(preflight)
|
||||
setYoloPreflightError(null)
|
||||
@@ -370,46 +383,63 @@ export function useDetectionWorkflow({
|
||||
}
|
||||
|
||||
setDetectionWorkflowStage('detecting')
|
||||
await executeDetection(selectedProjectId, datasetId, manifestPath)
|
||||
const result = await executeDetection(
|
||||
selectedProjectId,
|
||||
datasetId,
|
||||
manifestPath,
|
||||
effectiveModelId,
|
||||
effectiveModelAssetId,
|
||||
)
|
||||
setDetectionWorkflowStage('complete')
|
||||
return true
|
||||
return result
|
||||
} catch (error) {
|
||||
setDetectionRunError(formatError(error, 'De beeldanalyse is mislukt'))
|
||||
setDetectionWorkflowStage('failed')
|
||||
return false
|
||||
return null
|
||||
} finally {
|
||||
setRunningDetection(false)
|
||||
}
|
||||
}
|
||||
|
||||
const runDetectionQa = async () => {
|
||||
if (!selectedDetectionRunId) {
|
||||
const compareDetectionRunWithReference = async (
|
||||
analysisRunId: string,
|
||||
referenceDatasetId: string,
|
||||
useCurrentFilters = true,
|
||||
): Promise<DetectionQaResult | null> => {
|
||||
if (!analysisRunId) {
|
||||
setDetectionQaError('Select a detection run')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (!detectionReferenceDatasetId) {
|
||||
if (!referenceDatasetId) {
|
||||
setDetectionQaError('Select a reference dataset')
|
||||
return
|
||||
return null
|
||||
}
|
||||
setSelectedDetectionRunId(analysisRunId)
|
||||
setDetectionReferenceDatasetId(referenceDatasetId)
|
||||
setDetectionQaError(null)
|
||||
setDetectionQaResult(null)
|
||||
setRunningDetectionQa(true)
|
||||
try {
|
||||
const result = await detectionApi.compareWithReference(selectedDetectionRunId, {
|
||||
reference_dataset_id: detectionReferenceDatasetId,
|
||||
const result = await detectionApi.compareWithReference(analysisRunId, {
|
||||
reference_dataset_id: referenceDatasetId,
|
||||
iou_threshold: qaIouThreshold,
|
||||
class_name: detectionClassFilter || null,
|
||||
min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
|
||||
class_name: useCurrentFilters ? detectionClassFilter || null : null,
|
||||
min_confidence: useCurrentFilters && detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
|
||||
})
|
||||
setDetectionQaResult(result)
|
||||
await loadQualityChecks(selectedProjectId)
|
||||
return result
|
||||
} catch (error) {
|
||||
setDetectionQaError(formatError(error, 'Detection QA failed'))
|
||||
return null
|
||||
} finally {
|
||||
setRunningDetectionQa(false)
|
||||
}
|
||||
}
|
||||
|
||||
const runDetectionQa = async (): Promise<DetectionQaResult | null> =>
|
||||
compareDetectionRunWithReference(selectedDetectionRunId, detectionReferenceDatasetId)
|
||||
|
||||
const runDetectionCalibration = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionCalibrationError('Select a project before calibration')
|
||||
@@ -560,6 +590,7 @@ export function useDetectionWorkflow({
|
||||
runDetection,
|
||||
uploadDetectionRaster,
|
||||
prepareAndRunDetection,
|
||||
compareDetectionRunWithReference,
|
||||
runDetectionQa,
|
||||
runDetectionCalibration,
|
||||
applyDetectionOperatorProfile,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useState } from 'react'
|
||||
import { datasetsApi } from '../services/api'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DetectionQaResult,
|
||||
DetectionRunResponse,
|
||||
OrthophotoAcquisitionResult,
|
||||
VectorSelectionBBox,
|
||||
} from '../types'
|
||||
import { formatError } from '../lib/formatError'
|
||||
|
||||
export type MapOrthophotoAnalysisStage =
|
||||
| 'idle'
|
||||
| 'acquiring'
|
||||
| 'detecting'
|
||||
| 'validating'
|
||||
| 'complete'
|
||||
| 'failed'
|
||||
|
||||
interface MapOrthophotoAnalysisOptions {
|
||||
selectedProjectId: string | null
|
||||
selectedAreaId: string
|
||||
datasets: DatasetCreateResponse[]
|
||||
loadProjectData: (projectId: string) => Promise<unknown>
|
||||
prepareAndRunDetection: (datasetId?: string) => Promise<DetectionRunResponse | null>
|
||||
compareDetectionRunWithReference: (
|
||||
analysisRunId: string,
|
||||
referenceDatasetId: string,
|
||||
) => Promise<DetectionQaResult | null>
|
||||
onAnalysisReady: () => void
|
||||
}
|
||||
|
||||
function findBuildingReference(datasets: DatasetCreateResponse[]): DatasetCreateResponse | null {
|
||||
return datasets.find(
|
||||
(dataset) =>
|
||||
dataset.status === 'ready' &&
|
||||
dataset.dataset_role === 'reference' &&
|
||||
dataset.source_name === 'grb' &&
|
||||
dataset.reference_layer_name === 'buildings',
|
||||
) ?? null
|
||||
}
|
||||
|
||||
export function useMapOrthophotoAnalysis({
|
||||
selectedProjectId,
|
||||
selectedAreaId,
|
||||
datasets,
|
||||
loadProjectData,
|
||||
prepareAndRunDetection,
|
||||
compareDetectionRunWithReference,
|
||||
onAnalysisReady,
|
||||
}: MapOrthophotoAnalysisOptions) {
|
||||
const [stage, setStage] = useState<MapOrthophotoAnalysisStage>('idle')
|
||||
const [status, setStatus] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastResult, setLastResult] = useState<OrthophotoAcquisitionResult | null>(null)
|
||||
|
||||
const run = async (bbox: VectorSelectionBBox): Promise<boolean> => {
|
||||
if (!selectedProjectId) {
|
||||
setError('De regionale werkruimte is nog niet geladen.')
|
||||
setStage('failed')
|
||||
return false
|
||||
}
|
||||
setError(null)
|
||||
setLastResult(null)
|
||||
setStage('acquiring')
|
||||
setStatus('1/3 Officieel luchtbeeld voor de rechthoek ophalen...')
|
||||
try {
|
||||
const job = await datasetsApi.acquireOrthophoto(selectedProjectId, {
|
||||
bbox,
|
||||
area_id: selectedAreaId || undefined,
|
||||
})
|
||||
const acquisition = job.result_json as unknown as OrthophotoAcquisitionResult | null
|
||||
const datasetId = job.output_dataset_id || acquisition?.output_dataset_id
|
||||
if (job.status !== 'success' || !datasetId || !acquisition) {
|
||||
throw new Error(job.error_message || 'Het officiële luchtbeeld werd niet als dataset bewaard.')
|
||||
}
|
||||
setLastResult(acquisition)
|
||||
await loadProjectData(selectedProjectId)
|
||||
|
||||
setStage('detecting')
|
||||
setStatus('2/3 Lokaal AI-model herkent gebouwen...')
|
||||
const detection = await prepareAndRunDetection(datasetId)
|
||||
if (!detection) {
|
||||
throw new Error('De beeldanalyse stopte. Open Beeldanalyse voor de technische oorzaak.')
|
||||
}
|
||||
|
||||
const reference = findBuildingReference(datasets)
|
||||
if (reference) {
|
||||
setStage('validating')
|
||||
setStatus('3/3 Resultaat vergelijken met officiële GRB-gebouwen...')
|
||||
const quality = await compareDetectionRunWithReference(detection.analysis_run_id, reference.id)
|
||||
setStatus(
|
||||
quality
|
||||
? `Analyse klaar: ${detection.detection_count.toLocaleString('nl-BE')} gebouwen herkend en gecontroleerd.`
|
||||
: `Analyse klaar: ${detection.detection_count.toLocaleString('nl-BE')} gebouwen herkend; kwaliteitscontrole kon niet afronden.`,
|
||||
)
|
||||
} else {
|
||||
setStatus(
|
||||
`Analyse klaar: ${detection.detection_count.toLocaleString('nl-BE')} gebouwen herkend. De GRB-referentielaag ontbreekt voor automatische controle.`,
|
||||
)
|
||||
}
|
||||
setStage('complete')
|
||||
onAnalysisReady()
|
||||
return true
|
||||
} catch (caught) {
|
||||
setError(formatError(caught, 'De kaartgestuurde beeldanalyse is mislukt'))
|
||||
setStatus('Analyse gestopt.')
|
||||
setStage('failed')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stage,
|
||||
status,
|
||||
error,
|
||||
lastResult,
|
||||
running: stage === 'acquiring' || stage === 'detecting' || stage === 'validating',
|
||||
run,
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
RasterNdviRequest,
|
||||
RasterNdwiRequest,
|
||||
RasterNdbiRequest,
|
||||
OrthophotoAcquireRequest,
|
||||
} from '../../types'
|
||||
|
||||
export const datasetsApi = {
|
||||
@@ -81,6 +82,8 @@ export const datasetsApi = {
|
||||
}
|
||||
return apiMultipart<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/upload`, form)
|
||||
},
|
||||
acquireOrthophoto: (projectId: string, payload: OrthophotoAcquireRequest): Promise<JobRead> =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/orthophoto/acquire`, 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> =>
|
||||
|
||||
@@ -6045,6 +6045,68 @@ section {
|
||||
animation: geo-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.geo-image-analysis {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
border: 1px solid #b9cec7;
|
||||
border-left: 3px solid #176a5c;
|
||||
border-radius: 6px;
|
||||
padding: 0.65rem;
|
||||
background: #f4faf8;
|
||||
}
|
||||
|
||||
.geo-image-analysis > div {
|
||||
display: grid;
|
||||
gap: 0.14rem;
|
||||
}
|
||||
|
||||
.geo-image-analysis span {
|
||||
color: #4f6a62;
|
||||
font-size: 0.61rem;
|
||||
font-weight: 850;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.geo-image-analysis strong {
|
||||
color: #173e38;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.geo-image-analysis small,
|
||||
.geo-image-analysis p {
|
||||
margin: 0;
|
||||
color: #64736d;
|
||||
font-size: 0.65rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.geo-image-analysis > button {
|
||||
width: 100%;
|
||||
min-height: 2.35rem;
|
||||
}
|
||||
|
||||
.geo-image-analysis-acquiring,
|
||||
.geo-image-analysis-detecting,
|
||||
.geo-image-analysis-validating {
|
||||
border-left-color: #b17a31;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.geo-image-analysis-complete {
|
||||
border-left-color: #277749;
|
||||
background: #f1faf4;
|
||||
}
|
||||
|
||||
.geo-image-analysis-failed {
|
||||
border-color: #e2b9b5;
|
||||
border-left-color: #aa3f37;
|
||||
background: #fff7f6;
|
||||
}
|
||||
|
||||
.geo-image-analysis .error {
|
||||
color: #8b2d2d;
|
||||
}
|
||||
|
||||
@keyframes geo-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@@ -298,6 +298,26 @@ export interface VectorSelectionBBox {
|
||||
crs?: 'EPSG:4326'
|
||||
}
|
||||
|
||||
export interface OrthophotoAcquireRequest {
|
||||
bbox: VectorSelectionBBox
|
||||
area_id?: string
|
||||
force_refresh?: boolean
|
||||
}
|
||||
|
||||
export interface OrthophotoAcquisitionResult {
|
||||
output_dataset_id: string
|
||||
reused: boolean
|
||||
provider: string
|
||||
layer: string
|
||||
width: number
|
||||
height: number
|
||||
resolution_m: number
|
||||
bbox_epsg4326: number[]
|
||||
bbox_epsg31370: number[]
|
||||
attribution: string
|
||||
limitation_message: string
|
||||
}
|
||||
|
||||
export interface MapViewportState {
|
||||
bbox: VectorSelectionBBox
|
||||
zoom: number
|
||||
|
||||
Reference in New Issue
Block a user