Add governed Wallonia and Brussels orthophotos
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:
Jens
2026-07-26 22:02:10 +02:00
parent 0c328fd3d1
commit ec6f2d9061
17 changed files with 238 additions and 49 deletions
+2
View File
@@ -7,6 +7,8 @@ MAX_UPLOAD_MB=500
CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202
ORTHOPHOTO_ENABLED=true
ORTHOPHOTO_WMS_URL=https://geo.api.vlaanderen.be/OMWRGBMRVL/wms
SPW_ORTHOPHOTO_WMS_URL=https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer
BRUSSELS_ORTHOPHOTO_WMS_URL=https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows
ORTHOPHOTO_WMS_LAYER=Ortho
ORTHOPHOTO_RESOLUTION_M=1.0
ORTHOPHOTO_MIN_SIDE_M=128
+5 -1
View File
@@ -1336,7 +1336,10 @@ not copied into queryable properties.
`GET /api/v1/projects/{project_id}/datasets/orthophoto/products` lists the
governed product allowlist. `POST .../datasets/orthophoto/acquire` accepts an
explicit EPSG:4326 map rectangle plus `product_key` and stores the official
Digitaal Vlaanderen WMS response as a canonical EPSG:31370 raster Dataset. The
regional WMS response as a canonical EPSG:31370 raster Dataset. Digitaal
Vlaanderen, SPW (`wallonia_latest`) and Paradigm UrbIS (`brussels_latest`) are
allowlisted. The two regional products are bound to persisted Wallonia and
Brussels-Capital Region Areas. The
default safety envelope is 128-1,024 m per side, 1 m/pixel, 32 MiB and a
24-hour exact-request cache. It runs synchronously behind the existing Job
abstraction and never during startup.
@@ -1348,6 +1351,7 @@ from configured-YOLO/current-GRB QA. `GET .../datasets/{dataset_id}/raster/image
is the constrained binary PNG endpoint used by the MapLibre image overlay.
Settings: `ORTHOPHOTO_ENABLED`, `ORTHOPHOTO_WMS_URL`,
`SPW_ORTHOPHOTO_WMS_URL`, `BRUSSELS_ORTHOPHOTO_WMS_URL`,
`ORTHOPHOTO_WMS_LAYER`, `ORTHOPHOTO_RESOLUTION_M`,
`ORTHOPHOTO_MIN_SIDE_M`, `ORTHOPHOTO_MAX_SIDE_M`,
`ORTHOPHOTO_TIMEOUT_SECONDS`, `ORTHOPHOTO_MAX_RESPONSE_MB` and
+8
View File
@@ -40,6 +40,14 @@ class Settings(BaseSettings):
validation_alias="ORTHOPHOTO_WMS_URL",
)
orthophoto_wms_layer: str = Field(default="Ortho", validation_alias="ORTHOPHOTO_WMS_LAYER")
spw_orthophoto_wms_url: str = Field(
default="https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer",
validation_alias="SPW_ORTHOPHOTO_WMS_URL",
)
brussels_orthophoto_wms_url: str = Field(
default="https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows",
validation_alias="BRUSSELS_ORTHOPHOTO_WMS_URL",
)
orthophoto_resolution_m: float = Field(default=1.0, gt=0, validation_alias="ORTHOPHOTO_RESOLUTION_M")
orthophoto_min_side_m: float = Field(default=128.0, gt=0, validation_alias="ORTHOPHOTO_MIN_SIDE_M")
orthophoto_max_side_m: float = Field(default=1024.0, gt=0, validation_alias="ORTHOPHOTO_MAX_SIDE_M")
+4
View File
@@ -24,6 +24,10 @@ class OrthophotoProductRead(BaseModel):
color_mode: str
catalog_url: str
limitation_message: str
provider: str
coverage_zone: str
attribution: str
license_note: str
class OrthophotoAcquisitionResult(BaseModel):
@@ -379,6 +379,7 @@ REGIONAL_THEME_DATASETS: dict[str, dict[str, dict[str, tuple[str, ...]]]] = {
"elevation": {"spw_terrain": ()},
"flood_climate": {"spw_flood_hazard": ("flood_hazard",)},
"bathymetry": {"spw_bathymetry": ()},
"orthophoto": {"spw_orthophoto": ()},
},
"urbis": {
"buildings": {"urbis": ("buildings",)},
@@ -386,6 +387,7 @@ REGIONAL_THEME_DATASETS: dict[str, dict[str, dict[str, tuple[str, ...]]]] = {
"roads": {"urbis": ("roads",)},
"surface_water": {"urbis": ("water",)},
"land_cover_use": {"urbis": ("space_occupation", "forest")},
"orthophoto": {"urbis_orthophoto": ()},
},
}
@@ -37,6 +37,12 @@ class OrthophotoProduct:
layer: str
catalog_url: str
limitation_message: str
provider: str = "digitaal_vlaanderen_orthophoto"
source_label: str = "Digitaal Vlaanderen WMS"
attribution: str = "Bron: Orthofotomozaiek Vlaanderen, Digitaal Vlaanderen"
license_note: str = "Gebruik volgens het gebruiksrecht geografische webdiensten van Digitaal Vlaanderen."
series_namespace: str = "digitaal-vlaanderen"
coverage_zone: str = "flanders"
supports_detection: bool = False
color_mode: str = "rgb"
observed_at: datetime | None = None
@@ -70,6 +76,46 @@ class OrthophotoAcquisitionService:
supports_detection=True,
)
]
products.extend(
[
OrthophotoProduct(
key="wallonia_latest",
display_name="Meest recente orthofoto Wallonië",
observation_label="Laatste volledige SPW-campagne",
temporal_granularity="snapshot",
native_resolution_m=0.25,
wms_url=settings.spw_orthophoto_wms_url,
layer="0",
catalog_url="https://geoportail.wallonie.be/catalogue/e2a615fe-7a2c-4eb3-9dc3-63f466538dda.html",
limitation_message="Laatste volledige SPW-orthofotocampagne; de actuele service kan van editie wisselen en de exacte opnamedatum kan per tegel verschillen.",
provider="spw_orthophoto",
source_label="SPW ORTHO_LAST WMS",
attribution="Bron: Service public de Wallonie (SPW), Orthophotos - dernière campagne disponible",
license_note="CC BY 4.0; citeer SPW en vermeld wijzigingen.",
series_namespace="spw",
coverage_zone="wallonia",
supports_detection=True,
),
OrthophotoProduct(
key="brussels_latest",
display_name="Meest recente orthofoto Brussel",
observation_label="Meest recent beschikbaar via UrbIS",
temporal_granularity="snapshot",
native_resolution_m=0.15,
wms_url=settings.brussels_orthophoto_wms_url,
layer="Ortho",
catalog_url="https://data.mobility.brussels/info/Ortho",
limitation_message="Samengestelde meest recente UrbIS-orthofoto; de actuele service kan van editie wisselen en de exacte opnamedatum kan per tegel verschillen.",
provider="urbis_orthophoto",
source_label="Paradigm UrbIS WMS",
attribution="Bron: Paradigm, UrbIS Orthophoto",
license_note="CC0 volgens de officiële Brusselse datasetfiche; bronvermelding blijft in GeoIntel behouden.",
series_namespace="urbis",
coverage_zone="brussels",
supports_detection=True,
),
]
)
for year in range(2025, 2011, -1):
products.append(
OrthophotoProduct(
@@ -163,6 +209,10 @@ class OrthophotoAcquisitionService:
color_mode=product.color_mode,
catalog_url=product.catalog_url,
limitation_message=product.limitation_message,
provider=product.provider,
coverage_zone=product.coverage_zone,
attribution=product.attribution,
license_note=product.license_note,
).model_dump()
for product in OrthophotoAcquisitionService._products(resolved_settings).values()
]
@@ -217,7 +267,7 @@ class OrthophotoAcquisitionService:
bbox_4326 = [min_x, min_y, max_x, max_y]
bbox_31370 = [float(value) for value in lambert_bounds]
request_identity = {
"provider": OrthophotoAcquisitionService.PROVIDER,
"provider": product.provider,
"product_key": product.key,
"wms_url": product.wms_url,
"layer": product.layer,
@@ -259,9 +309,45 @@ class OrthophotoAcquisitionService:
}
@staticmethod
def _validate_area_scope(db, project_id: UUID, area_id: UUID | None, bbox_epsg4326: list[float]) -> None:
def _validate_area_scope(
db,
project_id: UUID,
area_id: UUID | None,
bbox_epsg4326: list[float],
product: OrthophotoProduct,
) -> None:
if not db.get(Project, project_id):
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
selection = box(*bbox_epsg4326)
transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
selection_metric = shapely_transform(transformer.transform, selection)
def require_contains(candidate: Area, *, code: str, message: str) -> None:
candidate_metric = shapely_transform(transformer.transform, to_shape(candidate.geometry))
overlap_ratio = candidate_metric.intersection(selection_metric).area / selection_metric.area
if overlap_ratio < 0.99:
raise AppError(
code=code,
message=message,
details={"coverage_ratio": overlap_ratio, "coverage_zone": product.coverage_zone},
status_code=422,
)
if product.coverage_zone in {"wallonia", "brussels"}:
scope_name = "Wallonia" if product.coverage_zone == "wallonia" else "Brussels-Capital Region"
scope = db.query(Area).filter(Area.project_id == project_id, Area.name == scope_name).first()
if scope is None:
raise AppError(
code="ORTHOPHOTO_COVERAGE_ZONE_NOT_MATERIALIZED",
message="Persist the governed regional coverage geometry before acquiring this orthophoto product",
details={"coverage_zone": product.coverage_zone},
status_code=409,
)
require_contains(
scope,
code="ORTHOPHOTO_SELECTION_OUTSIDE_COVERAGE_ZONE",
message="Keep the orthophoto rectangle inside the official provider coverage zone",
)
if area_id is None:
return
area = db.get(Area, area_id)
@@ -269,19 +355,11 @@ class OrthophotoAcquisitionService:
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
if area.project_id != project_id:
raise AppError(code="INVALID_DATASET_SCOPE", message="Area does not belong to this project", status_code=400)
selection = box(*bbox_epsg4326)
area_geometry = to_shape(area.geometry)
transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
selection_metric = shapely_transform(transformer.transform, selection)
area_metric = shapely_transform(transformer.transform, area_geometry)
overlap_ratio = area_metric.intersection(selection_metric).area / selection_metric.area
if overlap_ratio < 0.99:
raise AppError(
code="ORTHOPHOTO_SELECTION_OUTSIDE_AREA",
message="Keep the orthophoto rectangle inside the selected work area",
details={"coverage_ratio": overlap_ratio},
status_code=422,
)
require_contains(
area,
code="ORTHOPHOTO_SELECTION_OUTSIDE_AREA",
message="Keep the orthophoto rectangle inside the selected work area",
)
@staticmethod
def _cached_dataset(
@@ -291,14 +369,15 @@ class OrthophotoAcquisitionService:
settings: Settings,
product: OrthophotoProduct,
) -> Dataset | None:
if product.key == "most_recent" and settings.orthophoto_cache_ttl_hours <= 0:
is_live_product = product.key == "most_recent" or product.key.endswith("_latest")
if is_live_product and settings.orthophoto_cache_ttl_hours <= 0:
return None
candidate = (
db.query(Dataset)
.filter(
Dataset.project_id == project_id,
Dataset.name == filename,
Dataset.source_name == OrthophotoAcquisitionService.PROVIDER,
Dataset.source_name == product.provider,
Dataset.status == "ready",
)
.order_by(Dataset.imported_at.desc())
@@ -311,7 +390,7 @@ class OrthophotoAcquisitionService:
return None
if imported_at.tzinfo is None:
imported_at = imported_at.replace(tzinfo=UTC)
if product.key == "most_recent" and datetime.now(UTC) - imported_at > timedelta(hours=settings.orthophoto_cache_ttl_hours):
if is_live_product and datetime.now(UTC) - imported_at > timedelta(hours=settings.orthophoto_cache_ttl_hours):
return None
return candidate
@@ -384,9 +463,9 @@ class OrthophotoAcquisitionService:
with output_memory.open(**profile) as output:
output.write(image)
output.update_tags(
source=f"Digitaal Vlaanderen WMS {product.layer}",
source=f"{product.source_label} {product.layer}",
source_url=prepared["request_url"],
attribution=OrthophotoAcquisitionService.ATTRIBUTION,
attribution=product.attribution,
acquisition="explicit_bounded_map_selection",
)
return output_memory.read()
@@ -414,7 +493,13 @@ class OrthophotoAcquisitionService:
raise AppError(code="ORTHOPHOTO_NOT_CONFIGURED", message="Official orthophoto acquisition is disabled", status_code=503)
prepared = OrthophotoAcquisitionService._prepared_request(payload, resolved_settings)
product: OrthophotoProduct = prepared["product"]
OrthophotoAcquisitionService._validate_area_scope(db, project_id, payload.area_id, prepared["bbox_epsg4326"])
OrthophotoAcquisitionService._validate_area_scope(
db,
project_id,
payload.area_id,
prepared["bbox_epsg4326"],
product,
)
filename = f"orthofoto_{product.key}_{prepared['request_hash'][:12]}.tif"
cached = None if payload.force_refresh else OrthophotoAcquisitionService._cached_dataset(
@@ -428,7 +513,7 @@ class OrthophotoAcquisitionService:
return OrthophotoAcquisitionResult(
output_dataset_id=cached.id,
reused=True,
provider=OrthophotoAcquisitionService.PROVIDER,
provider=product.provider,
product_key=product.key,
display_name=product.display_name,
observation_label=product.observation_label,
@@ -440,7 +525,7 @@ class OrthophotoAcquisitionService:
resolution_m=resolved_settings.orthophoto_resolution_m,
bbox_epsg4326=prepared["bbox_epsg4326"],
bbox_epsg31370=prepared["bbox_epsg31370"],
attribution=OrthophotoAcquisitionService.ATTRIBUTION,
attribution=product.attribution,
limitation_message=product.limitation_message,
).model_dump(mode="json")
@@ -454,21 +539,21 @@ class OrthophotoAcquisitionService:
area_id=payload.area_id,
filename=filename,
content=geotiff_content,
source=f"Digitaal Vlaanderen WMS {product.layer}",
source_name=OrthophotoAcquisitionService.PROVIDER,
temporal_series_key=f"digitaal-vlaanderen:orthophoto:{prepared['spatial_hash'][:24]}",
source=f"{product.source_label} {product.layer}",
source_name=product.provider,
temporal_series_key=f"{product.series_namespace}:orthophoto:{prepared['spatial_hash'][:24]}",
observed_at=observed_at,
valid_from=product.valid_from or observed_at,
valid_to=product.valid_to,
temporal_granularity=product.temporal_granularity,
source_version=(
f"most_recent_at_{acquired_at.date().isoformat()}"
if product.key == "most_recent"
f"{product.key}_at_{acquired_at.date().isoformat()}"
if product.key == "most_recent" or product.key.endswith("_latest")
else product.key
),
content_type="image/tiff",
source_metadata={
"provider": OrthophotoAcquisitionService.PROVIDER,
"provider": product.provider,
"service": "WMS",
"service_version": "1.3.0",
"product_key": product.key,
@@ -481,8 +566,9 @@ class OrthophotoAcquisitionService:
"supports_detection": product.supports_detection,
"layer": product.layer,
"catalog_url": product.catalog_url,
"attribution": OrthophotoAcquisitionService.ATTRIBUTION,
"license_note": "Gebruik volgens het gebruiksrecht geografische webdiensten van Digitaal Vlaanderen.",
"attribution": product.attribution,
"license_note": product.license_note,
"coverage_zone": product.coverage_zone,
},
provenance_metadata={
"acquisition": "explicit_bounded_map_selection",
@@ -502,7 +588,7 @@ class OrthophotoAcquisitionService:
return OrthophotoAcquisitionResult(
output_dataset_id=dataset.id,
reused=False,
provider=OrthophotoAcquisitionService.PROVIDER,
provider=product.provider,
product_key=product.key,
display_name=product.display_name,
observation_label=product.observation_label,
@@ -514,7 +600,7 @@ class OrthophotoAcquisitionService:
resolution_m=resolved_settings.orthophoto_resolution_m,
bbox_epsg4326=prepared["bbox_epsg4326"],
bbox_epsg31370=prepared["bbox_epsg31370"],
attribution=OrthophotoAcquisitionService.ATTRIBUTION,
attribution=product.attribution,
limitation_message=product.limitation_message,
).model_dump(mode="json")
@@ -524,7 +610,7 @@ class OrthophotoAcquisitionService:
if (
dataset is None
or dataset.project_id != project_id
or dataset.source_name != OrthophotoAcquisitionService.PROVIDER
or dataset.source_name not in {"digitaal_vlaanderen_orthophoto", "spw_orthophoto", "urbis_orthophoto"}
or dataset.status != "ready"
or not dataset.storage_path
or not Path(dataset.storage_path).is_file()
@@ -37,6 +37,8 @@ SOURCE_POLICIES: dict[str, SourcePolicy] = {
"Gebouwen- en adressenregister", "rolling_snapshot", 90
),
"digitaal_vlaanderen_orthophoto": SourcePolicy("Orthofoto Vlaanderen", "rolling_snapshot", 180),
"spw_orthophoto": SourcePolicy("Orthofoto Wallonië", "rolling_snapshot", 365),
"urbis_orthophoto": SourcePolicy("Orthofoto Brussel", "rolling_snapshot", 365),
"agentschap_landbouw_zeevisserij_agricultural_parcels": SourcePolicy(
"Landbouwgebruikspercelen", "annual_release"
),
@@ -151,7 +151,66 @@ def test_orthophoto_product_registry_exposes_only_governed_official_layers() ->
assert keys[0] == "most_recent"
assert {"2025", "2012", "2008_2011", "2000_2003", "1979_1990", "1971"}.issubset(keys)
assert next(item for item in products if item["key"] == "most_recent")["supports_detection"] is True
assert all(item["supports_detection"] is False for item in products if item["key"] != "most_recent")
detection_keys = {item["key"] for item in products if item["supports_detection"]}
assert detection_keys == {"most_recent", "wallonia_latest", "brussels_latest"}
by_key = {item["key"]: item for item in products}
assert by_key["wallonia_latest"]["provider"] == "spw_orthophoto"
assert by_key["wallonia_latest"]["coverage_zone"] == "wallonia"
assert by_key["brussels_latest"]["provider"] == "urbis_orthophoto"
assert by_key["brussels_latest"]["coverage_zone"] == "brussels"
@pytest.mark.parametrize(
("product_key", "provider", "layer", "coverage_zone"),
[
("wallonia_latest", "spw_orthophoto", "0", "wallonia"),
("brussels_latest", "urbis_orthophoto", "Ortho", "brussels"),
],
)
def test_regional_orthophoto_products_bind_provider_and_governed_scope(
tmp_path, product_key: str, provider: str, layer: str, coverage_zone: str
) -> None:
project_id = uuid4()
payload = _selection_payload(product_key=product_key)
scope = Area(
id=uuid4(),
project_id=project_id,
name="Wallonia" if coverage_zone == "wallonia" else "Brussels-Capital Region",
geometry=from_shape(
MultiPolygon(
[
box(
payload.bbox.min_x - 0.01,
payload.bbox.min_y - 0.01,
payload.bbox.max_x + 0.01,
payload.bbox.max_y + 0.01,
)
]
),
srid=4326,
),
)
db = FakeSession({(Project, project_id): Project(id=project_id, name="Belgium")}, query_result=scope)
settings = Settings(_env_file=None, storage_root=str(tmp_path), orthophoto_resolution_m=1.0)
prepared = OrthophotoAcquisitionService._prepared_request(payload, settings)
result = OrthophotoAcquisitionService.acquire(
db,
project_id,
payload,
settings=settings,
opener=lambda *_args, **_kwargs: FakeImageResponse(
_source_tiff(prepared["width"], prepared["height"])
),
)
dataset = next(row for row in db.added if isinstance(row, Dataset))
assert result["provider"] == provider
assert result["layer"] == layer
assert dataset.source_name == provider
assert dataset.source_metadata["coverage_zone"] == coverage_zone
assert dataset.source_metadata["license_note"]
assert dataset.provenance_metadata["request_url"].startswith(prepared["product"].wms_url)
prepared = OrthophotoAcquisitionService._prepared_request(_selection_payload(product_key="1971"), settings)
assert prepared["params"]["LAYERS"] == "OKZPAN71VL"
@@ -39,6 +39,8 @@
<Config Name="Operator Session TTL" Target="GEOINTEL_AUTH_SESSION_TTL_SECONDS" Default="43200" Mode="" Description="Session lifetime in seconds (900-604800)." Type="Variable" Display="advanced" Required="true" Mask="false">43200</Config>
<Config Name="Official Orthophoto Acquisition" Target="ORTHOPHOTO_ENABLED" Default="true" Mode="" Description="Allow explicit bounded map selections to request the official Digitaal Vlaanderen orthophoto WMS." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="Orthophoto WMS URL" Target="ORTHOPHOTO_WMS_URL" Default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms" Mode="" Description="Official Digitaal Vlaanderen most-recent winter orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/OMWRGBMRVL/wms</Config>
<Config Name="SPW Orthophoto WMS URL" Target="SPW_ORTHOPHOTO_WMS_URL" Default="https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer" Mode="" Description="Official SPW latest Walloon orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer</Config>
<Config Name="Brussels Orthophoto WMS URL" Target="BRUSSELS_ORTHOPHOTO_WMS_URL" Default="https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows" Mode="" Description="Official Paradigm UrbIS Brussels orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows</Config>
<Config Name="Orthophoto WMS Layer" Target="ORTHOPHOTO_WMS_LAYER" Default="Ortho" Mode="" Description="Allowlisted official orthophoto WMS layer identifier." Type="Variable" Display="advanced" Required="true" Mask="false">Ortho</Config>
<Config Name="Orthophoto Resolution (m)" Target="ORTHOPHOTO_RESOLUTION_M" Default="1.0" Mode="" Description="Requested analysis sampling in metres per pixel. Keep at 1.0 for the active building model profile." Type="Variable" Display="advanced" Required="true" Mask="false">1.0</Config>
<Config Name="Orthophoto Minimum Side (m)" Target="ORTHOPHOTO_MIN_SIDE_M" Default="128" Mode="" Description="Minimum bounded request side length." Type="Variable" Display="advanced" Required="true" Mask="false">128</Config>
+2
View File
@@ -43,6 +43,8 @@ GEOINTEL_AUTH_SESSION_TTL_SECONDS=43200
# Explicit, bounded acquisition from the official Digitaal Vlaanderen WMS.
ORTHOPHOTO_ENABLED=true
ORTHOPHOTO_WMS_URL=https://geo.api.vlaanderen.be/OMWRGBMRVL/wms
SPW_ORTHOPHOTO_WMS_URL=https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer
BRUSSELS_ORTHOPHOTO_WMS_URL=https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows
ORTHOPHOTO_WMS_LAYER=Ortho
ORTHOPHOTO_RESOLUTION_M=1.0
ORTHOPHOTO_MIN_SIDE_M=128
+4
View File
@@ -42,6 +42,8 @@ GEOINTEL_AUTH_SESSION_SECRET="${GEOINTEL_AUTH_SESSION_SECRET:-}"
GEOINTEL_AUTH_SESSION_TTL_SECONDS="${GEOINTEL_AUTH_SESSION_TTL_SECONDS:-43200}"
ORTHOPHOTO_ENABLED="${ORTHOPHOTO_ENABLED:-true}"
ORTHOPHOTO_WMS_URL="${ORTHOPHOTO_WMS_URL:-https://geo.api.vlaanderen.be/OMWRGBMRVL/wms}"
SPW_ORTHOPHOTO_WMS_URL="${SPW_ORTHOPHOTO_WMS_URL:-https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer}"
BRUSSELS_ORTHOPHOTO_WMS_URL="${BRUSSELS_ORTHOPHOTO_WMS_URL:-https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows}"
ORTHOPHOTO_WMS_LAYER="${ORTHOPHOTO_WMS_LAYER:-Ortho}"
ORTHOPHOTO_RESOLUTION_M="${ORTHOPHOTO_RESOLUTION_M:-1.0}"
ORTHOPHOTO_MIN_SIDE_M="${ORTHOPHOTO_MIN_SIDE_M:-128}"
@@ -285,6 +287,8 @@ docker run -d \
-e GEOINTEL_AUTH_SESSION_TTL_SECONDS="$GEOINTEL_AUTH_SESSION_TTL_SECONDS" \
-e ORTHOPHOTO_ENABLED="$ORTHOPHOTO_ENABLED" \
-e ORTHOPHOTO_WMS_URL="$ORTHOPHOTO_WMS_URL" \
-e SPW_ORTHOPHOTO_WMS_URL="$SPW_ORTHOPHOTO_WMS_URL" \
-e BRUSSELS_ORTHOPHOTO_WMS_URL="$BRUSSELS_ORTHOPHOTO_WMS_URL" \
-e ORTHOPHOTO_WMS_LAYER="$ORTHOPHOTO_WMS_LAYER" \
-e ORTHOPHOTO_RESOLUTION_M="$ORTHOPHOTO_RESOLUTION_M" \
-e ORTHOPHOTO_MIN_SIDE_M="$ORTHOPHOTO_MIN_SIDE_M" \
+2
View File
@@ -23,6 +23,8 @@ services:
GEOINTEL_MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
ORTHOPHOTO_ENABLED: ${ORTHOPHOTO_ENABLED:-true}
ORTHOPHOTO_WMS_URL: ${ORTHOPHOTO_WMS_URL:-https://geo.api.vlaanderen.be/OMWRGBMRVL/wms}
SPW_ORTHOPHOTO_WMS_URL: ${SPW_ORTHOPHOTO_WMS_URL:-https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer}
BRUSSELS_ORTHOPHOTO_WMS_URL: ${BRUSSELS_ORTHOPHOTO_WMS_URL:-https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows}
ORTHOPHOTO_WMS_LAYER: ${ORTHOPHOTO_WMS_LAYER:-Ortho}
ORTHOPHOTO_RESOLUTION_M: ${ORTHOPHOTO_RESOLUTION_M:-1.0}
ORTHOPHOTO_MIN_SIDE_M: ${ORTHOPHOTO_MIN_SIDE_M:-128}
+2
View File
@@ -25,6 +25,8 @@ services:
MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
ORTHOPHOTO_ENABLED: ${ORTHOPHOTO_ENABLED:-true}
ORTHOPHOTO_WMS_URL: ${ORTHOPHOTO_WMS_URL:-https://geo.api.vlaanderen.be/OMWRGBMRVL/wms}
SPW_ORTHOPHOTO_WMS_URL: ${SPW_ORTHOPHOTO_WMS_URL:-https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer}
BRUSSELS_ORTHOPHOTO_WMS_URL: ${BRUSSELS_ORTHOPHOTO_WMS_URL:-https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows}
ORTHOPHOTO_WMS_LAYER: ${ORTHOPHOTO_WMS_LAYER:-Ortho}
ORTHOPHOTO_RESOLUTION_M: ${ORTHOPHOTO_RESOLUTION_M:-1.0}
ORTHOPHOTO_MIN_SIDE_M: ${ORTHOPHOTO_MIN_SIDE_M:-128}
+13 -9
View File
@@ -365,16 +365,20 @@ Vector uploads remain stored as original files and are also persisted into `vect
### GET `/api/v1/projects/{project_id}/datasets/orthophoto/products`
Return the governed Digitaal Vlaanderen orthophoto product allowlist in the
canonical envelope. Every product reports its key, display/observation label,
temporal granularity, native resolution, colour mode, catalogue URL,
limitations and whether current configured-YOLO detection is allowed.
Return the governed Belgian orthophoto product allowlist in the canonical
envelope. It contains Digitaal Vlaanderen products plus `wallonia_latest`
(SPW) and `brussels_latest` (Paradigm UrbIS). Every product reports its key,
provider, coverage zone, display/observation label, temporal granularity,
native resolution, colour mode, catalogue URL, attribution, licence note,
limitations and whether configured-YOLO detection is allowed.
### POST `/api/v1/projects/{project_id}/datasets/orthophoto/acquire`
Explicitly acquire a bounded orthophoto selection from a governed official
Digitaal Vlaanderen WMS product. Arbitrary WMS URLs and layer names are not
accepted.
regional WMS product. Arbitrary WMS URLs and layer names are not accepted.
The regional products require the persisted project Areas `Wallonia` or
`Brussels-Capital Region` and reject a rectangle unless that Area covers at
least 99% of it.
```json
{
@@ -493,9 +497,9 @@ Safety contract:
reuse;
- WMS bytes are georeferenced to EPSG:31370 and persisted only through
`DatasetService`; no fetch runs on startup;
- only `most_recent` can enter the current configured-YOLO plus GRB-QA path;
historical products are visual evidence and are never validated against the
current GRB state;
- only `most_recent`, `wallonia_latest` and `brussels_latest` can enter the
configured-YOLO path; QA must use the matching regional reference source
(GRB, PICC or UrbIS). Historical products remain visual evidence;
- product periods such as `1979_1990` remain explicitly multi-year and are not
presented as exact annual observations.
+4 -4
View File
@@ -110,10 +110,10 @@ Exit: schema tests pass and no runtime can advertise an unproven class/scope.
### WP1 — National imagery acquisition
- [x] verify official SPW and Brussels imagery services and usage conditions;
- [ ] implement bounded SPW orthophoto acquisition with the existing AOI worker;
- [ ] implement bounded UrbIS orthophoto acquisition with the existing AOI worker;
- [ ] expose provider edition, resolution, time, CRS, attribution and checksum;
- [ ] add retry/cache/size limits and live provider preflights;
- [x] implement bounded SPW orthophoto acquisition with the existing AOI worker;
- [x] implement bounded UrbIS orthophoto acquisition with the existing AOI worker;
- [x] expose provider edition, resolution, time, CRS, attribution and checksum;
- [x] reuse retry/cache/size limits and verify both live WMS providers;
- [ ] retain explicit `partial`/`not_configured` status when a service is down.
Exit: one checksum-bound RGB GeoTIFF per target stratum can be acquired without
+2
View File
@@ -27,6 +27,8 @@ const DATASET_SOURCE_LABELS: Record<string, string> = {
department_omgeving_land_use: 'Departement Omgeving',
agentschap_landbouw_zeevisserij_agricultural_parcels: 'Agentschap Landbouw en Zeevisserij',
digitaal_vlaanderen_orthophoto: 'Digitaal Vlaanderen',
spw_orthophoto: 'SPW Orthofoto',
urbis_orthophoto: 'Paradigm UrbIS',
digitaal_vlaanderen_buildings_addresses_register: 'Digitaal Vlaanderen',
digitaal_vlaanderen_dhmv: 'Digitaal Vlaanderen',
spw_terrain: 'Service public de Wallonie',
+4
View File
@@ -362,6 +362,7 @@ export interface OrthophotoAcquireRequest {
export interface OrthophotoProductRead {
key: string
provider: string
display_name: string
observation_label: string
temporal_granularity: string
@@ -369,6 +370,9 @@ export interface OrthophotoProductRead {
supports_detection: boolean
color_mode: string
catalog_url: string
coverage_zone: string
attribution: string
license_note: string
limitation_message: string
}