diff --git a/lidar_pipeline/mapui.py b/lidar_pipeline/mapui.py index 1532e4f..179061c 100644 --- a/lidar_pipeline/mapui.py +++ b/lidar_pipeline/mapui.py @@ -389,8 +389,28 @@ const map = L.map('map', { map.attributionControl.setPrefix(''); L.control.scale({ metric: true, imperial: false, position: 'bottomleft' }).addTo(map); -const base = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { +// Pendant l'animation de zoom, transformations SEULES (aucun niveau créé, +// tuiles chargées à la fin) : un niveau neuf n'est pas composé par Chrome +// (vide au départ, il est animé sur le thread principal, gelé ~90 ms par le +// chargement). Le fond sautait alors que le relief orienté, qui garde son +// niveau pair, glissait normalement — les deux couches se désynchronisaient. +// Leaflet 1.9 déclenche aussi « zoom » dès le DÉBUT de l'animation : même +// garde dans _resetView tant que la carte anime. +const ZoomAnimTileLayer = L.TileLayer.extend({ + _animateZoom(e) { this._setView(e.center, e.zoom, true, true); }, + _resetView(e) { + if (this._map && this._map._animatingZoom) { + this._setView(this._map.getCenter(), this._map.getZoom(), true, true); + return; + } + L.TileLayer.prototype._resetView.call(this, e); + } +}); + +const base = new ZoomAnimTileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 22, maxNativeZoom: 19, keepBuffer: 2, className: 'base-dark', + // Sinon _setView recrée un niveau en cours d'animation malgré noUpdate. + updateWhenZooming: false, attribution: '© OpenStreetMap' }).addTo(map); @@ -545,7 +565,7 @@ function tileUrl(key) { // 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). -const EvenLevelTileLayer = L.TileLayer.extend({ +const EvenLevelTileLayer = ZoomAnimTileLayer.extend({ _clampZoom(zoom) { let z = L.TileLayer.prototype._clampZoom.call(this, zoom); if (this.options.evenLevels && z % 2) { diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index 9048f81..2ef2cc1 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -405,7 +405,7 @@ def test_ui_reorder_shows_destination(): def test_ui_uses_standard_tilelayer(): """L'interface s'appuie sur le LOD natif de Leaflet, pas sur un palier maison.""" from lidar_pipeline.mapui import _MAP_JS, render_html, ui_version - assert "L.tileLayer(" in _MAP_JS + assert "L.TileLayer.extend(" in _MAP_JS # TileLayer Leaflet, LOD natif 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 @@ -1169,3 +1169,21 @@ def test_background_tiles_before_sources(tmp_path, monkeypatch): assert fetched == [] and mapserve._bg["stats"]["telechargees"] == 1 assert mapserve._bg_process_one() is True assert fetched == [1] + + +def test_zoom_animation_transforms_only_on_all_tile_layers(): + """Pendant l'animation de zoom, AUCUNE couche ne crée de niveau : un + niveau neuf n'est pas composé par Chrome (animé sur le thread principal, + gelé ~90 ms par le chargement) — le fond sautait quand le relief orienté, + qui garde son niveau pair, glissait normalement : désynchronisés.""" + import re + from lidar_pipeline.mapui import _MAP_JS + assert "_animateZoom(e) { this._setView(e.center, e.zoom, true, true); }" in _MAP_JS + # « zoom » part aussi au début de l'animation (Leaflet 1.9) : même garde + assert "if (this._map && this._map._animatingZoom) {" in _MAP_JS + # Sans quoi _setView recrée un niveau malgré noUpdate (tileZoomChanged) + assert _MAP_JS.count("updateWhenZooming: false") >= 2 + # Le fond OSM ET les couches LiDAR héritent du comportement + assert re.search(r"const base = new ZoomAnimTileLayer\(", _MAP_JS) + assert "const EvenLevelTileLayer = ZoomAnimTileLayer.extend(" in _MAP_JS + assert "L.tileLayer(" not in _MAP_JS.replace("L.tileLayer` standard", "") diff --git a/lidar_pipeline/tiles.py b/lidar_pipeline/tiles.py index bacd69b..c1c791a 100644 --- a/lidar_pipeline/tiles.py +++ b/lidar_pipeline/tiles.py @@ -849,6 +849,12 @@ def _write_atomic(path, data): tmp = path.with_name(path.name + f".{os.getpid()}.part") try: tmp.write_bytes(data) + # Datée à l'horloge précise (time.time) : celle des dates de première + # apparition des dalles (_apply_seen). L'horloge grossière du noyau + # (mtime par défaut) retarde de quelques ms — une tuile écrite juste + # après l'arrivée d'une dalle paraîtrait sinon plus vieille qu'elle. + now = time.time() + os.utime(tmp, (now, now)) os.replace(tmp, path) except OSError as e: logger.debug(f"Écriture de tuile impossible ({path}) : {e}")