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:
Jens
2026-08-22 14:33:54 +02:00
co-authored by Claude Opus 5
parent 23d6e0372b
commit b146b2143d
8 changed files with 208 additions and 7 deletions
+4 -1
View File
@@ -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,
+9 -3
View File
@@ -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.')
})
})
+4 -1
View File
@@ -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,
+4 -1
View File
@@ -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,