Add V1 selected area map overlay
This commit is contained in:
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.models import Area
|
||||
from app.schemas.area import AreaCreate, AreaRead, AreaUpdate
|
||||
from app.schemas.area import AreaCreate, AreaUpdate
|
||||
from app.services.area_service import AreaService
|
||||
from app.utils.response import envelope
|
||||
|
||||
@@ -23,13 +23,13 @@ def list_areas(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
areas, total = AreaService.list_areas(db, project_id=project_id, limit=limit, offset=offset)
|
||||
return envelope({"items": [AreaRead.model_validate(area).model_dump() for area in areas], "total": total, "limit": limit, "offset": offset})
|
||||
return envelope({"items": [AreaService.serialize_area(area) for area in areas], "total": total, "limit": limit, "offset": offset})
|
||||
|
||||
|
||||
@router.post("", status_code=201, response_model=dict)
|
||||
def create_area(project_id: UUID, payload: AreaCreate, db: Session = Depends(get_db)):
|
||||
area = AreaService.create_area(db, project_id, payload)
|
||||
return envelope(AreaRead.model_validate(area).model_dump())
|
||||
return envelope(AreaService.serialize_area(area))
|
||||
|
||||
|
||||
@router.get("/{area_id}", response_model=dict)
|
||||
@@ -41,7 +41,7 @@ def get_area(
|
||||
area = AreaService.get_area(db, area_id)
|
||||
if area.project_id != project_id:
|
||||
raise HTTPException(status_code=404, detail="Area not found")
|
||||
return envelope(AreaRead.model_validate(area).model_dump())
|
||||
return envelope(AreaService.serialize_area(area))
|
||||
|
||||
|
||||
@router.patch("/{area_id}", response_model=dict)
|
||||
@@ -55,4 +55,4 @@ def update_area(
|
||||
if not existing or existing.project_id != project_id:
|
||||
raise HTTPException(status_code=404, detail="Area not found")
|
||||
area = AreaService.update_area(db, area_id, payload)
|
||||
return envelope(AreaRead.model_validate(area).model_dump())
|
||||
return envelope(AreaService.serialize_area(area))
|
||||
|
||||
@@ -3,7 +3,8 @@ from __future__ import annotations
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from geoalchemy2.shape import from_shape
|
||||
from geoalchemy2.shape import from_shape, to_shape
|
||||
from shapely.geometry import mapping
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.models import Area, Project
|
||||
@@ -13,7 +14,13 @@ from app.utils.geometry import area_m2, geometry_bbox_polygon, normalize_to_mult
|
||||
|
||||
class AreaService:
|
||||
@staticmethod
|
||||
def list_areas(db: Session, project_id: uuid.UUID, limit: int = 50, offset: int = 0) -> tuple[list[AreaRead], int]:
|
||||
def serialize_area(area: Area) -> dict:
|
||||
payload = AreaRead.model_validate(area).model_dump()
|
||||
payload["geometry"] = mapping(to_shape(area.geometry)) if area.geometry else None
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
def list_areas(db: Session, project_id: uuid.UUID, limit: int = 50, offset: int = 0) -> tuple[list[Area], int]:
|
||||
total = db.query(Area).filter(Area.project_id == project_id).count()
|
||||
areas = (
|
||||
db.query(Area)
|
||||
@@ -23,10 +30,10 @@ class AreaService:
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [AreaRead.model_validate(area) for area in areas], total
|
||||
return areas, total
|
||||
|
||||
@staticmethod
|
||||
def create_area(db: Session, project_id: uuid.UUID, payload: AreaCreate) -> AreaRead:
|
||||
def create_area(db: Session, project_id: uuid.UUID, payload: AreaCreate) -> Area:
|
||||
if not db.get(Project, project_id):
|
||||
raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404)
|
||||
|
||||
@@ -46,17 +53,17 @@ class AreaService:
|
||||
db.add(area)
|
||||
db.commit()
|
||||
db.refresh(area)
|
||||
return AreaRead.model_validate(area)
|
||||
return area
|
||||
|
||||
@staticmethod
|
||||
def get_area(db: Session, area_id: uuid.UUID) -> AreaRead:
|
||||
def get_area(db: Session, area_id: uuid.UUID) -> Area:
|
||||
area = db.get(Area, area_id)
|
||||
if not area:
|
||||
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
|
||||
return AreaRead.model_validate(area)
|
||||
return area
|
||||
|
||||
@staticmethod
|
||||
def update_area(db: Session, area_id: uuid.UUID, payload: AreaUpdate) -> AreaRead:
|
||||
def update_area(db: Session, area_id: uuid.UUID, payload: AreaUpdate) -> Area:
|
||||
area = db.get(Area, area_id)
|
||||
if not area:
|
||||
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
|
||||
@@ -74,4 +81,4 @@ class AreaService:
|
||||
db.add(area)
|
||||
db.commit()
|
||||
db.refresh(area)
|
||||
return AreaRead.model_validate(area)
|
||||
return area
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from geoalchemy2.shape import from_shape
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
|
||||
from app.models import Area
|
||||
from app.services.area_service import AreaService
|
||||
|
||||
|
||||
def test_area_serializer_exposes_geojson_geometry_for_map_overlay() -> None:
|
||||
project_id = uuid4()
|
||||
area = Area(
|
||||
id=uuid4(),
|
||||
project_id=project_id,
|
||||
name="Map AOI",
|
||||
original_crs="EPSG:4326",
|
||||
area_m2=100.0,
|
||||
geometry=from_shape(
|
||||
MultiPolygon(
|
||||
[
|
||||
Polygon(
|
||||
[
|
||||
(4.35, 51.28),
|
||||
(4.36, 51.28),
|
||||
(4.36, 51.29),
|
||||
(4.35, 51.29),
|
||||
(4.35, 51.28),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
srid=4326,
|
||||
),
|
||||
)
|
||||
|
||||
payload = AreaService.serialize_area(area)
|
||||
|
||||
assert payload["id"] == area.id
|
||||
assert payload["project_id"] == project_id
|
||||
assert payload["geometry"]["type"] == "MultiPolygon"
|
||||
assert payload["geometry"]["coordinates"][0][0][0] == (4.35, 51.28)
|
||||
|
||||
|
||||
def test_frontend_wires_selected_area_map_overlay_contract() -> None:
|
||||
root = __import__("pathlib").Path(__file__).resolve().parents[2]
|
||||
app = (root / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||
geomap = (root / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
||||
area_panel = (root / "frontend" / "src" / "components" / "project" / "AreaPanel.tsx").read_text(encoding="utf-8")
|
||||
|
||||
assert "selectedMapAreaId" in app
|
||||
assert "areaFeatureCollection" in app
|
||||
assert "areaData={areaFeatureCollection}" in app
|
||||
assert "Area visible" in app
|
||||
assert "area-fill" in geomap
|
||||
assert "area-line" in geomap
|
||||
assert "onSelectMapArea" in area_panel
|
||||
assert "Show on map" in area_panel
|
||||
Reference in New Issue
Block a user