Persist map selections as derived datasets
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -15,9 +16,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.models import Area, Dataset
|
||||
from app.schemas.dataset import DatasetCreateResponse
|
||||
from app.schemas.operations import VectorOperationResult
|
||||
from app.services.geojson_service import parse_geojson_payload
|
||||
from app.services.storage_service import StorageService
|
||||
from app.services.vector_feature_service import VectorFeatureService
|
||||
|
||||
|
||||
class VectorOperationsService:
|
||||
@@ -276,6 +279,125 @@ class VectorOperationsService:
|
||||
default_name="vector_intersect",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def derive_selection_dataset(
|
||||
db: Session,
|
||||
dataset_id: uuid.UUID,
|
||||
bbox: dict[str, Any],
|
||||
limit: int = 250,
|
||||
output_name: str | None = None,
|
||||
) -> DatasetCreateResponse:
|
||||
source_dataset = db.get(Dataset, dataset_id)
|
||||
if not source_dataset:
|
||||
raise AppError(code="DATASET_NOT_FOUND", message="Dataset not found", status_code=404)
|
||||
VectorOperationsService._require_vector_dataset(source_dataset)
|
||||
|
||||
selection = VectorFeatureService.select_features_by_bbox(db, dataset_id=dataset_id, bbox=bbox, limit=limit)
|
||||
if selection["feature_count"] <= 0:
|
||||
raise AppError(
|
||||
code="VECTOR_OPERATION_EMPTY_RESULT",
|
||||
message="Selection produced no output features",
|
||||
status_code=422,
|
||||
)
|
||||
|
||||
feature_collection = VectorOperationsService._selection_geojson_for_derived_dataset(
|
||||
selection["geojson"],
|
||||
source_dataset_id=dataset_id,
|
||||
)
|
||||
derived_id = VectorOperationsService._persist_derived_dataset(
|
||||
db=db,
|
||||
source_dataset=source_dataset,
|
||||
source_id=dataset_id,
|
||||
operation="selection",
|
||||
feature_collection=feature_collection,
|
||||
output_name=output_name,
|
||||
default_name="map_selection",
|
||||
dataset_role="derived",
|
||||
source_name="map_selection",
|
||||
source_metadata={
|
||||
"selection_bbox": selection["selection_bbox"],
|
||||
"feature_count": selection["feature_count"],
|
||||
"limit": selection["limit"],
|
||||
"truncated": selection["truncated"],
|
||||
"source_table": "vector_features",
|
||||
},
|
||||
provenance_metadata={
|
||||
"operation": "map_bbox_selection",
|
||||
"source_dataset_id": str(dataset_id),
|
||||
"source_table": "vector_features",
|
||||
"selection_bbox": selection["selection_bbox"],
|
||||
},
|
||||
metadata_extra={
|
||||
"selection_bbox": selection["selection_bbox"],
|
||||
"source_feature_count": selection["feature_count"],
|
||||
"selection_limit": selection["limit"],
|
||||
"selection_truncated": selection["truncated"],
|
||||
"source_dataset_id": str(dataset_id),
|
||||
"source_table": "vector_features",
|
||||
},
|
||||
persist_vector_features=True,
|
||||
)
|
||||
derived = db.get(Dataset, derived_id)
|
||||
if not derived:
|
||||
raise AppError(code="DATASET_NOT_FOUND", message="Derived dataset was not persisted", status_code=500)
|
||||
metadata = derived.metadata_json or {}
|
||||
return DatasetCreateResponse(
|
||||
id=derived.id,
|
||||
name=derived.name,
|
||||
dataset_type=derived.dataset_type,
|
||||
source=derived.source,
|
||||
dataset_role=derived.dataset_role,
|
||||
source_name=derived.source_name,
|
||||
reference_layer_name=derived.reference_layer_name,
|
||||
source_metadata=derived.source_metadata,
|
||||
provenance_metadata=derived.provenance_metadata,
|
||||
imported_at=derived.imported_at,
|
||||
project_id=derived.project_id,
|
||||
area_id=derived.area_id,
|
||||
storage_path=derived.storage_path,
|
||||
original_filename=derived.original_filename,
|
||||
stored_filename=derived.stored_filename,
|
||||
content_type=derived.content_type,
|
||||
size_bytes=derived.size_bytes,
|
||||
checksum_sha256=derived.checksum_sha256,
|
||||
crs=derived.crs,
|
||||
bounds_json=derived.bounds_json,
|
||||
resolution_json=derived.resolution_json,
|
||||
bands_json=derived.bands_json,
|
||||
metadata_json=derived.metadata_json,
|
||||
vector_summary=None,
|
||||
status=derived.status,
|
||||
derived_from_dataset_id=derived.derived_from_dataset_id,
|
||||
created_at=derived.created_at,
|
||||
feature_count=metadata.get("feature_count") if isinstance(metadata, dict) else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _selection_geojson_for_derived_dataset(payload: dict[str, Any], source_dataset_id: uuid.UUID) -> dict[str, Any]:
|
||||
features = payload.get("features")
|
||||
if payload.get("type") != "FeatureCollection" or not isinstance(features, list):
|
||||
raise AppError(code="INVALID_GEOJSON", message="Selection payload must be a FeatureCollection", status_code=500)
|
||||
|
||||
output_features: list[dict[str, Any]] = []
|
||||
for feature in features:
|
||||
if not isinstance(feature, dict):
|
||||
continue
|
||||
properties = dict(feature.get("properties") or {})
|
||||
source_vector_feature_id = properties.pop("vector_feature_id", feature.get("id"))
|
||||
properties.pop("dataset_id", None)
|
||||
properties["source_dataset_id"] = str(source_dataset_id)
|
||||
if source_vector_feature_id is not None:
|
||||
properties["source_vector_feature_id"] = str(source_vector_feature_id)
|
||||
output_features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": feature.get("geometry"),
|
||||
"properties": properties,
|
||||
}
|
||||
)
|
||||
|
||||
return {"type": "FeatureCollection", "features": output_features}
|
||||
|
||||
@staticmethod
|
||||
def _persist_derived_dataset(
|
||||
db: Session,
|
||||
@@ -285,6 +407,12 @@ class VectorOperationsService:
|
||||
feature_collection: dict[str, Any],
|
||||
output_name: str | None,
|
||||
default_name: str,
|
||||
dataset_role: str = "derived",
|
||||
source_name: str | None = None,
|
||||
source_metadata: dict[str, Any] | None = None,
|
||||
provenance_metadata: dict[str, Any] | None = None,
|
||||
metadata_extra: dict[str, Any] | None = None,
|
||||
persist_vector_features: bool = False,
|
||||
) -> uuid.UUID:
|
||||
derived_id = uuid.uuid4()
|
||||
output_name_value = f"{(output_name or default_name)}.geojson"
|
||||
@@ -302,6 +430,8 @@ class VectorOperationsService:
|
||||
)
|
||||
|
||||
metadata = parse_geojson_payload(json.dumps(feature_collection, ensure_ascii=False, separators=(",", ":")))
|
||||
if metadata_extra:
|
||||
metadata.update(metadata_extra)
|
||||
derived_dataset = Dataset(
|
||||
id=derived_id,
|
||||
project_id=source_dataset.project_id,
|
||||
@@ -309,6 +439,11 @@ class VectorOperationsService:
|
||||
name=output_name_value,
|
||||
dataset_type="vector",
|
||||
source=f"operation:{operation}",
|
||||
dataset_role=dataset_role,
|
||||
source_name=source_name,
|
||||
source_metadata=source_metadata,
|
||||
provenance_metadata=provenance_metadata,
|
||||
imported_at=datetime.now(timezone.utc),
|
||||
storage_path=storage_info["storage_path"],
|
||||
original_filename=storage_info["original_filename"],
|
||||
stored_filename=storage_info["stored_filename"],
|
||||
@@ -326,4 +461,10 @@ class VectorOperationsService:
|
||||
db.add(derived_dataset)
|
||||
db.commit()
|
||||
db.refresh(derived_dataset)
|
||||
if persist_vector_features:
|
||||
VectorFeatureService.persist_geojson_features(
|
||||
db=db,
|
||||
dataset_id=derived_dataset.id,
|
||||
payload=feature_collection,
|
||||
)
|
||||
return derived_id
|
||||
|
||||
Reference in New Issue
Block a user