fix(map): make area analysis scale-aware
This commit is contained in:
@@ -225,6 +225,7 @@ class FloodHazardAnalysisService:
|
||||
selection_geometry_4326=selection_4326,
|
||||
nodata=FloodHazardAcquisitionService.NODATA,
|
||||
max_pixels=resolved_settings.flood_hazard_max_pixels,
|
||||
dataset_ids=payload.dataset_ids,
|
||||
)
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
@@ -51,17 +51,17 @@ class RasterPartitionAnalysisService:
|
||||
source_name: str,
|
||||
product_key: str,
|
||||
bbox: tuple[float, float, float, float],
|
||||
dataset_ids: list[UUID] | None = None,
|
||||
) -> list[Dataset]:
|
||||
rows = (
|
||||
db.query(Dataset)
|
||||
.filter(
|
||||
Dataset.project_id == project_id,
|
||||
Dataset.source_name == source_name,
|
||||
Dataset.dataset_type == "raster",
|
||||
Dataset.status == "ready",
|
||||
)
|
||||
.all()
|
||||
query = db.query(Dataset).filter(
|
||||
Dataset.project_id == project_id,
|
||||
Dataset.source_name == source_name,
|
||||
Dataset.dataset_type == "raster",
|
||||
Dataset.status == "ready",
|
||||
)
|
||||
if dataset_ids is not None:
|
||||
query = query.filter(Dataset.id.in_(dataset_ids))
|
||||
rows = query.all()
|
||||
candidates = [
|
||||
dataset
|
||||
for dataset in rows
|
||||
@@ -78,6 +78,13 @@ class RasterPartitionAnalysisService:
|
||||
details={"source_name": source_name, "product_key": product_key},
|
||||
status_code=404,
|
||||
)
|
||||
if dataset_ids is not None and {dataset.id for dataset in candidates} != set(dataset_ids):
|
||||
raise AppError(
|
||||
code="RASTER_PARTITION_SOURCE_MISMATCH",
|
||||
message="Every requested raster partition must match the governed source product and selection",
|
||||
details={"requested_count": len(dataset_ids), "eligible_count": len(candidates)},
|
||||
status_code=409,
|
||||
)
|
||||
if len(candidates) > RasterPartitionAnalysisService.MAX_PARTITIONS:
|
||||
raise AppError(
|
||||
code="RASTER_PARTITION_LIMIT_EXCEEDED",
|
||||
@@ -100,6 +107,7 @@ class RasterPartitionAnalysisService:
|
||||
selection_geometry_4326,
|
||||
nodata: float,
|
||||
max_pixels: int,
|
||||
dataset_ids: list[UUID] | None = None,
|
||||
) -> RasterPartitionSelection:
|
||||
try:
|
||||
import numpy as np
|
||||
@@ -120,6 +128,7 @@ class RasterPartitionAnalysisService:
|
||||
source_name=source_name,
|
||||
product_key=product_key,
|
||||
bbox=bbox,
|
||||
dataset_ids=dataset_ids,
|
||||
)
|
||||
transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
|
||||
selection_metric = shapely_transform(transformer.transform, selection_geometry_4326)
|
||||
|
||||
@@ -235,6 +235,7 @@ class TerrainAnalysisService:
|
||||
selection_geometry_4326=selection_4326,
|
||||
nodata=DhmvAcquisitionService.NODATA,
|
||||
max_pixels=resolved_settings.dhmv_max_pixels,
|
||||
dataset_ids=payload.dataset_ids,
|
||||
)
|
||||
surface_models = {
|
||||
str((dataset.source_metadata or {}).get("surface_model") or "")
|
||||
|
||||
@@ -11,7 +11,7 @@ from geoalchemy2.shape import to_shape
|
||||
from shapely.geometry import box, mapping, shape
|
||||
from shapely.ops import transform as transform_geometry
|
||||
from shapely.validation import make_valid
|
||||
from sqlalchemy import Float, cast, func
|
||||
from sqlalchemy import Float, String, cast, func
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.models import Dataset, VectorFeature
|
||||
@@ -548,6 +548,8 @@ class VectorFeatureService:
|
||||
selection_area_id: UUID | None = None,
|
||||
full_dataset_area: bool = False,
|
||||
preclipped_partition_filter: tuple[str, str] | None = None,
|
||||
dataset_ids: list[UUID] | None = None,
|
||||
deduplicate_source_features: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||
safe_limit = max(1, min(int(limit), 1000))
|
||||
@@ -561,13 +563,19 @@ class VectorFeatureService:
|
||||
4326,
|
||||
)
|
||||
|
||||
query = db.query(VectorFeature).filter(VectorFeature.dataset_id == dataset_id)
|
||||
selected_dataset_ids = dataset_ids or [dataset_id]
|
||||
query = db.query(VectorFeature).filter(VectorFeature.dataset_id.in_(selected_dataset_ids))
|
||||
if preclipped_partition_filter is not None:
|
||||
partition_property, partition_value = preclipped_partition_filter
|
||||
query = query.filter(VectorFeature.properties_json.op("->>")(partition_property) == partition_value)
|
||||
if not full_dataset_area:
|
||||
query = query.filter(ST_Intersects(VectorFeature.geometry, selection_shape))
|
||||
if hasattr(query, "count"):
|
||||
if deduplicate_source_features:
|
||||
identity = func.coalesce(VectorFeature.source_feature_id, cast(VectorFeature.id, String))
|
||||
total_feature_count = int(
|
||||
query.with_entities(func.count(func.distinct(identity))).scalar() or 0
|
||||
)
|
||||
elif hasattr(query, "count"):
|
||||
total_feature_count = int(query.count())
|
||||
else: # Lightweight unit-test sessions do not always implement Query.count().
|
||||
total_feature_count = len(query.all())
|
||||
@@ -585,6 +593,7 @@ class VectorFeatureService:
|
||||
summary = VectorFeatureService.summarize_features_by_bbox(
|
||||
db,
|
||||
dataset=dataset,
|
||||
dataset_ids=selected_dataset_ids,
|
||||
bbox=normalized_bbox,
|
||||
total_feature_count=total_feature_count,
|
||||
selection_geometry=selection_geometry,
|
||||
|
||||
Reference in New Issue
Block a user