Constrain selections to active work areas
This commit is contained in:
@@ -398,14 +398,20 @@ def select_vector_features(
|
||||
full_dataset_area = False
|
||||
preclipped_partition_filter = None
|
||||
if selection_area is not None:
|
||||
full_dataset_area = VectorFeatureService.can_use_full_area_fast_path(dataset, selection_area.id)
|
||||
if not full_dataset_area:
|
||||
preclipped_partition_filter = VectorFeatureService.preclipped_partition_filter(
|
||||
dataset,
|
||||
getattr(selection_area, "name", None),
|
||||
)
|
||||
selection_geometry, covers_full_area = VectorFeatureService.constrain_bbox_to_area(
|
||||
payload.bbox.model_dump(),
|
||||
selection_area.geometry,
|
||||
)
|
||||
full_dataset_area = covers_full_area and VectorFeatureService.can_use_full_area_fast_path(
|
||||
dataset,
|
||||
selection_area.id,
|
||||
)
|
||||
preclipped_partition_filter = VectorFeatureService.preclipped_partition_filter(
|
||||
dataset,
|
||||
getattr(selection_area, "name", None),
|
||||
)
|
||||
selection_kwargs.update(
|
||||
selection_geometry=selection_area.geometry,
|
||||
selection_geometry=selection_geometry,
|
||||
selection_area_id=selection_area.id,
|
||||
full_dataset_area=full_dataset_area,
|
||||
preclipped_partition_filter=preclipped_partition_filter,
|
||||
@@ -418,7 +424,7 @@ def select_vector_features(
|
||||
"total_feature_count": result.get("total_feature_count"),
|
||||
}
|
||||
if selection_area is not None:
|
||||
summary_kwargs["selection_geometry"] = selection_area.geometry
|
||||
summary_kwargs["selection_geometry"] = selection_geometry
|
||||
summary_kwargs["full_dataset_area"] = full_dataset_area
|
||||
summary_kwargs["preclipped_partition_filter"] = preclipped_partition_filter
|
||||
result["summary"] = VectorFeatureService.summarize_features_by_bbox(db, **summary_kwargs)
|
||||
@@ -437,10 +443,23 @@ def derive_vector_selection_dataset(
|
||||
raise HTTPException(status_code=404, detail="Dataset not found")
|
||||
if dataset.dataset_type not in {"vector", "geojson"}:
|
||||
raise AppError(code="DATASET_NOT_VECTOR", message="Area selection requires a vector dataset", status_code=400)
|
||||
selection_geometry = None
|
||||
selection_area_id = None
|
||||
if payload.area_id is not None:
|
||||
selection_area = db.get(Area, payload.area_id)
|
||||
if selection_area is None or selection_area.project_id != project_id:
|
||||
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
|
||||
selection_geometry, _covers_full_area = VectorFeatureService.constrain_bbox_to_area(
|
||||
payload.bbox.model_dump(),
|
||||
selection_area.geometry,
|
||||
)
|
||||
selection_area_id = selection_area.id
|
||||
derived = VectorOperationsService.derive_selection_dataset(
|
||||
db=db,
|
||||
dataset_id=dataset_id,
|
||||
bbox=payload.bbox.model_dump(),
|
||||
selection_geometry=selection_geometry,
|
||||
selection_area_id=selection_area_id,
|
||||
limit=payload.limit,
|
||||
output_name=payload.output_name,
|
||||
)
|
||||
|
||||
@@ -227,14 +227,20 @@ class ExportService:
|
||||
area = db.get(Area, area_id)
|
||||
if not area or area.project_id != dataset.project_id:
|
||||
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
|
||||
full_dataset_area = VectorFeatureService.can_use_full_area_fast_path(dataset, area.id)
|
||||
preclipped_partition_filter = (
|
||||
None
|
||||
if full_dataset_area
|
||||
else VectorFeatureService.preclipped_partition_filter(dataset, getattr(area, "name", None))
|
||||
selection_geometry, covers_full_area = VectorFeatureService.constrain_bbox_to_area(
|
||||
bbox,
|
||||
area.geometry,
|
||||
)
|
||||
full_dataset_area = covers_full_area and VectorFeatureService.can_use_full_area_fast_path(
|
||||
dataset,
|
||||
area.id,
|
||||
)
|
||||
preclipped_partition_filter = VectorFeatureService.preclipped_partition_filter(
|
||||
dataset,
|
||||
getattr(area, "name", None),
|
||||
)
|
||||
selection_kwargs.update(
|
||||
selection_geometry=area.geometry,
|
||||
selection_geometry=selection_geometry,
|
||||
selection_area_id=area.id,
|
||||
full_dataset_area=full_dataset_area,
|
||||
preclipped_partition_filter=preclipped_partition_filter,
|
||||
|
||||
@@ -111,6 +111,13 @@ class TemporalAnalysisService:
|
||||
|
||||
bbox = payload.bbox.model_dump()
|
||||
selection_area = TemporalAnalysisService._get_selection_area(db, project_id, payload.area_id)
|
||||
selection_geometry = None
|
||||
selection_covers_full_area = False
|
||||
if selection_area is not None:
|
||||
selection_geometry, selection_covers_full_area = VectorFeatureService.constrain_bbox_to_area(
|
||||
bbox,
|
||||
selection_area.geometry,
|
||||
)
|
||||
summaries: dict[UUID, dict[str, Any]] = {}
|
||||
|
||||
def summarize(dataset: Dataset) -> dict[str, Any]:
|
||||
@@ -119,10 +126,13 @@ class TemporalAnalysisService:
|
||||
return cached
|
||||
kwargs: dict[str, Any] = {"dataset": dataset, "bbox": bbox}
|
||||
if selection_area is not None:
|
||||
kwargs["selection_geometry"] = selection_area.geometry
|
||||
kwargs["full_dataset_area"] = VectorFeatureService.can_use_full_area_fast_path(
|
||||
dataset,
|
||||
selection_area.id,
|
||||
kwargs["selection_geometry"] = selection_geometry
|
||||
kwargs["full_dataset_area"] = (
|
||||
selection_covers_full_area
|
||||
and VectorFeatureService.can_use_full_area_fast_path(
|
||||
dataset,
|
||||
selection_area.id,
|
||||
)
|
||||
)
|
||||
summary = VectorFeatureService.summarize_features_by_bbox(db, **kwargs)
|
||||
summaries[dataset.id] = summary
|
||||
@@ -154,14 +164,16 @@ class TemporalAnalysisService:
|
||||
later=later,
|
||||
bbox=bbox,
|
||||
preview_limit=payload.preview_limit,
|
||||
selection_geometry=selection_area.geometry if selection_area is not None else None,
|
||||
selection_geometry=selection_geometry,
|
||||
earlier_full_dataset_area=(
|
||||
VectorFeatureService.can_use_full_area_fast_path(earlier, selection_area.id)
|
||||
selection_covers_full_area
|
||||
and VectorFeatureService.can_use_full_area_fast_path(earlier, selection_area.id)
|
||||
if selection_area is not None
|
||||
else False
|
||||
),
|
||||
later_full_dataset_area=(
|
||||
VectorFeatureService.can_use_full_area_fast_path(later, selection_area.id)
|
||||
selection_covers_full_area
|
||||
and VectorFeatureService.can_use_full_area_fast_path(later, selection_area.id)
|
||||
if selection_area is not None
|
||||
else False
|
||||
),
|
||||
|
||||
@@ -8,8 +8,7 @@ from uuid import UUID
|
||||
from geoalchemy2.functions import ST_Intersects, ST_MakeEnvelope
|
||||
from geoalchemy2.shape import from_shape
|
||||
from geoalchemy2.shape import to_shape
|
||||
from shapely.geometry import mapping
|
||||
from shapely.geometry import 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
|
||||
@@ -189,6 +188,27 @@ class VectorFeatureService:
|
||||
source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {}
|
||||
return isinstance(source_metadata.get("selection_aggregation"), dict) or VectorFeatureService._dataset_theme(dataset) is not None
|
||||
|
||||
@staticmethod
|
||||
def constrain_bbox_to_area(
|
||||
bbox: dict[str, Any],
|
||||
area_geometry: Any,
|
||||
) -> tuple[Any, bool]:
|
||||
bbox_geometry = box(
|
||||
float(bbox["min_x"]),
|
||||
float(bbox["min_y"]),
|
||||
float(bbox["max_x"]),
|
||||
float(bbox["max_y"]),
|
||||
)
|
||||
area_shape = to_shape(area_geometry)
|
||||
constrained_geometry = bbox_geometry.intersection(area_shape)
|
||||
if constrained_geometry.is_empty or constrained_geometry.area <= 0:
|
||||
raise AppError(
|
||||
code="VECTOR_SELECTION_OUTSIDE_AREA",
|
||||
message="Selection does not overlap the selected work area",
|
||||
status_code=422,
|
||||
)
|
||||
return from_shape(constrained_geometry, srid=4326), constrained_geometry.equals(area_shape)
|
||||
|
||||
@staticmethod
|
||||
def can_use_full_area_fast_path(dataset: Dataset, selection_area_id: UUID | None) -> bool:
|
||||
if selection_area_id is None or dataset.area_id != selection_area_id:
|
||||
@@ -343,7 +363,7 @@ class VectorFeatureService:
|
||||
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)
|
||||
elif not full_dataset_area:
|
||||
if not full_dataset_area:
|
||||
query = query.filter(ST_Intersects(VectorFeature.geometry, selection_shape))
|
||||
if hasattr(query, "count"):
|
||||
total_feature_count = int(query.count())
|
||||
@@ -413,9 +433,9 @@ class VectorFeatureService:
|
||||
selection_filter += (
|
||||
VectorFeature.properties_json.op("->>")(partition_property) == partition_value,
|
||||
)
|
||||
elif not full_dataset_area:
|
||||
if not full_dataset_area:
|
||||
selection_filter += (ST_Intersects(VectorFeature.geometry, selection_shape),)
|
||||
selection_is_preclipped = full_dataset_area or preclipped_partition_filter is not None
|
||||
selection_is_preclipped = full_dataset_area
|
||||
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)
|
||||
|
||||
@@ -284,6 +284,8 @@ class VectorOperationsService:
|
||||
db: Session,
|
||||
dataset_id: uuid.UUID,
|
||||
bbox: dict[str, Any],
|
||||
selection_geometry: Any | None = None,
|
||||
selection_area_id: uuid.UUID | None = None,
|
||||
limit: int = 250,
|
||||
output_name: str | None = None,
|
||||
) -> DatasetCreateResponse:
|
||||
@@ -292,7 +294,14 @@ class VectorOperationsService:
|
||||
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)
|
||||
selection = VectorFeatureService.select_features_by_bbox(
|
||||
db,
|
||||
dataset_id=dataset_id,
|
||||
bbox=bbox,
|
||||
selection_geometry=selection_geometry,
|
||||
selection_area_id=selection_area_id,
|
||||
limit=limit,
|
||||
)
|
||||
if selection["feature_count"] <= 0:
|
||||
raise AppError(
|
||||
code="VECTOR_OPERATION_EMPTY_RESULT",
|
||||
@@ -316,6 +325,7 @@ class VectorOperationsService:
|
||||
source_name="map_selection",
|
||||
source_metadata={
|
||||
"selection_bbox": selection["selection_bbox"],
|
||||
"selection_area_id": selection.get("selection_area_id"),
|
||||
"feature_count": selection["feature_count"],
|
||||
"limit": selection["limit"],
|
||||
"truncated": selection["truncated"],
|
||||
@@ -326,9 +336,11 @@ class VectorOperationsService:
|
||||
"source_dataset_id": str(dataset_id),
|
||||
"source_table": "vector_features",
|
||||
"selection_bbox": selection["selection_bbox"],
|
||||
"selection_area_id": selection.get("selection_area_id"),
|
||||
},
|
||||
metadata_extra={
|
||||
"selection_bbox": selection["selection_bbox"],
|
||||
"selection_area_id": selection.get("selection_area_id"),
|
||||
"source_feature_count": selection["feature_count"],
|
||||
"selection_limit": selection["limit"],
|
||||
"selection_truncated": selection["truncated"],
|
||||
|
||||
Reference in New Issue
Block a user