fix(map): honor raster coverage and temporal dates
This commit is contained in:
@@ -32,6 +32,37 @@ class TemporalAnalysisService:
|
|||||||
"provision_regional_grb_context.py",
|
"provision_regional_grb_context.py",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _canonical_observation_snapshots(datasets: list[Dataset]) -> list[Dataset]:
|
||||||
|
by_observation: dict[datetime, Dataset] = {}
|
||||||
|
for dataset in datasets:
|
||||||
|
if dataset.observed_at is None:
|
||||||
|
continue
|
||||||
|
current = by_observation.get(dataset.observed_at)
|
||||||
|
dataset_recency = max(
|
||||||
|
(
|
||||||
|
value.timestamp()
|
||||||
|
for value in (dataset.imported_at, dataset.updated_at, dataset.created_at)
|
||||||
|
if value is not None
|
||||||
|
),
|
||||||
|
default=0.0,
|
||||||
|
)
|
||||||
|
current_recency = max(
|
||||||
|
(
|
||||||
|
value.timestamp()
|
||||||
|
for value in (
|
||||||
|
getattr(current, "imported_at", None),
|
||||||
|
getattr(current, "updated_at", None),
|
||||||
|
getattr(current, "created_at", None),
|
||||||
|
)
|
||||||
|
if value is not None
|
||||||
|
),
|
||||||
|
default=0.0,
|
||||||
|
)
|
||||||
|
if current is None or (dataset_recency, str(dataset.id)) > (current_recency, str(current.id)):
|
||||||
|
by_observation[dataset.observed_at] = dataset
|
||||||
|
return sorted(by_observation.values(), key=lambda item: item.observed_at)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_series(db: Session, project_id: UUID) -> list[TemporalSeriesRead]:
|
def list_series(db: Session, project_id: UUID) -> list[TemporalSeriesRead]:
|
||||||
rows = (
|
rows = (
|
||||||
@@ -49,6 +80,7 @@ class TemporalAnalysisService:
|
|||||||
|
|
||||||
result: list[TemporalSeriesRead] = []
|
result: list[TemporalSeriesRead] = []
|
||||||
for key, datasets in grouped.items():
|
for key, datasets in grouped.items():
|
||||||
|
datasets = TemporalAnalysisService._canonical_observation_snapshots(datasets)
|
||||||
observed = [item.observed_at for item in datasets if item.observed_at is not None]
|
observed = [item.observed_at for item in datasets if item.observed_at is not None]
|
||||||
if not observed:
|
if not observed:
|
||||||
continue
|
continue
|
||||||
@@ -118,6 +150,13 @@ class TemporalAnalysisService:
|
|||||||
bbox,
|
bbox,
|
||||||
selection_area.geometry,
|
selection_area.geometry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_preclipped_to_selection_area(dataset: Dataset) -> bool:
|
||||||
|
return bool(
|
||||||
|
selection_area
|
||||||
|
and VectorFeatureService.can_use_full_area_fast_path(dataset, selection_area.id)
|
||||||
|
)
|
||||||
|
|
||||||
summaries: dict[UUID, dict[str, Any]] = {}
|
summaries: dict[UUID, dict[str, Any]] = {}
|
||||||
|
|
||||||
def summarize(dataset: Dataset) -> dict[str, Any]:
|
def summarize(dataset: Dataset) -> dict[str, Any]:
|
||||||
@@ -126,14 +165,9 @@ class TemporalAnalysisService:
|
|||||||
return cached
|
return cached
|
||||||
kwargs: dict[str, Any] = {"dataset": dataset, "bbox": bbox}
|
kwargs: dict[str, Any] = {"dataset": dataset, "bbox": bbox}
|
||||||
if selection_area is not None:
|
if selection_area is not None:
|
||||||
kwargs["selection_geometry"] = selection_geometry
|
dataset_is_preclipped = is_preclipped_to_selection_area(dataset)
|
||||||
kwargs["full_dataset_area"] = (
|
kwargs["selection_geometry"] = None if dataset_is_preclipped else selection_geometry
|
||||||
selection_covers_full_area
|
kwargs["full_dataset_area"] = selection_covers_full_area and dataset_is_preclipped
|
||||||
and VectorFeatureService.can_use_full_area_fast_path(
|
|
||||||
dataset,
|
|
||||||
selection_area.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
summary = VectorFeatureService.summarize_features_by_bbox(db, **kwargs)
|
summary = VectorFeatureService.summarize_features_by_bbox(db, **kwargs)
|
||||||
summaries[dataset.id] = summary
|
summaries[dataset.id] = summary
|
||||||
return summary
|
return summary
|
||||||
@@ -164,16 +198,20 @@ class TemporalAnalysisService:
|
|||||||
later=later,
|
later=later,
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
preview_limit=payload.preview_limit,
|
preview_limit=payload.preview_limit,
|
||||||
selection_geometry=selection_geometry,
|
selection_geometry=(
|
||||||
|
None
|
||||||
|
if is_preclipped_to_selection_area(earlier) and is_preclipped_to_selection_area(later)
|
||||||
|
else selection_geometry
|
||||||
|
),
|
||||||
earlier_full_dataset_area=(
|
earlier_full_dataset_area=(
|
||||||
selection_covers_full_area
|
selection_covers_full_area
|
||||||
and VectorFeatureService.can_use_full_area_fast_path(earlier, selection_area.id)
|
and is_preclipped_to_selection_area(earlier)
|
||||||
if selection_area is not None
|
if selection_area is not None
|
||||||
else False
|
else False
|
||||||
),
|
),
|
||||||
later_full_dataset_area=(
|
later_full_dataset_area=(
|
||||||
selection_covers_full_area
|
selection_covers_full_area
|
||||||
and VectorFeatureService.can_use_full_area_fast_path(later, selection_area.id)
|
and is_preclipped_to_selection_area(later)
|
||||||
if selection_area is not None
|
if selection_area is not None
|
||||||
else False
|
else False
|
||||||
),
|
),
|
||||||
@@ -298,8 +336,7 @@ class TemporalAnalysisService:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
datasets = fallback_datasets
|
datasets = fallback_datasets
|
||||||
unique = {dataset.id: dataset for dataset in datasets}
|
ordered = TemporalAnalysisService._canonical_observation_snapshots(datasets)
|
||||||
ordered = sorted(unique.values(), key=lambda item: item.observed_at or datetime.min.replace(tzinfo=timezone.utc))
|
|
||||||
observations: list[TemporalObservation] = []
|
observations: list[TemporalObservation] = []
|
||||||
for dataset in ordered:
|
for dataset in ordered:
|
||||||
if dataset.observed_at is None:
|
if dataset.observed_at is None:
|
||||||
|
|||||||
@@ -133,6 +133,20 @@ def temporal_dataset(*, project_id, observed_year: int, metric_method: str = "fe
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_temporal_series_keeps_only_latest_snapshot_per_observation_date() -> None:
|
||||||
|
project_id = uuid4()
|
||||||
|
old = temporal_dataset(project_id=project_id, observed_year=2025)
|
||||||
|
old.imported_at = datetime(2026, 7, 19, tzinfo=timezone.utc)
|
||||||
|
latest = temporal_dataset(project_id=project_id, observed_year=2025)
|
||||||
|
latest.imported_at = datetime(2026, 7, 21, tzinfo=timezone.utc)
|
||||||
|
earlier = temporal_dataset(project_id=project_id, observed_year=2022)
|
||||||
|
earlier.imported_at = datetime(2026, 7, 21, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
canonical = TemporalAnalysisService._canonical_observation_snapshots([old, latest, earlier])
|
||||||
|
|
||||||
|
assert [dataset.id for dataset in canonical] == [earlier.id, latest.id]
|
||||||
|
|
||||||
|
|
||||||
def governed_grb_dataset(*, project_id, observed_day: int) -> Dataset:
|
def governed_grb_dataset(*, project_id, observed_day: int) -> Dataset:
|
||||||
dataset = temporal_dataset(project_id=project_id, observed_year=2026, metric_method="intersection_area")
|
dataset = temporal_dataset(project_id=project_id, observed_year=2026, metric_method="intersection_area")
|
||||||
dataset.observed_at = datetime(2026, 7, observed_day, tzinfo=timezone.utc)
|
dataset.observed_at = datetime(2026, 7, observed_day, tzinfo=timezone.utc)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def test_flanders_workspace_exposes_governed_thematic_products_on_demand() -> No
|
|||||||
|
|
||||||
assert "activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME" in workspace
|
assert "activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME" in workspace
|
||||||
assert "new Map<DataThemeId, OnDemandMapProduct>" in workspace
|
assert "new Map<DataThemeId, OnDemandMapProduct>" in workspace
|
||||||
assert "'Op aanvraag'" in workspace
|
assert "'Automatisch'" in workspace
|
||||||
assert "theme.id === 'space_occupation'" in workspace
|
assert "theme.id === 'space_occupation'" in workspace
|
||||||
assert "setActiveThemeId(fallbackTheme.id)" in workspace
|
assert "setActiveThemeId(fallbackTheme.id)" in workspace
|
||||||
assert "return `referentiejaar ${observationYear}`" in workspace
|
assert "return `referentiejaar ${observationYear}`" in workspace
|
||||||
|
|||||||
@@ -391,6 +391,6 @@ def test_grb_frontend_and_contracts_use_only_the_governed_backend_path() -> None
|
|||||||
assert "result[product.key] = null" in workspace
|
assert "result[product.key] = null" in workspace
|
||||||
assert ": onDemandThemeActive\n ? null\n : mapFeatureCollection" in workspace
|
assert ": onDemandThemeActive\n ? null\n : mapFeatureCollection" in workspace
|
||||||
assert "onSetContextLayerLabel" in workspace
|
assert "onSetContextLayerLabel" in workspace
|
||||||
assert "'Op aanvraag'" in workspace
|
assert "'Automatisch'" in workspace
|
||||||
assert "/datasets/grb/acquire" in contracts
|
assert "/datasets/grb/acquire" in contracts
|
||||||
assert "geo.api.vlaanderen.be" not in workspace
|
assert "geo.api.vlaanderen.be" not in workspace
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# GeoIntel data coverage status
|
||||||
|
|
||||||
|
Status date: 2026-07-21
|
||||||
|
|
||||||
|
This document is the operational interpretation of the source registry. It
|
||||||
|
does not replace the legal/source provenance stored with each Dataset.
|
||||||
|
|
||||||
|
## Status labels in the workbench
|
||||||
|
|
||||||
|
- `Beschikbaar`: a persisted Dataset covers the selection and can be queried
|
||||||
|
immediately.
|
||||||
|
- `Automatisch`: an audited official source contract exists. GeoIntel obtains
|
||||||
|
and persists only the bounded selection when analysis starts.
|
||||||
|
- `Alleen huidig`: one official observation exists, so no honest evolution can
|
||||||
|
be calculated.
|
||||||
|
- `Ontbreekt`: no operational source contract covers that theme and zone.
|
||||||
|
|
||||||
|
`Automatisch` therefore does not mean unavailable or simulated. It means that
|
||||||
|
the source is materialized on first use and reused afterwards. AI training
|
||||||
|
cannot replace missing official GIS observations and is not used to fabricate
|
||||||
|
coverage or historical dates.
|
||||||
|
|
||||||
|
## Current operational coverage
|
||||||
|
|
||||||
|
| Zone | Persisted or bounded operational sources | Historical comparison |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Belgium | NGI administrative boundaries; Statbel population/statistical sectors | Statbel population 2021-2025 |
|
||||||
|
| Flanders | GRB buildings, roads, water and parcels; DHMV terrain/surface; VMM flood scenarios; BWK/Natura 2000; DOV soil; policy rasters for space, open space, accessibility and services; agriculture and orthophoto where governed | Population 2021-2025; land-use/land-cover series where retained; agriculture editions; historical maps/orthophotos where the selected product has a real observation date |
|
||||||
|
| Wallonia | Bounded PICC buildings, roads and hydrography; governed SPW bed-elevation/bathymetry products | No general cross-theme regional history yet |
|
||||||
|
| Brussels | Bounded UrbIS buildings, street axes and cadastral parcels | No general cross-theme regional history yet |
|
||||||
|
| Belgian North Sea | RBINS reporting units; Marine Spatial Plan 2026-2034; governed MDK bathymetry only when runtime acquisition is explicitly configured | No multi-epoch bathymetry or marine-plan trend yet |
|
||||||
|
|
||||||
|
Mol and the Kempen are golden regression areas. Their persisted partitions are
|
||||||
|
not national coverage. A partition is only selected when its recorded
|
||||||
|
`bbox_epsg4326` intersects the drawn rectangle; otherwise GeoIntel uses an
|
||||||
|
applicable bounded official source or reports the theme as unsupported.
|
||||||
|
|
||||||
|
## Priority coverage gaps
|
||||||
|
|
||||||
|
1. Govern the public Walloon WALOUS land-cover editions (including the
|
||||||
|
published 2018/2020 change product) as a real regional time series. A class
|
||||||
|
crosswalk is required because the older COSW 2005/2007 methodology differs.
|
||||||
|
Official catalogue:
|
||||||
|
`https://geoportail.wallonie.be/catalogue-donnees?search-text=occupation+du+sol`.
|
||||||
|
2. Govern the current public Walloon flood-hazard vector/raster products and
|
||||||
|
retain their model scenario semantics separately from observed floods.
|
||||||
|
Official record:
|
||||||
|
`https://geoportail.wallonie.be/catalogue/14084108-2c7b-4091-b62d-ff0fc235213a.html`.
|
||||||
|
3. Add the public UrbIS Land Cover product (regional situation 2024) for
|
||||||
|
Brussels through its official WFS/download contract. Keep it separate from
|
||||||
|
cadastral parcels and buildings. Product specification:
|
||||||
|
`https://urbisdownload.datastore.brussels/UrbIS/TechSpec/LandCover_TechSpec_FR20240401.pdf`.
|
||||||
|
4. Add a common Belgium-wide topographic baseline with normalized theme
|
||||||
|
semantics across NGI, Flanders, Wallonia and Brussels.
|
||||||
|
5. Govern comparable Walloon and Brussels historical editions before exposing
|
||||||
|
evolution for buildings, roads, land cover, soil, elevation or flood risk.
|
||||||
|
6. Add nationally comparable land-cover history with explicit class crosswalks
|
||||||
|
and uncertainty; never compare incompatible legends silently.
|
||||||
|
7. Add multi-epoch marine bathymetry and survey-footprint metadata before
|
||||||
|
presenting seabed evolution.
|
||||||
|
8. Expand persisted raster partition manifests beyond the regression regions
|
||||||
|
only where repeated use justifies caching; bounded acquisition remains the
|
||||||
|
default for one-off selections.
|
||||||
|
9. Add source freshness probes only for publishers with stable official edition
|
||||||
|
contracts. Do not infer a new observation from an import or HTTP date.
|
||||||
|
|
||||||
|
## Acceptance rules for a new source
|
||||||
|
|
||||||
|
A source is visible as operational only after its licence, authority,
|
||||||
|
observation time, CRS, spatial coverage, schema, units and limitations are
|
||||||
|
validated. It must persist through DatasetService and the canonical feature or
|
||||||
|
raster flow, retain provenance/checksums, return semantic metrics, and have a
|
||||||
|
selection-level regression test. Historical support additionally requires at
|
||||||
|
least two distinct, methodologically comparable official observation dates.
|
||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
bboxToInputState,
|
bboxToInputState,
|
||||||
bboxesEqual,
|
bboxesEqual,
|
||||||
copyText,
|
copyText,
|
||||||
|
datasetIntersectsSelection,
|
||||||
|
deduplicateTemporalSnapshots,
|
||||||
downloadJsonFile,
|
downloadJsonFile,
|
||||||
formatArea,
|
formatArea,
|
||||||
formatBboxLabel,
|
formatBboxLabel,
|
||||||
@@ -571,13 +573,11 @@ function listThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataT
|
|||||||
groups.set(dataset.temporal_series_key, items)
|
groups.set(dataset.temporal_series_key, items)
|
||||||
}
|
}
|
||||||
return Array.from(groups.entries())
|
return Array.from(groups.entries())
|
||||||
.filter(([, items]) => items.length >= 2)
|
|
||||||
.map(([key, items]) => {
|
.map(([key, items]) => {
|
||||||
const ordered = [...items].sort(
|
const ordered = deduplicateTemporalSnapshots(items)
|
||||||
(left, right) => new Date(left.observed_at ?? 0).getTime() - new Date(right.observed_at ?? 0).getTime(),
|
|
||||||
)
|
|
||||||
return { key, label: temporalSeriesLabel(ordered), items: ordered }
|
return { key, label: temporalSeriesLabel(ordered), items: ordered }
|
||||||
})
|
})
|
||||||
|
.filter((group) => group.items.length >= 2)
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
if (right.items.length !== left.items.length) {
|
if (right.items.length !== left.items.length) {
|
||||||
return right.items.length - left.items.length
|
return right.items.length - left.items.length
|
||||||
@@ -1048,7 +1048,7 @@ export function MapWorkspace({
|
|||||||
productKey: product.key,
|
productKey: product.key,
|
||||||
displayName: product.display_name,
|
displayName: product.display_name,
|
||||||
theme: product.theme,
|
theme: product.theme,
|
||||||
availabilityLabel: `${product.native_resolution_m} m · ${product.observation_year} · laad bij selectie`,
|
availabilityLabel: `${product.native_resolution_m} m · ${product.observation_year} · automatisch bij selectie`,
|
||||||
attribution: product.attribution,
|
attribution: product.attribution,
|
||||||
limitationMessage: product.limitation_message,
|
limitationMessage: product.limitation_message,
|
||||||
})
|
})
|
||||||
@@ -1059,7 +1059,7 @@ export function MapWorkspace({
|
|||||||
productKey: product.key,
|
productKey: product.key,
|
||||||
displayName: product.display_name,
|
displayName: product.display_name,
|
||||||
theme: product.key,
|
theme: product.key,
|
||||||
availabilityLabel: 'officiële vectorbron · laad bij selectie',
|
availabilityLabel: 'officiële vectorbron · automatisch bij selectie',
|
||||||
attribution: product.attribution,
|
attribution: product.attribution,
|
||||||
limitationMessage: product.limitation_message,
|
limitationMessage: product.limitation_message,
|
||||||
})
|
})
|
||||||
@@ -1075,7 +1075,7 @@ export function MapWorkspace({
|
|||||||
productKey: source.key,
|
productKey: source.key,
|
||||||
displayName: source.display_name,
|
displayName: source.display_name,
|
||||||
theme: 'bathymetry',
|
theme: 'bathymetry',
|
||||||
availabilityLabel: 'historische profielpunten · laad bij selectie',
|
availabilityLabel: 'historische profielpunten · automatisch bij selectie',
|
||||||
attribution: source.attribution,
|
attribution: source.attribution,
|
||||||
limitationMessage: source.limitation_message,
|
limitationMessage: source.limitation_message,
|
||||||
})
|
})
|
||||||
@@ -1089,7 +1089,7 @@ export function MapWorkspace({
|
|||||||
productKey: product.key,
|
productKey: product.key,
|
||||||
displayName: product.display_name,
|
displayName: product.display_name,
|
||||||
theme: product.theme,
|
theme: product.theme,
|
||||||
availabilityLabel: `${product.observation_label} · officiële vectorbron · laad bij selectie`,
|
availabilityLabel: `${product.observation_label} · officiële vectorbron · automatisch bij selectie`,
|
||||||
attribution: product.attribution,
|
attribution: product.attribution,
|
||||||
limitationMessage: product.limitation_message,
|
limitationMessage: product.limitation_message,
|
||||||
})
|
})
|
||||||
@@ -1103,7 +1103,7 @@ export function MapWorkspace({
|
|||||||
productKey: dhmvProduct.key,
|
productKey: dhmvProduct.key,
|
||||||
displayName: dhmvProduct.display_name,
|
displayName: dhmvProduct.display_name,
|
||||||
theme: 'elevation',
|
theme: 'elevation',
|
||||||
availabilityLabel: `${dhmvProduct.native_resolution_m} m · ${dhmvProduct.acquisition_period} · laad bij selectie`,
|
availabilityLabel: `${dhmvProduct.native_resolution_m} m · ${dhmvProduct.acquisition_period} · automatisch bij selectie`,
|
||||||
attribution: dhmvProduct.attribution,
|
attribution: dhmvProduct.attribution,
|
||||||
limitationMessage: dhmvProduct.limitation_message,
|
limitationMessage: dhmvProduct.limitation_message,
|
||||||
})
|
})
|
||||||
@@ -1119,7 +1119,7 @@ export function MapWorkspace({
|
|||||||
productKey: floodProduct.key,
|
productKey: floodProduct.key,
|
||||||
displayName: floodProduct.display_name,
|
displayName: floodProduct.display_name,
|
||||||
theme: 'flood_hazard',
|
theme: 'flood_hazard',
|
||||||
availabilityLabel: `${floodProduct.native_resolution_m} m · ${floodProduct.climate_context} · T${floodProduct.return_period_years} · laad bij selectie`,
|
availabilityLabel: `${floodProduct.native_resolution_m} m · ${floodProduct.climate_context} · T${floodProduct.return_period_years} · automatisch bij selectie`,
|
||||||
attribution: floodProduct.attribution,
|
attribution: floodProduct.attribution,
|
||||||
limitationMessage: floodProduct.limitation_message,
|
limitationMessage: floodProduct.limitation_message,
|
||||||
})
|
})
|
||||||
@@ -1329,6 +1329,19 @@ export function MapWorkspace({
|
|||||||
const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey)
|
const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey)
|
||||||
?? activeTemporalSeriesGroups[0]
|
?? activeTemporalSeriesGroups[0]
|
||||||
const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES
|
const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES
|
||||||
|
const earlierTemporalOptions = activeTemporalSeries.slice(0, -1)
|
||||||
|
const selectedEarlierSnapshot = activeTemporalSeries.find((dataset) => dataset.id === earlierDatasetId)
|
||||||
|
const selectedLaterSnapshot = activeTemporalSeries.find((dataset) => dataset.id === laterDatasetId)
|
||||||
|
const selectedEarlierTime = new Date(selectedEarlierSnapshot?.observed_at ?? 0).getTime()
|
||||||
|
const laterTemporalOptions = activeTemporalSeries.filter(
|
||||||
|
(dataset) => new Date(dataset.observed_at ?? 0).getTime() > selectedEarlierTime,
|
||||||
|
)
|
||||||
|
const temporalSelectionValid = Boolean(
|
||||||
|
selectedEarlierSnapshot
|
||||||
|
&& selectedLaterSnapshot
|
||||||
|
&& selectedEarlierSnapshot.id !== selectedLaterSnapshot.id
|
||||||
|
&& selectedEarlierTime < new Date(selectedLaterSnapshot.observed_at ?? 0).getTime(),
|
||||||
|
)
|
||||||
const activeSeriesIsDailyGrb = activeTemporalSeries.length >= 2
|
const activeSeriesIsDailyGrb = activeTemporalSeries.length >= 2
|
||||||
&& activeTemporalSeries.every((dataset) => dataset.source_name === 'grb')
|
&& activeTemporalSeries.every((dataset) => dataset.source_name === 'grb')
|
||||||
&& new Date(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at ?? 0).getTime()
|
&& new Date(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at ?? 0).getTime()
|
||||||
@@ -1378,7 +1391,7 @@ export function MapWorkspace({
|
|||||||
activeSelectionResult.total_feature_count
|
activeSelectionResult.total_feature_count
|
||||||
?? activeSelectionResult.feature_count
|
?? activeSelectionResult.feature_count
|
||||||
).toLocaleString('nl-BE')} objecten gemeten`
|
).toLocaleString('nl-BE')} objecten gemeten`
|
||||||
: 'Op aanvraag'
|
: 'Automatisch bij selectie'
|
||||||
: null
|
: null
|
||||||
onSetContextLayerLabel(contextLayerLabel)
|
onSetContextLayerLabel(contextLayerLabel)
|
||||||
return () => onSetContextLayerLabel(null)
|
return () => onSetContextLayerLabel(null)
|
||||||
@@ -1841,13 +1854,26 @@ export function MapWorkspace({
|
|||||||
const resultFeatureLimit = selectionFeatureLimit(bbox)
|
const resultFeatureLimit = selectionFeatureLimit(bbox)
|
||||||
for (const theme of DATA_THEMES) {
|
for (const theme of DATA_THEMES) {
|
||||||
const dataset = themeDatasetMap[theme.id]
|
const dataset = themeDatasetMap[theme.id]
|
||||||
if (dataset && persistedDatasetSupportsSelection(dataset, bbox)) {
|
const partitioned = Boolean(
|
||||||
|
dataset
|
||||||
|
&& regionalScopeSelected
|
||||||
|
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
|
||||||
|
)
|
||||||
|
const coveringPartitions = partitioned
|
||||||
|
? themePartitionMap[theme.id].filter((partition) => datasetIntersectsSelection(partition, bbox))
|
||||||
|
: []
|
||||||
|
const persistedCoverageAvailable = Boolean(
|
||||||
|
dataset
|
||||||
|
&& persistedDatasetSupportsSelection(dataset, bbox)
|
||||||
|
&& (!partitioned || coveringPartitions.length > 0),
|
||||||
|
)
|
||||||
|
if (dataset && persistedCoverageAvailable) {
|
||||||
availableThemes.push({
|
availableThemes.push({
|
||||||
themeId: theme.id,
|
themeId: theme.id,
|
||||||
dataset,
|
dataset,
|
||||||
|
datasetIds: coveringPartitions.map((partition) => partition.id),
|
||||||
featureLimit: resultFeatureLimit,
|
featureLimit: resultFeatureLimit,
|
||||||
partitioned: regionalScopeSelected
|
partitioned,
|
||||||
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
|
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1877,16 +1903,20 @@ export function MapWorkspace({
|
|||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
setMapAnalysisDurationMs(null)
|
setMapAnalysisDurationMs(null)
|
||||||
setSelectionBbox(bbox)
|
setSelectionBbox(bbox)
|
||||||
const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)]
|
const tasks: Array<Promise<unknown>> = analysisMode === 'current'
|
||||||
|
? [loadAllThemeResults(bbox, areaId)]
|
||||||
|
: []
|
||||||
const activeDatasetSupportsSelection = !activeThemeDataset
|
const activeDatasetSupportsSelection = !activeThemeDataset
|
||||||
|| persistedDatasetSupportsSelection(activeThemeDataset, bbox)
|
|| persistedDatasetSupportsSelection(activeThemeDataset, bbox)
|
||||||
if (
|
if (
|
||||||
activeThemeAvailable && !regionalPartitionedThemeActive && !onDemandThemeActive
|
analysisMode === 'current'
|
||||||
|
&& advancedMode
|
||||||
|
&& activeThemeAvailable && !regionalPartitionedThemeActive && !onDemandThemeActive
|
||||||
&& activeDatasetSupportsSelection
|
&& activeDatasetSupportsSelection
|
||||||
) {
|
) {
|
||||||
tasks.push(onRunMapSelectionExtract(bbox, areaId))
|
tasks.push(onRunMapSelectionExtract(bbox, areaId))
|
||||||
}
|
}
|
||||||
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
|
if (analysisMode === 'evolution' && temporalSelectionValid) {
|
||||||
tasks.push(compareTemporalSnapshots(earlierDatasetId, laterDatasetId, bbox, areaId))
|
tasks.push(compareTemporalSnapshots(earlierDatasetId, laterDatasetId, bbox, areaId))
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1899,7 +1929,7 @@ export function MapWorkspace({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runTemporalComparison = () => {
|
const runTemporalComparison = () => {
|
||||||
if (!mapSelectionBbox || !earlierDatasetId || !laterDatasetId) {
|
if (!mapSelectionBbox || !temporalSelectionValid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void compareTemporalSnapshots(
|
void compareTemporalSnapshots(
|
||||||
@@ -2127,7 +2157,7 @@ export function MapWorkspace({
|
|||||||
? 'Alleen huidige toestand'
|
? 'Alleen huidige toestand'
|
||||||
: 'Bron nog niet ingeladen'
|
: 'Bron nog niet ingeladen'
|
||||||
: dataset
|
: dataset
|
||||||
? datasetAvailabilityLabel(dataset, partitions)
|
? `${datasetAvailabilityLabel(dataset, partitions)}${onDemandProduct ? ' · zo nodig automatisch aangevuld' : ''}`
|
||||||
: onDemandProduct
|
: onDemandProduct
|
||||||
? onDemandProduct.availabilityLabel
|
? onDemandProduct.availabilityLabel
|
||||||
: 'Bron nog niet ingeladen'}
|
: 'Bron nog niet ingeladen'}
|
||||||
@@ -2138,7 +2168,7 @@ export function MapWorkspace({
|
|||||||
? 'Laden'
|
? 'Laden'
|
||||||
: analysisMode === 'evolution'
|
: analysisMode === 'evolution'
|
||||||
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
|
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
|
||||||
: dataset ? 'Beschikbaar' : onDemandProduct ? 'Op aanvraag' : 'Ontbreekt'}
|
: dataset ? 'Beschikbaar' : onDemandProduct ? 'Automatisch' : 'Ontbreekt'}
|
||||||
</i>
|
</i>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -2172,7 +2202,7 @@ export function MapWorkspace({
|
|||||||
: regionalBathymetryThemeActive
|
: regionalBathymetryThemeActive
|
||||||
? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd`
|
? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd`
|
||||||
: activeThemeDataset
|
: activeThemeDataset
|
||||||
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}`
|
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}${onDemandProductMap.get(activeTheme.id) ? ' · ontbrekende lokale dekking wordt automatisch aangevuld' : ''}`
|
||||||
: activeOnDemandMapProduct
|
: activeOnDemandMapProduct
|
||||||
? `${activeOnDemandMapProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen`
|
? `${activeOnDemandMapProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen`
|
||||||
: activeTheme.description}
|
: activeTheme.description}
|
||||||
@@ -2286,16 +2316,32 @@ export function MapWorkspace({
|
|||||||
) : null}
|
) : null}
|
||||||
<label>
|
<label>
|
||||||
Van
|
Van
|
||||||
<select value={earlierDatasetId} onChange={(event) => { setEarlierDatasetId(event.target.value); clearTemporalComparison() }} disabled={activeTemporalSeries.length < 2}>
|
<select
|
||||||
{activeTemporalSeries.map((dataset) => (
|
value={earlierDatasetId}
|
||||||
|
onChange={(event) => {
|
||||||
|
const nextEarlierId = event.target.value
|
||||||
|
const nextEarlier = activeTemporalSeries.find((dataset) => dataset.id === nextEarlierId)
|
||||||
|
setEarlierDatasetId(nextEarlierId)
|
||||||
|
if (
|
||||||
|
nextEarlier
|
||||||
|
&& new Date(selectedLaterSnapshot?.observed_at ?? 0).getTime()
|
||||||
|
<= new Date(nextEarlier.observed_at ?? 0).getTime()
|
||||||
|
) {
|
||||||
|
setLaterDatasetId(activeTemporalSeries[activeTemporalSeries.length - 1]?.id ?? '')
|
||||||
|
}
|
||||||
|
clearTemporalComparison()
|
||||||
|
}}
|
||||||
|
disabled={earlierTemporalOptions.length === 0}
|
||||||
|
>
|
||||||
|
{earlierTemporalOptions.map((dataset) => (
|
||||||
<option key={dataset.id} value={dataset.id}>{formatObservationDate(dataset.observed_at)}</option>
|
<option key={dataset.id} value={dataset.id}>{formatObservationDate(dataset.observed_at)}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Naar
|
Naar
|
||||||
<select value={laterDatasetId} onChange={(event) => { setLaterDatasetId(event.target.value); clearTemporalComparison() }} disabled={activeTemporalSeries.length < 2}>
|
<select value={laterDatasetId} onChange={(event) => { setLaterDatasetId(event.target.value); clearTemporalComparison() }} disabled={laterTemporalOptions.length === 0}>
|
||||||
{activeTemporalSeries.map((dataset) => (
|
{laterTemporalOptions.map((dataset) => (
|
||||||
<option key={dataset.id} value={dataset.id}>{formatObservationDate(dataset.observed_at)}</option>
|
<option key={dataset.id} value={dataset.id}>{formatObservationDate(dataset.observed_at)}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -2303,7 +2349,7 @@ export function MapWorkspace({
|
|||||||
<button
|
<button
|
||||||
className="primary-action"
|
className="primary-action"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!mapSelectionBbox || !earlierDatasetId || !laterDatasetId || temporalComparisonLoading}
|
disabled={!mapSelectionBbox || !temporalSelectionValid || temporalComparisonLoading}
|
||||||
onClick={runTemporalComparison}
|
onClick={runTemporalComparison}
|
||||||
>
|
>
|
||||||
{temporalComparisonLoading ? 'Vergelijken…' : 'Vergelijk periode'}
|
{temporalComparisonLoading ? 'Vergelijken…' : 'Vergelijk periode'}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
bboxesEqual,
|
bboxesEqual,
|
||||||
|
datasetIntersectsSelection,
|
||||||
|
deduplicateTemporalSnapshots,
|
||||||
isMunicipalityAreaName,
|
isMunicipalityAreaName,
|
||||||
isSelectionBoundedDataset,
|
isSelectionBoundedDataset,
|
||||||
normalizeBboxFromCorners,
|
normalizeBboxFromCorners,
|
||||||
@@ -160,4 +162,31 @@ describe('map workspace selection guards', () => {
|
|||||||
expect(selectionFeatureLimit(overview)).toBe(25)
|
expect(selectionFeatureLimit(overview)).toBe(25)
|
||||||
expect(selectionFeatureLimit({ ...overview, min_x: 5, max_x: 5.1, min_y: 51, max_y: 51.1 })).toBe(1000)
|
expect(selectionFeatureLimit({ ...overview, min_x: 5, max_x: 5.1, min_y: 51, max_y: 51.1 })).toBe(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses persisted raster partitions only where their recorded bounds overlap', () => {
|
||||||
|
const molPartition = { source_metadata: { bbox_epsg4326: [5.03, 51.15, 5.24, 51.32] } }
|
||||||
|
expect(datasetIntersectsSelection(molPartition, {
|
||||||
|
min_x: 5.08,
|
||||||
|
min_y: 51.17,
|
||||||
|
max_x: 5.12,
|
||||||
|
max_y: 51.2,
|
||||||
|
crs: 'EPSG:4326',
|
||||||
|
})).toBe(true)
|
||||||
|
expect(datasetIntersectsSelection(molPartition, {
|
||||||
|
min_x: 4.3,
|
||||||
|
min_y: 50.8,
|
||||||
|
max_x: 4.4,
|
||||||
|
max_y: 50.9,
|
||||||
|
crs: 'EPSG:4326',
|
||||||
|
})).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('counts one canonical temporal snapshot per official observation date', () => {
|
||||||
|
const snapshots = deduplicateTemporalSnapshots([
|
||||||
|
{ id: 'old-2025', observed_at: '2025-12-31T23:59:59Z', imported_at: '2026-07-19T00:00:00Z' },
|
||||||
|
{ id: 'new-2025', observed_at: '2025-12-31T23:59:59Z', imported_at: '2026-07-21T00:00:00Z' },
|
||||||
|
{ id: 'year-2022', observed_at: '2022-12-31T23:59:59Z', imported_at: '2026-07-21T00:00:00Z' },
|
||||||
|
])
|
||||||
|
expect(snapshots.map((dataset) => dataset.id)).toEqual(['year-2022', 'new-2025'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -117,6 +117,49 @@ export function persistedDatasetSupportsSelection(
|
|||||||
return scale === 'detail'
|
return scale === 'detail'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function datasetIntersectsSelection(
|
||||||
|
dataset: { source_metadata?: Record<string, unknown> | null },
|
||||||
|
bbox: VectorSelectionBBox,
|
||||||
|
): boolean {
|
||||||
|
const bounds = dataset.source_metadata?.['bbox_epsg4326']
|
||||||
|
if (!Array.isArray(bounds) || bounds.length !== 4) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const [minX, minY, maxX, maxY] = bounds.map(Number)
|
||||||
|
if (![minX, minY, maxX, maxY].every(Number.isFinite)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !(
|
||||||
|
bbox.max_x < minX
|
||||||
|
|| bbox.min_x > maxX
|
||||||
|
|| bbox.max_y < minY
|
||||||
|
|| bbox.min_y > maxY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deduplicateTemporalSnapshots<
|
||||||
|
T extends {
|
||||||
|
id: string
|
||||||
|
observed_at?: string | null
|
||||||
|
imported_at?: string | null
|
||||||
|
created_at?: string | null
|
||||||
|
},
|
||||||
|
>(datasets: T[]): T[] {
|
||||||
|
const byObservation = new Map<string, T>()
|
||||||
|
for (const dataset of datasets) {
|
||||||
|
if (!dataset.observed_at) continue
|
||||||
|
const current = byObservation.get(dataset.observed_at)
|
||||||
|
const recency = new Date(dataset.imported_at ?? dataset.created_at ?? 0).getTime()
|
||||||
|
const currentRecency = new Date(current?.imported_at ?? current?.created_at ?? 0).getTime()
|
||||||
|
if (!current || recency > currentRecency || (recency === currentRecency && dataset.id > current.id)) {
|
||||||
|
byObservation.set(dataset.observed_at, dataset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(byObservation.values()).sort(
|
||||||
|
(left, right) => new Date(left.observed_at ?? 0).getTime() - new Date(right.observed_at ?? 0).getTime(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function selectionFeatureLimit(bbox: VectorSelectionBBox): number {
|
export function selectionFeatureLimit(bbox: VectorSelectionBBox): number {
|
||||||
const scale = selectionAnalysisScale(bbox)
|
const scale = selectionAnalysisScale(bbox)
|
||||||
if (scale === 'overview') return 25
|
if (scale === 'overview') return 25
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface MapThemeAcquisition {
|
|||||||
export interface MapThemeQuery<TThemeId extends string> {
|
export interface MapThemeQuery<TThemeId extends string> {
|
||||||
themeId: TThemeId
|
themeId: TThemeId
|
||||||
dataset?: DatasetCreateResponse
|
dataset?: DatasetCreateResponse
|
||||||
|
datasetIds?: string[]
|
||||||
partitioned?: boolean
|
partitioned?: boolean
|
||||||
acquisition?: MapThemeAcquisition
|
acquisition?: MapThemeAcquisition
|
||||||
acquisitionBboxes?: VectorSelectionBBox[]
|
acquisitionBboxes?: VectorSelectionBBox[]
|
||||||
@@ -102,10 +103,11 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
setThemeInsightsLoading(true)
|
setThemeInsightsLoading(true)
|
||||||
setThemeInsightsError(null)
|
setThemeInsightsError(null)
|
||||||
try {
|
try {
|
||||||
|
const queryOrder = new Map(queries.map((query, index) => [query.themeId, index]))
|
||||||
const settled = await settleWithConcurrency(
|
const settled = await settleWithConcurrency(
|
||||||
queries,
|
queries,
|
||||||
3,
|
3,
|
||||||
async ({ themeId, dataset: existingDataset, partitioned, acquisition, acquisitionBboxes, featureLimit }) => {
|
async ({ themeId, dataset: existingDataset, datasetIds, partitioned, acquisition, acquisitionBboxes, featureLimit }) => {
|
||||||
let dataset = existingDataset
|
let dataset = existingDataset
|
||||||
let acquiredDatasets: DatasetCreateResponse[] = []
|
let acquiredDatasets: DatasetCreateResponse[] = []
|
||||||
const resultLimit = featureLimit ?? 1000
|
const resultLimit = featureLimit ?? 1000
|
||||||
@@ -162,20 +164,16 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
throw new Error(`Geen persistente databron beschikbaar voor thema ${themeId}.`)
|
throw new Error(`Geen persistente databron beschikbaar voor thema ${themeId}.`)
|
||||||
}
|
}
|
||||||
const acquiredDatasetIds = acquiredDatasets.map((item) => item.id)
|
const acquiredDatasetIds = acquiredDatasets.map((item) => item.id)
|
||||||
|
const selectedDatasetIds = acquiredDatasetIds.length > 0 ? acquiredDatasetIds : datasetIds ?? []
|
||||||
const acquiredAsPartitions = acquiredDatasetIds.length > 1
|
const acquiredAsPartitions = acquiredDatasetIds.length > 1
|
||||||
return {
|
const result = dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
|
||||||
themeId,
|
|
||||||
dataset,
|
|
||||||
partitioned,
|
|
||||||
acquisition,
|
|
||||||
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
|
|
||||||
? terrainSelectionToMapSelection(
|
? terrainSelectionToMapSelection(
|
||||||
partitioned || acquiredAsPartitions
|
partitioned || acquiredAsPartitions
|
||||||
? await datasetsApi.selectTerrainPartitions(selectedProjectId, {
|
? await datasetsApi.selectTerrainPartitions(selectedProjectId, {
|
||||||
bbox,
|
bbox,
|
||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
product_key: String(dataset.source_metadata?.['product_key'] ?? 'dtm_1m'),
|
product_key: String(dataset.source_metadata?.['product_key'] ?? 'dtm_1m'),
|
||||||
...(acquiredAsPartitions ? { dataset_ids: acquiredDatasetIds } : {}),
|
...(selectedDatasetIds.length > 0 ? { dataset_ids: selectedDatasetIds } : {}),
|
||||||
})
|
})
|
||||||
: await datasetsApi.selectTerrain(selectedProjectId, dataset.id, {
|
: await datasetsApi.selectTerrain(selectedProjectId, dataset.id, {
|
||||||
bbox,
|
bbox,
|
||||||
@@ -189,7 +187,7 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
bbox,
|
bbox,
|
||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
product_key: String(dataset.source_metadata?.['product_key'] ?? 'pluviaal_current_t100'),
|
product_key: String(dataset.source_metadata?.['product_key'] ?? 'pluviaal_current_t100'),
|
||||||
...(acquiredAsPartitions ? { dataset_ids: acquiredDatasetIds } : {}),
|
...(selectedDatasetIds.length > 0 ? { dataset_ids: selectedDatasetIds } : {}),
|
||||||
})
|
})
|
||||||
: await datasetsApi.selectFloodHazard(selectedProjectId, dataset.id, {
|
: await datasetsApi.selectFloodHazard(selectedProjectId, dataset.id, {
|
||||||
bbox,
|
bbox,
|
||||||
@@ -201,30 +199,45 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
bbox,
|
bbox,
|
||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
}))
|
}))
|
||||||
: dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry'
|
: dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry'
|
||||||
? bathymetryRasterSelectionToMapSelection(await datasetsApi.selectBathymetryRaster(selectedProjectId, dataset.id, {
|
? bathymetryRasterSelectionToMapSelection(await datasetsApi.selectBathymetryRaster(selectedProjectId, dataset.id, {
|
||||||
bbox,
|
bbox,
|
||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
}))
|
}))
|
||||||
: acquiredAsPartitions && dataset.dataset_type !== 'raster'
|
: selectedDatasetIds.length > 1 && dataset.dataset_type !== 'raster'
|
||||||
? await datasetsApi.selectVectorFeaturePartitions(selectedProjectId, {
|
? await datasetsApi.selectVectorFeaturePartitions(selectedProjectId, {
|
||||||
dataset_ids: acquiredDatasetIds,
|
dataset_ids: selectedDatasetIds,
|
||||||
bbox,
|
bbox,
|
||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
limit: resultLimit,
|
limit: resultLimit,
|
||||||
})
|
})
|
||||||
: dataset.source_name === 'vmm_vha_bathymetry_profiles' && partitioned
|
: dataset.source_name === 'vmm_vha_bathymetry_profiles' && partitioned
|
||||||
? await datasetsApi.selectBathymetryProfilePartitions(selectedProjectId, {
|
? await datasetsApi.selectBathymetryProfilePartitions(selectedProjectId, {
|
||||||
bbox,
|
bbox,
|
||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
limit: resultLimit,
|
limit: resultLimit,
|
||||||
})
|
})
|
||||||
: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
|
: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
|
||||||
bbox,
|
bbox,
|
||||||
area_id: areaId,
|
area_id: areaId,
|
||||||
limit: resultLimit,
|
limit: resultLimit,
|
||||||
}),
|
})
|
||||||
|
const insight: MapThemeInsight<TThemeId> = {
|
||||||
|
themeId,
|
||||||
|
dataset,
|
||||||
|
partitioned,
|
||||||
|
acquisition,
|
||||||
|
result,
|
||||||
}
|
}
|
||||||
|
if (requestSequence.current === sequence) {
|
||||||
|
setThemeInsights((current) => (
|
||||||
|
[...current.filter((item) => item.themeId !== themeId), insight]
|
||||||
|
.sort(
|
||||||
|
(left, right) => (queryOrder.get(left.themeId) ?? 0) - (queryOrder.get(right.themeId) ?? 0),
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return insight
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
|
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
|
||||||
|
|||||||
Reference in New Issue
Block a user