"""Resolve browser overlays only from immutable Server-ingested snapshots.""" from __future__ import annotations from math import isfinite from app.domain.domain_errors import require from app.domain.schemas.operational_data import TaskDataSnapshot def _number(value) -> bool: return isinstance(value, (int, float)) and not isinstance(value, bool) and isfinite(value) def _position(value): require(isinstance(value, list) and len(value) in {2, 3} and all(_number(item) for item in value) and -180 <= value[0] <= 180 and -90 <= value[1] <= 90, "OVERLAY_GEOMETRY_INVALID", "Overlay coordinates must be finite WGS84 positions", status=422) return value[:2] def _geometry(value): require(isinstance(value, dict), "OVERLAY_GEOMETRY_INVALID", "Overlay geometry is missing", status=422) kind = value.get("type") depths = {"Point": 0, "MultiPoint": 1, "LineString": 1, "MultiLineString": 2, "Polygon": 2, "MultiPolygon": 3} require(kind in depths, "OVERLAY_GEOMETRY_INVALID", "Unsupported overlay geometry", status=422) positions = 0 def coordinates(items, depth): nonlocal positions if depth == 0: positions += 1 require(positions <= 20000, "OVERLAY_TOO_LARGE", "Overlay geometry is too large", status=422) return _position(items) require(isinstance(items, list) and 0 < len(items) <= 20000, "OVERLAY_GEOMETRY_INVALID", "Overlay coordinates are empty or too large", status=422) return [coordinates(item, depth - 1) for item in items] normalized = coordinates(value.get("coordinates"), depths[kind]) lines = ([normalized] if kind == "LineString" else normalized if kind == "MultiLineString" else []) require(all(len(line) >= 2 for line in lines), "OVERLAY_GEOMETRY_INVALID", "Overlay lines need at least two positions", status=422) rings = (normalized if kind == "Polygon" else [ring for polygon in normalized for ring in polygon] if kind == "MultiPolygon" else []) require(all(len(ring) >= 4 and ring[0] == ring[-1] for ring in rings), "OVERLAY_GEOMETRY_INVALID", "Overlay polygon rings must be closed", status=422) return {"type": kind, "coordinates": normalized} def resolve_overlay(snapshot: TaskDataSnapshot, mode: str) -> dict: """Use recorded GeoJSON or recorded coordinates; never geocode or infer a boundary.""" data = snapshot.data candidates = [data] if isinstance(data, dict): candidates.extend([data.get("map_payload"), data.get("drawing_payload")]) latest = data.get("latest_analysis") if isinstance(latest, dict): candidates.append(latest.get("map_payload")) collection = next((item for item in candidates if isinstance(item, dict) and item.get("type") == "FeatureCollection"), None) if collection is not None: raw_features = collection.get("features") require(isinstance(raw_features, list), "OVERLAY_GEOMETRY_INVALID", "Snapshot FeatureCollection is invalid", status=422) else: rows = data.get("items") if isinstance(data, dict) else data require(isinstance(rows, list), "OVERLAY_DATA_MISSING", "Evidence snapshot has no recorded coordinates or GeoJSON", status=422) raw_features = [] for row in rows: if not isinstance(row, dict) or row.get("longitude") is None or row.get("latitude") is None: continue raw_features.append({ "type": "Feature", "geometry": {"type": "Point", "coordinates": [row["longitude"], row["latitude"]]}, "id": row.get("device_id") or row.get("point_id") or row.get("work_order_id") or row.get("id"), "properties": {"weight": row.get("weight", 1)}, }) require(0 < len(raw_features) <= 1000, "OVERLAY_DATA_MISSING", "Overlay needs between one and 1000 recorded features", status=422) features = [] for feature in raw_features: require(isinstance(feature, dict) and feature.get("type") == "Feature", "OVERLAY_GEOMETRY_INVALID", "Snapshot contains an invalid feature", status=422) geometry = _geometry(feature.get("geometry")) require(mode != "heatmap" or geometry["type"] == "Point", "OVERLAY_HEATMAP_POINTS_REQUIRED", "Heatmaps require recorded points, not inferred polygon centroids", status=422) properties = feature.get("properties") or {} require(isinstance(properties, dict), "OVERLAY_GEOMETRY_INVALID", "Feature properties are invalid", status=422) normalized = {"type": "Feature", "geometry": geometry, "properties": dict(properties)} if isinstance(feature.get("id"), (str, int)) and not isinstance(feature.get("id"), bool): normalized["id"] = feature["id"] if mode == "heatmap": weight = properties.get("weight", 1) require(_number(weight) and weight >= 0, "OVERLAY_WEIGHT_INVALID", "Heatmap weight must be a finite nonnegative number", status=422) normalized["properties"]["weight"] = weight features.append(normalized) return {"type": "FeatureCollection", "features": features}