feat: add temporal Mol explorer
This commit is contained in:
@@ -9,9 +9,10 @@ from geoalchemy2.shape import to_shape
|
||||
from shapely.geometry import mapping
|
||||
from shapely.geometry import shape
|
||||
from shapely.validation import make_valid
|
||||
from sqlalchemy import Float, cast, func
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.models import VectorFeature
|
||||
from app.models import Dataset, VectorFeature
|
||||
|
||||
|
||||
class VectorFeatureService:
|
||||
@@ -88,6 +89,7 @@ class VectorFeatureService:
|
||||
dataset_id: UUID,
|
||||
bbox: dict[str, Any],
|
||||
limit: int = 100,
|
||||
dataset: Dataset | None = None,
|
||||
) -> dict[str, Any]:
|
||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||
safe_limit = max(1, min(int(limit), 1000))
|
||||
@@ -121,6 +123,14 @@ class VectorFeatureService:
|
||||
truncated = total_feature_count > safe_limit
|
||||
selected_rows = rows[:safe_limit]
|
||||
features = [VectorFeatureService._row_to_geojson_feature(row) for row in selected_rows]
|
||||
summary = None
|
||||
if dataset and isinstance(dataset.source_metadata, dict) and dataset.source_metadata.get("selection_aggregation"):
|
||||
summary = VectorFeatureService.summarize_features_by_bbox(
|
||||
db,
|
||||
dataset=dataset,
|
||||
bbox=normalized_bbox,
|
||||
total_feature_count=total_feature_count,
|
||||
)
|
||||
|
||||
return {
|
||||
"selection_bbox": normalized_bbox,
|
||||
@@ -132,6 +142,97 @@ class VectorFeatureService:
|
||||
"type": "FeatureCollection",
|
||||
"features": features,
|
||||
},
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def summarize_features_by_bbox(
|
||||
db,
|
||||
*,
|
||||
dataset: Dataset,
|
||||
bbox: dict[str, Any],
|
||||
total_feature_count: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||
envelope = ST_MakeEnvelope(
|
||||
normalized_bbox["min_x"],
|
||||
normalized_bbox["min_y"],
|
||||
normalized_bbox["max_x"],
|
||||
normalized_bbox["max_y"],
|
||||
4326,
|
||||
)
|
||||
selection_filter = (
|
||||
VectorFeature.dataset_id == dataset.id,
|
||||
ST_Intersects(VectorFeature.geometry, envelope),
|
||||
)
|
||||
feature_count = total_feature_count
|
||||
if feature_count is None:
|
||||
feature_count = int(db.query(func.count(VectorFeature.id)).filter(*selection_filter).scalar() or 0)
|
||||
|
||||
source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {}
|
||||
config = source_metadata.get("selection_aggregation")
|
||||
if not isinstance(config, dict):
|
||||
config = {}
|
||||
method = str(config.get("method") or "feature_count")
|
||||
label = str(config.get("label") or "Objecten")
|
||||
unit = str(config.get("unit") or "objecten")
|
||||
warning = str(config["warning"]) if config.get("warning") else None
|
||||
is_estimate = bool(config.get("is_estimate", False))
|
||||
|
||||
metric_value = float(feature_count)
|
||||
if method == "intersection_area":
|
||||
intersection = func.ST_Intersection(VectorFeature.geometry, envelope)
|
||||
area_expression = func.ST_Area(func.ST_Transform(intersection, 31370))
|
||||
area_m2 = db.query(func.coalesce(func.sum(area_expression), 0.0)).filter(*selection_filter).scalar()
|
||||
divisor = 10_000.0 if unit == "ha" else 1.0
|
||||
metric_value = float(area_m2 or 0.0) / divisor
|
||||
elif method == "intersection_length":
|
||||
intersection = func.ST_Intersection(VectorFeature.geometry, envelope)
|
||||
length_expression = func.ST_Length(func.ST_Transform(intersection, 31370))
|
||||
length_m = db.query(func.coalesce(func.sum(length_expression), 0.0)).filter(*selection_filter).scalar()
|
||||
divisor = 1_000.0 if unit == "km" else 1.0
|
||||
metric_value = float(length_m or 0.0) / divisor
|
||||
elif method in {"sum", "area_weighted_sum"}:
|
||||
property_name = str(config.get("property") or "").strip()
|
||||
if not property_name:
|
||||
raise AppError(
|
||||
code="INVALID_SELECTION_AGGREGATION",
|
||||
message="Dataset selection aggregation requires a numeric property",
|
||||
details={"dataset_id": str(dataset.id), "method": method},
|
||||
status_code=500,
|
||||
)
|
||||
numeric_value = cast(VectorFeature.properties_json.op("->>")(property_name), Float)
|
||||
value_expression = numeric_value
|
||||
if method == "area_weighted_sum":
|
||||
source_area = func.ST_Area(func.ST_Transform(VectorFeature.geometry, 31370))
|
||||
intersection_area = func.ST_Area(
|
||||
func.ST_Transform(func.ST_Intersection(VectorFeature.geometry, envelope), 31370)
|
||||
)
|
||||
value_expression = numeric_value * intersection_area / func.nullif(source_area, 0.0)
|
||||
is_estimate = True
|
||||
aggregate_value = (
|
||||
db.query(func.coalesce(func.sum(value_expression), 0.0))
|
||||
.filter(*selection_filter)
|
||||
.filter(VectorFeature.properties_json.op("->>")(property_name).isnot(None))
|
||||
.scalar()
|
||||
)
|
||||
metric_value = float(aggregate_value or 0.0)
|
||||
elif method != "feature_count":
|
||||
raise AppError(
|
||||
code="INVALID_SELECTION_AGGREGATION",
|
||||
message="Unsupported dataset selection aggregation",
|
||||
details={"dataset_id": str(dataset.id), "method": method},
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
return {
|
||||
"metric_label": label,
|
||||
"metric_value": metric_value,
|
||||
"metric_unit": unit,
|
||||
"aggregation_method": method,
|
||||
"feature_count": feature_count,
|
||||
"is_estimate": is_estimate,
|
||||
"warning": warning,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user