Complete the V1 analysis handoff
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-17 02:52:15 +02:00
parent d9bc8fdbed
commit 03c6154cac
9 changed files with 240 additions and 14 deletions
+8
View File
@@ -71,6 +71,14 @@ but is not required for the primary 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.
Every cross-theme result now names the measured quantity next to the value, so
a station water level, mapped area and line length cannot appear as an
unexplained bare number. Current vector selections download as GeoJSON,
current raster analyses download as JSON and historical comparisons download
as JSON with their complete timeline and provenance. After a successful
analysis, compact next actions carry the same spatial context directly into
`AI-vragen` or `Downloads`.
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
+2
View File
@@ -1079,6 +1079,8 @@ function App(): JSX.Element {
onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run}
onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey}
onClearQualityEvidence={clearQualityEvidenceGeoJson}
onOpenAssistant={() => setActiveWorkspace('assistant')}
onOpenExports={() => setActiveWorkspace('exports')}
/>
) : null}
+62 -11
View File
@@ -625,8 +625,8 @@ function copyText(text: string): void {
fallbackCopyText(text)
}
function downloadJsonFile(filename: string, payload: unknown): void {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/geo+json' })
function downloadJsonFile(filename: string, payload: unknown, contentType = 'application/json'): void {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: contentType })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
@@ -716,6 +716,8 @@ interface MapWorkspaceProps {
onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise<boolean>
onSelectOrthophotoProduct: (productKey: string) => void
onClearQualityEvidence?: () => void
onOpenAssistant: () => void
onOpenExports: () => void
}
export function MapWorkspace({
@@ -797,6 +799,8 @@ export function MapWorkspace({
onRunOrthophotoAnalysis,
onSelectOrthophotoProduct,
onClearQualityEvidence,
onOpenAssistant,
onOpenExports,
}: MapWorkspaceProps): JSX.Element {
const [advancedMode, setAdvancedMode] = useState(false)
const [activeThemeId, setActiveThemeId] = useState<DataThemeId>(() => {
@@ -1135,7 +1139,7 @@ export function MapWorkspace({
if (!selectedFeatureGeoJson) {
return
}
downloadJsonFile(selectedFeatureFilename, selectedFeatureGeoJson)
downloadJsonFile(selectedFeatureFilename, selectedFeatureGeoJson, 'application/geo+json')
}
const copySelectedMapFeatureProperties = () => {
@@ -1192,22 +1196,46 @@ export function MapWorkspace({
if (!mapSelectionResult) {
return
}
downloadJsonFile(DEFAULT_AREA_SELECTION_FILENAME, mapSelectionResult.geojson)
downloadJsonFile(DEFAULT_AREA_SELECTION_FILENAME, mapSelectionResult.geojson, 'application/geo+json')
}
const copyAreaSelection = () => {
copyText(JSON.stringify(mapSelectionResult?.geojson ?? { type: 'FeatureCollection', features: [] }, null, 2))
}
const downloadActiveThemeSelection = () => {
const downloadActiveThemeResult = () => {
if (!activeSelectionResult) {
return
}
downloadJsonFile(`${activeTheme.id}-selection.geojson`, activeSelectionResult.geojson)
if (activeThemeDataset?.dataset_type === 'raster') {
downloadJsonFile(`${activeTheme.id}-analysis.json`, {
project_id: selectedProjectId,
area_id: selectedMapArea?.id ?? null,
area_name: selectedMapArea?.name ?? null,
theme: activeTheme,
dataset_id: activeThemeDataset.id,
dataset_name: activeThemeDataset.name,
source_name: activeThemeDataset.source_name,
result: activeSelectionResult,
})
return
}
downloadJsonFile(`${activeTheme.id}-selection.geojson`, activeSelectionResult.geojson, 'application/geo+json')
}
const copyActiveThemeSelection = () => {
copyText(JSON.stringify(activeSelectionResult?.geojson ?? { type: 'FeatureCollection', features: [] }, null, 2))
const copyActiveThemeResult = () => {
copyText(JSON.stringify(activeSelectionResult ?? {}, null, 2))
}
const downloadTemporalComparison = () => {
if (!temporalComparison) {
return
}
downloadJsonFile(`${activeTheme.id}-evolution-${temporalComparison.earlier.observed_at.slice(0, 10)}-${temporalComparison.later.observed_at.slice(0, 10)}.json`, temporalComparison)
}
const copyTemporalComparison = () => {
copyText(JSON.stringify(temporalComparison ?? {}, null, 2))
}
const saveAreaSelectionExport = () => {
@@ -1888,7 +1916,10 @@ export function MapWorkspace({
<strong>{theme.label}</strong>
<small>{dataset ? getDatasetSourceDisplayName(dataset) : 'Geen bron gekoppeld'}</small>
</span>
<b>{item ? resultMetricLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'}</b>
<span className="geo-theme-result-value">
<b>{item ? resultMetricLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'}</b>
{item?.result.summary ? <small>{item.result.summary.metric_label}</small> : null}
</span>
</div>
)
})}
@@ -1930,8 +1961,28 @@ export function MapWorkspace({
{analysisMode === 'current' ? (
<div className="geo-result-actions">
<button className="secondary-action" disabled={!activeSelectionResult || activeThemeDataset?.dataset_type === 'raster'} type="button" onClick={downloadActiveThemeSelection}>Download GeoJSON</button>
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeSelection}>Kopieer gegevens</button>
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeResult}>
{activeThemeDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'}
</button>
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeResult}>Kopieer gegevens</button>
</div>
) : null}
{analysisMode === 'evolution' && temporalComparison ? (
<div className="geo-result-actions">
<button className="secondary-action" type="button" onClick={downloadTemporalComparison}>Download vergelijking</button>
<button className="secondary-action" type="button" onClick={copyTemporalComparison}>Kopieer vergelijking</button>
</div>
) : null}
{activeSelectionResult || temporalComparison ? (
<div className="geo-result-next-actions" aria-label="Volgende stap">
<span>
<strong>Analyse klaar</strong>
<small>Stel een vraag over dit gebied of open je bewaarde resultaten.</small>
</span>
<button className="primary-action" type="button" onClick={onOpenAssistant}>Stel AI-vraag</button>
<button className="secondary-action" type="button" onClick={onOpenExports}>Open downloads</button>
</div>
) : null}
</>
+36 -2
View File
@@ -6459,6 +6459,17 @@ section {
text-align: right;
}
.geo-theme-result-value {
display: grid;
justify-items: end;
min-width: 8.5rem;
text-align: right;
}
.geo-theme-result-value small {
max-width: 13rem;
}
.geo-data-notice {
margin: 0;
border-left: 3px solid #b17a31;
@@ -6542,6 +6553,27 @@ section {
margin-top: auto;
}
.geo-result-next-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 0.45rem;
align-items: center;
border-top: 1px solid #dfe7e3;
padding-top: 0.65rem;
}
.geo-result-next-actions > span {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.geo-result-next-actions small {
color: #687570;
font-size: 0.66rem;
line-height: 1.35;
}
.geo-explorer-footer {
display: flex;
flex-wrap: wrap;
@@ -6581,7 +6613,8 @@ section {
.geo-results-panel > .geo-panel-heading,
.geo-results-panel > .error,
.geo-results-panel > .geo-data-notice,
.geo-results-panel > .geo-result-actions {
.geo-results-panel > .geo-result-actions,
.geo-results-panel > .geo-result-next-actions {
grid-column: 1 / -1;
}
@@ -6661,7 +6694,8 @@ section {
}
.geo-map-actions,
.geo-result-actions {
.geo-result-actions,
.geo-result-next-actions {
display: grid;
grid-template-columns: 1fr;
}