| from __future__ import annotations |
|
|
| import json |
| import uuid |
| from pathlib import Path |
| from typing import Any |
|
|
| from .geometry import calculate_site_metrics |
| from .models import SiteSelection |
|
|
|
|
| def parse_geojson_file(path: str | Path) -> SiteSelection: |
| source = Path(path) |
| data = json.loads(source.read_text(encoding="utf-8")) |
| if not isinstance(data, dict): |
| raise ValueError("GeoJSON root must be a Feature, FeatureCollection, Polygon, or MultiPolygon object.") |
| geometry = _extract_geometry(data) |
| metrics = calculate_site_metrics(geometry) |
| return SiteSelection( |
| id=f"S-{uuid.uuid4().hex[:8]}", |
| selection_type="geojson_boundary", |
| coordinate_mode="wgs84", |
| geometry_geojson=geometry, |
| local_geometry=None, |
| anchor_lat=metrics["centroid"][0], |
| anchor_lon=metrics["centroid"][1], |
| radius_m=None, |
| area_sqm=metrics["area_sqm"], |
| perimeter_m=metrics["perimeter_m"], |
| centroid=metrics["centroid"], |
| bbox=metrics["bbox"], |
| unit_source="WGS84 GeoJSON", |
| accuracy_label="uploaded georeferenced boundary", |
| source_files=[source.name], |
| selected_boundary_id=None, |
| limitations=[ |
| "GeoJSON is treated as WGS84 if coordinates look like longitude/latitude.", |
| "Verify boundary with the original source drawing or site survey.", |
| ], |
| ) |
|
|
|
|
| def _extract_geometry(data: dict[str, Any]) -> dict[str, Any]: |
| if data.get("type") == "Feature": |
| geometry = data.get("geometry") |
| elif data.get("type") == "FeatureCollection": |
| features = data.get("features") or [] |
| if not features: |
| raise ValueError("GeoJSON FeatureCollection has no features.") |
| geometry = features[0].get("geometry") |
| else: |
| geometry = data |
| if not isinstance(geometry, dict) or geometry.get("type") not in {"Polygon", "MultiPolygon"}: |
| raise ValueError("Only Polygon and MultiPolygon GeoJSON are supported.") |
| _validate_wgs84(geometry) |
| return geometry |
|
|
|
|
| def _validate_wgs84(geometry: dict[str, Any]) -> None: |
| try: |
| if geometry.get("type") == "Polygon": |
| points = geometry["coordinates"][0] |
| else: |
| points = geometry["coordinates"][0][0] |
| except (KeyError, IndexError, TypeError) as exc: |
| raise ValueError("GeoJSON polygon coordinates are missing or malformed.") from exc |
|
|
| if not isinstance(points, list) or len(points) < 4: |
| raise ValueError("GeoJSON polygon needs at least three vertices and a closed ring.") |
|
|
| for point in points[:50]: |
| try: |
| lon, lat = point[:2] |
| lon_f = float(lon) |
| lat_f = float(lat) |
| except (TypeError, ValueError) as exc: |
| raise ValueError("GeoJSON coordinates must be longitude/latitude number pairs.") from exc |
| if not (-180 <= lon_f <= 180 and -90 <= lat_f <= 90): |
| raise ValueError("GeoJSON coordinates do not look like WGS84 longitude/latitude.") |
|
|