From 93317dadd05aacbdc6df9b3f36b2a48af52ed2a7 Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 27 Sep 2026 02:34:12 +0200 Subject: [PATCH] =?UTF-8?q?Garder=20localement=20les=20sources=20des=20dal?= =?UTF-8?q?les=20sur=20une=20carte=20aliment=C3=A9e=20par=20l'amont?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avec LIDAR_SOURCE_URL (Pi), la maintenance rapatrie et conserve les sources de chaque dalle dès qu'elle apparaît, sans attendre de visite : les niveaux rendus à la volée restent disponibles quand le conteneur de rendu est éteint. Seuls les paliers utiles sont pris (vignettes, quadrants 500 m ; la dalle entière, doublon de ses quadrants, ne l'est pas) ; une source manquée pendant que l'amont était éteint est reprise au scan suivant. Co-Authored-By: Claude Opus 5.5 --- docs/MAPS.md | 7 ++++++ lidar_pipeline/mapserve.py | 29 ++++++++++++++++++++++- lidar_pipeline/tests/test_mapserve.py | 34 +++++++++++++++++++++++++++ lidar_pipeline/tiles.py | 16 +++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/docs/MAPS.md b/docs/MAPS.md index ee7f9e2..acfdbbc 100644 --- a/docs/MAPS.md +++ b/docs/MAPS.md @@ -91,6 +91,13 @@ pairs : à un zoom impair, elle réduit les tuiles du niveau supérieur (nettes) Mesuré sur 8 dalles : environ 24 Mo de pyramide avant, 1,2 Mo après. `LIDAR_TILE_EVEN_LEVELS=0` rétablit le stockage de tous les niveaux. +Une carte alimentée par un serveur de dalles amont (`LIDAR_SOURCE_URL`, cas +du Pi) rapatrie et **garde localement** les sources de chaque dalle dès +qu'elle apparaît (vignettes et quadrants 500 m ; la dalle entière, doublon +de ses quadrants, n'est pas rapatriée), sans attendre de visite. Tous les +niveaux restent disponibles quand le conteneur de rendu est éteint ; une +source manquée pendant qu'il était éteint est reprise au scan suivant. + ## Fiche de dalle et rose des vents Un clic sur la carte sélectionne la dalle LiDAR HD sous le curseur (cadre diff --git a/lidar_pipeline/mapserve.py b/lidar_pipeline/mapserve.py index 77b1e14..8206add 100644 --- a/lidar_pipeline/mapserve.py +++ b/lidar_pipeline/mapserve.py @@ -169,6 +169,7 @@ def _bg_conf(): _bg = {"threads": [], "stop": threading.Event(), "queue": deque(), "queued": set(), "guard": threading.Lock(), "snapshot": None, + "prefetch": deque(), "running": False, "stats": {"rendues": 0, "a_jour": 0, "vides": 0, "scans": 0, "dernier_scan": None, "ajouts_dernier_scan": 0}} @@ -236,6 +237,22 @@ def background_scan(output_dir=None): continue changed.setdefault(layer, []).append((col, row)) _bg["snapshot"] = snapshot + # Carte alimentée par un serveur de dalles amont (Pi) : les sources des + # dalles nouvelles ou modifiées sont rapatriées et GARDÉES localement, + # sans attendre une visite — les niveaux rendus à la volée restent + # disponibles quand le conteneur de rendu est éteint. + # Toutes les dalles sont revérifiées (simples tests d'existence) : une + # source manquée pendant qu'il était éteint est reprise au scan suivant. + if tiles_mod.REMOTE_SOURCE_URL: + index = tiles_mod.source_index(output_dir) + pending = {str(s_.path) for s_ in _bg["prefetch"]} + for per_cell in index.values(): + for tiers in per_cell.values(): + for src in tiles_mod.sources_to_keep(tiers): + key = str(src.path) + if src.url is not None and key not in pending and not src.path.is_file(): + _bg["prefetch"].append(src) + pending.add(key) items = [] for layer, cells in changed.items(): for col, row in cells: @@ -261,7 +278,17 @@ def background_scan(output_dir=None): def _bg_process_one(): - """Traite une tuile de la file. Vrai si elle a dû être rendue.""" + """Traite une source à rapatrier, sinon une tuile de la file. Vrai si + un travail a été fait (rapatriement ou rendu).""" + try: + src = _bg["prefetch"].popleft() + except IndexError: + src = None + if src is not None: + if src.ensure(): + _bg["stats"]["sources_locales"] = _bg["stats"].get("sources_locales", 0) + 1 + return True + return False # amont éteint : repris au prochain scan item = _bg_pop() if item is None: return False diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index 1fb4915..62d992b 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -973,3 +973,37 @@ def test_background_scan_only_queues_stored_levels(tmp_path, monkeypatch): zs = {z for (_l, z, _x, _y) in mapserve._bg["queue"]} assert zs and all(tiles.zoom_cached(z, 2) for z in zs) assert zs <= {5, 7, 9, 11, 13, 15} + + +def test_remote_fed_map_keeps_sources_locally(tmp_path, monkeypatch): + """Carte alimentée par l'amont : les sources nécessaires sont rapatriées + et gardées sans attendre de visite ; une source manquée (amont éteint) + est reprise au scan suivant ; la dalle entière (doublon) n'est pas prise.""" + import lidar_pipeline.mapserve as mapserve + from lidar_pipeline import tiles + S = tiles._Source + thumb = S(tmp_path / "t.webp", (0, 0, 1000, 1000), 3.9, url="http://amont/t") + quads = [S(tmp_path / f"q{i}.avif", (0, 0, 500, 500), 0.2, url=f"http://amont/q{i}") for i in range(4)] + full = S(tmp_path / "d.avif", (0, 0, 1000, 1000), 0.2, url="http://amont/d") + assert tiles.sources_to_keep([[thumb], quads, [full]]) == [thumb] + quads + monkeypatch.setattr(tiles, "REMOTE_SOURCE_URL", "http://amont") + monkeypatch.setattr(mapserve, "OUTPUT_DIR", tmp_path) + monkeypatch.setattr(tiles, "source_index", lambda *a, **k: {"relief_oriente": {(1, 2): [[thumb], quads, [full]]}}) + for key, val in (("queue", __import__("collections").deque()), ("queued", set()), + ("snapshot", None), ("prefetch", __import__("collections").deque())): + monkeypatch.setitem(mapserve._bg, key, val) + online = {"up": False} + def fake_fetch(url, dest): + if not online["up"]: + return False + dest.write_bytes(b"x"); return True + monkeypatch.setattr(tiles, "_fetch_source", fake_fetch) + mapserve.background_scan(tmp_path) + while mapserve._bg["prefetch"]: + mapserve._bg_process_one() + assert not any(s_.path.exists() for s_ in [thumb] + quads) # amont éteint + online["up"] = True + mapserve.background_scan(tmp_path) # dalle inchangée : reprise quand même + while mapserve._bg["prefetch"]: + mapserve._bg_process_one() + assert all(s_.path.exists() for s_ in [thumb] + quads) and not full.path.exists() diff --git a/lidar_pipeline/tiles.py b/lidar_pipeline/tiles.py index e01e82c..12157dc 100644 --- a/lidar_pipeline/tiles.py +++ b/lidar_pipeline/tiles.py @@ -609,6 +609,22 @@ def clear_source_cache(): _source_cache.clear() +def sources_to_keep(tiers): + """Paliers qu'une machine autonome doit garder pour rendre TOUS ses + niveaux sans l'amont : du plus grossier jusqu'au premier palier qui + atteint la résolution la plus fine (une dalle entière de même résolution + que ses quadrants serait un doublon).""" + if not tiers: + return [] + finest = min(group[0].res for group in tiers) + keep = [] + for group in tiers: + keep.extend(group) + if group[0].res <= finest: + break + return keep + + def _pick_tier(tiers, target_res): """Palier le plus grossier dont la résolution suffit à la tuile visée.""" for group in tiers: