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:
Antoine Jacquin
2026-09-27 13:18:42 +02:00
parent 3d415de86c
commit 6e1b26986c
2 changed files with 101 additions and 3 deletions

View File

@ -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);
}