|
|
|
@@ -1,9 +1,11 @@
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import hashlib
|
|
|
|
|
import io
|
|
|
|
|
import json
|
|
|
|
|
import math
|
|
|
|
|
import warnings
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any, Callable
|
|
|
|
@@ -20,21 +22,169 @@ from shapely.ops import transform as shapely_transform
|
|
|
|
|
from app.core.config import Settings, get_settings
|
|
|
|
|
from app.core.errors import AppError
|
|
|
|
|
from app.models import Area, Dataset, Project
|
|
|
|
|
from app.schemas.orthophoto import OrthophotoAcquireRequest, OrthophotoAcquisitionResult
|
|
|
|
|
from app.schemas.orthophoto import OrthophotoAcquireRequest, OrthophotoAcquisitionResult, OrthophotoProductRead
|
|
|
|
|
from app.services.dataset_service import DatasetService
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class OrthophotoProduct:
|
|
|
|
|
key: str
|
|
|
|
|
display_name: str
|
|
|
|
|
observation_label: str
|
|
|
|
|
temporal_granularity: str
|
|
|
|
|
native_resolution_m: float
|
|
|
|
|
wms_url: str
|
|
|
|
|
layer: str
|
|
|
|
|
catalog_url: str
|
|
|
|
|
limitation_message: str
|
|
|
|
|
supports_detection: bool = False
|
|
|
|
|
color_mode: str = "rgb"
|
|
|
|
|
observed_at: datetime | None = None
|
|
|
|
|
valid_from: datetime | None = None
|
|
|
|
|
valid_to: datetime | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OrthophotoAcquisitionService:
|
|
|
|
|
PROVIDER = "digitaal_vlaanderen_orthophoto"
|
|
|
|
|
ATTRIBUTION = "Bron: Orthofotomozaiek Vlaanderen, Digitaal Vlaanderen"
|
|
|
|
|
CATALOG_URL = "https://www.vlaanderen.be/datavindplaats/catalogus/orthofotomozaiek-middenschalig-winteropnamen-kleur-meest-recent-vlaanderen"
|
|
|
|
|
LIMITATION = "Meest recente samengestelde winterorthofoto op het moment van de aanvraag; geen historische opnamedatum per pixel."
|
|
|
|
|
HISTORICAL_WINTER_WMS_URL = "https://geo.api.vlaanderen.be/OMW/wms"
|
|
|
|
|
HISTORICAL_WINTER_CATALOG_URL = "https://www.vlaanderen.be/datavindplaats/catalogus/wmts-orthofotomozaiek-middenschalig-winteropnamen"
|
|
|
|
|
HISTORICAL_SUMMER_WMS_URL = "https://geo.api.vlaanderen.be/OKZ/wms"
|
|
|
|
|
HISTORICAL_SUMMER_CATALOG_URL = "https://www.vlaanderen.be/datavindplaats/catalogus/orthofotomozaiek-kleinschalig-zomeropnamen"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _products(settings: Settings) -> dict[str, OrthophotoProduct]:
|
|
|
|
|
products: list[OrthophotoProduct] = [
|
|
|
|
|
OrthophotoProduct(
|
|
|
|
|
key="most_recent",
|
|
|
|
|
display_name="Meest recente winterluchtbeeld",
|
|
|
|
|
observation_label="Meest recent beschikbaar",
|
|
|
|
|
temporal_granularity="snapshot",
|
|
|
|
|
native_resolution_m=0.15,
|
|
|
|
|
wms_url=settings.orthophoto_wms_url,
|
|
|
|
|
layer=settings.orthophoto_wms_layer,
|
|
|
|
|
catalog_url=OrthophotoAcquisitionService.CATALOG_URL,
|
|
|
|
|
limitation_message=OrthophotoAcquisitionService.LIMITATION,
|
|
|
|
|
supports_detection=True,
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
for year in range(2025, 2011, -1):
|
|
|
|
|
products.append(
|
|
|
|
|
OrthophotoProduct(
|
|
|
|
|
key=str(year),
|
|
|
|
|
display_name=f"Winterluchtbeeld {year}",
|
|
|
|
|
observation_label=str(year),
|
|
|
|
|
temporal_granularity="year",
|
|
|
|
|
native_resolution_m=0.15 if year >= 2022 else 0.25,
|
|
|
|
|
wms_url=OrthophotoAcquisitionService.HISTORICAL_WINTER_WMS_URL,
|
|
|
|
|
layer=f"OMWRGB{year % 100:02d}VL",
|
|
|
|
|
catalog_url=OrthophotoAcquisitionService.HISTORICAL_WINTER_CATALOG_URL,
|
|
|
|
|
limitation_message=(
|
|
|
|
|
"Officiële samengestelde winterorthofoto voor deze jaargang; de exacte opnamedatum kan per tegel verschillen. "
|
|
|
|
|
"Historische beelden worden niet met de actuele GRB-toestand gevalideerd."
|
|
|
|
|
),
|
|
|
|
|
observed_at=datetime(year, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_from=datetime(year, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_to=datetime(year, 12, 31, 23, 59, 59, tzinfo=UTC),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
for key, start_year, end_year, layer in (
|
|
|
|
|
("2008_2011", 2008, 2011, "OMWRGB08_11VL"),
|
|
|
|
|
("2005_2007", 2005, 2007, "OMWRGB05_07VL"),
|
|
|
|
|
("2000_2003", 2000, 2003, "OMWRGB00_03VL"),
|
|
|
|
|
):
|
|
|
|
|
products.append(
|
|
|
|
|
OrthophotoProduct(
|
|
|
|
|
key=key,
|
|
|
|
|
display_name=f"Winterluchtbeeld {start_year}-{end_year}",
|
|
|
|
|
observation_label=f"{start_year}-{end_year}",
|
|
|
|
|
temporal_granularity="period",
|
|
|
|
|
native_resolution_m=0.25,
|
|
|
|
|
wms_url=OrthophotoAcquisitionService.HISTORICAL_WINTER_WMS_URL,
|
|
|
|
|
layer=layer,
|
|
|
|
|
catalog_url=OrthophotoAcquisitionService.HISTORICAL_WINTER_CATALOG_URL,
|
|
|
|
|
limitation_message=(
|
|
|
|
|
"Officiële samengestelde winterorthofoto uit een meerjarige opnameperiode; dit is geen exacte jaaropname. "
|
|
|
|
|
"Historische beelden worden niet met de actuele GRB-toestand gevalideerd."
|
|
|
|
|
),
|
|
|
|
|
observed_at=datetime(start_year, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_from=datetime(start_year, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_to=datetime(end_year, 12, 31, 23, 59, 59, tzinfo=UTC),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
products.extend(
|
|
|
|
|
[
|
|
|
|
|
OrthophotoProduct(
|
|
|
|
|
key="1979_1990",
|
|
|
|
|
display_name="Zomerluchtbeeld 1979-1990",
|
|
|
|
|
observation_label="1979-1990",
|
|
|
|
|
temporal_granularity="period",
|
|
|
|
|
native_resolution_m=1.0,
|
|
|
|
|
wms_url=OrthophotoAcquisitionService.HISTORICAL_SUMMER_WMS_URL,
|
|
|
|
|
layer="OKZRGB79_90VL",
|
|
|
|
|
catalog_url=OrthophotoAcquisitionService.HISTORICAL_SUMMER_CATALOG_URL,
|
|
|
|
|
limitation_message="Kleinschalig RGB-mozaïek uit meerdere zomervluchten tussen 1979 en 1990; geen exacte jaartoestand.",
|
|
|
|
|
observed_at=datetime(1979, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_from=datetime(1979, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_to=datetime(1990, 12, 31, 23, 59, 59, tzinfo=UTC),
|
|
|
|
|
),
|
|
|
|
|
OrthophotoProduct(
|
|
|
|
|
key="1971",
|
|
|
|
|
display_name="Zomerluchtbeeld 1971",
|
|
|
|
|
observation_label="1971",
|
|
|
|
|
temporal_granularity="year",
|
|
|
|
|
native_resolution_m=1.0,
|
|
|
|
|
wms_url=OrthophotoAcquisitionService.HISTORICAL_SUMMER_WMS_URL,
|
|
|
|
|
layer="OKZPAN71VL",
|
|
|
|
|
catalog_url=OrthophotoAcquisitionService.HISTORICAL_SUMMER_CATALOG_URL,
|
|
|
|
|
limitation_message="Kleinschalig panchromatisch mozaïek uit 1971; zwart-wit en niet geschikt voor het huidige RGB-detectiemodel.",
|
|
|
|
|
color_mode="panchromatic",
|
|
|
|
|
observed_at=datetime(1971, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_from=datetime(1971, 1, 1, tzinfo=UTC),
|
|
|
|
|
valid_to=datetime(1971, 12, 31, 23, 59, 59, tzinfo=UTC),
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
return {product.key: product for product in products}
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def list_products(settings: Settings | None = None) -> list[dict[str, Any]]:
|
|
|
|
|
resolved_settings = settings or get_settings()
|
|
|
|
|
return [
|
|
|
|
|
OrthophotoProductRead(
|
|
|
|
|
key=product.key,
|
|
|
|
|
display_name=product.display_name,
|
|
|
|
|
observation_label=product.observation_label,
|
|
|
|
|
temporal_granularity=product.temporal_granularity,
|
|
|
|
|
native_resolution_m=product.native_resolution_m,
|
|
|
|
|
supports_detection=product.supports_detection,
|
|
|
|
|
color_mode=product.color_mode,
|
|
|
|
|
catalog_url=product.catalog_url,
|
|
|
|
|
limitation_message=product.limitation_message,
|
|
|
|
|
).model_dump()
|
|
|
|
|
for product in OrthophotoAcquisitionService._products(resolved_settings).values()
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _product(product_key: str, settings: Settings) -> OrthophotoProduct:
|
|
|
|
|
product = OrthophotoAcquisitionService._products(settings).get(product_key.strip().lower())
|
|
|
|
|
if product is None:
|
|
|
|
|
raise AppError(
|
|
|
|
|
code="ORTHOPHOTO_PRODUCT_NOT_SUPPORTED",
|
|
|
|
|
message="Select an orthophoto product from the official product registry",
|
|
|
|
|
details={"product_key": product_key},
|
|
|
|
|
status_code=422,
|
|
|
|
|
)
|
|
|
|
|
return product
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _prepared_request(
|
|
|
|
|
payload: OrthophotoAcquireRequest,
|
|
|
|
|
settings: Settings,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
product = OrthophotoAcquisitionService._product(payload.product_key, settings)
|
|
|
|
|
if payload.bbox.crs.upper() != "EPSG:4326":
|
|
|
|
|
raise AppError(code="INVALID_CRS", message="Orthophoto selection bbox must use EPSG:4326", status_code=400)
|
|
|
|
|
min_x = float(payload.bbox.min_x)
|
|
|
|
@@ -68,8 +218,9 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
bbox_31370 = [float(value) for value in lambert_bounds]
|
|
|
|
|
request_identity = {
|
|
|
|
|
"provider": OrthophotoAcquisitionService.PROVIDER,
|
|
|
|
|
"wms_url": settings.orthophoto_wms_url,
|
|
|
|
|
"layer": settings.orthophoto_wms_layer,
|
|
|
|
|
"product_key": product.key,
|
|
|
|
|
"wms_url": product.wms_url,
|
|
|
|
|
"layer": product.layer,
|
|
|
|
|
"bbox_epsg4326": [round(value, 8) for value in bbox_4326],
|
|
|
|
|
"bbox_epsg31370": [round(value, 3) for value in bbox_31370],
|
|
|
|
|
"width": width,
|
|
|
|
@@ -77,11 +228,18 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
"resolution_m": settings.orthophoto_resolution_m,
|
|
|
|
|
}
|
|
|
|
|
request_hash = hashlib.sha256(json.dumps(request_identity, sort_keys=True).encode("utf-8")).hexdigest()
|
|
|
|
|
spatial_identity = {
|
|
|
|
|
"bbox_epsg4326": request_identity["bbox_epsg4326"],
|
|
|
|
|
"width": width,
|
|
|
|
|
"height": height,
|
|
|
|
|
"resolution_m": settings.orthophoto_resolution_m,
|
|
|
|
|
}
|
|
|
|
|
spatial_hash = hashlib.sha256(json.dumps(spatial_identity, sort_keys=True).encode("utf-8")).hexdigest()
|
|
|
|
|
params = {
|
|
|
|
|
"SERVICE": "WMS",
|
|
|
|
|
"VERSION": "1.3.0",
|
|
|
|
|
"REQUEST": "GetMap",
|
|
|
|
|
"LAYERS": settings.orthophoto_wms_layer,
|
|
|
|
|
"LAYERS": product.layer,
|
|
|
|
|
"STYLES": "",
|
|
|
|
|
"FORMAT": "image/tiff",
|
|
|
|
|
"CRS": "EPSG:31370",
|
|
|
|
@@ -91,8 +249,10 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
**request_identity,
|
|
|
|
|
"product": product,
|
|
|
|
|
"spatial_hash": spatial_hash,
|
|
|
|
|
"request_hash": request_hash,
|
|
|
|
|
"request_url": f"{settings.orthophoto_wms_url}?{urlencode(params)}",
|
|
|
|
|
"request_url": f"{product.wms_url}?{urlencode(params)}",
|
|
|
|
|
"params": params,
|
|
|
|
|
"bbox_epsg4326": bbox_4326,
|
|
|
|
|
"bbox_epsg31370": bbox_31370,
|
|
|
|
@@ -124,8 +284,14 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _cached_dataset(db, project_id: UUID, filename: str, settings: Settings) -> Dataset | None:
|
|
|
|
|
if settings.orthophoto_cache_ttl_hours <= 0:
|
|
|
|
|
def _cached_dataset(
|
|
|
|
|
db,
|
|
|
|
|
project_id: UUID,
|
|
|
|
|
filename: str,
|
|
|
|
|
settings: Settings,
|
|
|
|
|
product: OrthophotoProduct,
|
|
|
|
|
) -> Dataset | None:
|
|
|
|
|
if product.key == "most_recent" and settings.orthophoto_cache_ttl_hours <= 0:
|
|
|
|
|
return None
|
|
|
|
|
candidate = (
|
|
|
|
|
db.query(Dataset)
|
|
|
|
@@ -145,7 +311,7 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
return None
|
|
|
|
|
if imported_at.tzinfo is None:
|
|
|
|
|
imported_at = imported_at.replace(tzinfo=UTC)
|
|
|
|
|
if datetime.now(UTC) - imported_at > timedelta(hours=settings.orthophoto_cache_ttl_hours):
|
|
|
|
|
if product.key == "most_recent" and datetime.now(UTC) - imported_at > timedelta(hours=settings.orthophoto_cache_ttl_hours):
|
|
|
|
|
return None
|
|
|
|
|
return candidate
|
|
|
|
|
|
|
|
|
@@ -196,7 +362,9 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
with warnings.catch_warnings():
|
|
|
|
|
warnings.simplefilter("ignore", NotGeoreferencedWarning)
|
|
|
|
|
with source_memory.open() as source:
|
|
|
|
|
if source.width != prepared["width"] or source.height != prepared["height"] or source.count < 3:
|
|
|
|
|
product: OrthophotoProduct = prepared["product"]
|
|
|
|
|
minimum_band_count = 1 if product.color_mode == "panchromatic" else 3
|
|
|
|
|
if source.width != prepared["width"] or source.height != prepared["height"] or source.count < minimum_band_count:
|
|
|
|
|
raise AppError(
|
|
|
|
|
code="ORTHOPHOTO_PROVIDER_INVALID_RESPONSE",
|
|
|
|
|
message="Official orthophoto dimensions or RGB bands do not match the bounded request",
|
|
|
|
@@ -216,7 +384,7 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
with output_memory.open(**profile) as output:
|
|
|
|
|
output.write(image)
|
|
|
|
|
output.update_tags(
|
|
|
|
|
source="Digitaal Vlaanderen OMWRGBMRVL WMS Ortho layer",
|
|
|
|
|
source=f"Digitaal Vlaanderen WMS {product.layer}",
|
|
|
|
|
source_url=prepared["request_url"],
|
|
|
|
|
attribution=OrthophotoAcquisitionService.ATTRIBUTION,
|
|
|
|
|
acquisition="explicit_bounded_map_selection",
|
|
|
|
@@ -245,44 +413,74 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
if not resolved_settings.orthophoto_enabled:
|
|
|
|
|
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"])
|
|
|
|
|
filename = f"orthofoto_selectie_{prepared['request_hash'][:12]}.tif"
|
|
|
|
|
filename = f"orthofoto_{product.key}_{prepared['request_hash'][:12]}.tif"
|
|
|
|
|
|
|
|
|
|
cached = None if payload.force_refresh else OrthophotoAcquisitionService._cached_dataset(db, project_id, filename, resolved_settings)
|
|
|
|
|
cached = None if payload.force_refresh else OrthophotoAcquisitionService._cached_dataset(
|
|
|
|
|
db,
|
|
|
|
|
project_id,
|
|
|
|
|
filename,
|
|
|
|
|
resolved_settings,
|
|
|
|
|
product,
|
|
|
|
|
)
|
|
|
|
|
if cached is not None:
|
|
|
|
|
return OrthophotoAcquisitionResult(
|
|
|
|
|
output_dataset_id=cached.id,
|
|
|
|
|
reused=True,
|
|
|
|
|
provider=OrthophotoAcquisitionService.PROVIDER,
|
|
|
|
|
layer=resolved_settings.orthophoto_wms_layer,
|
|
|
|
|
product_key=product.key,
|
|
|
|
|
display_name=product.display_name,
|
|
|
|
|
observation_label=product.observation_label,
|
|
|
|
|
temporal_granularity=product.temporal_granularity,
|
|
|
|
|
supports_detection=product.supports_detection,
|
|
|
|
|
layer=product.layer,
|
|
|
|
|
width=prepared["width"],
|
|
|
|
|
height=prepared["height"],
|
|
|
|
|
resolution_m=resolved_settings.orthophoto_resolution_m,
|
|
|
|
|
bbox_epsg4326=prepared["bbox_epsg4326"],
|
|
|
|
|
bbox_epsg31370=prepared["bbox_epsg31370"],
|
|
|
|
|
attribution=OrthophotoAcquisitionService.ATTRIBUTION,
|
|
|
|
|
limitation_message=OrthophotoAcquisitionService.LIMITATION,
|
|
|
|
|
limitation_message=product.limitation_message,
|
|
|
|
|
).model_dump(mode="json")
|
|
|
|
|
|
|
|
|
|
raw_content, response_content_type = OrthophotoAcquisitionService._fetch(prepared["request_url"], resolved_settings, opener)
|
|
|
|
|
geotiff_content = OrthophotoAcquisitionService._georeference_tiff(raw_content, prepared)
|
|
|
|
|
acquired_at = datetime.now(UTC)
|
|
|
|
|
observed_at = product.observed_at or acquired_at
|
|
|
|
|
dataset = DatasetService.import_raster_bytes(
|
|
|
|
|
db,
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
area_id=payload.area_id,
|
|
|
|
|
filename=filename,
|
|
|
|
|
content=geotiff_content,
|
|
|
|
|
source="Digitaal Vlaanderen OMWRGBMRVL WMS",
|
|
|
|
|
source=f"Digitaal Vlaanderen WMS {product.layer}",
|
|
|
|
|
source_name=OrthophotoAcquisitionService.PROVIDER,
|
|
|
|
|
source_version=f"most_recent_at_{acquired_at.date().isoformat()}",
|
|
|
|
|
temporal_series_key=f"digitaal-vlaanderen: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"
|
|
|
|
|
else product.key
|
|
|
|
|
),
|
|
|
|
|
content_type="image/tiff",
|
|
|
|
|
source_metadata={
|
|
|
|
|
"provider": OrthophotoAcquisitionService.PROVIDER,
|
|
|
|
|
"service": "WMS",
|
|
|
|
|
"service_version": "1.3.0",
|
|
|
|
|
"layer": resolved_settings.orthophoto_wms_layer,
|
|
|
|
|
"catalog_url": OrthophotoAcquisitionService.CATALOG_URL,
|
|
|
|
|
"product_key": product.key,
|
|
|
|
|
"product_display_name": product.display_name,
|
|
|
|
|
"observation_label": product.observation_label,
|
|
|
|
|
"observation_date_precision": product.temporal_granularity,
|
|
|
|
|
"native_resolution_m": product.native_resolution_m,
|
|
|
|
|
"requested_resolution_m": resolved_settings.orthophoto_resolution_m,
|
|
|
|
|
"color_mode": product.color_mode,
|
|
|
|
|
"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.",
|
|
|
|
|
},
|
|
|
|
@@ -290,6 +488,7 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
"acquisition": "explicit_bounded_map_selection",
|
|
|
|
|
"acquired_at": acquired_at.isoformat(),
|
|
|
|
|
"request_hash": prepared["request_hash"],
|
|
|
|
|
"spatial_hash": prepared["spatial_hash"],
|
|
|
|
|
"request_url": prepared["request_url"],
|
|
|
|
|
"response_content_type": response_content_type,
|
|
|
|
|
"bbox_epsg4326": prepared["bbox_epsg4326"],
|
|
|
|
@@ -297,19 +496,70 @@ class OrthophotoAcquisitionService:
|
|
|
|
|
"width": prepared["width"],
|
|
|
|
|
"height": prepared["height"],
|
|
|
|
|
"resolution_m": resolved_settings.orthophoto_resolution_m,
|
|
|
|
|
"limitation_message": OrthophotoAcquisitionService.LIMITATION,
|
|
|
|
|
"limitation_message": product.limitation_message,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return OrthophotoAcquisitionResult(
|
|
|
|
|
output_dataset_id=dataset.id,
|
|
|
|
|
reused=False,
|
|
|
|
|
provider=OrthophotoAcquisitionService.PROVIDER,
|
|
|
|
|
layer=resolved_settings.orthophoto_wms_layer,
|
|
|
|
|
product_key=product.key,
|
|
|
|
|
display_name=product.display_name,
|
|
|
|
|
observation_label=product.observation_label,
|
|
|
|
|
temporal_granularity=product.temporal_granularity,
|
|
|
|
|
supports_detection=product.supports_detection,
|
|
|
|
|
layer=product.layer,
|
|
|
|
|
width=prepared["width"],
|
|
|
|
|
height=prepared["height"],
|
|
|
|
|
resolution_m=resolved_settings.orthophoto_resolution_m,
|
|
|
|
|
bbox_epsg4326=prepared["bbox_epsg4326"],
|
|
|
|
|
bbox_epsg31370=prepared["bbox_epsg31370"],
|
|
|
|
|
attribution=OrthophotoAcquisitionService.ATTRIBUTION,
|
|
|
|
|
limitation_message=OrthophotoAcquisitionService.LIMITATION,
|
|
|
|
|
limitation_message=product.limitation_message,
|
|
|
|
|
).model_dump(mode="json")
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def render_png(db, project_id: UUID, dataset_id: UUID, *, max_dimension: int = 1600) -> bytes:
|
|
|
|
|
dataset = db.get(Dataset, dataset_id)
|
|
|
|
|
if (
|
|
|
|
|
dataset is None
|
|
|
|
|
or dataset.project_id != project_id
|
|
|
|
|
or dataset.source_name != OrthophotoAcquisitionService.PROVIDER
|
|
|
|
|
or dataset.status != "ready"
|
|
|
|
|
or not dataset.storage_path
|
|
|
|
|
or not Path(dataset.storage_path).is_file()
|
|
|
|
|
):
|
|
|
|
|
raise AppError(code="ORTHOPHOTO_NOT_FOUND", message="Orthophoto dataset not found", status_code=404)
|
|
|
|
|
try:
|
|
|
|
|
import numpy as np
|
|
|
|
|
import rasterio
|
|
|
|
|
from PIL import Image
|
|
|
|
|
from rasterio.enums import Resampling
|
|
|
|
|
except ImportError as exc:
|
|
|
|
|
raise AppError(code="RASTER_PROCESSING_UNAVAILABLE", message="Raster preview dependencies are unavailable", status_code=503) from exc
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
with rasterio.open(dataset.storage_path) as source:
|
|
|
|
|
scale = min(1.0, max_dimension / max(source.width, source.height))
|
|
|
|
|
width = max(1, round(source.width * scale))
|
|
|
|
|
height = max(1, round(source.height * scale))
|
|
|
|
|
indexes = [1] if source.count == 1 else list(range(1, min(source.count, 3) + 1))
|
|
|
|
|
pixels = source.read(indexes, out_shape=(len(indexes), height, width), resampling=Resampling.bilinear)
|
|
|
|
|
if pixels.dtype != np.uint8:
|
|
|
|
|
pixels = np.clip(pixels, 0, 255).astype(np.uint8)
|
|
|
|
|
if len(indexes) == 1:
|
|
|
|
|
image = Image.fromarray(pixels[0])
|
|
|
|
|
else:
|
|
|
|
|
image = Image.fromarray(np.moveaxis(pixels[:3], 0, 2))
|
|
|
|
|
output = io.BytesIO()
|
|
|
|
|
image.save(output, format="PNG", optimize=True)
|
|
|
|
|
return output.getvalue()
|
|
|
|
|
except AppError:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
raise AppError(
|
|
|
|
|
code="ORTHOPHOTO_PREVIEW_FAILED",
|
|
|
|
|
message="The persisted orthophoto could not be rendered",
|
|
|
|
|
details={"reason": str(exc)},
|
|
|
|
|
status_code=500,
|
|
|
|
|
) from exc
|
|
|
|
|