Add QA evidence map overlay
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-25 03:17:49 +02:00
parent 7a54d01993
commit b9674a0e42
16 changed files with 778 additions and 4 deletions
+82 -1
View File
@@ -7,6 +7,7 @@ interface GeoMapProps {
areaData?: GeoJSON.FeatureCollection | null
selectedFeature?: GeoJSON.Feature | null
selectionData?: GeoJSON.FeatureCollection | null
qaEvidenceData?: GeoJSON.FeatureCollection | null
selectionBbox?: { min_x: number; min_y: number; max_x: number; max_y: number } | null
bboxSelectionMode?: boolean
visible?: boolean
@@ -97,6 +98,7 @@ function GeoMap({
areaData = null,
selectedFeature = null,
selectionData = null,
qaEvidenceData = null,
selectionBbox = null,
bboxSelectionMode = false,
visible = true,
@@ -148,7 +150,18 @@ function GeoMap({
onMapCoordinateSelectRef.current?.([event.lngLat.lng, event.lngLat.lat])
return
}
const layers = ['dataset-fill', 'dataset-line', 'area-fill', 'area-line'].filter((layerId) => map.getLayer(layerId))
const layers = [
'qa-evidence-fill',
'qa-evidence-line',
'qa-evidence-circle',
'selection-result-fill',
'selection-result-line',
'selection-result-circle',
'dataset-fill',
'dataset-line',
'area-fill',
'area-line',
].filter((layerId) => map.getLayer(layerId))
if (layers.length === 0) {
onFeatureSelectRef.current?.(null)
return
@@ -463,6 +476,74 @@ function GeoMap({
})
}, [selectionData, mapStyleReady])
useEffect(() => {
const map = mapRef.current
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
return
}
const evidenceCollection = qaEvidenceData ?? EMPTY_FEATURE_COLLECTION
if (map.getSource('qa-evidence')) {
;(map.getSource('qa-evidence') as maplibregl.GeoJSONSource).setData(evidenceCollection)
return
}
map.addSource('qa-evidence', { type: 'geojson', data: evidenceCollection })
const evidenceColor = [
'match',
['get', 'qa_evidence_role'],
'match_candidate',
'#2563eb',
'match_reference',
'#0f766e',
'false_positive',
'#dc2626',
'false_negative',
'#d97706',
'#475569',
] as maplibregl.ExpressionSpecification
map.addLayer({
id: 'qa-evidence-fill',
type: 'fill',
source: 'qa-evidence',
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false],
paint: {
'fill-color': evidenceColor,
'fill-opacity': 0.28,
},
})
map.addLayer({
id: 'qa-evidence-line',
type: 'line',
source: 'qa-evidence',
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false],
paint: {
'line-color': evidenceColor,
'line-width': [
'match',
['get', 'qa_evidence_role'],
'false_positive',
4,
'false_negative',
4,
3,
],
},
})
map.addLayer({
id: 'qa-evidence-circle',
type: 'circle',
source: 'qa-evidence',
filter: ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false],
paint: {
'circle-color': evidenceColor,
'circle-radius': 7,
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 2,
},
})
}, [qaEvidenceData, mapStyleReady])
return <div className="map-container" ref={containerRef} />
}
@@ -176,6 +176,11 @@ interface MapWorkspaceProps {
selectedMapAreaId: string
areaFeatureCollection: GeoJSON.FeatureCollection | null
mapFeatureCollection: GeoJSON.FeatureCollection | null
qualityEvidenceGeoJson?: GeoJSON.FeatureCollection | null
qualityEvidenceFeatureCount?: number
qualityEvidenceLoading?: boolean
qualityEvidenceError?: string | null
qualityEvidenceWarnings?: string[]
mapLayerLabel: string
mapLayerSourceLabel: string
mapLayerProvenance: string
@@ -219,6 +224,7 @@ interface MapWorkspaceProps {
onSelectMapQaReferenceDataset: (datasetId: string) => void
onRunMapSelectionQa: () => void
onOpenMapSelectionQualityEvidence: () => void
onClearQualityEvidence?: () => void
}
export function MapWorkspace({
@@ -226,6 +232,11 @@ export function MapWorkspace({
selectedMapAreaId,
areaFeatureCollection,
mapFeatureCollection,
qualityEvidenceGeoJson = null,
qualityEvidenceFeatureCount = 0,
qualityEvidenceLoading = false,
qualityEvidenceError = null,
qualityEvidenceWarnings = [],
mapLayerLabel,
mapLayerSourceLabel,
mapLayerProvenance,
@@ -269,6 +280,7 @@ export function MapWorkspace({
onSelectMapQaReferenceDataset,
onRunMapSelectionQa,
onOpenMapSelectionQualityEvidence,
onClearQualityEvidence,
}: MapWorkspaceProps): JSX.Element {
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
@@ -398,6 +410,11 @@ export function MapWorkspace({
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No layer rendered'}</strong>
<small>{mapLayerProvenance}</small>
</div>
<div>
<span>QA/QC evidence</span>
<strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} evidence features` : 'No evidence overlay'}</strong>
<small>{qualityEvidenceLoading ? 'Loading persisted evidence' : 'Matches, false positives and false negatives'}</small>
</div>
</div>
<div className="map-control-surface" aria-label="Map workspace controls">
@@ -483,9 +500,37 @@ export function MapWorkspace({
<span>Draw state</span>
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active vector or result layer'}</strong>
</div>
<div>
<span>QA evidence overlay</span>
<strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} rendered` : 'off'}</strong>
</div>
</div>
</div>
{qualityEvidenceGeoJson || qualityEvidenceError || qualityEvidenceWarnings.length > 0 ? (
<div className="qa-evidence-map-status" aria-label="QA/QC evidence map overlay status">
<div>
<span>QA/QC evidence overlay</span>
<strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} persisted features` : 'Not loaded'}</strong>
{qualityEvidenceError ? <p className="error">{qualityEvidenceError}</p> : null}
{qualityEvidenceWarnings.length > 0 ? (
<p className="muted">{qualityEvidenceWarnings.length} evidence id{qualityEvidenceWarnings.length === 1 ? '' : 's'} could not be resolved.</p>
) : null}
</div>
<div className="qa-evidence-legend" aria-label="QA/QC evidence overlay legend">
<span><i className="qa-evidence-swatch qa-evidence-swatch-match-candidate" /> Match candidate</span>
<span><i className="qa-evidence-swatch qa-evidence-swatch-match-reference" /> Match reference</span>
<span><i className="qa-evidence-swatch qa-evidence-swatch-false-positive" /> False positive</span>
<span><i className="qa-evidence-swatch qa-evidence-swatch-false-negative" /> False negative</span>
</div>
{onClearQualityEvidence ? (
<button className="secondary-action" type="button" onClick={onClearQualityEvidence}>
Clear QA evidence
</button>
) : null}
</div>
) : null}
{!mapFeatureCollection ? (
<div className="empty-state map-empty-state">
<strong>No active vector or result layer</strong>
@@ -514,6 +559,7 @@ export function MapWorkspace({
areaData={areaFeatureCollection}
selectedFeature={selectedFeature}
selectionData={mapSelectionResult?.geojson ?? null}
qaEvidenceData={qualityEvidenceGeoJson}
selectionBbox={mapSelectionBbox}
bboxSelectionMode={bboxSelectionMode}
visible={mapLayerVisible}
@@ -17,6 +17,9 @@ interface QualityResultsPanelProps {
candidateDatasets: DatasetCreateResponse[]
referenceDatasets: DatasetCreateResponse[]
onRefresh: () => void
onOpenEvidenceMap?: (qualityCheckId: string) => void
evidenceLoading?: boolean
evidenceError?: string | null
}
function qualityMetricLabel(metricKey: string): string {
@@ -95,6 +98,9 @@ export function QualityResultsPanel({
candidateDatasets,
referenceDatasets,
onRefresh,
onOpenEvidenceMap,
evidenceLoading = false,
evidenceError = null,
}: QualityResultsPanelProps): JSX.Element {
const [showAllQualityChecks, setShowAllQualityChecks] = useState(false)
const [qualityStatusFilter, setQualityStatusFilter] = useState('all')
@@ -204,7 +210,21 @@ export function QualityResultsPanel({
>
Inspect latest check
</button>
<button
type="button"
className="secondary-action"
onClick={() => latestCheck?.id && onOpenEvidenceMap?.(latestCheck.id)}
disabled={!latestCheck || evidenceLoading || !onOpenEvidenceMap}
>
{evidenceLoading ? 'Loading map evidence...' : 'Show latest on map'}
</button>
</div>
{evidenceError ? (
<div className="result-state result-state-error">
<strong>QA/QC evidence overlay could not be loaded.</strong>
<p>{evidenceError}</p>
</div>
) : null}
{selectedQualityCheck ? (
<>
<div className="quality-drilldown-grid">
@@ -254,6 +274,14 @@ export function QualityResultsPanel({
<span>Map evidence handoff</span>
<strong>{selectedCandidateName} / {selectedReferenceName}</strong>
<p>Use the candidate and reference datasets as map layers for spatial review.</p>
<button
type="button"
className="secondary-action"
onClick={() => onOpenEvidenceMap?.(selectedQualityCheck.id)}
disabled={evidenceLoading || !onOpenEvidenceMap}
>
{evidenceLoading ? 'Loading overlay...' : 'Show evidence overlay'}
</button>
</div>
</div>
<div className="quality-feature-evidence-grid" aria-label="Feature-level QA/QC evidence">
@@ -440,6 +468,14 @@ export function QualityResultsPanel({
<button type="button" className="secondary-action" onClick={() => setSelectedQualityCheckId(check.id)}>
Inspect check
</button>
<button
type="button"
className="secondary-action"
onClick={() => onOpenEvidenceMap?.(check.id)}
disabled={evidenceLoading || !onOpenEvidenceMap}
>
Show evidence on map
</button>
</div>
<div className="quality-score-row">
<div>