Mettre à jour la pyramide pendant le rendu et réduire son stockage
Le pipeline régénère l'inventaire après chaque dalle par défaut (tous lanceurs, sauf --no-index), avec une passe différée quand l'anti-rebond ignore une dalle. La carte surveille l'inventaire toutes les 10 s et place les tuiles des dalles modifiées en tête de file de maintenance : la pyramide suit le rendu au lieu d'attendre la fin du lot. Stockage réduit pour le Raspberry Pi : seuls les niveaux standard pairs jusqu'à z16 sont écrits sur disque, en AVIF ; les autres, dont le niveau le plus fin (~75 % de la pyramide), sont rendus à la volée avec un cache mémoire, y compris en mode cache seule. L'interface ne demande que les niveaux pairs et réduit ceux du niveau supérieur aux zooms impairs. Mesuré sur 8 dalles : ~24 Mo de pyramide avant, 1,2 Mo après. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -205,7 +205,8 @@ def test_map_meta(tmp_path, monkeypatch):
|
||||
meta = mapserve.map_meta()
|
||||
assert {l["key"] for l in meta["layers"]} == {"relief_oriente", "aspect", "slope"}
|
||||
assert all(l["label"] for l in meta["layers"])
|
||||
assert meta["tile_url"] == "tiles/{layer}/{z}/{x}/{y}@2x.webp"
|
||||
assert meta["tile_url"] == "tiles/{layer}/{z}/{x}/{y}@2x.avif"
|
||||
assert meta["even_levels"] is True
|
||||
assert meta["tile_size"] == 512 and meta["zoom_offset"] == -1
|
||||
assert meta["max_native_zoom"] == 18
|
||||
assert meta["bounds"] and meta["stamp"] > 0
|
||||
@ -558,6 +559,9 @@ def _reset_bg(mapserve):
|
||||
|
||||
def test_tile_cache_only_never_renders(tmp_path, monkeypatch):
|
||||
"""Mode cache seule : tuile manquante = transparente non mémorisable."""
|
||||
from lidar_pipeline import tiles as _t
|
||||
monkeypatch.setattr(_t, "TILE_EVEN_LEVELS", False) # règle historique : tout est stocké
|
||||
monkeypatch.setattr(_t, "TILE_CACHE_MAX_Z", 99)
|
||||
mapserve = _setup(tmp_path, monkeypatch, layers=("slope",))
|
||||
from lidar_pipeline import tiles
|
||||
monkeypatch.setenv("LIDAR_TILE_CACHE_ONLY", "1")
|
||||
@ -857,6 +861,9 @@ def test_cache_only_fetches_missing_tile_upstream(tmp_path, monkeypatch):
|
||||
"""Cache seule + LIDAR_MAPS_URL : une tuile absente est rapatriee du serveur
|
||||
amont (aucun rendu local : les zooms au-dela de la maintenance s'affichent).
|
||||
"""
|
||||
from lidar_pipeline import tiles as _t
|
||||
monkeypatch.setattr(_t, "TILE_EVEN_LEVELS", False) # règle historique : tout est stocké
|
||||
monkeypatch.setattr(_t, "TILE_CACHE_MAX_Z", 99)
|
||||
import lidar_pipeline.mapserve as mapserve
|
||||
from lidar_pipeline import tiles
|
||||
_setup(tmp_path, monkeypatch)
|
||||
@ -922,3 +929,47 @@ def test_ui_tile_card_and_wind_rose():
|
||||
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°)
|
||||
|
||||
|
||||
def test_fresh_tiles_jump_the_maintenance_queue(monkeypatch):
|
||||
"""Tuiles d'une dalle tout juste rendue : en tête de file, devant l'arriéré."""
|
||||
import lidar_pipeline.mapserve as mapserve
|
||||
monkeypatch.setitem(mapserve._bg, "queue", __import__("collections").deque())
|
||||
monkeypatch.setitem(mapserve._bg, "queued", set())
|
||||
backlog = [("relief_oriente", 16, i, 0) for i in range(5)]
|
||||
mapserve._bg_enqueue(backlog)
|
||||
fresh = [("relief_oriente", 12, 9, 9), ("relief_oriente", 16, 3, 0)]
|
||||
mapserve._bg_enqueue(fresh, front=True)
|
||||
q = list(mapserve._bg["queue"])
|
||||
assert q[:2] == fresh and len(q) == 6 and q.count(("relief_oriente", 16, 3, 0)) == 1
|
||||
|
||||
|
||||
def test_cache_only_renders_unstored_levels(tmp_path, monkeypatch):
|
||||
"""Cache seule (Pi) : un niveau non stocké est rendu à la volée — jamais
|
||||
une tuile transparente « en attente » qui n'arriverait jamais."""
|
||||
import lidar_pipeline.mapserve as mapserve
|
||||
from lidar_pipeline import tiles
|
||||
monkeypatch.setattr(tiles, "TILE_EVEN_LEVELS", True)
|
||||
monkeypatch.setattr(tiles, "TILE_CACHE_MAX_Z", 16)
|
||||
_setup(tmp_path, monkeypatch)
|
||||
monkeypatch.setenv("LIDAR_TILE_CACHE_ONLY", "1")
|
||||
z = 17
|
||||
x, y = _tile_of_cell(1054, 6882, z)
|
||||
resp = asyncio.run(mapserve.tile("aspect", z, x, f"{y}@2x.avif"))
|
||||
assert resp.headers.get("X-Tile-Pending") != "1" and resp.headers["X-Tile-Empty"] == "0"
|
||||
assert not tiles.tile_cache_path(tmp_path, "aspect", z, x, y, 2, "avif").exists()
|
||||
|
||||
|
||||
def test_background_scan_only_queues_stored_levels(tmp_path, monkeypatch):
|
||||
import lidar_pipeline.mapserve as mapserve
|
||||
from lidar_pipeline import tiles
|
||||
monkeypatch.setattr(tiles, "TILE_EVEN_LEVELS", True)
|
||||
monkeypatch.setattr(tiles, "TILE_CACHE_MAX_Z", 16)
|
||||
_setup(tmp_path, monkeypatch)
|
||||
monkeypatch.setitem(mapserve._bg, "queue", __import__("collections").deque())
|
||||
monkeypatch.setitem(mapserve._bg, "queued", set())
|
||||
monkeypatch.setitem(mapserve._bg, "snapshot", None)
|
||||
mapserve.background_scan(tmp_path)
|
||||
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}
|
||||
|
||||
@ -33,6 +33,28 @@ class TestVizSteps:
|
||||
p = LidarArchaeoPipeline(tmp_path, tmp_path / "out2", only_viz=["slope"])
|
||||
assert [n for n, _ in p.viz_steps] == ["slope"]
|
||||
|
||||
def test_incremental_index_on_by_default(self, tmp_path):
|
||||
"""La carte suit le rendu en cours quel que soit le lanceur."""
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
assert LidarArchaeoPipeline(tmp_path, tmp_path / "o").incremental_index
|
||||
assert not LidarArchaeoPipeline(tmp_path, tmp_path / "o2", no_index=True).incremental_index
|
||||
|
||||
def test_debounced_tile_is_indexed_later(self, tmp_path, monkeypatch):
|
||||
"""Une dalle terminée pendant l'anti-rebond est reprise par une passe
|
||||
différée, sans attendre la dalle suivante."""
|
||||
import time
|
||||
import lidar_pipeline.index as index
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
calls = []
|
||||
monkeypatch.setattr(index, "build_index", lambda *a, **k: calls.append(time.time()))
|
||||
p = LidarArchaeoPipeline(tmp_path, tmp_path / "o")
|
||||
p._rebuild_index_incremental() # passe immédiate
|
||||
p._last_index_rebuild = time.time() - 2.8 # anti-rebond presque écoulé
|
||||
p._rebuild_index_incremental() # différée (~0,2 s)
|
||||
p._rebuild_index_incremental() # déjà programmée : pas de doublon
|
||||
time.sleep(0.6)
|
||||
assert len(calls) == 2
|
||||
|
||||
def test_ortho_and_topo_present(self):
|
||||
from lidar_pipeline.pipeline import VIZ_STEPS
|
||||
names = [name for name, _ in VIZ_STEPS]
|
||||
|
||||
@ -239,8 +239,11 @@ def test_tile_edges_transparent_outside_data(tmp_path):
|
||||
assert img.getpixel((255, 255))[3] == 0
|
||||
|
||||
|
||||
def test_get_tile_cache_and_staleness(tmp_path):
|
||||
def test_get_tile_cache_and_staleness(tmp_path, monkeypatch):
|
||||
"""Le cache disque est réutilisé, puis invalidé par une dalle régénérée."""
|
||||
from lidar_pipeline import tiles as _t
|
||||
monkeypatch.setattr(_t, "TILE_EVEN_LEVELS", False) # règle historique : tout est stocké
|
||||
monkeypatch.setattr(_t, "TILE_CACHE_MAX_Z", 99)
|
||||
import os
|
||||
import time
|
||||
from lidar_pipeline import tiles
|
||||
@ -270,8 +273,11 @@ def test_get_tile_cache_and_staleness(tmp_path):
|
||||
assert cache.stat().st_mtime > first
|
||||
|
||||
|
||||
def test_get_tile_empty_marker(tmp_path):
|
||||
def test_get_tile_empty_marker(tmp_path, monkeypatch):
|
||||
"""Tuile sans donnée : None + marqueur .empty mémorisé."""
|
||||
from lidar_pipeline import tiles as _t
|
||||
monkeypatch.setattr(_t, "TILE_EVEN_LEVELS", False) # règle historique : tout est stocké
|
||||
monkeypatch.setattr(_t, "TILE_CACHE_MAX_Z", 99)
|
||||
from lidar_pipeline import tiles
|
||||
_make_dalle(tmp_path, 1054, 6882, ["aspect"])
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
@ -514,3 +520,33 @@ def test_cached_tile_states(tmp_path):
|
||||
data, state = tiles.cached_tile(tmp_path, "slope", 14, 0, 0)
|
||||
assert state == "empty" and data is None
|
||||
assert tiles.empty_marker_exists(tmp_path, "slope", 14, 0, 0)
|
||||
|
||||
|
||||
def test_storage_policy_even_levels_up_to_max(monkeypatch):
|
||||
"""Stockage réduit : niveaux standard pairs ≤ LIDAR_TILE_CACHE_MAX_Z (une
|
||||
tuile @2x de niveau z vaut une 256 px de z+1)."""
|
||||
from lidar_pipeline import tiles
|
||||
monkeypatch.setattr(tiles, "TILE_EVEN_LEVELS", True)
|
||||
monkeypatch.setattr(tiles, "TILE_CACHE_MAX_Z", 16)
|
||||
assert tiles.zoom_cached(16, 1) and not tiles.zoom_cached(15, 1)
|
||||
assert tiles.zoom_cached(15, 2) and not tiles.zoom_cached(16, 2) # @2x z15 = z16 standard
|
||||
assert not tiles.zoom_cached(18, 1) and not tiles.zoom_cached(17, 2) # niveau fin : à la volée
|
||||
|
||||
|
||||
def test_unstored_level_rendered_on_the_fly_without_disk(tmp_path, monkeypatch):
|
||||
"""Niveau non stocké : tuile rendue, rien d'écrit sur disque, seconde
|
||||
demande servie par le cache mémoire."""
|
||||
from lidar_pipeline import tiles
|
||||
monkeypatch.setattr(tiles, "TILE_EVEN_LEVELS", True)
|
||||
monkeypatch.setattr(tiles, "TILE_CACHE_MAX_Z", 16)
|
||||
_make_dalle(tmp_path, 1054, 6882, ["aspect"])
|
||||
tiles.source_index(tmp_path, force=True)
|
||||
z = 17
|
||||
x, y = _tile_of_cell(1054, 6882, z)
|
||||
calls = []
|
||||
real = tiles.render_tile
|
||||
monkeypatch.setattr(tiles, "render_tile", lambda *a, **k: calls.append(1) or real(*a, **k))
|
||||
data = tiles.get_tile(tmp_path, "aspect", z, x, y, 1, "png")
|
||||
assert data and data[:4] == b"\x89PNG"
|
||||
assert not tiles.tile_cache_path(tmp_path, "aspect", z, x, y, 1, "png").exists()
|
||||
assert tiles.get_tile(tmp_path, "aspect", z, x, y, 1, "png") == data and len(calls) == 1
|
||||
|
||||
Reference in New Issue
Block a user