feat: AR-GPS initial commit — Python + JavaScript PyQt5 (standalone desktop app) + FastAPI (charts REST router) + OpenLayers (frontend map)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user