From 34152269aa451552542411304ad5efc5053030ab Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 27 Sep 2026 12:50:53 +0200 Subject: [PATCH] =?UTF-8?q?Afficher=20la=20r=C3=A9solution=20native=200,2?= =?UTF-8?q?=20m=20au=20lieu=20d'=C3=A9tirer=20le=20niveau=200,4=20m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- lidar_pipeline/mapserve.py | 5 ++++- lidar_pipeline/mapui.py | 16 +++++++++------- lidar_pipeline/tests/test_mapserve.py | 5 ++++- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2f9e842..dc2edd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ - **Génération imposée** : la carte ne propose plus de réglage de classification ni de raccord — `_build_command` (`mapserve.py`) passe toujours `--ground-classification ign --ign-classes sol --edge-buffer 100` ; les champs `ground_class`/`ign_classes`/`reclassify`/`edge_buffer` éventuellement envoyés sont ignorés. Défauts CLI alignés (`ign`, `100`). - **Classification IGN par extraction directe** : `_extract_ign_ground` (`dtm.py`) filtre les classes avec laspy (retours ≥ 1) et écrit le LAS sol sans passer par PDAL (4,9 s au lieu de 13,5 s par dalle) ; la lecture de la détection automatique est réutilisée (`_LAST_READ`). PDAL reste le repli. - **Pyramide mise à jour pendant le rendu** : le pipeline régénère l'inventaire après chaque dalle **par défaut** (`incremental_index` sauf `--no-index`, quel que soit le lanceur), avec passe différée quand l'anti-rebond de 3 s ignore une dalle. Côté carte, `_bg_poller` surveille l'inventaire toutes les `BG_WATCH_S` = 10 s (fichier local ou charge utile amont) et lance un scan dès qu'il change ; les tuiles des dalles modifiées passent **en tête** de file (`_bg_enqueue(front=True)`), devant l'arriéré. -- **Pyramide à stockage réduit** : `tiles.zoom_cached` — seuls les niveaux standard pairs ≤ `TILE_CACHE_MAX_Z` (16) vont sur disque (une tuile @2x de z vaut une 256 px de z+1) ; les autres sont rendus à la volée (`get_tile`, cache mémoire `_mem_tiles`), même en `LIDAR_TILE_CACHE_ONLY`. Interface en AVIF (`@2x.avif`) avec `EvenLevelTileLayer` (niveau de tuile arrondi au pair supérieur) ; maintenance de fond en AVIF sur les seuls niveaux stockés. ~24 Mo → 1,2 Mo sur 8 dalles. +- **Pyramide à stockage réduit** : `tiles.zoom_cached` — seuls les niveaux standard pairs ≤ `TILE_CACHE_MAX_Z` (16) vont sur disque (une tuile @2x de z vaut une 256 px de z+1) ; les autres sont rendus à la volée (`get_tile`, cache mémoire `_mem_tiles`), même en `LIDAR_TILE_CACHE_ONLY`. Interface en AVIF (`@2x.avif`) avec `EvenLevelTileLayer` (niveau de tuile arrondi au pair supérieur, sauf le natif 19 = 0,2 m/px demandé tel quel en `18@2x`, rendu à la volée ; au-delà Leaflet étire le natif, jamais de tuile agrandie côté serveur ; `max_native_zoom` de `/api/map/meta` = 19 en numérotation standard) ; maintenance de fond en AVIF sur les seuls niveaux stockés. ~24 Mo → 1,2 Mo sur 8 dalles. - **Mémoire bornée de la carte (Pi, `mem_limit: 1g`)** : `_open_source` (`tiles.py`) met en cache une **copie détachée** de chaque source (une image AVIF ouverte garde son décodeur, ~18 Mo de plus par quadrant 2500²) et compte 4 octets/pixel (PIL stocke le RGB sur 32 bits) ; `Dockerfile.maps` fixe `MALLOC_MMAP_THRESHOLD_=1048576` pour que glibc rende les grands tampons au système. Sans ces deux points, la navigation à fort zoom (niveaux rendus à la volée) montait à ~700 Mo de RSS et le conteneur était tué en boucle par l'OOM killer (502 côté Traefik). - **Carte autonome (Pi sans worker)** : amont (`LIDAR_SOURCE_URL`/`LIDAR_MAPS_URL`) éteint ⇒ la carte reste servie depuis le disque. `_remote_payload` mémorise aussi l'échec (TTL 60 s, délai 10 s : sinon chaque requête repayait le délai réseau et `/api/map/meta` ne répondait plus), puis l'index local (`_build_index`) prend le relais ; coupe-circuits sur les sources (`_SOURCE_OFFLINE`, 60 s) et les tuiles (`_UPSTREAM`, réarmé à expiration) ; source rapatriée datée à sa version amont (`os.utime`) pour que l'index local garde les mêmes dates ; pas de requête amont pour une tuile sans dalle quand l'inventaire vient de l'amont. En cache seule, une tuile périmée reste servie (`no-store`, `X-Tile-Pending`) en attendant la nouvelle. - **Pyramide téléchargée en tâche de fond** : avec `LIDAR_MAPS_URL`, la maintenance (`_bg_process_one`) TÉLÉCHARGE les tuiles des niveaux stockés depuis l'amont (stat `telechargees`), sans attendre l'affichage ; rendu local en repli si l'amont ne répond pas. diff --git a/lidar_pipeline/mapserve.py b/lidar_pipeline/mapserve.py index 2311846..4b1f2d4 100644 --- a/lidar_pipeline/mapserve.py +++ b/lidar_pipeline/mapserve.py @@ -962,7 +962,10 @@ def map_meta(): "tile_size": tiles_mod.TILE_SIZE * 2, "zoom_offset": -1, "min_zoom": tiles_mod.TILE_MIN_Z, - "max_native_zoom": tiles_mod.TILE_MAX_NATIVE_Z - 1, + # Numérotation standard, comme le niveau de tuile Leaflet (l'URL @2x + # est un cran en dessous via zoom_offset) : 19 = 0,2 m/px natif, + # servi en 18@2x. Annoncer 18 plafonnait l'affichage à 0,4 m/px. + "max_native_zoom": tiles_mod.TILE_MAX_NATIVE_Z, "bounds": tiles_mod.grid_bounds_wgs84(OUTPUT_DIR), "stamp": tiles_mod.tiles_stamp(OUTPUT_DIR), "attribution": ATTRIBUTION, diff --git a/lidar_pipeline/mapui.py b/lidar_pipeline/mapui.py index 599d7ca..6ecb930 100644 --- a/lidar_pipeline/mapui.py +++ b/lidar_pipeline/mapui.py @@ -565,16 +565,18 @@ function tileUrl(key) { // Couche à niveaux standard pairs seulement (stockage réduit côté serveur) : // à un zoom impair, les tuiles du niveau pair SUPÉRIEUR sont affichées -// réduites de moitié (nettes) ; au-delà du zoom natif, le niveau inférieur. -// Le niveau de tuile Leaflet est ici la numérotation standard (zoomOffset −1 -// avec des tuiles de 512 px). +// réduites de moitié (nettes). Exception : le niveau natif lui-même (19, +// impair — 0,2 m/px) est demandé tel quel, rendu à la volée par le serveur. +// Le replier sur le niveau pair inférieur plafonnait l'affichage à 0,4 m/px, +// étiré ensuite par le navigateur : la pleine résolution n'était jamais vue. +// Au-delà du natif, Leaflet étire le niveau natif (jamais de tuile agrandie +// côté serveur). Le niveau de tuile Leaflet est ici la numérotation standard +// (zoomOffset −1 avec des tuiles de 512 px). const EvenLevelTileLayer = ZoomAnimTileLayer.extend({ _clampZoom(zoom) { let z = L.TileLayer.prototype._clampZoom.call(this, zoom); - if (this.options.evenLevels && z % 2) { - const maxN = this.options.maxNativeZoom; - z = (maxN !== undefined && z + 1 > maxN) ? z - 1 : z + 1; - } + const maxN = this.options.maxNativeZoom; + if (this.options.evenLevels && z % 2 && (maxN === undefined || z < maxN)) z += 1; return z; } }); diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index 2ef2cc1..6bf4902 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -208,7 +208,7 @@ def test_map_meta(tmp_path, monkeypatch): 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["max_native_zoom"] == 19 assert meta["bounds"] and meta["stamp"] > 0 assert meta["default_layers"] == ["relief_oriente"] @@ -409,6 +409,9 @@ def test_ui_uses_standard_tilelayer(): assert "tileSize: META.tile_size" in _MAP_JS assert "zoomOffset: META.zoom_offset" in _MAP_JS assert "maxNativeZoom: META.max_native_zoom" in _MAP_JS + # Le niveau natif impair (19 = 0,2 m/px) est demandé tel quel, jamais + # replié sur le pair inférieur (affichage plafonné à 0,4 m/px). + assert "z % 2 && (maxN === undefined || z < maxN)" in _MAP_JS # Plus aucune mécanique de l'ancienne carte for banned in ("quadMatrix3d", "FULL_CAP", "ovTick", "layoutTiles"): assert banned not in _MAP_JS