Ajouter la fiche de dalle IGN et la rose des vents, imposer la génération

Un clic sélectionne toujours la dalle sous le curseur (cadre sur la carte),
générée ou non : emprise, rendu et recalage des passes s'affichent tout de
suite ; la fiche IGN (date et heure du scan LiDAR, capteurs, mission,
édition, nombre de points, lien de téléchargement du .copc.laz) arrive à
part via /api/map/ign, mise en cache, sans jamais bloquer la sélection.

Une rose des vents donne la couleur de chaque orientation de pente du
relief orienté, avec la même formule CIELAB que le rendu.

La génération impose la classification IGN (sol) et le raccord de 100 m
avec les dalles voisines : plus de réglage dans la carte, défauts CLI
alignés.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 01:57:04 +02:00
parent d32502b74e
commit f836408d6f
7 changed files with 427 additions and 109 deletions

View File

@ -697,7 +697,6 @@ 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:
@ -819,13 +818,29 @@ def test_tiles_inventory_endpoint(tmp_path, monkeypatch):
assert light["tiles"] is None
def test_generation_always_ign_with_edge_buffer(tmp_path, monkeypatch):
"""Classification IGN (sol) et raccord des bords de 100 m imposés, quel
que soit ce que le client envoie."""
mapserve = _gen_setup(tmp_path, monkeypatch)
cmd = mapserve._build_command([(1054, 6882)])
i = cmd.index("--edge-buffer")
assert cmd[i + 1] == "100"
assert cmd[cmd.index("--ground-classification") + 1] == "ign"
assert cmd[cmd.index("--ign-classes") + 1] == "sol"
req = mapserve.GenerateRequest(tiles=[[1054, 6882]], ground_class="csf", edge_buffer=False)
assert not hasattr(req, "ground_class") or getattr(req, "ground_class", None) != "csf"
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
assert 'id="genViz"' in _MAP_HTML
# Classification et raccord des bords imposés : plus de réglage
for gone in ('id="genClass"', 'id="genIgnClasses"', 'id="genReclass"', 'id="genEdge"'):
assert gone not 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
@ -869,3 +884,41 @@ def test_cache_only_fetches_missing_tile_upstream(tmp_path, monkeypatch):
assert tiles.tile_cache_path(tmp_path, "aspect", z, x, y, 2, "webp").is_file()
finally:
monkeypatch.delenv("LIDAR_TILE_CACHE_ONLY")
def test_tile_selection_always_answers(tmp_path, monkeypatch):
"""La sélection d'une dalle répond toujours, sans appel réseau : dalle
absente comprise, avec nom, emprise L93 et cadre de sélection."""
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
def boom(*a, **k):
raise AssertionError("aucun appel IGN dans /api/map/tile")
monkeypatch.setattr("lidar_pipeline.fetch_ign.tile_ign_metadata", boom)
d = mapserve.map_tile_info(lat=48.0, lng=2.0) # loin de toute dalle
assert d["found"] is False and d["name"].startswith("LHD_FXX_")
assert len(d["corners"]) == 4 and d["l93_bbox"][2] - d["l93_bbox"][0] == 1000
def test_ign_metadata_cached_and_resilient(tmp_path, monkeypatch):
"""Fiche IGN : un seul appel au catalogue (cache disque) ; catalogue
injoignable = message, jamais d'exception."""
mapserve = _setup(tmp_path, monkeypatch)
calls = []
def fake(col, row, timeout=10):
calls.append((col, row))
return {"found": True, "download_url": "https://data.geopf.fr/x.copc.laz",
"acquisition_start": "2022-03-05T16:20:51Z"}
monkeypatch.setattr("lidar_pipeline.fetch_ign.tile_ign_metadata", fake)
a = mapserve.map_tile_ign(col=1004, row=6832)
b = mapserve.map_tile_ign(col=1004, row=6832)
assert a["download_url"].endswith(".copc.laz") and b == a and len(calls) == 1
def down(col, row, timeout=10):
raise OSError("réseau coupé")
monkeypatch.setattr("lidar_pipeline.fetch_ign.tile_ign_metadata", down)
assert mapserve.map_tile_ign(col=1, row=2)["found"] is False
def test_ui_tile_card_and_wind_rose():
from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS
assert 'id="rose"' in _MAP_HTML and 'id="roseCanvas"' in _MAP_HTML
assert "api/map/ign?col=" in _MAP_JS and "Télécharger le nuage LiDAR" in _MAP_JS
assert "compass + 90" in _MAP_JS # même repère que le rendu (aspect = boussole + 90°)