surface the new analysis caveats in the workbench
The backend now says when a result covers a different area than was drawn, or when a score belongs to one confidence cut only. None of that helps an operator while it stays in the response body. - the raster selection adapters carry the model-coverage and widened-cell warnings into the map panel and mark the result an estimate when either applies, so an existing warning slot renders them; - the map workspace shows the selection-edge disclosure next to the object count; - the detection panel shows average precision and the F1-optimal threshold beside the single-threshold figures, and the box-versus-footprint interpretation when candidates are detector boxes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,10 @@ export function bathymetryRasterSelectionToMapSelection(
|
||||
...result.summary,
|
||||
feature_count: result.valid_cell_count,
|
||||
is_estimate: false,
|
||||
warning: result.limitation_message,
|
||||
// A selection finer than one source cell was widened to the cells it
|
||||
// touches; the result then covers more ground than was drawn.
|
||||
warning: [result.cell_selection_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.cell_selection_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: false,
|
||||
|
||||
@@ -12,9 +12,15 @@ export function floodHazardSelectionToMapSelection(result: FloodHazardSelectionR
|
||||
summary: {
|
||||
...result.summary,
|
||||
feature_count: result.inundated_cell_count,
|
||||
is_estimate: false,
|
||||
warning: result.limitation_message,
|
||||
metrics: result.summary.metrics.map((metric) => ({ ...metric, is_estimate: false })),
|
||||
// Coverage is not certain when the model does not span the selection, or
|
||||
// when the selection was widened to whole cells.
|
||||
is_estimate: Boolean(result.coverage_warning),
|
||||
warning: [result.coverage_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.coverage_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: Boolean(result.coverage_warning),
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { floodHazardSelectionToMapSelection } from './floodHazardSelection'
|
||||
import { thematicRasterSelectionToMapSelection } from './thematicRaster'
|
||||
import type { FloodHazardSelectionResponse, ThematicRasterSelectionResponse } from '../types'
|
||||
|
||||
/**
|
||||
* Raster analyses answer over whole cells. When a selection is finer than one
|
||||
* source cell, or when the flood model does not span it, the value describes a
|
||||
* different area than the operator drew. The adapters must carry that statement
|
||||
* into the map panel; without it the number reads as an exact answer.
|
||||
*/
|
||||
|
||||
const BBOX = { min_x: 5.0, min_y: 51.1, max_x: 5.2, max_y: 51.3, crs: 'EPSG:4326' }
|
||||
|
||||
function floodResponse(overrides: Partial<FloodHazardSelectionResponse> = {}): FloodHazardSelectionResponse {
|
||||
return {
|
||||
dataset_id: 'dataset-1',
|
||||
dataset_ids: ['dataset-1'],
|
||||
partition_count: 1,
|
||||
product_key: 'fluviaal_current_t100',
|
||||
mechanism: 'fluviaal',
|
||||
climate_context: 'current',
|
||||
probability_class: 'grote kans',
|
||||
return_period_years: 100,
|
||||
selection_bbox: BBOX,
|
||||
selected_cell_count: 400,
|
||||
inundated_cell_count: 200,
|
||||
inundated_fraction: 1,
|
||||
resolution_m: 5,
|
||||
summary: {
|
||||
metric_label: 'Gemodelleerd overstroomd oppervlak',
|
||||
metric_value: 0.5,
|
||||
metric_unit: 'ha',
|
||||
aggregation_method: 'positive_depth_cells_times_cell_area',
|
||||
primary_metric_key: 'modelled_inundated_area_ha',
|
||||
metrics: [],
|
||||
},
|
||||
unsupported_metrics: [],
|
||||
limitation_message: 'Scenariodiepte is geen bathymetrie.',
|
||||
generated_at: '2026-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
} as FloodHazardSelectionResponse
|
||||
}
|
||||
|
||||
function thematicResponse(
|
||||
overrides: Partial<ThematicRasterSelectionResponse> = {},
|
||||
): ThematicRasterSelectionResponse {
|
||||
return {
|
||||
dataset_id: 'dataset-2',
|
||||
product_key: 'population_density',
|
||||
selection_bbox: BBOX,
|
||||
selected_cell_count: 1,
|
||||
valid_cell_count: 1,
|
||||
coverage_ratio: 1,
|
||||
resolution_m: 100,
|
||||
summary: {
|
||||
metric_label: 'Inwoners',
|
||||
metric_value: 42,
|
||||
metric_unit: 'inwoners',
|
||||
aggregation_method: 'sum_valid_source_cells',
|
||||
primary_metric_key: 'population',
|
||||
metrics: [],
|
||||
},
|
||||
limitation_message: 'Rasterbron op 100 m.',
|
||||
generated_at: '2026-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
} as ThematicRasterSelectionResponse
|
||||
}
|
||||
|
||||
describe('flood hazard selection adapter', () => {
|
||||
it('surfaces a partial model coverage warning and marks the result an estimate', () => {
|
||||
const mapped = floodHazardSelectionToMapSelection(
|
||||
floodResponse({ coverage_warning: 'Het VMM-model dekt 50.0% van deze selectie.' }),
|
||||
)
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toContain('50.0%')
|
||||
expect(mapped.summary?.warning).toContain('50.0%')
|
||||
expect(mapped.summary?.warning).toContain('Scenariodiepte is geen bathymetrie.')
|
||||
expect(mapped.summary?.is_estimate).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves a fully modelled selection exact', () => {
|
||||
const mapped = floodHazardSelectionToMapSelection(floodResponse())
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toBeNull()
|
||||
expect(mapped.summary?.is_estimate).toBe(false)
|
||||
expect(mapped.summary?.warning).toBe('Scenariodiepte is geen bathymetrie.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('thematic raster selection adapter', () => {
|
||||
it('reports that a sub-cell selection was widened to whole cells', () => {
|
||||
const mapped = thematicRasterSelectionToMapSelection(
|
||||
thematicResponse({
|
||||
cell_selection_warning: 'De selectie is kleiner dan één rastercel van deze bron.',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toContain('rastercel')
|
||||
expect(mapped.summary?.warning).toContain('rastercel')
|
||||
expect(mapped.summary?.warning).toContain('Rasterbron op 100 m.')
|
||||
})
|
||||
|
||||
it('keeps a normal selection free of an invented caveat', () => {
|
||||
const mapped = thematicRasterSelectionToMapSelection(thematicResponse())
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toBeNull()
|
||||
expect(mapped.summary?.warning).toBe('Rasterbron op 100 m.')
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,10 @@ export function terrainSelectionToMapSelection(result: TerrainSelectionResponse)
|
||||
...result.summary,
|
||||
feature_count: result.sample_count,
|
||||
is_estimate: false,
|
||||
warning: result.limitation_message,
|
||||
// A selection finer than one source cell was widened to the cells it
|
||||
// touches; the result then covers more ground than was drawn.
|
||||
warning: [result.cell_selection_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.cell_selection_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: false,
|
||||
|
||||
@@ -21,7 +21,10 @@ export function thematicRasterSelectionToMapSelection(result: ThematicRasterSele
|
||||
...result.summary,
|
||||
feature_count: result.valid_cell_count,
|
||||
is_estimate: true,
|
||||
warning: result.limitation_message,
|
||||
// A selection finer than one source cell was widened to the cells it
|
||||
// touches; the result then covers more ground than was drawn.
|
||||
warning: [result.cell_selection_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.cell_selection_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: metric.is_estimate ?? true,
|
||||
|
||||
Reference in New Issue
Block a user