Initial commit — multi-tenant filtering, port constraints, chart bbox
This commit is contained in:
+110
-3
@@ -17,6 +17,7 @@ from services.chart_manager import (
|
||||
delete_cell, get_all_features, get_all_depths,
|
||||
get_all_land, get_all_hazards, get_all_zones,
|
||||
CHARTS_DIR, set_meta, get_region,
|
||||
install_from_csv_zip, install_from_csv_dir,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/charts", tags=["charts"])
|
||||
@@ -210,9 +211,28 @@ async def download_noaa(cell_id: str):
|
||||
return {"installed": installed}
|
||||
|
||||
|
||||
def _zip_contains_csvs(zip_path: Path) -> bool:
|
||||
"""Return True if the ZIP has *.csv files but no *.000 ENC files."""
|
||||
import zipfile as _zf
|
||||
with _zf.ZipFile(zip_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)
|
||||
return has_csv and not has_enc
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_chart(file: UploadFile = File(...)):
|
||||
suffix = Path(file.filename).suffix.lower()
|
||||
"""
|
||||
Universal chart upload.
|
||||
|
||||
Accepts:
|
||||
• .000 — single S-57 ENC cell
|
||||
• .zip — either a NOAA ENC zip (contains .000) OR a CSV-based custom
|
||||
chart zip (contains *.csv, no .000). The ZIP auto-detection
|
||||
determines which parser is used.
|
||||
"""
|
||||
suffix = Path(file.filename or "").suffix.lower()
|
||||
if suffix not in (".zip", ".000"):
|
||||
raise HTTPException(400, "Only .zip or .000 files accepted")
|
||||
|
||||
@@ -223,8 +243,13 @@ async def upload_chart(file: UploadFile = File(...)):
|
||||
|
||||
try:
|
||||
if suffix == ".zip":
|
||||
installed = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_zip, tmp_path)
|
||||
# Auto-detect: CSV zip vs ENC zip
|
||||
if _zip_contains_csvs(tmp_path):
|
||||
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(
|
||||
@@ -238,6 +263,58 @@ async def upload_chart(file: UploadFile = File(...)):
|
||||
return {"installed": installed}
|
||||
|
||||
|
||||
@router.post("/upload-csv")
|
||||
async def upload_csv_chart(file: UploadFile = File(...),
|
||||
cell_id: str | None = None):
|
||||
"""
|
||||
Upload a ZIP archive containing CSV navigation-aid files to create a
|
||||
custom chart cell. Use this when your source data is in DIMAR / custom
|
||||
CSV format rather than S-57 .000.
|
||||
|
||||
The cell_id query parameter overrides the inferred name from the ZIP.
|
||||
|
||||
Workflow:
|
||||
1. Edit BOYLAT.csv, BOYCAR.csv, BOYSPEC.csv, etc. in your local
|
||||
capas_ctg/ folder.
|
||||
2. Zip the entire folder.
|
||||
3. POST the zip here (optionally with ?cell_id=BAHIA_DE_CARTAGENA).
|
||||
4. AidsMonitoring reads the CSVs directly, preserving all light
|
||||
attributes (LITCHR, SIGPER, VALNMR …) without GDAL round-trip loss.
|
||||
"""
|
||||
if not (file.filename or "").lower().endswith(".zip"):
|
||||
raise HTTPException(400, "Only .zip files accepted for CSV upload")
|
||||
|
||||
data = await file.read()
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
||||
tmp.write(data)
|
||||
tmp_path = Path(tmp.name)
|
||||
|
||||
try:
|
||||
if cell_id:
|
||||
# Extract to temp dir then install with explicit cell_id
|
||||
import zipfile as _zf
|
||||
import tempfile as _tf
|
||||
with _tf.TemporaryDirectory() as td:
|
||||
td_p = Path(td)
|
||||
with _zf.ZipFile(tmp_path) as z:
|
||||
for member in z.namelist():
|
||||
if member.lower().endswith(".csv"):
|
||||
data_bytes = z.read(member)
|
||||
(td_p / Path(member).name).write_bytes(data_bytes)
|
||||
installed_id = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_csv_dir, td_p, cell_id)
|
||||
installed = [installed_id]
|
||||
else:
|
||||
installed = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_csv_zip, tmp_path)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
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)
|
||||
@@ -305,6 +382,36 @@ async def rebuild_cache():
|
||||
return {"rebuilt": rebuilt}
|
||||
|
||||
|
||||
@router.post("/cells/{cell_id}/rebuild-from-csv")
|
||||
async def rebuild_cell_from_csv(cell_id: str, file: UploadFile = File(...)):
|
||||
"""
|
||||
Update an existing cell's features.geojson by re-uploading its CSV zip.
|
||||
Equivalent to DELETE + upload-csv but preserves meta.json settings
|
||||
(e.g. region override).
|
||||
"""
|
||||
if not (file.filename or "").lower().endswith(".zip"):
|
||||
raise HTTPException(400, "Only .zip files accepted")
|
||||
data = await file.read()
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
||||
tmp.write(data)
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
import zipfile as _zf, tempfile as _tf
|
||||
with _tf.TemporaryDirectory() as td:
|
||||
td_p = Path(td)
|
||||
with _zf.ZipFile(tmp_path) as z:
|
||||
for member in z.namelist():
|
||||
if member.lower().endswith(".csv"):
|
||||
(td_p / Path(member).name).write_bytes(z.read(member))
|
||||
installed_id = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_csv_dir, td_p, cell_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
return {"rebuilt": installed_id}
|
||||
|
||||
|
||||
@router.post("/cells/{cell_id}/rebuild")
|
||||
async def rebuild_cell(cell_id: str):
|
||||
"""Re-parse a single ENC cell and regenerate its feature cache."""
|
||||
|
||||
Reference in New Issue
Block a user