Exposer le cadre imprimable et l'export PDF dans mapserve
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -1764,6 +1764,48 @@ def tiles_inventory(stamp: Optional[float] = None):
|
||||
return data
|
||||
|
||||
|
||||
# --- export PDF (planche d'impression terrain) ---
|
||||
# Toujours calculé localement (jamais délégué à LIDAR_GENERATION_URL) : la
|
||||
# carte légère seule doit savoir exporter. Un export à la fois : l'image de
|
||||
# la planche occupe jusqu'à ~40 Mo (Pi, mem_limit 1g).
|
||||
_export_lock = threading.Lock()
|
||||
|
||||
|
||||
@app.get("/api/export/frame")
|
||||
def export_frame(lat: float, lon: float, paper: str = "A4", orient: str = "paysage",
|
||||
scale: int = 2000):
|
||||
"""Cadre imprimable (coins WGS84) centré sur (lat, lon) pour ces réglages."""
|
||||
from . import export_pdf
|
||||
try:
|
||||
return export_pdf.frame(lat, lon, paper, orient, scale)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
|
||||
|
||||
@app.get("/api/export/pdf")
|
||||
def export_pdf_file(lat: float, lon: float, paper: str = "A4", orient: str = "paysage",
|
||||
scale: int = 2000, title: Optional[str] = None):
|
||||
"""Planche PDF du relief orienté centrée sur (lat, lon)."""
|
||||
from . import export_pdf
|
||||
if not _export_lock.acquire(blocking=False):
|
||||
raise HTTPException(429, "un export PDF est déjà en cours, réessayez dans quelques secondes")
|
||||
try:
|
||||
data, name = export_pdf.build_pdf(OUTPUT_DIR, lat, lon, paper, orient, scale,
|
||||
title=title)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
except export_pdf.NoDataError as e:
|
||||
raise HTTPException(404, str(e))
|
||||
except Exception: # noqa: BLE001 — message propre côté carte
|
||||
logger.exception("Échec de l'export PDF")
|
||||
raise HTTPException(500, "échec de l'export PDF (voir les journaux du serveur)")
|
||||
finally:
|
||||
_export_lock.release()
|
||||
return Response(data, media_type="application/pdf",
|
||||
headers={"Content-Disposition": f'attachment; filename="{name}"',
|
||||
"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
@app.post("/api/preview", dependencies=[Depends(_require_token),
|
||||
Depends(_require_lan_for_generation)])
|
||||
def preview(req: PreviewRequest):
|
||||
|
||||
@ -1251,3 +1251,73 @@ def test_background_scan_retries_cells_beyond_queue_max(tmp_path, monkeypatch):
|
||||
assert seen == expected
|
||||
# Pyramide passée en entier : les scans suivants se taisent
|
||||
assert mapserve.background_scan(tmp_path) == 0
|
||||
|
||||
|
||||
def _center(col=1054, row=6882):
|
||||
from lidar_pipeline.tiles import _transformer
|
||||
lon, lat = _transformer("EPSG:2154", "EPSG:4326").transform(col * 1000 + 500, (row - 1) * 1000 + 500)
|
||||
return lat, lon
|
||||
|
||||
|
||||
def test_export_frame_returns_corners(tmp_path, monkeypatch):
|
||||
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||
lat, lon = _center()
|
||||
d = mapserve.export_frame(lat, lon, "A4", "paysage", 2000)
|
||||
assert len(d["corners"]) == 4 and d["width_m"] > 0
|
||||
|
||||
|
||||
def test_export_pdf_ok(tmp_path, monkeypatch):
|
||||
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||
lat, lon = _center()
|
||||
resp = mapserve.export_pdf_file(lat, lon, "A4", "paysage", 2000, None)
|
||||
assert resp.media_type == "application/pdf" and resp.body.startswith(b"%PDF")
|
||||
assert 'attachment; filename="relief_' in resp.headers["Content-Disposition"]
|
||||
|
||||
|
||||
def test_export_pdf_errors(tmp_path, monkeypatch):
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||
lat, lon = _center()
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve.export_pdf_file(lat, lon, "A4", "paysage", 1234, None)
|
||||
assert e.value.status_code == 400
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve.export_pdf_file(46.5, 2.5, "A4", "paysage", 2000, None)
|
||||
assert e.value.status_code == 404
|
||||
|
||||
|
||||
def test_export_pdf_rejects_non_finite(tmp_path, monkeypatch):
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||
for lat, lon in ((float("nan"), 2.0), (48.0, float("inf"))):
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve.export_pdf_file(lat, lon, "A4", "paysage", 2000, None)
|
||||
assert e.value.status_code == 400
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve.export_frame(lat, lon, "A4", "paysage", 2000)
|
||||
assert e.value.status_code == 400
|
||||
|
||||
|
||||
def test_export_pdf_concurrent_429(tmp_path, monkeypatch):
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||
lat, lon = _center()
|
||||
assert mapserve._export_lock.acquire(blocking=False)
|
||||
try:
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve.export_pdf_file(lat, lon, "A4", "paysage", 2000, None)
|
||||
assert e.value.status_code == 429
|
||||
finally:
|
||||
mapserve._export_lock.release()
|
||||
assert mapserve.export_pdf_file(lat, lon, "A4", "paysage", 2000, None).body.startswith(b"%PDF")
|
||||
|
||||
|
||||
def test_export_pdf_never_delegated(tmp_path, monkeypatch):
|
||||
"""Carte légère avec générateur distant : l'export reste local."""
|
||||
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||
monkeypatch.setattr(mapserve, "GENERATION_URL", "http://injoignable.invalid")
|
||||
lat, lon = _center()
|
||||
assert mapserve.export_pdf_file(lat, lon, "A4", "paysage", 2000, None).body.startswith(b"%PDF")
|
||||
|
||||
Reference in New Issue
Block a user