From 3b4d106cc2a5683940ca644f4614c1293bc67bf9 Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 27 Sep 2026 15:29:06 +0200 Subject: [PATCH] =?UTF-8?q?Diffuser=20la=20qualit=C3=A9=20des=20dalles=20d?= =?UTF-8?q?ans=20l'inventaire=20et=20la=20conserver=20sur=20la=20carte=20l?= =?UTF-8?q?=C3=A9g=C3=A8re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- lidar_pipeline/index.py | 4 ++++ lidar_pipeline/tests/test_index.py | 12 ++++++++++++ lidar_pipeline/tests/test_tiles.py | 17 +++++++++++++++++ lidar_pipeline/tiles.py | 25 +++++++++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/lidar_pipeline/index.py b/lidar_pipeline/index.py index 69932fa..d384c6c 100644 --- a/lidar_pipeline/index.py +++ b/lidar_pipeline/index.py @@ -1220,11 +1220,15 @@ def build_index(output_dir, output_format='avif'): for k in _VIZ_FALLBACK_ORDER if k in all_viz_keys} for k in sorted(all_viz_keys): viz_meta.setdefault(k, {'label': VIZ_LABELS.get(k, k)}) + from .quality import load_quality_table inventory_path = output_dir / 'index_tiles.json' inventory_path.write_text(json.dumps({ 'tiles': display_tiles, 'viz_meta': viz_meta, 'stats': {'n_tiles': len(display_tiles)}, + # Qualité par dalle (encart de l'export PDF) : recopiée sur les + # machines légères pour rester disponible amont éteint. + 'quality': load_quality_table(output_dir), }, ensure_ascii=False), encoding='utf-8') logger.info(f"Inventaire généré : {inventory_path} " diff --git a/lidar_pipeline/tests/test_index.py b/lidar_pipeline/tests/test_index.py index 9e05d61..56d0fe6 100644 --- a/lidar_pipeline/tests/test_index.py +++ b/lidar_pipeline/tests/test_index.py @@ -653,3 +653,15 @@ def test_urls_versioned_by_served_file_mtime(tmp_path): url_after = tiles05b[0]['viz']['hillshade_multi']['thumb'] assert url_after.split('?')[0] == url_before.split('?')[0] assert url_after != url_before + + +def test_inventory_contains_quality_table(tmp_path): + import json + from lidar_pipeline.index import build_index + from lidar_pipeline.quality import write_quality + from lidar_pipeline.tests.test_tiles import _make_dalle + base = _make_dalle(tmp_path, 1054, 6882, ["relief_oriente"]) + write_quality(tmp_path, base, {"version": 1, "ground_density": 7.5}) + build_index(tmp_path, "webp") + inv = json.loads((tmp_path / "index_tiles.json").read_text(encoding="utf-8")) + assert inv["quality"] == {base: {"version": 1, "ground_density": 7.5}} diff --git a/lidar_pipeline/tests/test_tiles.py b/lidar_pipeline/tests/test_tiles.py index 69675b0..96fb9a6 100644 --- a/lidar_pipeline/tests/test_tiles.py +++ b/lidar_pipeline/tests/test_tiles.py @@ -712,3 +712,20 @@ def test_webp_subtiles_indexed_and_rendered_nearest(tmp_path): assert img is not None grays = {p[0] for p in img.getdata() if p[3] == 255} assert grays <= {0, 255} # aucun gris intermédiaire inventé + + +def test_remote_quality_persisted_locally(tmp_path, monkeypatch): + """La table qualité amont est recopiée en sidecars locaux (Pi autonome).""" + from lidar_pipeline import tiles + from lidar_pipeline.quality import read_quality + payload = _remote_payload() + base = "LHD_FXX_1054_6882_PTS_LAMB93_IGN69" + payload["quality"] = {base: {"version": 1, "ground_density": 3.0}, + "../evasion": {"version": 1}} + monkeypatch.setattr(tiles, "REMOTE_SOURCE_URL", "http://amont") + monkeypatch.setattr(tiles, "_remote_payload", lambda force=False: payload) + monkeypatch.setattr(tiles, "_remote_cache", dict(tiles._remote_cache, index=None, built=None)) + tiles._remote_index(tmp_path, force=True) + assert read_quality(tmp_path, base) == {"version": 1, "ground_density": 3.0} + assert not (tmp_path / "evasion.json").exists() + assert list((tmp_path / "quality").iterdir()) == [tmp_path / "quality" / f"{base}.json"] diff --git a/lidar_pipeline/tiles.py b/lidar_pipeline/tiles.py index 5c9c261..e8e5994 100644 --- a/lidar_pipeline/tiles.py +++ b/lidar_pipeline/tiles.py @@ -305,6 +305,30 @@ def _remote_payload(force=False): return payload +_SAFE_BASENAME = re.compile(r"^[A-Za-z0-9_.-]+$") + + +def _persist_remote_quality(output_dir, payload): + """Recopie la table qualité amont en sidecars locaux. + + L'encart qualité de l'export PDF lit toujours le disque local : la + carte autonome (amont éteint) garde ainsi les dernières valeurs connues. + Returns: nombre de sidecars (ré)écrits. + """ + from .quality import QUALITY_VERSION, write_quality + written = 0 + for base, data in (payload.get("quality") or {}).items(): + if (not isinstance(base, str) or not _SAFE_BASENAME.match(base) + or base.startswith(".") or not isinstance(data, dict) + or data.get("version") != QUALITY_VERSION): + continue + try: + written += bool(write_quality(output_dir, base, data)) + except OSError as e: + logger.debug(f"Sidecar qualité non recopié ({base}) : {e}") + return written + + def _remote_index(output_dir, force=False): """Inventaire construit depuis l'amont : sources non encore rapatriées. @@ -320,6 +344,7 @@ def _remote_index(output_dir, force=False): and _remote_cache.get("built") is payload): return _remote_cache["index"] output_dir = Path(output_dir) + _persist_remote_quality(output_dir, payload) layers = {} for entry in payload["tiles"]: col, row = entry.get("col"), entry.get("row")