- Web map: multi-select picker in the generation bar (aspect, slope, positive openness, anisotropic openness, wavelet) passed to the API; the layer panel is restricted to the same shortlist (PANEL_VIZ) and a refresh button rebuilds the index when new layers appear on disk; jobs started outside the UI are now adopted into the visible queue - Uniform colors across tiles: openness/anisotropic/sailore now store local z-scores, and all renderers use fixed ranges (0-3 sigma, SVF 0-1 physical, slope 0-30 deg) instead of per-tile percentile stretches - Ray-tracing falls back to CPU when VRAM is exhausted so openness and SVF no longer fail silently on shared GPUs - build_index merges visualizations available at only one resolution into the displayed tile so in-progress layers stay visible - 11 new tests (142 passing)
176 lines
7.2 KiB
Python
176 lines
7.2 KiB
Python
"""Tests du serveur web de génération de zones (webapp)."""
|
|
|
|
|
|
def test_bbox_to_cells_single_km_cell():
|
|
"""Une bbox couvrant ~1 km² retourne la cellule L93 correspondante."""
|
|
from lidar_pipeline.webapp import bbox_to_cells
|
|
# Cellule 1054,6882 : X∈[1054000,1055000], Y∈[6881000,6882000] (L93)
|
|
from rasterio.warp import transform as warp_transform
|
|
lons, lats = warp_transform('EPSG:2154', 'EPSG:4326',
|
|
[1054100, 1054900], [6881100, 6881900])
|
|
cells = bbox_to_cells(min(lons), min(lats), max(lons), max(lats))
|
|
assert (1054, 6882) in cells
|
|
# La sélection reste locale : pas de cellule lointaine
|
|
for (c, r) in cells:
|
|
assert abs(c - 1054) <= 1 and abs(r - 6882) <= 1
|
|
|
|
|
|
def test_bbox_to_cells_empty_for_tiny_bbox():
|
|
"""Une bbox quasi ponctuelle ne sélectionne rien (rétrécie sous 1 m)."""
|
|
from lidar_pipeline.webapp import bbox_to_cells
|
|
assert bbox_to_cells(7.850000, 48.930000, 7.850001, 48.930001) == []
|
|
|
|
|
|
def test_processed_cells(tmp_path):
|
|
"""processed_cells lit les dossiers de visualisations."""
|
|
from lidar_pipeline.webapp import processed_cells
|
|
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_r0p2"
|
|
vis.mkdir(parents=True)
|
|
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_r0p2_aspect.avif").write_bytes(b"x")
|
|
assert processed_cells(tmp_path) == {(1054, 6882)}
|
|
|
|
|
|
def test_missing_cells_filters_processed(tmp_path):
|
|
"""Les cellules déjà traitées sont exclues, les autres gardent leurs coins."""
|
|
from lidar_pipeline.webapp import missing_cells_with_corners
|
|
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
|
|
vis.mkdir(parents=True)
|
|
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_aspect.avif").write_bytes(b"x")
|
|
todo = missing_cells_with_corners([(1054, 6882), (1055, 6882)], tmp_path)
|
|
assert len(todo) == 1
|
|
assert todo[0]['col'] == 1055 and todo[0]['row'] == 6882
|
|
assert len(todo[0]['corners']) == 4 # SW, SE, NE, NW
|
|
|
|
|
|
def test_missing_cells_include_done(tmp_path):
|
|
"""include_done=True conserve les cellules déjà traitées (régénération)."""
|
|
from lidar_pipeline.webapp import missing_cells_with_corners
|
|
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
|
|
vis.mkdir(parents=True)
|
|
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_aspect.avif").write_bytes(b"x")
|
|
todo = missing_cells_with_corners([(1054, 6882), (1055, 6882)], tmp_path,
|
|
include_done=True)
|
|
assert {(t['col'], t['row']) for t in todo} == {(1054, 6882), (1055, 6882)}
|
|
|
|
|
|
def test_build_command_regenerate():
|
|
"""regenerate=True ajoute --force --force-classification à la commande."""
|
|
from lidar_pipeline.webapp import _build_command
|
|
cmd = " ".join(_build_command([(1054, 6882)], regenerate=True))
|
|
assert "--force" in cmd
|
|
assert "--force-classification" in cmd
|
|
cmd = " ".join(_build_command([(1054, 6882)]))
|
|
assert "--force" not in cmd
|
|
assert "--force-classification" not in cmd
|
|
|
|
|
|
def test_build_command_ground_classification():
|
|
"""La commande utilise la méthode de classification demandée (défaut : ign)."""
|
|
from lidar_pipeline.webapp import _build_command, GROUND_CLASS_METHODS
|
|
# Défaut : ign (pré-classification)
|
|
cmd = _build_command([(1054, 6882)])
|
|
i = cmd.index("--ground-classification")
|
|
assert cmd[i + 1] == "ign"
|
|
# Chaque méthode valide est transmise telle quelle, avec ou sans régénération
|
|
for method in GROUND_CLASS_METHODS:
|
|
for regenerate in (False, True):
|
|
cmd = _build_command([(1054, 6882)], regenerate=regenerate, ground_class=method)
|
|
i = cmd.index("--ground-classification")
|
|
assert cmd[i + 1] == method
|
|
assert ("--force" in cmd) == regenerate
|
|
assert ("--force-classification" in cmd) == regenerate
|
|
|
|
|
|
def test_build_command_default_viz_aspect():
|
|
"""Sans choix de visualisation, la commande génère uniquement aspect."""
|
|
from lidar_pipeline.webapp import _build_command
|
|
cmd = _build_command([(1054, 6882)])
|
|
i = cmd.index("--only")
|
|
assert cmd[i + 1] == "aspect"
|
|
|
|
|
|
def test_build_command_custom_viz():
|
|
"""Les visualisations demandées sont passées à --only dans l'ordre."""
|
|
from lidar_pipeline.webapp import _build_command
|
|
cmd = _build_command([(1054, 6882)], viz=["aspect", "wavelet", "aniso_open"])
|
|
i = cmd.index("--only")
|
|
assert cmd[i + 1:i + 4] == ["aspect", "wavelet", "aniso_open"]
|
|
|
|
|
|
def test_viz_step_names_match_pipeline():
|
|
"""Les noms acceptés par l'API sont exactement les étapes VIZ_STEPS du pipeline."""
|
|
from lidar_pipeline.webapp import _viz_step_names
|
|
from lidar_pipeline.pipeline import VIZ_STEPS
|
|
assert _viz_step_names() == [n for n, _ in VIZ_STEPS]
|
|
|
|
|
|
def test_generate_rejects_unknown_viz():
|
|
"""L'API refuse un nom de visualisation inconnu (HTTPException 400)."""
|
|
from fastapi import HTTPException
|
|
from lidar_pipeline.webapp import generate, GenerateRequest
|
|
req = GenerateRequest(tiles=[[1054, 6882]], viz=["wavelet", "inconnu"])
|
|
try:
|
|
generate(req)
|
|
assert False, "une HTTPException était attendue"
|
|
except HTTPException as e:
|
|
assert e.status_code == 400
|
|
assert "inconnu" in e.detail
|
|
|
|
|
|
def test_generate_accepts_wavelet_and_aniso():
|
|
"""L'API accepte wavelet + aniso_open et les transmet à --only."""
|
|
import subprocess
|
|
import lidar_pipeline.webapp as webapp
|
|
captured = {}
|
|
|
|
def fake_popen(cmd, **kwargs):
|
|
captured["cmd"] = cmd
|
|
|
|
class _P:
|
|
def wait(self):
|
|
return 0
|
|
|
|
def poll(self):
|
|
return 0
|
|
return _P()
|
|
|
|
webapp.subprocess.Popen = fake_popen
|
|
try:
|
|
req = webapp.GenerateRequest(tiles=[[1054, 6882]],
|
|
viz=["wavelet", "aniso_open"])
|
|
result = webapp.generate(req)
|
|
assert result["demarré"] is True
|
|
i = captured["cmd"].index("--only")
|
|
assert captured["cmd"][i + 1:i + 3] == ["wavelet", "aniso_open"]
|
|
finally:
|
|
webapp.subprocess.Popen = subprocess.Popen
|
|
|
|
|
|
def test_available_layers_scans_disk(tmp_path, monkeypatch):
|
|
"""/api/layers retourne les couches présentes (clé → label)."""
|
|
import lidar_pipeline.webapp as webapp
|
|
monkeypatch.setattr(webapp, "OUTPUT_DIR", tmp_path)
|
|
vis = tmp_path / "visualisations" / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
|
|
vis.mkdir(parents=True)
|
|
(vis / "LHD_FXX_1054_6882_PTS_LAMB93_IGN69_wavelet.avif").write_bytes(b"x")
|
|
layers = webapp.available_layers()
|
|
assert layers.get("wavelet")
|
|
assert "aspect" not in layers
|
|
|
|
|
|
def test_rebuild_index_background(tmp_path, monkeypatch):
|
|
"""POST /api/rebuild lance build_index en arrière-plan puis termine."""
|
|
import time as _time
|
|
import lidar_pipeline.webapp as webapp
|
|
import lidar_pipeline.index as index_mod
|
|
calls = []
|
|
monkeypatch.setattr(index_mod, "build_index", lambda out: calls.append(out))
|
|
assert webapp.rebuild_index()["demarré"] is True
|
|
for _ in range(100):
|
|
if not webapp.rebuild_status()["running"]:
|
|
break
|
|
_time.sleep(0.05)
|
|
assert calls # build_index a bien été appelé
|
|
assert webapp.rebuild_status()["running"] is False
|
|
assert webapp.rebuild_status()["done"] is not None
|