Raccorder les bords de tuiles voisines et encadrer les rendus en cours

Le MNT de chaque dalle s'étend d'une bande de 100 m remplie avec les
points sol des 8 tuiles voisines (option « Raccord des bords ») : les
rendus à grand noyau (openness, SVF, LRM) deviennent continus d'une
dalle à l'autre, les images restent recadrées sur le kilomètre exact.

Pendant une génération, la carte encadre les dalles du run : orange
pulsant en cours de rendu, rouge en échec — les coins WGS84 sont portés
par /api/status pour toutes les dalles non terminées.

La file de génération trie les dalles du nord au sud et passe à 10
workers GPU.
This commit is contained in:
Antoine Jacquin
2026-09-19 20:44:44 +02:00
parent 204b2ad111
commit 5f018fe2b6
15 changed files with 1075 additions and 247 deletions

View File

@ -1352,8 +1352,8 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<option value="sol,unclassified,eau,virtuel">Sol+non-classé+eau+virtuel (2+1+9+66)</option>
</select>
</label>
<label class="gen-opt" title="Ramène le MNT au retour le plus bas de chaque cellule (sol nu)">
<input type="checkbox" id="genBareEarth"> Sol nu
<label class="gen-opt" title="Complète les bords du MNT avec les points sol des 8 tuiles voisines (bande de 100 m) : les rendus à grand noyau (openness, SVF, LRM) deviennent continus d'une tuile à l'autre. Les images restent recadrées sur la dalle 1 km exacte. Régénère le MNT des tuiles concernées (~1 min de lecture des voisines par tuile en plus).">
<input type="checkbox" id="genEdgeBuffer"> Raccord des bords (tuiles voisines)
</label>
<div class="gen-run">
<button id="genGo" class="primary">Lancer</button>
@ -1903,6 +1903,12 @@ body.exporting, body.exporting #viewport { cursor: pointer; }
}
body.genSel, body.genSel #viewport { cursor: pointer; }
/* Cadres des dalles du run de génération (pane genDraw) : seule la dalle
active pulse ; attente (bleu pointillé) et échec (rouge) restent statiques,
posés en JS par updateRenderFrames(). */
.render-frame.render-running { animation: frame-pulse 1.4s ease-in-out infinite; }
@keyframes frame-pulse { 0%, 100% { stroke-opacity: 1; } 50% { stroke-opacity: 0.3; } }
/* Barres de défilement fines et discrètes */
.tab-page::-webkit-scrollbar,
.gen-task-log::-webkit-scrollbar { width: 8px; }
@ -3019,7 +3025,7 @@ const genClassInfo = document.getElementById('genClassInfo');
const genClass = document.getElementById('genClass');
const genIgnClasses = document.getElementById('genIgnClasses');
const genViz = document.getElementById('genViz');
const genBareEarth = document.getElementById('genBareEarth');
const genEdgeBuffer = document.getElementById('genEdgeBuffer');
const genQueue = document.getElementById('genQueue');
const genBadge = document.getElementById('genBadge');
const genStop = document.getElementById('genStop');
@ -3034,6 +3040,7 @@ let genSelMode = false;
const genSel = new Set(); // clés "col,row" des dalles retenues (clics + zones cumulés)
const genSelCorners = new Map(); // clé "col,row" → coins GPS (dalles sans données)
const genSelPolys = new Map(); // clé "col,row" → polygone Leaflet de surlignage
const renderFrames = new Map(); // clé "col,row" → cadre coloré de la dalle en cours de rendu
const GEN_MAX_CELLS = 400; // garde-fou serveur : ~400 km² max par demande
// Affiche la barre de confirmation de génération, dans l'onglet Génération
@ -3193,6 +3200,46 @@ function genSelCells() {
return [...genSel].map(k => k.split(',').map(Number));
}
// Cadres colorés sur la carte autour des dalles du run en cours : orange
// pulsant pour la dalle en cours de rendu, bleu pointillé pour celles qui
// attendent, rouge pour les échecs. Les coins viennent de /api/status
// (attachés aux dalles non terminées, y compris sans données existantes) ;
// un sondage sans coins n'affiche rien — la file de l'onglet Génération
// reste la référence.
const RENDER_FRAME_STYLES = {
running: { color: '#ffb454', weight: 3, fillOpacity: 0.08,
className: 'render-frame render-running' },
pending: { color: '#5aa9e6', weight: 1.5, dashArray: '6 5', fill: false,
className: 'render-frame render-pending' },
failed: { color: '#e94560', weight: 2, dashArray: '2 4', fillOpacity: 0.08,
className: 'render-frame render-failed' }
};
function updateRenderFrames(tiles) {
const wanted = new Map();
for (const t of (tiles || [])) {
if (t.state === 'done' || !t.corners || t.col == null || t.row == null) continue;
if (!RENDER_FRAME_STYLES[t.state]) continue;
wanted.set(genSelKey(t.col, t.row), t);
}
for (const [k, frame] of renderFrames) {
if (wanted.has(k) && wanted.get(k).state === frame.state) continue;
frame.rect.remove();
renderFrames.delete(k);
}
for (const [k, t] of wanted) {
if (renderFrames.has(k)) continue;
const rect = L.polygon(t.corners, Object.assign(
{ pane: 'genDraw', interactive: false }, RENDER_FRAME_STYLES[t.state])).addTo(map);
renderFrames.set(k, { rect: rect, state: t.state });
}
}
function clearRenderFrames() {
renderFrames.forEach(f => f.rect.remove());
renderFrames.clear();
}
function updateGenSelBar() {
showGenBar(!genSel.size
? 'Mode sélection : clic sur la carte = ajouter / retirer une tuile'
@ -3318,7 +3365,7 @@ genGo.addEventListener('click', () => {
all_missing: pendingCompleteAll,
ground_class: genClass ? genClass.value : 'ign',
ign_classes: genIgnClasses ? genIgnClasses.value : 'sol',
bare_earth: genBareEarth ? genBareEarth.checked : false,
edge_buffer: genEdgeBuffer ? genEdgeBuffer.checked : false,
reclassify: genReclass ? genReclass.checked : false })
}).then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.detail || ('HTTP ' + r.status))))
.then(d => {
@ -3850,6 +3897,7 @@ function pollGenStatus() {
if (d.tiles && d.tiles.length) t.tiles = d.tiles;
}
if (!d.running) {
clearRenderFrames();
finishRunningTasks(d);
if (genTasks.some(t => t.state === 'queued')) {
// Des demandes attendent encore leur tour : poursuivre la veille
@ -3867,6 +3915,7 @@ function pollGenStatus() {
syncThenReload();
} else {
saveGenTasks(); renderGenQueue();
updateRenderFrames(d.tiles);
pollLiveTiles();
}
}).catch(() => {});
@ -3965,6 +4014,7 @@ else fetch('/api/status').then(r => r.ok ? r.json() : null).then(s => {
state: 'running', returncode: null, log: [], logOpen: false, tiles: s.tiles || [],
});
saveGenTasks(); renderGenQueue(); pollGenStatus();
updateRenderFrames(s.tiles);
} else if (s.returncode !== null && s.tiles && s.tiles.length) {
genTasks.push({
id: Date.now(), label: 'Dernière génération',