Afficher le contour de dalle au-dessus des tuiles et ajouter un compteur de tuiles restantes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -147,6 +147,11 @@ _MAP_HTML = """<!DOCTYPE html>
|
||||
<button id="gps" title="Centrer sur ma position">⌖</button>
|
||||
</nav>
|
||||
<div id="toast" class="toast" hidden></div>
|
||||
<div id="tileload" class="tileload" hidden aria-live="polite">
|
||||
<span class="tl-spin"></span>
|
||||
<span id="tileloadText"></span>
|
||||
<span class="tl-bar"><span id="tileloadBar"></span></span>
|
||||
</div>
|
||||
|
||||
<script src="assets/vendor/leaflet/leaflet.js"></script>
|
||||
<script src="assets/app.js?v=__ASSETS_V__"></script>
|
||||
@ -280,6 +285,23 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius
|
||||
#tools button { width: 38px; height: 38px; padding: 0; font-size: 17px; background: var(--card);
|
||||
border: 1px solid var(--border); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }
|
||||
#gps.busy { color: var(--accent); border-color: var(--accent); }
|
||||
/* Indicateur de chargement : tuiles restantes pour finir la vue courante
|
||||
(fond + LiDAR), barre de progression du lot en cours. */
|
||||
.tileload { position: fixed; left: 50%; top: 12px; transform: translateX(-50%); z-index: 905;
|
||||
display: flex; align-items: center; gap: 8px; padding: 5px 12px; border-radius: 999px;
|
||||
background: var(--card); border: 1px solid var(--border); font-size: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); pointer-events: none;
|
||||
transition: opacity 0.3s ease; white-space: nowrap; }
|
||||
.tileload.done { opacity: 0; }
|
||||
.tl-spin { width: 12px; height: 12px; border-radius: 50%; flex: none;
|
||||
border: 2px solid rgba(90, 169, 230, 0.25); border-top-color: var(--accent);
|
||||
animation: tlspin 0.8s linear infinite; }
|
||||
.tileload.done .tl-spin { animation: none; border-color: var(--accent); }
|
||||
@keyframes tlspin { to { transform: rotate(360deg); } }
|
||||
.tl-bar { width: 60px; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden; flex: none; }
|
||||
.tl-bar span { display: block; height: 100%; width: 0; background: var(--accent);
|
||||
transition: width 0.2s ease; }
|
||||
.toast { position: fixed; left: 50%; bottom: 76px; transform: translateX(-50%); z-index: 1000;
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 7px 12px; font-size: 12px; color: var(--text); max-width: 80vw; text-align: center; }
|
||||
@ -412,6 +434,62 @@ const ZoomAnimTileLayer = L.TileLayer.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// --- chargement des tuiles -------------------------------------------------
|
||||
// Compteur recalculé depuis l'état réel des tuiles de chaque couche (tuiles
|
||||
// du niveau courant pas encore chargées) plutôt que par +1/−1 sur les
|
||||
// événements : une tuile retirée avant la fin de son chargement (déplacement
|
||||
// rapide) ne déclenche ni « tileload » ni « tileerror », un compteur par
|
||||
// événements dériverait. Lot : de la première tuile en attente au retour à
|
||||
// zéro ; la barre donne la part déjà chargée du lot.
|
||||
const TILE_LOAD = { layers: new Set(), batch: 0, done: 0, raf: 0, hideT: 0 };
|
||||
|
||||
function tilesPending() {
|
||||
let n = 0;
|
||||
for (const layer of TILE_LOAD.layers) {
|
||||
if (!layer._map) continue;
|
||||
for (const key in layer._tiles) {
|
||||
const t = layer._tiles[key];
|
||||
if (t.current && !t.loaded) n += 1;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function updateTileLoad() {
|
||||
TILE_LOAD.raf = 0;
|
||||
const box = el('tileload');
|
||||
if (!box) return;
|
||||
const pending = tilesPending();
|
||||
if (pending > 0) {
|
||||
clearTimeout(TILE_LOAD.hideT);
|
||||
TILE_LOAD.hideT = 0;
|
||||
if (TILE_LOAD.batch === 0) TILE_LOAD.done = 0;
|
||||
TILE_LOAD.batch = Math.max(TILE_LOAD.batch, TILE_LOAD.done + pending);
|
||||
el('tileloadText').textContent = pending + (pending > 1 ? ' tuiles restantes' : ' tuile restante');
|
||||
el('tileloadBar').style.width = Math.round(100 * TILE_LOAD.done / TILE_LOAD.batch) + '%';
|
||||
box.classList.remove('done');
|
||||
box.hidden = false;
|
||||
} else if (TILE_LOAD.batch > 0 && !TILE_LOAD.hideT) {
|
||||
TILE_LOAD.batch = 0;
|
||||
el('tileloadText').textContent = 'Vue chargée';
|
||||
el('tileloadBar').style.width = '100%';
|
||||
box.classList.add('done');
|
||||
TILE_LOAD.hideT = setTimeout(() => { box.hidden = true; TILE_LOAD.hideT = 0; }, 600);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleTileLoad() {
|
||||
if (!TILE_LOAD.raf) TILE_LOAD.raf = requestAnimationFrame(updateTileLoad);
|
||||
}
|
||||
|
||||
ZoomAnimTileLayer.addInitHook(function () {
|
||||
TILE_LOAD.layers.add(this);
|
||||
const onDone = () => { TILE_LOAD.done += 1; scheduleTileLoad(); };
|
||||
this.on('tileload', onDone);
|
||||
this.on('tileerror', onDone);
|
||||
this.on('tileloadstart tileunload load add remove', scheduleTileLoad);
|
||||
});
|
||||
|
||||
const base = new ZoomAnimTileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19, maxNativeZoom: 19, keepBuffer: 2, className: 'base-dark',
|
||||
// Sinon _setView recrée un niveau en cours d'animation malgré noUpdate.
|
||||
@ -833,8 +911,11 @@ function kv(rows) {
|
||||
function selectTileOutline(data) {
|
||||
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
||||
if (!data || !data.corners) return;
|
||||
tileSel = L.polygon(data.corners, { color: '#ffd24a', weight: 2, fill: false,
|
||||
dashArray: '6 4', interactive: false }).addTo(map);
|
||||
// Panneau dédié au-dessus de la pile LiDAR : dans l'overlayPane par défaut
|
||||
// (z-index 400, comme lidarStack), le contour passait sous les tuiles.
|
||||
tileSel = L.polygon(data.corners, { pane: 'tileSel', color: '#ffd24a', weight: 2,
|
||||
fill: false, dashArray: '6 4',
|
||||
interactive: false }).addTo(map);
|
||||
}
|
||||
|
||||
function ignSection(ign) {
|
||||
@ -1033,7 +1114,7 @@ let genDrawing = false, genDrawStart = null, genDrawRect = null;
|
||||
let genIgnoreClickUntil = 0;
|
||||
let lastTileQuery = null; // latlng du dernier clic (fiche dalle)
|
||||
|
||||
for (const [pane, z] of [['genSel', 650], ['genFrames', 660], ['genDraw', 670]]) {
|
||||
for (const [pane, z] of [['genSel', 650], ['genFrames', 660], ['genDraw', 670], ['tileSel', 680]]) {
|
||||
map.createPane(pane).style.zIndex = String(z);
|
||||
}
|
||||
|
||||
|
||||
@ -283,6 +283,23 @@ def test_ui_offers_reorder_and_blend_modes():
|
||||
assert "const b = validBlend(f[3]);" in _MAP_JS
|
||||
|
||||
|
||||
def test_map_ui_tile_load_indicator_and_selection_pane():
|
||||
"""Chargement : compteur de tuiles restantes + contour de dalle visible.
|
||||
|
||||
Le compteur lit l'état réel des tuiles (une tuile retirée en cours de
|
||||
chargement n'émet aucun événement de fin). Le contour de sélection vit
|
||||
dans un panneau au-dessus de la pile LiDAR (z-index 400), sinon les
|
||||
tuiles le masquent.
|
||||
"""
|
||||
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
||||
assert 'id="tileload"' in _MAP_HTML and 'id="tileloadText"' in _MAP_HTML
|
||||
assert "@keyframes tlspin" in _MAP_CSS
|
||||
assert "ZoomAnimTileLayer.addInitHook" in _MAP_JS
|
||||
assert "t.current && !t.loaded" in _MAP_JS
|
||||
assert "['tileSel', 680]" in _MAP_JS
|
||||
assert "pane: 'tileSel'" in _MAP_JS
|
||||
|
||||
|
||||
def test_defaults_roundtrip(tmp_path, monkeypatch):
|
||||
"""L'état réglé depuis la carte devient la configuration servie à tous."""
|
||||
import lidar_pipeline.mapserve as mapserve
|
||||
|
||||
Reference in New Issue
Block a user