perf: optimize trusted full-area analysis
This commit is contained in:
@@ -18,7 +18,25 @@ from app.core.errors import AppError
|
||||
from app.models import Dataset, VectorFeature
|
||||
|
||||
|
||||
FULL_AREA_CLIPPED_OPERATOR_TOOLS = {
|
||||
"provision_mol_population_history.py",
|
||||
"provision_official_landuse_timeseries.py",
|
||||
"provision_regional_grb_buildings.py",
|
||||
"provision_regional_grb_context.py",
|
||||
}
|
||||
|
||||
|
||||
class VectorFeatureService:
|
||||
@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:
|
||||
return False
|
||||
source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {}
|
||||
if source_metadata.get("geometry_clipped_to_area") is True:
|
||||
return True
|
||||
provenance = dataset.provenance_metadata if isinstance(dataset.provenance_metadata, dict) else {}
|
||||
return provenance.get("operator_tool") in FULL_AREA_CLIPPED_OPERATOR_TOOLS
|
||||
|
||||
@staticmethod
|
||||
def _feature_row(dataset_id: UUID, feature: dict[str, Any], index: int, feature_class: str | None) -> VectorFeature | None:
|
||||
geometry_payload = feature.get("geometry")
|
||||
@@ -126,6 +144,7 @@ class VectorFeatureService:
|
||||
dataset: Dataset | None = None,
|
||||
selection_geometry: Any | None = None,
|
||||
selection_area_id: UUID | None = None,
|
||||
full_dataset_area: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||
safe_limit = max(1, min(int(limit), 1000))
|
||||
@@ -139,11 +158,9 @@ class VectorFeatureService:
|
||||
4326,
|
||||
)
|
||||
|
||||
query = (
|
||||
db.query(VectorFeature)
|
||||
.filter(VectorFeature.dataset_id == dataset_id)
|
||||
.filter(ST_Intersects(VectorFeature.geometry, selection_shape))
|
||||
)
|
||||
query = db.query(VectorFeature).filter(VectorFeature.dataset_id == dataset_id)
|
||||
if not full_dataset_area:
|
||||
query = query.filter(ST_Intersects(VectorFeature.geometry, selection_shape))
|
||||
if hasattr(query, "count"):
|
||||
total_feature_count = int(query.count())
|
||||
else: # Lightweight unit-test sessions do not always implement Query.count().
|
||||
@@ -165,6 +182,7 @@ class VectorFeatureService:
|
||||
bbox=normalized_bbox,
|
||||
total_feature_count=total_feature_count,
|
||||
selection_geometry=selection_geometry,
|
||||
full_dataset_area=full_dataset_area,
|
||||
)
|
||||
|
||||
result = {
|
||||
@@ -191,6 +209,7 @@ class VectorFeatureService:
|
||||
bbox: dict[str, Any],
|
||||
total_feature_count: int | None = None,
|
||||
selection_geometry: Any | None = None,
|
||||
full_dataset_area: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||
selection_shape = selection_geometry
|
||||
@@ -202,10 +221,9 @@ class VectorFeatureService:
|
||||
normalized_bbox["max_y"],
|
||||
4326,
|
||||
)
|
||||
selection_filter = (
|
||||
VectorFeature.dataset_id == dataset.id,
|
||||
ST_Intersects(VectorFeature.geometry, selection_shape),
|
||||
)
|
||||
selection_filter = (VectorFeature.dataset_id == dataset.id,)
|
||||
if not full_dataset_area:
|
||||
selection_filter += (ST_Intersects(VectorFeature.geometry, selection_shape),)
|
||||
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)
|
||||
@@ -222,14 +240,22 @@ class VectorFeatureService:
|
||||
|
||||
metric_value = float(feature_count)
|
||||
if method == "intersection_area":
|
||||
intersection = func.ST_Intersection(VectorFeature.geometry, selection_shape)
|
||||
area_expression = func.ST_Area(func.ST_Transform(intersection, 31370))
|
||||
measured_geometry = (
|
||||
VectorFeature.geometry
|
||||
if full_dataset_area
|
||||
else func.ST_Intersection(VectorFeature.geometry, selection_shape)
|
||||
)
|
||||
area_expression = func.ST_Area(func.ST_Transform(measured_geometry, 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, selection_shape)
|
||||
length_expression = func.ST_Length(func.ST_Transform(intersection, 31370))
|
||||
measured_geometry = (
|
||||
VectorFeature.geometry
|
||||
if full_dataset_area
|
||||
else func.ST_Intersection(VectorFeature.geometry, selection_shape)
|
||||
)
|
||||
length_expression = func.ST_Length(func.ST_Transform(measured_geometry, 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
|
||||
@@ -244,7 +270,7 @@ class VectorFeatureService:
|
||||
)
|
||||
numeric_value = cast(VectorFeature.properties_json.op("->>")(property_name), Float)
|
||||
value_expression = numeric_value
|
||||
if method == "area_weighted_sum":
|
||||
if method == "area_weighted_sum" and not full_dataset_area:
|
||||
source_area = func.ST_Area(func.ST_Transform(VectorFeature.geometry, 31370))
|
||||
intersection_area = func.ST_Area(
|
||||
func.ST_Transform(func.ST_Intersection(VectorFeature.geometry, selection_shape), 31370)
|
||||
@@ -258,7 +284,7 @@ class VectorFeatureService:
|
||||
.scalar()
|
||||
)
|
||||
metric_value = float(aggregate_value or 0.0)
|
||||
if method == "area_weighted_sum":
|
||||
if method == "area_weighted_sum" and not full_dataset_area:
|
||||
partial_feature_count = (
|
||||
db.query(func.count(VectorFeature.id))
|
||||
.filter(*selection_filter)
|
||||
@@ -268,6 +294,10 @@ class VectorFeatureService:
|
||||
is_estimate = bool(partial_feature_count)
|
||||
if not is_estimate and config.get("warning_only_when_estimate", True):
|
||||
warning = None
|
||||
elif method == "area_weighted_sum":
|
||||
is_estimate = False
|
||||
if config.get("warning_only_when_estimate", True):
|
||||
warning = None
|
||||
elif method != "feature_count":
|
||||
raise AppError(
|
||||
code="INVALID_SELECTION_AGGREGATION",
|
||||
|
||||
Reference in New Issue
Block a user