from __future__ import annotations from typing import Any from uuid import UUID from geoalchemy2.shape import from_shape from shapely.geometry import shape from shapely.validation import make_valid from app.core.errors import AppError from app.models import VectorFeature class VectorFeatureService: @staticmethod def persist_geojson_features( db, dataset_id: UUID, payload: dict[str, Any], feature_class: str | None = None, *, commit: bool = True, ) -> list[VectorFeature]: features = payload.get("features") if payload.get("type") != "FeatureCollection" or not isinstance(features, list): raise AppError(code="INVALID_GEOJSON", message="GeoJSON payload must be a FeatureCollection", status_code=400) persisted: list[VectorFeature] = [] for index, feature in enumerate(features): if not isinstance(feature, dict): raise AppError(code="INVALID_GEOJSON", message=f"Feature {index} must be an object", status_code=400) geometry_payload = feature.get("geometry") if geometry_payload is None: continue try: geometry = shape(geometry_payload) except Exception as exc: raise AppError(code="INVALID_GEOJSON", message=f"Invalid feature geometry at index {index}", status_code=400) from exc if geometry.is_empty: continue if not geometry.is_valid: geometry = make_valid(geometry) if geometry.is_empty or not geometry.is_valid: raise AppError(code="INVALID_GEOMETRY", message=f"Invalid feature geometry at index {index}", status_code=400) properties = feature.get("properties") if isinstance(feature.get("properties"), dict) else {} source_feature_id = feature.get("id") if source_feature_id is None: source_feature_id = properties.get("id") or properties.get("source_feature_id") row = VectorFeature( dataset_id=dataset_id, feature_class=feature_class, source_feature_id=str(source_feature_id) if source_feature_id is not None else None, properties_json=properties, geometry=from_shape(geometry, srid=4326), ) db.add(row) persisted.append(row) if commit: db.commit() for row in persisted: db.refresh(row) return persisted