feat: operationalize Flemish land and nature themes
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 22:38:25 +02:00
parent 0718d0ebba
commit c787fb2184
27 changed files with 2010 additions and 12 deletions
@@ -45,6 +45,7 @@ class ThematicRasterProduct:
legend_min_label: str
legend_max_label: str
limitation_message: str
included_source_values: tuple[int, ...] = ()
class ThematicRasterAcquisitionService:
@@ -100,6 +101,44 @@ class ThematicRasterAcquisitionService:
"synoniem met natuur, bos, publieke toegankelijkheid of planologische bestemming."
),
),
ThematicRasterProduct(
key="forest_land_use_2025",
display_name="Bos volgens Landgebruik Vlaanderen 2025",
theme="forest",
metric_kind="binary_area",
coverage_id="lu:lu_landgebruik_vlaa_2025_v3",
native_resolution_m=10.0,
source_value_unit="class_0_1",
observation_year=2025,
source_version="Toestand 2025 versie 3",
catalog_url="https://www.vlaanderen.be/datavindplaats/catalogus/landgebruik-vlaanderen-toestand-2025",
legend_min_label="Geen bosklasse",
legend_max_label="Bos",
limitation_message=(
"10 m-afleiding van bronklasse 12 (bos) uit Landgebruik Vlaanderen 2025. De oppervlakte is "
"resolutiegebonden en vormt geen juridische bosgrens, boomtelling, kroonbedekking of houtvolume."
),
included_source_values=(12,),
),
ThematicRasterProduct(
key="agricultural_land_use_2025",
display_name="Akker en landbouwgrasland 2025",
theme="agriculture",
metric_kind="binary_area",
coverage_id="lu:lu_landgebruik_vlaa_2025_v3",
native_resolution_m=10.0,
source_value_unit="class_0_1",
observation_year=2025,
source_version="Toestand 2025 versie 3",
catalog_url="https://www.vlaanderen.be/datavindplaats/catalogus/landgebruik-vlaanderen-toestand-2025",
legend_min_label="Ander landgebruik",
legend_max_label="Akker of landbouwgrasland",
limitation_message=(
"10 m-afleiding van bronklassen 13 (akker) en 14 (grasland in landbouwgebruik). Dit is werkelijk "
"landgebruik en geen ALZ-perceelaangifte, eigendomsgrens, teeltregister of juridische bestemming."
),
included_source_values=(13, 14),
),
ThematicRasterProduct(
key="population_density_2019",
display_name="Inwonersdichtheid per hectare 2019",
@@ -176,6 +215,7 @@ class ThematicRasterAcquisitionService:
license_note=ThematicRasterAcquisitionService.LICENSE_NOTE,
legend_min_label=product.legend_min_label,
legend_max_label=product.legend_max_label,
included_source_values=list(product.included_source_values),
limitation_message=product.limitation_message,
).model_dump()
for product in ThematicRasterAcquisitionService._products().values()
@@ -461,7 +501,29 @@ class ThematicRasterAcquisitionService:
invalid = np.ma.getmaskarray(band) | ~np.isfinite(raw)
if source.nodata is not None:
invalid |= np.isclose(raw, float(source.nodata))
normalized = np.ma.array(raw, mask=invalid)
source_values = np.ma.array(raw, mask=invalid).compressed().astype("float64")
if product.included_source_values:
rounded = np.rint(source_values)
if not np.allclose(source_values, rounded, atol=0.0001):
raise AppError(
code="THEMATIC_RASTER_INVALID_VALUES",
message="Categorical land-use coverage contains non-integer source classes",
status_code=502,
)
if source_values.size and (
float(source_values.min()) < 0
or float(source_values.max()) > 255
):
raise AppError(
code="THEMATIC_RASTER_INVALID_VALUES",
message="Categorical land-use coverage contains source classes outside the governed range",
status_code=502,
)
source_classes = np.where(invalid, 0, np.rint(raw)).astype("int16")
binary = np.isin(source_classes, product.included_source_values).astype("float32")
normalized = np.ma.array(binary, mask=invalid)
else:
normalized = np.ma.array(raw, mask=invalid)
values = normalized.compressed().astype("float64")
ThematicRasterAcquisitionService._validate_values(values, product)
profile = source.profile.copy()
@@ -482,6 +544,9 @@ class ThematicRasterAcquisitionService:
"maximum_value": float(values.max()),
"p02_value": float(np.percentile(values, 2)),
"p98_value": float(np.percentile(values, 98)),
"included_source_values": list(product.included_source_values),
"source_minimum_value": float(source_values.min()),
"source_maximum_value": float(source_values.max()),
}
except AppError:
raise
@@ -563,6 +628,7 @@ class ThematicRasterAcquisitionService:
"analysis_resolution_m": product.native_resolution_m,
"source_crs": ThematicRasterAcquisitionService.SOURCE_CRS,
"source_value_unit": product.source_value_unit,
"included_source_values": list(product.included_source_values),
"observation_year": product.observation_year,
"observation_date_precision": "year",
"valid_pixel_count": validation["valid_pixel_count"],