fix: align WALOUS with official class codes
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-22 04:30:54 +02:00
parent 20da1d3dd4
commit 86dd1a5689
9 changed files with 144 additions and 53 deletions
@@ -41,13 +41,15 @@ class WalousProduct:
download_url: str
source_sha256_filename: str
accuracy_label: str
observation_start: datetime
observation_end: datetime
class WalousLandCoverService:
PROVIDER = "spw_walous_land_cover"
SOURCE_CRS = "EPSG:3812"
SOURCE_RESOLUTION_M = 1.0
SOURCE_VALUE_UNIT = "class_1_11"
SOURCE_VALUE_UNIT = "walous_class_code"
THEME = "land_cover_use"
METRIC_KIND = "categorical_area"
NODATA = 255
@@ -58,31 +60,33 @@ class WalousLandCoverService:
"geconfigureerde analyseresolutie. Oppervlakten zijn celgebaseerde schattingen; de kaart is landbedekking, "
"geen juridisch landgebruik, eigendom, boomtelling of actuele terreinwaarneming."
)
# WALOUS has 11 semantic classes, but its official raster codes are not a
# continuous 1..11 range. Codes 80 and 90 distinguish low woody cover.
CLASS_LABELS = {
1: "Jaarlijks wisselende kruidlaag",
2: "Jaarronde kruidlaag",
3: "Naaldbomen hoger dan 3 m",
4: "Loofbomen hoger dan 3 m",
5: "Naaldbomen tot 3 m",
6: "Loofbomen tot 3 m",
7: "Kale bodem",
8: "Oppervlaktewater",
9: "Kunstmatige bodembedekking",
10: "Spoorweg",
11: "Kunstmatige constructies boven maaiveld",
1: "Kunstmatige bodembedekking",
2: "Kunstmatige constructies boven maaiveld",
3: "Spoorweg",
4: "Kale bodem",
5: "Oppervlaktewater",
6: "Jaarlijks wisselende kruidlaag",
7: "Jaarronde kruidlaag",
8: "Naaldbomen hoger dan 3 m",
9: "Loofbomen hoger dan 3 m",
80: "Naaldbomen tot 3 m",
90: "Loofbomen tot 3 m",
}
CLASS_COLORS = {
1: (236, 202, 73),
2: (161, 201, 78),
3: (28, 89, 51),
4: (52, 132, 72),
5: (78, 125, 70),
6: (107, 164, 87),
7: (194, 165, 119),
8: (44, 129, 185),
9: (155, 155, 155),
10: (68, 68, 68),
11: (183, 72, 67),
1: (155, 155, 155),
2: (183, 72, 67),
3: (68, 68, 68),
4: (194, 165, 119),
5: (44, 129, 185),
6: (236, 202, 73),
7: (161, 201, 78),
8: (28, 89, 51),
9: (52, 132, 72),
80: (78, 125, 70),
90: (107, 164, 87),
}
@staticmethod
@@ -101,6 +105,8 @@ class WalousLandCoverService:
),
source_sha256_filename="walous_land_cover_2020_3812.sha256",
accuracy_label="Officiele globale nauwkeurigheid 83,30%",
observation_start=datetime(2020, 4, 1, tzinfo=UTC),
observation_end=datetime(2020, 4, 24, 23, 59, 59, tzinfo=UTC),
),
WalousProduct(
key="walous_land_cover_2023",
@@ -115,6 +121,8 @@ class WalousLandCoverService:
),
source_sha256_filename="walous_land_cover_2023_3812.sha256",
accuracy_label="Officiele globale nauwkeurigheid 87,10%",
observation_start=datetime(2023, 5, 27, tzinfo=UTC),
observation_end=datetime(2023, 6, 25, 23, 59, 59, tzinfo=UTC),
),
)
return {product.key: product for product in products}
@@ -145,8 +153,8 @@ class WalousLandCoverService:
catalog_url=product.catalog_url,
attribution=WalousLandCoverService.ATTRIBUTION,
license_note=WalousLandCoverService.LICENSE_NOTE,
legend_min_label="WALOUS klasse 1",
legend_max_label="WALOUS klasse 11",
legend_min_label="WALOUS klasse 1 (kunstmatige bodem)",
legend_max_label="WALOUS klasse 90 (loofbomen tot 3 m)",
included_source_values=list(WalousLandCoverService.CLASS_LABELS),
limitation_message=f"{WalousLandCoverService.LIMITATION} {product.accuracy_label}.",
coverage_zones=["wallonia"],
@@ -250,7 +258,12 @@ class WalousLandCoverService:
classes = set(np.unique(valid).astype(int).tolist())
unexpected = sorted(classes - set(WalousLandCoverService.CLASS_LABELS))
if unexpected:
raise AppError(code="WALOUS_SOURCE_INVALID_VALUES", message="WALOUS contains classes outside the governed 1-11 legend", details={"unexpected_classes": unexpected}, status_code=409)
raise AppError(
code="WALOUS_SOURCE_INVALID_VALUES",
message="WALOUS contains classes outside the governed 11-class code set",
details={"unexpected_classes": unexpected},
status_code=409,
)
profile = {
"driver": "GTiff",
"width": width,
@@ -345,7 +358,7 @@ class WalousLandCoverService:
source_sha256_path = source_path.with_name(product.source_sha256_filename)
source_sha256 = source_sha256_path.read_text(encoding="ascii").strip().split()[0] if source_sha256_path.is_file() else None
acquired_at = datetime.now(UTC)
observed_at = datetime(product.observation_year, 12, 31, 23, 59, 59, tzinfo=UTC)
observed_at = product.observation_end
spatial_series_hash = hashlib.sha256(json.dumps({"bbox": identity["bbox_epsg4326"], "area_id": identity["area_id"], "resolution": identity["analysis_resolution_m"]}, sort_keys=True).encode()).hexdigest()[:24]
dataset = DatasetService.import_raster_bytes(
db,
@@ -357,8 +370,8 @@ class WalousLandCoverService:
source_name=WalousLandCoverService.PROVIDER,
temporal_series_key=f"spw:walous:land-cover:{spatial_series_hash}",
observed_at=observed_at,
valid_from=datetime(product.observation_year, 1, 1, tzinfo=UTC),
valid_to=observed_at,
valid_from=product.observation_start,
valid_to=product.observation_end,
temporal_granularity="year",
source_version=product.source_version,
source_metadata={
@@ -374,6 +387,8 @@ class WalousLandCoverService:
"source_value_unit": WalousLandCoverService.SOURCE_VALUE_UNIT,
"class_labels": WalousLandCoverService.CLASS_LABELS,
"observation_year": product.observation_year,
"observation_start": product.observation_start.isoformat(),
"observation_end": product.observation_end.isoformat(),
"valid_pixel_count": validation["valid_pixel_count"],
"classes_present": validation["classes_present"],
"bbox_epsg4326": bbox_4326,
@@ -480,12 +495,12 @@ class WalousLandCoverService:
metric_specs = [
("land_cover_observed_area_ha", "Gekarteerde landbedekking", set(WalousLandCoverService.CLASS_LABELS)),
("forest_cover_area_ha", "Boom- en bosbedekking", {3, 4, 5, 6}),
("surface_water_area_ha", "Oppervlaktewater", {8}),
("artificial_cover_area_ha", "Kunstmatige bedekking en constructies", {9, 10, 11}),
("annual_herbaceous_cover_area_ha", "Jaarlijks wisselende kruidlaag", {1}),
("permanent_herbaceous_cover_area_ha", "Jaarronde kruidlaag", {2}),
("bare_soil_area_ha", "Kale bodem", {7}),
("forest_cover_area_ha", "Boom- en bosbedekking", {8, 9, 80, 90}),
("surface_water_area_ha", "Oppervlaktewater", {5}),
("artificial_cover_area_ha", "Kunstmatige bedekking en constructies", {1, 2, 3}),
("annual_herbaceous_cover_area_ha", "Jaarlijks wisselende kruidlaag", {6}),
("permanent_herbaceous_cover_area_ha", "Jaarronde kruidlaag", {7}),
("bare_soil_area_ha", "Kale bodem", {4}),
]
metrics = [
ThematicRasterMetric(