""" REST API for S-57 ENC chart management — GPS Navigator. """ import asyncio import logging import tempfile from pathlib import Path from fastapi import APIRouter, HTTPException, UploadFile, File from fastapi.responses import JSONResponse from pydantic import BaseModel class ScanPathRequest(BaseModel): path: str from backend.chart_manager import ( install_from_zip, install_from_enc, list_cells, delete_cell, get_all_features, get_all_depths, get_all_land, get_all_hazards, get_all_zones, CHARTS_DIR, set_meta, install_from_csv_zip, scan_and_install, ) router = APIRouter(prefix="/charts", tags=["charts"]) log = logging.getLogger(__name__) @router.get("/cells") def get_cells(): return list_cells() @router.post("/upload") async def upload_chart(file: UploadFile = File(...)): """ Upload a chart file. Accepts: • .000 — single S-57 ENC cell • .zip — NOAA ENC zip (contains .000) OR CSV-based custom zip """ suffix = Path(file.filename or "").suffix.lower() if suffix not in (".zip", ".000"): raise HTTPException(400, "Only .zip or .000 files accepted") data = await file.read() with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: tmp.write(data) tmp_path = Path(tmp.name) try: if suffix == ".zip": # Auto-detect CSV vs ENC zip import zipfile as _zf with _zf.ZipFile(tmp_path) as z: names = z.namelist() has_csv = any(n.lower().endswith(".csv") for n in names) has_enc = any(n.upper().endswith(".000") for n in names) if has_csv and not has_enc: installed = await asyncio.get_event_loop().run_in_executor( None, install_from_csv_zip, tmp_path) else: installed = await asyncio.get_event_loop().run_in_executor( None, install_from_zip, tmp_path) else: orig_name = Path(file.filename).stem.upper() if file.filename else None cell_id = await asyncio.get_event_loop().run_in_executor( None, install_from_enc, tmp_path, orig_name) installed = [cell_id] except Exception as e: log.exception("Chart upload failed: %s", e) raise HTTPException(500, "Chart processing failed — check server logs for details") finally: tmp_path.unlink(missing_ok=True) return {"installed": installed} @router.delete("/cells/{cell_id}") def remove_cell(cell_id: str): delete_cell(cell_id) return {"deleted": cell_id.upper()} @router.patch("/cells/{cell_id}/region") def set_cell_region(cell_id: str, region: str): region = (region or "").upper().strip() if region not in ("A", "B"): raise HTTPException(400, "region must be 'A' or 'B'") try: set_meta(cell_id, region=region) except FileNotFoundError: raise HTTPException(404, f"Cell {cell_id} not installed") return {"id": cell_id.upper(), "region": region} @router.get("/features") def chart_features(): return JSONResponse(get_all_features()) @router.get("/depths") def chart_depths(w: float | None = None, s: float | None = None, e: float | None = None, n: float | None = None): bbox = (w, s, e, n) if None not in (w, s, e, n) else None return JSONResponse(get_all_depths(bbox)) @router.get("/land") def chart_land(): return JSONResponse(get_all_land()) @router.get("/hazards") def chart_hazards(): return JSONResponse(get_all_hazards()) @router.get("/zones") def chart_zones(): return JSONResponse(get_all_zones()) @router.post("/scan-path") async def scan_path(body: ScanPathRequest): """ Scan a local directory (e.g. SD card drive letter) for .000 / .zip chart files and install them. Body: { "path": "E:\\ENC_Charts" } """ directory = (body.path or "").strip() if not directory: raise HTTPException(400, "path is required") try: result = await asyncio.get_event_loop().run_in_executor( None, scan_and_install, directory) except (FileNotFoundError, NotADirectoryError) as exc: raise HTTPException(404, str(exc)) except Exception as exc: log.exception("scan-path failed: %s", exc) raise HTTPException(500, "Scan failed — check server logs for details") return result