Files
insar-management-system-v2/backend/app/services/flood_product_service.py
T

200 lines
8.2 KiB
Python

"""Flood product listing and manifest helpers."""
from __future__ import annotations
import json
import os
from datetime import datetime
from typing import Any
from fastapi import HTTPException
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from ..models import FloodDetectionORM, FloodOverlayORM, FloodProductORM, SARSceneGeoORM
def _iso(value: Any) -> str | None:
return value.isoformat() if value else None
def _product_to_dict(product: FloodProductORM) -> dict[str, Any]:
summary = product.summary_json if isinstance(product.summary_json, dict) else {}
detection = product.detection
overlay = product.overlay
return {
"id": product.id,
"product_id": product.product_id,
"detection_id": product.detection_id,
"overlay_id": product.overlay_id,
"display_name": product.display_name,
"status": product.status,
"publish_dir": product.publish_dir,
"manifest_path": product.manifest_path,
"summary": summary,
"created_at": _iso(product.created_at),
"flood_area_km2": getattr(detection, "flood_area_km2", None),
"affected_area_km2": getattr(overlay, "affected_area_km2", None),
}
async def list_flood_products(
*,
db: AsyncSession,
limit: int = 20,
offset: int = 0,
status: str | None = None,
) -> dict[str, Any]:
count_query = select(func.count()).select_from(FloodProductORM)
query = (
select(FloodProductORM)
.options(
selectinload(FloodProductORM.detection),
selectinload(FloodProductORM.overlay),
)
.order_by(FloodProductORM.id.desc())
.limit(limit)
.offset(offset)
)
if status:
count_query = count_query.where(FloodProductORM.status == status)
query = query.where(FloodProductORM.status == status)
total = (await db.execute(count_query)).scalar_one()
rows = (await db.execute(query)).scalars().all()
return {"items": [_product_to_dict(row) for row in rows], "total": total}
async def get_flood_product(product_id_or_pk: str, db: AsyncSession) -> dict[str, Any]:
product = await _get_product(product_id_or_pk, db)
return _product_to_dict(product)
async def get_flood_product_manifest(product_id_or_pk: str, db: AsyncSession) -> dict[str, Any]:
product = await _get_product(product_id_or_pk, db)
if product.manifest_path and os.path.isfile(product.manifest_path):
try:
with open(product.manifest_path, "r", encoding="utf-8") as stream:
payload = json.load(stream)
if isinstance(payload, dict):
return payload
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to read manifest: {exc}") from exc
return _build_manifest_from_db(product)
async def create_flood_product_for_detection(detection_id: int, db: AsyncSession) -> dict[str, Any]:
detection = await db.get(FloodDetectionORM, detection_id)
if not detection:
raise HTTPException(status_code=404, detail=f"Flood detection id={detection_id} not found")
if detection.status != "DONE":
raise HTTPException(status_code=400, detail=f"Flood detection is not DONE: {detection.status}")
existing = (
await db.execute(
select(FloodProductORM)
.options(
selectinload(FloodProductORM.detection),
selectinload(FloodProductORM.overlay),
)
.where(FloodProductORM.detection_id == detection_id)
.order_by(FloodProductORM.id.desc())
)
).scalars().first()
if existing:
return _product_to_dict(existing)
overlay = (
await db.execute(
select(FloodOverlayORM)
.where(FloodOverlayORM.detection_id == detection_id)
.order_by(FloodOverlayORM.id.desc())
)
).scalars().first()
product = FloodProductORM(
product_id=f"FLOOD-{detection_id:06d}",
detection_id=detection_id,
overlay_id=overlay.id if overlay else None,
display_name=f"Flood detection #{detection_id}",
status="READY",
publish_dir=detection.output_dir,
manifest_path=None,
summary_json={
"created_from": "flood_detection",
"flood_area_km2": detection.flood_area_km2,
"stable_water_area_km2": detection.stable_water_area_km2,
"classified_path": detection.classified_path,
"created_at": datetime.utcnow().isoformat(),
},
)
db.add(product)
await db.commit()
return await get_flood_product(str(product.id), db)
async def _get_product(product_id_or_pk: str, db: AsyncSession) -> FloodProductORM:
value = str(product_id_or_pk).strip()
query = select(FloodProductORM).options(
selectinload(FloodProductORM.detection).selectinload(FloodDetectionORM.pre_scene).selectinload(SARSceneGeoORM.radar_data),
selectinload(FloodProductORM.detection).selectinload(FloodDetectionORM.post_scene).selectinload(SARSceneGeoORM.radar_data),
selectinload(FloodProductORM.overlay),
)
if value.isdigit():
query = query.where(FloodProductORM.id == int(value))
else:
query = query.where(FloodProductORM.product_id == value)
product = (await db.execute(query)).scalars().first()
if not product:
raise HTTPException(status_code=404, detail=f"Flood product {product_id_or_pk} not found")
return product
def _build_manifest_from_db(product: FloodProductORM) -> dict[str, Any]:
detection = product.detection
overlay = product.overlay
pre_scene = detection.pre_scene if detection else None
post_scene = detection.post_scene if detection else None
pre_radar = pre_scene.radar_data if pre_scene else None
post_radar = post_scene.radar_data if post_scene else None
return {
"schema": "flood_product_manifest.v1",
"product": _product_to_dict(product),
"detection": {
"id": detection.id if detection else None,
"status": detection.status if detection else None,
"classified_path": detection.classified_path if detection else None,
"flood_area_km2": detection.flood_area_km2 if detection else None,
"stable_water_area_km2": detection.stable_water_area_km2 if detection else None,
"pre_scene": {
"id": pre_scene.id if pre_scene else None,
"radar_data_id": pre_scene.radar_data_id if pre_scene else None,
"satellite": pre_radar.satellite if pre_radar else None,
"imaging_date": pre_radar.imaging_date if pre_radar else None,
"geo_path": pre_scene.geo_path if pre_scene else None,
"analysis_tif_path": pre_scene.analysis_tif_path if pre_scene else None,
"analysis_engine": pre_scene.analysis_engine if pre_scene else None,
"analysis_profile": pre_scene.analysis_profile if pre_scene else None,
},
"post_scene": {
"id": post_scene.id if post_scene else None,
"radar_data_id": post_scene.radar_data_id if post_scene else None,
"satellite": post_radar.satellite if post_radar else None,
"imaging_date": post_radar.imaging_date if post_radar else None,
"geo_path": post_scene.geo_path if post_scene else None,
"analysis_tif_path": post_scene.analysis_tif_path if post_scene else None,
"analysis_engine": post_scene.analysis_engine if post_scene else None,
"analysis_profile": post_scene.analysis_profile if post_scene else None,
},
},
"overlay": {
"id": overlay.id if overlay else None,
"flood_vector_path": overlay.flood_vector_path if overlay else None,
"affected_area_km2": overlay.affected_area_km2 if overlay else None,
"hazard_points_hit": overlay.hazard_points_hit if overlay else 0,
"hazard_points_near": overlay.hazard_points_near if overlay else 0,
"dinsar_products_intersecting": overlay.dinsar_products_intersecting if overlay else 0,
"summary": overlay.summary_json if overlay else None,
},
}