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:
Antoine Jacquin
2026-09-27 02:31:11 +02:00
parent f836408d6f
commit 434b7f12fd
11 changed files with 299 additions and 35 deletions

View File

@ -158,8 +158,8 @@ def _bg_conf():
"LIDAR_TILE_BACKGROUND_MAX_Z", "16") or 16)),
"scale": 1 if (os.environ.get(
"LIDAR_TILE_BACKGROUND_SCALE", "2").strip() == "1") else 2,
"fmt": (os.environ.get("LIDAR_TILE_BACKGROUND_FMT", "webp")
or "webp").strip(),
"fmt": (os.environ.get("LIDAR_TILE_BACKGROUND_FMT", "avif")
or "avif").strip(),
"pause": max(0.0, float(os.environ.get(
"LIDAR_TILE_BACKGROUND_PAUSE", "1.0") or 1.0)),
"queue_max": max(1024, int(os.environ.get(
@ -174,10 +174,24 @@ _bg = {"threads": [], "stop": threading.Event(), "queue": deque(),
"dernier_scan": None, "ajouts_dernier_scan": 0}}
def _bg_enqueue(items):
"""Ajoute des tuiles (layer, z, x, y) à la file : dédupliquée, bornée."""
def _bg_enqueue(items, front=False):
"""Ajoute des tuiles (layer, z, x, y) à la file : dédupliquée, bornée.
front=True : en tête de file, dans l'ordre donné (dalles tout juste
rendues) ; une tuile déjà en file y est remontée.
"""
added = 0
with _bg["guard"]:
if front:
fresh = [it for it in items]
keep = set(fresh)
rest = [it for it in _bg["queue"] if it not in keep]
_bg["queue"].clear()
_bg["queue"].extend(fresh[:_bg_conf()["queue_max"]])
_bg["queue"].extend(rest[:max(0, _bg_conf()["queue_max"] - len(_bg["queue"]))])
added = sum(1 for it in fresh if it not in _bg["queued"])
_bg["queued"] = set(_bg["queue"])
return added
for item in items:
if item in _bg["queued"] or len(_bg["queue"]) >= _bg_conf()["queue_max"]:
continue
@ -205,6 +219,7 @@ def background_scan(output_dir=None):
"""
output_dir = Path(output_dir or OUTPUT_DIR)
conf = _bg_conf()
first_scan = _bg["snapshot"] is None
snapshot = {}
changed = {}
for layer, per_cell in tiles_mod.source_index(output_dir, force=True).items():
@ -226,12 +241,15 @@ def background_scan(output_dir=None):
for col, row in cells:
bbox = tiles_mod.cell_bounds_wgs84(col, row)
for z in range(tiles_mod.TILE_MIN_Z, conf["max_z"] + 1):
if not tiles_mod.zoom_supported(z, conf["scale"]):
if not (tiles_mod.zoom_supported(z, conf["scale"])
and tiles_mod.zoom_cached(z, conf["scale"])):
continue
for x, y in tiles_mod.tiles_in_bounds(bbox, z):
items.append((layer, z, x, y))
items.sort(key=lambda t: (t[1], t[0], t[2], t[3])) # bas niveaux d'abord
added = _bg_enqueue(items)
# Dalles qui viennent de changer : en tête de file, devant l'arriéré —
# la pyramide suit le rendu en cours (hors premier scan, qui construit tout).
added = _bg_enqueue(items, front=first_scan is False)
_bg["stats"]["scans"] += 1
_bg["stats"]["dernier_scan"] = time.time()
_bg["stats"]["ajouts_dernier_scan"] = added
@ -276,13 +294,37 @@ def _bg_worker():
_bg["stop"].wait(0.5)
def _inventory_mark():
"""Signature bon marché de l'inventaire des dalles (fichier local ou
charge utile amont), pour déclencher un scan dès qu'une dalle arrive."""
try:
return (OUTPUT_DIR / "index_tiles.json").stat().st_mtime
except OSError:
pass
if not tiles_mod.REMOTE_SOURCE_URL:
return None
try:
payload = tiles_mod._remote_payload()
return payload.get("stamp") if payload else None
except Exception: # noqa: BLE001
return None
BG_WATCH_S = 10.0 # surveillance de l'inventaire pendant les runs
def _bg_poller():
last_scan, last_mark = 0.0, _inventory_mark()
while not _bg["stop"].is_set():
try:
background_scan()
except Exception as e: # noqa: BLE001 — prochain scan dans INTERVAL
logger.warning(f"Maintenance des tuiles : scan impossible ({e})")
_bg["stop"].wait(_bg_conf()["interval"])
mark = _inventory_mark()
due = time.time() - last_scan >= _bg_conf()["interval"]
if due or mark != last_mark:
try:
background_scan()
except Exception as e: # noqa: BLE001 — prochain scan au prochain tour
logger.warning(f"Maintenance des tuiles : scan impossible ({e})")
last_scan, last_mark = time.time(), mark
_bg["stop"].wait(min(BG_WATCH_S, _bg_conf()["interval"]))
def start_background():
@ -570,7 +612,7 @@ async def tile(layer: str, z: int, x: int, name: str, v: Optional[str] = None):
if not (0 <= x < n and 0 <= y < n):
raise HTTPException(404, "tuile hors grille")
if _env_flag("LIDAR_TILE_CACHE_ONLY"):
if _env_flag("LIDAR_TILE_CACHE_ONLY") and tiles_mod.zoom_cached(z, scale):
# Mode cache seule : aucune navigation ne déclenche de RENDU local.
# Une tuile absente (ex. zoom > LIDAR_TILE_BACKGROUND_MAX_Z, jamais
# couverte par la maintenance) est rapatriée du serveur amont
@ -847,7 +889,10 @@ def map_meta():
"defaults_saved": bool(saved),
# L'interface consomme des tuiles 512 px (2× moins de requêtes qu'en
# 256 : décisif en HTTP/1.1) ; les clients OSM gardent le 256 canonique.
"tile_url": "tiles/{layer}/{z}/{x}/{y}@2x.webp",
"tile_url": "tiles/{layer}/{z}/{x}/{y}@2x.avif",
# Niveaux standard pairs seulement (stockage réduit) : l'interface
# réduit ceux du niveau supérieur aux zooms impairs.
"even_levels": tiles_mod.TILE_EVEN_LEVELS,
"tile_size": tiles_mod.TILE_SIZE * 2,
"zoom_offset": -1,
"min_zoom": tiles_mod.TILE_MIN_Z,