Porter la génération de tuiles dans lidar-maps et retirer l'ancienne webapp
La carte à tuiles XYZ devient la seule interface : l'API de génération (preview, generate, status, stop, file, cell), le job unique + file persistante, la délégation LIDAR_GENERATION_URL et les garde-fous (token, LIDAR_REGEN_CIDR) passent de webapp.py à mapserve.py ; l'interface gagne les boutons + Zone / ⤒ Compléter / régénération à la dalle, les options de run et la progression cadre par cadre. mapserve sert aussi l'inventaire /api/tiles et les dalles en statique versionné : le worker image complète remplace la webapp sur le 8973 du générateur, le Pi n'exécute plus que lidar-maps. index.py se réduit aux registres partagés + vignettes/sous-tuiles/inventaire (l'UI HTML/JS et les mosaïques d'overview partent avec export.py et les compose/scripts de la webapp).
This commit is contained in:
@ -258,7 +258,7 @@ def test_ui_offers_reorder_and_blend_modes():
|
||||
# Pile isolée + fusion par couche et fusion de la pile sur le fond
|
||||
assert "createPane('lidarStack')" in _MAP_JS
|
||||
assert "isolation = 'isolate'" in _MAP_JS
|
||||
assert "map.createPane(pane, stack)" in _MAP_JS
|
||||
assert "map.createPane(paneName, stack)" in _MAP_JS
|
||||
assert "pane.style.mixBlendMode" in _MAP_JS
|
||||
assert "STATE.stackBlend" in _MAP_JS and 'id="stackBlend"' in _MAP_HTML
|
||||
# Le lien partagé transporte la fusion (4e champ, rétrocompatible)
|
||||
@ -595,3 +595,231 @@ def test_background_scan_detects_and_fills(tmp_path, monkeypatch):
|
||||
assert mapserve.background_scan(tmp_path) > 0
|
||||
finally:
|
||||
_reset_bg(mapserve)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Génération de tuiles (portée de la webapp historique)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
import types
|
||||
|
||||
from fastapi import HTTPException
|
||||
import pytest
|
||||
|
||||
|
||||
class _ClientRequest:
|
||||
"""Requête minimale avec IP de connexion (restriction LIDAR_REGEN_CIDR)."""
|
||||
|
||||
def __init__(self, ip, xff=None):
|
||||
self.client = types.SimpleNamespace(host=ip)
|
||||
self.headers = {"x-forwarded-for": xff} if xff else {}
|
||||
|
||||
|
||||
def _gen_setup(tmp_path, monkeypatch):
|
||||
import lidar_pipeline.mapserve as mapserve
|
||||
_make_dalle(tmp_path, 1054, 6882, ["aspect", "slope"])
|
||||
monkeypatch.setattr(mapserve, "OUTPUT_DIR", tmp_path)
|
||||
monkeypatch.setattr(mapserve, "INPUT_DIR", tmp_path / "input")
|
||||
monkeypatch.setattr(mapserve, "GENERATION_URL", "")
|
||||
monkeypatch.setattr(mapserve, "LOG_FILE", tmp_path / ".generation.log")
|
||||
monkeypatch.setattr(mapserve, "JOB_FILE", tmp_path / ".generation.job.json")
|
||||
monkeypatch.setattr(mapserve, "QUEUE_FILE", tmp_path / ".generation.queue.json")
|
||||
mapserve._queue.clear()
|
||||
mapserve._pipeline_import_cache.clear()
|
||||
return mapserve
|
||||
|
||||
|
||||
def test_bbox_to_cells_covers_1km():
|
||||
"""Une bbox d'1 km² WGS84 sélectionne la cellule L93 correspondante."""
|
||||
import lidar_pipeline.mapserve as mapserve
|
||||
from lidar_pipeline.tiles import _transformer
|
||||
x, y = 1054500, 6881500
|
||||
w, n = _transformer("EPSG:2154", "EPSG:4326").transform(x - 200, y + 200)
|
||||
e, s = _transformer("EPSG:2154", "EPSG:4326").transform(x + 200, y - 200)
|
||||
cells = mapserve.bbox_to_cells(w, s, e, n)
|
||||
assert (1054, 6882) in cells
|
||||
# Cellule (col, row) : X ∈ [col, col+1] km, Y ∈ [row-1, row] km
|
||||
assert all(1053 <= c <= 1055 and 6881 <= r <= 6883 for c, r in cells)
|
||||
|
||||
|
||||
def test_preview_bbox_lists_cells_with_corners(tmp_path, monkeypatch):
|
||||
"""Le preview retourne les cellules à traiter avec leurs coins GPS."""
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
from lidar_pipeline.tiles import _transformer
|
||||
x, y = 1054500, 6881500
|
||||
w, n = _transformer("EPSG:2154", "EPSG:4326").transform(x - 100, y + 100)
|
||||
e, s = _transformer("EPSG:2154", "EPSG:4326").transform(x + 100, y - 100)
|
||||
req = mapserve.PreviewRequest(bbox=[w, s, e, n], viz=["aspect"])
|
||||
d = mapserve.preview(req)
|
||||
assert d["capped"] is False
|
||||
cells = {(c["col"], c["row"]) for c in d["cells"]}
|
||||
assert (1054, 6882) in cells
|
||||
first = d["cells"][0]
|
||||
assert first["corners"] and len(first["corners"]) == 4
|
||||
|
||||
|
||||
def test_preview_rejects_unknown_viz(tmp_path, monkeypatch):
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
req = mapserve.PreviewRequest(bbox=[0.0, 44.0, 0.01, 44.01],
|
||||
viz=["inconnue"])
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve.preview(req)
|
||||
assert e.value.status_code == 400
|
||||
|
||||
|
||||
def test_preview_all_missing_without_laz(tmp_path, monkeypatch):
|
||||
"""Passe globale sans dalle dans input/ : liste vide (rien à compléter).
|
||||
|
||||
L'erreur 400 « aucune tuile à compléter » ne concerne que le lancement
|
||||
(/api/generate → _resolve_request) : le preview reste informatif.
|
||||
"""
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
(tmp_path / "input").mkdir()
|
||||
req = mapserve.PreviewRequest(all_missing=True, viz=["aspect"])
|
||||
d = mapserve.preview(req)
|
||||
assert d["count"] == 0 and d["cells"] == []
|
||||
|
||||
|
||||
def test_generate_validates_request(tmp_path, monkeypatch):
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
for kwargs in ({"tiles": []},
|
||||
{"tiles": [[1054]]},
|
||||
{"tiles": [[1054, 6882]], "ground_class": "inconnue"},
|
||||
{"tiles": [[c, 6882] for c in range(500)]}):
|
||||
req = mapserve.GenerateRequest(**kwargs)
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve._resolve_request(req)
|
||||
assert e.value.status_code == 400
|
||||
# Tri nord → sud (row décroissante) puis col croissante
|
||||
req = mapserve.GenerateRequest(tiles=[[1055, 6882], [1054, 6883],
|
||||
[1054, 6882]])
|
||||
tiles, _viz = mapserve._resolve_request(req)
|
||||
assert tiles == [(1054, 6883), (1054, 6882), (1055, 6882)]
|
||||
|
||||
|
||||
def test_generate_launches_and_queues(tmp_path, monkeypatch):
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
launched = []
|
||||
monkeypatch.setattr(mapserve, "_launch_job",
|
||||
lambda tiles, viz, req, qid=None:
|
||||
launched.append((tiles, qid)) or ["fake", "cmd"])
|
||||
|
||||
req = mapserve.GenerateRequest(tiles=[[1054, 6882]], viz=["aspect"])
|
||||
d = mapserve.generate(req)
|
||||
assert d["demarré"] is True and d["tuiles"] == 1
|
||||
|
||||
# Run en cours : la demande part en file (jamais de coupure du travail)
|
||||
import threading
|
||||
with mapserve._job_lock:
|
||||
mapserve._job["proc"] = types.SimpleNamespace(
|
||||
poll=lambda: None, pid=4242, terminate=lambda: None, wait=lambda: None)
|
||||
d = mapserve.generate(req)
|
||||
assert d["demarré"] is False and d["en_file"] == 1
|
||||
assert mapserve.generate(req)["en_file"] == 2
|
||||
assert len(mapserve._queue_summary()) == 2
|
||||
d = mapserve.queue_clear()
|
||||
assert d["retirées"] == 2
|
||||
with mapserve._job_lock:
|
||||
mapserve._job["proc"] = None
|
||||
mapserve._queue.clear()
|
||||
|
||||
|
||||
def test_status_reports_rights_and_availability(tmp_path, monkeypatch):
|
||||
"""/api/status annonce available (pipeline local) et regen_allowed (IP)."""
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr(mapserve, "_pipeline_import_cache", {"ok": True})
|
||||
d = mapserve.generation_status(_ClientRequest("192.168.3.10"))
|
||||
assert d["available"] is True and d["regen_allowed"] is True
|
||||
assert d["running"] is False and d["tiles"] == []
|
||||
|
||||
# Client hors réseau local : génération interdite (boutons masqués)
|
||||
d = mapserve.generation_status(_ClientRequest("203.0.113.7"))
|
||||
assert d["regen_allowed"] is False
|
||||
|
||||
# X-Forwarded-For honoré seulement depuis un pair de confiance
|
||||
d = mapserve.generation_status(
|
||||
_ClientRequest("172.25.0.1", xff="203.0.113.7"))
|
||||
assert d["regen_allowed"] is False
|
||||
d = mapserve.generation_status(
|
||||
_ClientRequest("172.25.0.1", xff="192.168.3.10"))
|
||||
assert d["regen_allowed"] is True
|
||||
|
||||
# Image légère sans pipeline ni LIDAR_GENERATION_URL : indisponible
|
||||
monkeypatch.setattr(mapserve, "_pipeline_import_cache", {"ok": False})
|
||||
d = mapserve.generation_status(_ClientRequest("192.168.3.10"))
|
||||
assert d["available"] is False
|
||||
|
||||
|
||||
def test_status_proxies_to_worker(tmp_path, monkeypatch):
|
||||
"""Webapp légère : /api/status relaie l'état du worker, 503 hors ligne."""
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr(mapserve, "GENERATION_URL", "http://worker:8973")
|
||||
monkeypatch.setattr(mapserve, "_pipeline_import_cache", {"ok": False})
|
||||
|
||||
def fake_proxy(method, path, payload=None, timeout=30):
|
||||
assert (method, path) == ("GET", "/api/status")
|
||||
return {"running": True, "tiles": []}
|
||||
|
||||
monkeypatch.setattr(mapserve, "_proxy_generation", fake_proxy)
|
||||
d = mapserve.generation_status(_ClientRequest("192.168.3.10"))
|
||||
assert d["distant"] is True and d["running"] is True
|
||||
assert d["available"] is True and d["regen_allowed"] is True
|
||||
|
||||
def offline(method, path, payload=None, timeout=30):
|
||||
raise HTTPException(503, "machine de traitement injoignable")
|
||||
|
||||
monkeypatch.setattr(mapserve, "_proxy_generation", offline)
|
||||
d = mapserve.generation_status(_ClientRequest("192.168.3.10"))
|
||||
assert d["worker_offline"] is True and d["running"] is None
|
||||
|
||||
|
||||
def test_generate_requires_lan(tmp_path, monkeypatch):
|
||||
"""Un client hors LIDAR_REGEN_CIDR ne peut pas lancer de génération."""
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
with pytest.raises(HTTPException) as e:
|
||||
mapserve._require_lan_for_generation(_ClientRequest("203.0.113.7"))
|
||||
assert e.value.status_code == 403
|
||||
|
||||
|
||||
def test_cell_at_point(tmp_path, monkeypatch):
|
||||
"""/api/cell donne la dalle sous un point (sélection manuelle)."""
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
from lidar_pipeline.tiles import _transformer
|
||||
lon, lat = _transformer("EPSG:2154", "EPSG:4326").transform(1054500, 6881500)
|
||||
d = mapserve.cell_at_point(lat=lat, lng=lon)
|
||||
assert (d["col"], d["row"]) == (1054, 6882)
|
||||
assert d["corners"] and len(d["corners"]) == 4
|
||||
|
||||
|
||||
def test_tiles_inventory_endpoint(tmp_path, monkeypatch):
|
||||
"""/api/tiles sert index_tiles.json (machines légères LIDAR_SOURCE_URL)."""
|
||||
mapserve = _gen_setup(tmp_path, monkeypatch)
|
||||
payload = {"tiles": [{"col": 1054, "row": 6882,
|
||||
"viz": {"aspect": {"full": "visualisations/x.avif?v=1"}}}],
|
||||
"viz_meta": {}, "stats": {"n_tiles": 1}}
|
||||
(tmp_path / "index_tiles.json").write_text(
|
||||
__import__("json").dumps(payload), encoding="utf-8")
|
||||
d = mapserve.tiles_inventory()
|
||||
assert d["tiles"] == payload["tiles"] and d["stamp"] > 0
|
||||
# ?stamp inchangé → réponse allégée
|
||||
light = mapserve.tiles_inventory(stamp=d["stamp"])
|
||||
assert light["tiles"] is None
|
||||
|
||||
|
||||
def test_ui_offers_generation_controls():
|
||||
"""L'interface embarque les contrôles de génération (+ Zone, options, run)."""
|
||||
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
||||
assert 'id="btnZone"' in _MAP_HTML and 'id="btnComplete"' in _MAP_HTML
|
||||
assert 'id="gencard"' in _MAP_HTML and 'id="genGo"' in _MAP_HTML
|
||||
assert 'id="genStop"' in _MAP_HTML and 'id="genQueueClear"' in _MAP_HTML
|
||||
assert 'id="genViz"' in _MAP_HTML and 'id="genEdge"' in _MAP_HTML
|
||||
# Boutons masqués par défaut : révélés seulement si /api/status autorise
|
||||
assert "GEN.available && GEN.allowed" in _MAP_JS
|
||||
# Dessin de zone + cadres de run + sondage d'état
|
||||
assert "genSetDrawing" in _MAP_JS and "pane: 'genDraw'" in _MAP_JS
|
||||
assert "gen-frame-running" in _MAP_CSS and "genDrawFrames" in _MAP_JS
|
||||
assert "api/status" in _MAP_JS and "api/preview" in _MAP_JS
|
||||
assert "api/generate" in _MAP_JS and "api/stop" in _MAP_JS
|
||||
assert "all_missing: true" in _MAP_JS
|
||||
# Fin de run : rafraîchissement des couches + relance de la maintenance
|
||||
assert "genAfterRun" in _MAP_JS and "api/map/background" in _MAP_JS
|
||||
|
||||
Reference in New Issue
Block a user