Porter la génération de tuiles dans lidar-maps et retirer l'ancienne webapp
La carte à tuiles XYZ devient la seule interface : l'API de génération (preview, generate, status, stop, file, cell), le job unique + file persistante, la délégation LIDAR_GENERATION_URL et les garde-fous (token, LIDAR_REGEN_CIDR) passent de webapp.py à mapserve.py ; l'interface gagne les boutons + Zone / ⤒ Compléter / régénération à la dalle, les options de run et la progression cadre par cadre. mapserve sert aussi l'inventaire /api/tiles et les dalles en statique versionné : le worker image complète remplace la webapp sur le 8973 du générateur, le Pi n'exécute plus que lidar-maps. index.py se réduit aux registres partagés + vignettes/sous-tuiles/inventaire (l'UI HTML/JS et les mosaïques d'overview partent avec export.py et les compose/scripts de la webapp).
This commit is contained in:
@ -101,7 +101,50 @@ _MAP_HTML = """<!DOCTYPE html>
|
||||
<div class="card-body" id="useBody"></div>
|
||||
</div>
|
||||
|
||||
<div id="gencard" class="card" hidden>
|
||||
<div class="card-head">
|
||||
<span class="card-title">Génération</span>
|
||||
<span id="genBadge" class="card-stat"></span>
|
||||
<button id="genClose" class="icon-btn" title="Fermer">×</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="genMsg" class="meta">Dessinez une zone (+) ou complétez les dalles existantes (⤒).</div>
|
||||
<div id="genOpts">
|
||||
<div class="gen-row"><span class="gen-lbl">Couches</span>
|
||||
<select id="genViz" multiple size="4" title="Laisser vide pour les couches du panneau"></select></div>
|
||||
<div class="gen-row"><span class="gen-lbl">Classification</span>
|
||||
<select id="genClass" title="Méthode de classification du sol">
|
||||
<option value="ign">IGN (pré-classée)</option>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="smrf">SMRF</option>
|
||||
<option value="csf">CSF</option>
|
||||
</select></div>
|
||||
<div class="gen-row"><span class="gen-lbl">Classes IGN</span>
|
||||
<input id="genIgnClasses" class="gen-input" value="sol"
|
||||
title="Classes LAS du MNT IGN : sol(2), unclassified(1), eau(9), virtuel(66), pont(17), sursol(64) — séparées par des virgules"></div>
|
||||
<label class="chk"><input type="checkbox" id="genRegen"> Régénérer les tuiles existantes</label>
|
||||
<label class="chk"><input type="checkbox" id="genReclass"> Reclasser le sol (même méthode)</label>
|
||||
<label class="chk"><input type="checkbox" id="genEdge"> Raccord des bords (bande 100 m)</label>
|
||||
</div>
|
||||
<div id="genClassInfo" class="meta"></div>
|
||||
<div class="card-foot" id="genActions">
|
||||
<button id="genGo" class="primary" title="Télécharge (géoplateforme IGN) puis traite les dalles sélectionnées">Générer</button>
|
||||
<button id="genClear" title="Vider la sélection de dalles">Effacer la sélection</button>
|
||||
</div>
|
||||
<div id="genRun" hidden>
|
||||
<div class="meta" id="genProgress"></div>
|
||||
<pre id="genLog" class="gen-log"></pre>
|
||||
<div class="card-foot">
|
||||
<button id="genStop" title="SIGTERM au pipeline (nettoyage propre), SIGKILL en repli">Arrêter</button>
|
||||
<button id="genQueueClear" title="Retirer les demandes en attente (le run en cours continue)">Vider la file</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav id="tools">
|
||||
<button id="btnZone" title="Générer une nouvelle zone : dessiner un rectangle, téléchargement IGN + traitement" hidden>+</button>
|
||||
<button id="btnComplete" title="Compléter : traiter toutes les dalles déjà téléchargées qui manquent des couches choisies" hidden>⤒</button>
|
||||
<button id="zoomIn" title="Zoomer">+</button>
|
||||
<button id="zoomOut" title="Dézoomer">−</button>
|
||||
<button id="zoomFit" title="Recadrer sur les données">⤢</button>
|
||||
@ -238,9 +281,41 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius
|
||||
.use-note { font-size: 11px; color: var(--muted); line-height: 1.5; }
|
||||
.leaflet-control-attribution { background: rgba(11,14,19,0.8) !important; color: var(--muted) !important; }
|
||||
.leaflet-control-attribution a { color: var(--accent) !important; }
|
||||
|
||||
/* --- génération de tuiles ------------------------------------------------ */
|
||||
#gencard { right: 12px; top: 12px; width: 340px; max-width: calc(100vw - 24px); }
|
||||
body.drawing { cursor: crosshair !important; }
|
||||
body.drawing .leaflet-container { cursor: crosshair !important; }
|
||||
.gen-row { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 6px;
|
||||
align-items: center; margin: 4px 0; font-size: 11.5px; color: var(--muted); }
|
||||
#genViz { background: rgba(255,255,255,0.06); color: var(--text); border: 1px solid var(--border);
|
||||
border-radius: 6px; padding: 3px 4px; font: inherit; font-size: 11.5px; }
|
||||
#genViz option { background-color: #10141b; }
|
||||
#genClass, .gen-input { background: rgba(255,255,255,0.06); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 6px; padding: 4px 6px;
|
||||
font: inherit; font-size: 11.5px; width: 100%; height: 24px; }
|
||||
.chk { display: flex; gap: 7px; align-items: center; font-size: 11.5px;
|
||||
color: var(--muted); margin: 4px 0; cursor: pointer; }
|
||||
.chk input { accent-color: var(--accent); }
|
||||
button.primary { background: var(--accent); border-color: var(--accent); color: #0b0e13; font-weight: 600; }
|
||||
button.primary:hover { filter: brightness(1.12); }
|
||||
.gen-log { font: 10.5px/1.5 ui-monospace, "SF Mono", Menlo, monospace; color: var(--muted);
|
||||
background: rgba(0,0,0,0.35); border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: 6px 8px; max-height: 130px; overflow-y: auto; white-space: pre-wrap;
|
||||
word-break: break-all; margin: 6px 0; }
|
||||
#tools button.accent { color: var(--accent); border-color: var(--accent); }
|
||||
#tools button.busy { color: var(--accent); border-color: var(--accent); }
|
||||
/* Cadres des dalles du run : orange pulsant = rendu en cours, bleu = en
|
||||
attente, rouge = échec. Les dalles faites n'ont pas de cadre. */
|
||||
@keyframes genpulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
||||
.gen-frame-running { stroke: #f4a261; fill: #f4a26122; animation: genpulse 1.4s ease-in-out infinite; }
|
||||
.gen-frame-pending { stroke: #4cc9f0; fill: #4cc9f014; stroke-dasharray: 6 4; }
|
||||
.gen-frame-failed { stroke: #e76f51; fill: #e76f5133; }
|
||||
.gen-sel { stroke: var(--accent); fill: #5aa9e61f; cursor: pointer; }
|
||||
@media (max-width: 720px) {
|
||||
#layercard { width: calc(100vw - 24px); }
|
||||
#tilecard { width: calc(100vw - 24px); }
|
||||
#gencard { width: calc(100vw - 24px); }
|
||||
}
|
||||
"""
|
||||
|
||||
@ -449,15 +524,18 @@ function buildLayers() {
|
||||
// Conteneur isolé : `isolation: isolate` cantonne les mix-blend-mode à la
|
||||
// pile LiDAR. Sans lui, « Produit » assombrirait aussi le fond OSM à travers
|
||||
// les zones sans donnée. La fusion de la pile ENTIÈRE sur le fond reste
|
||||
// réglable à part (STATE.stackBlend).
|
||||
const stack = map.createPane('lidarStack');
|
||||
// réglable à part (STATE.stackBlend). Les panneaux sont réutilisés si déjà
|
||||
// créés (rafraîchissement après un run de génération).
|
||||
const stack = map.getPane('lidarStack') || map.createPane('lidarStack');
|
||||
stack.style.zIndex = 400;
|
||||
stack.style.isolation = 'isolate';
|
||||
META.layers.forEach((info) => {
|
||||
const pane = 'lidar-' + info.key;
|
||||
map.createPane(pane, stack);
|
||||
const paneName = 'lidar-' + info.key;
|
||||
const pane = map.getPane(paneName) || map.createPane(paneName, stack);
|
||||
const existing = LAYERS.get(info.key);
|
||||
if (existing) { if (map.hasLayer(existing)) map.removeLayer(existing); }
|
||||
const layer = L.tileLayer(tileUrl(info.key), {
|
||||
pane: pane,
|
||||
pane: paneName,
|
||||
// Emprise des dalles : Leaflet ne demande aucune tuile en dehors —
|
||||
// pas de requêtes inutiles sur le reste du monde.
|
||||
bounds: META.bounds
|
||||
@ -664,11 +742,17 @@ function renderBaseRow() {
|
||||
}
|
||||
|
||||
// --- fiche dalle ----------------------------------------------------------
|
||||
function showTileInfo(data) {
|
||||
function showTileInfo(data, latlng) {
|
||||
lastTileQuery = latlng || lastTileQuery;
|
||||
const regenBtn = (GEN.available && GEN.allowed)
|
||||
? '<div class="card-foot"><button id="tileRegen" class="link-btn" title="Ajouter cette dalle à la sélection de génération (téléchargement IGN + traitement)">↻ Générer cette dalle</button></div>'
|
||||
: '';
|
||||
const card = el('tilecard');
|
||||
if (!data || !data.found) {
|
||||
el('tileTitle').textContent = 'Aucune dalle ici';
|
||||
el('tileBody').innerHTML = '<div class="meta">Pas de rendu disponible à cet endroit.</div>';
|
||||
el('tileBody').innerHTML = '<div class="meta">Pas de rendu disponible à cet endroit.</div>' + regenBtn;
|
||||
const btn = el('tileRegen');
|
||||
if (btn) btn.addEventListener('click', genAddCellFromQuery);
|
||||
card.hidden = false;
|
||||
return;
|
||||
}
|
||||
@ -682,7 +766,9 @@ function showTileInfo(data) {
|
||||
'<div class="meta">Résolution : <b>' + String(data.resolution).replace('.', ',') + ' m/px</b>' +
|
||||
' · Classification : <b>' + (data.method || 'inconnue') + '</b></div>' +
|
||||
(data.generated ? '<div class="meta">Généré le <b>' + data.generated + '</b></div>' : '') +
|
||||
'<div class="meta">Couches : ' + (viz || '—') + '</div>';
|
||||
'<div class="meta">Couches : ' + (viz || '—') + '</div>' + regenBtn;
|
||||
const btn = el('tileRegen');
|
||||
if (btn) btn.addEventListener('click', genAddCellFromQuery);
|
||||
card.hidden = false;
|
||||
}
|
||||
|
||||
@ -720,6 +806,288 @@ function renderUseCard() {
|
||||
el('usecard').hidden = false;
|
||||
}
|
||||
|
||||
// --- génération de dalles (API /api/status, /api/preview, /api/generate) ---
|
||||
// Portée de la webapp historique : dessin de zone (+), passe « tout
|
||||
// compléter » (⤒), sélection cumulée (clic fiche dalle), options de run,
|
||||
// file d'attente et progression dalle par dalle (cadres sur la carte).
|
||||
const GEN = { available: false, allowed: false, wasRunning: false, timer: null };
|
||||
const genSel = new Set(); // clés "col,row" des dalles retenues
|
||||
const genCorners = new Map(); // clé → coins GPS [[lat, lon] × 4]
|
||||
const genPolys = new Map(); // clé → polygone de surlignage
|
||||
const genFrames = new Map(); // nom → cadre coloré de dalle en cours
|
||||
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]]) {
|
||||
map.createPane(pane).style.zIndex = String(z);
|
||||
}
|
||||
|
||||
function genKey(c, r) { return c + ',' + r; }
|
||||
|
||||
function genApi(method, url, body) {
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: body ? { 'Content-Type': 'application/json' } : {},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}).then(r => {
|
||||
if (!r.ok) {
|
||||
return r.json().catch(() => ({})).then(d => {
|
||||
throw new Error((d && d.detail) || ('HTTP ' + r.status));
|
||||
});
|
||||
}
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function genOpenCard(msg) {
|
||||
if (msg !== undefined) el('genMsg').textContent = msg;
|
||||
el('gencard').hidden = false;
|
||||
}
|
||||
|
||||
function genFillViz() {
|
||||
el('genViz').innerHTML = META.layers.map(l =>
|
||||
'<option value="' + (l.step || l.key) + '">' + l.label + '</option>').join('');
|
||||
}
|
||||
|
||||
function genSelectedViz() {
|
||||
return Array.from(el('genViz').selectedOptions).map(o => o.value);
|
||||
}
|
||||
|
||||
function genRefreshSel() {
|
||||
for (const poly of genPolys.values()) poly.remove();
|
||||
genPolys.clear();
|
||||
for (const [key, corners] of genCorners.entries()) {
|
||||
if (!genSel.has(key) || !corners || corners.length < 4) continue;
|
||||
const poly = L.polygon(corners.map(c => [c[0], c[1]]), {
|
||||
pane: 'genSel', className: 'gen-sel', weight: 2, fillOpacity: 0.12,
|
||||
}).addTo(map);
|
||||
poly.on('click', (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
genRemoveCell(key);
|
||||
toast('Dalle retirée de la sélection');
|
||||
});
|
||||
genPolys.set(key, poly);
|
||||
}
|
||||
el('genGo').disabled = !genSel.size;
|
||||
if (genSel.size) {
|
||||
el('genMsg').textContent = genSel.size + ' dalle(s) sélectionnée(s) — ' +
|
||||
'cliquez un surlignage pour retirer une dalle, réglez les options puis « Générer ».';
|
||||
}
|
||||
}
|
||||
|
||||
function genRemoveCell(key) {
|
||||
genSel.delete(key);
|
||||
genCorners.delete(key);
|
||||
genRefreshSel();
|
||||
}
|
||||
|
||||
function genAddCells(cells) {
|
||||
let added = 0;
|
||||
for (const c of (cells || [])) {
|
||||
const k = genKey(c.col, c.row);
|
||||
if (!genSel.has(k)) { genSel.add(k); added++; }
|
||||
if (c.corners) genCorners.set(k, c.corners);
|
||||
}
|
||||
genRefreshSel();
|
||||
return added;
|
||||
}
|
||||
|
||||
function genAddCellFromQuery() {
|
||||
if (!lastTileQuery) return;
|
||||
genApi('GET', 'api/cell?lat=' + lastTileQuery.lat + '&lng=' + lastTileQuery.lng)
|
||||
.then(d => {
|
||||
genAddCells([{ col: d.col, row: d.row, corners: d.corners }]);
|
||||
genOpenCard('1 dalle sélectionnée — options puis « Générer ».');
|
||||
el('tilecard').hidden = true;
|
||||
})
|
||||
.catch(msg => toast('Échec : ' + msg.message, 6000));
|
||||
}
|
||||
|
||||
function genSetDrawing(on) {
|
||||
genDrawing = on;
|
||||
document.body.classList.toggle('drawing', on);
|
||||
el('btnZone').classList.toggle('accent', on);
|
||||
if (on) map.dragging.disable(); else map.dragging.enable();
|
||||
if (!on && genDrawRect) { genDrawRect.remove(); genDrawRect = null; }
|
||||
genDrawStart = null;
|
||||
if (on) genOpenCard('Dessinez un rectangle sur la carte (re-cliquez + pour annuler).');
|
||||
}
|
||||
|
||||
map.on('mousedown', (e) => {
|
||||
if (!genDrawing) return;
|
||||
genDrawStart = e.latlng;
|
||||
if (genDrawRect) genDrawRect.remove();
|
||||
genDrawRect = L.rectangle([genDrawStart, genDrawStart], {
|
||||
pane: 'genDraw', color: '#4cc9f0', weight: 2, dashArray: '6 4',
|
||||
fillOpacity: 0.05, interactive: false,
|
||||
}).addTo(map);
|
||||
});
|
||||
map.on('mousemove', (e) => {
|
||||
if (!genDrawing || !genDrawStart || !genDrawRect) return;
|
||||
genDrawRect.setBounds(L.latLngBounds(genDrawStart, e.latlng));
|
||||
});
|
||||
map.on('mouseup', (e) => {
|
||||
if (!genDrawing || !genDrawStart) return;
|
||||
const b = L.latLngBounds(genDrawStart, e.latlng);
|
||||
genDrawStart = null;
|
||||
genIgnoreClickUntil = Date.now() + 400; // pas de fiche dalle après un dessin
|
||||
if (Math.abs(b.getWest() - b.getEast()) < 1e-9 ||
|
||||
Math.abs(b.getNorth() - b.getSouth()) < 1e-9) return;
|
||||
genSetDrawing(false);
|
||||
if (genDrawRect) { genDrawRect.remove(); genDrawRect = null; }
|
||||
genOpenCard('Analyse de la zone…');
|
||||
genApi('POST', 'api/preview', {
|
||||
bbox: [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()],
|
||||
regenerate: el('genRegen').checked,
|
||||
viz: genSelectedViz(),
|
||||
}).then(d => {
|
||||
const added = genAddCells(d.cells);
|
||||
genOpenCard((added ? added + ' nouvelle(s) dalle(s) retenue(s)'
|
||||
: 'zone déjà couverte — cochez « Régénérer » pour forcer') +
|
||||
(d.capped ? ' — zone plafonnée, resserrez le rectangle' : '') + '.');
|
||||
}).catch(msg => genOpenCard('Erreur : ' + msg.message));
|
||||
});
|
||||
|
||||
function genComplete() {
|
||||
genOpenCard('Analyse des dalles déjà téléchargées…');
|
||||
genApi('POST', 'api/preview', {
|
||||
all_missing: true,
|
||||
regenerate: el('genRegen').checked,
|
||||
viz: genSelectedViz(),
|
||||
}).then(d => {
|
||||
const added = genAddCells(d.cells);
|
||||
genOpenCard((added || d.count || 0) + ' dalle(s) à compléter' +
|
||||
(d.capped ? ' — plafonnées à 10 000 par run' : '') + '.');
|
||||
if (genPolys.size) {
|
||||
const bounds = L.latLngBounds([]);
|
||||
genPolys.forEach(p => bounds.extend(p.getBounds()));
|
||||
map.fitBounds(bounds, { padding: [40, 40] });
|
||||
}
|
||||
}).catch(msg => genOpenCard('Erreur : ' + msg.message));
|
||||
}
|
||||
|
||||
function genLaunch() {
|
||||
const tiles = [...genSel].map(k => k.split(',').map(Number));
|
||||
if (!tiles.length) return;
|
||||
genApi('POST', 'api/generate', {
|
||||
tiles: tiles,
|
||||
regenerate: el('genRegen').checked,
|
||||
reclassify: el('genReclass').checked,
|
||||
ground_class: el('genClass').value,
|
||||
ign_classes: el('genIgnClasses').value.trim() || 'sol',
|
||||
edge_buffer: el('genEdge').checked,
|
||||
viz: genSelectedViz(),
|
||||
}).then(d => {
|
||||
genSel.clear();
|
||||
genCorners.clear();
|
||||
genRefreshSel();
|
||||
GEN.wasRunning = true;
|
||||
genPollNow();
|
||||
toast(d.demarré ? ('Génération lancée : ' + d.tuiles + ' dalle(s)')
|
||||
: ('En file d\'attente (' + d.en_file + ') : ' + d.tuiles + ' dalle(s)'));
|
||||
}).catch(msg => toast('Échec : ' + msg.message, 6000));
|
||||
}
|
||||
|
||||
// Cadres des dalles du run (orange pulsant = rendu en cours, bleu = en
|
||||
// attente, rouge = échec) — redraw complet à chaque sondage, c'est peu.
|
||||
function genDrawFrames(tiles) {
|
||||
for (const poly of genFrames.values()) poly.remove();
|
||||
genFrames.clear();
|
||||
for (const t of (tiles || [])) {
|
||||
if (t.state === 'done' || !t.corners || t.corners.length < 4) continue;
|
||||
const cls = t.state === 'running' ? 'gen-frame-running'
|
||||
: t.state === 'failed' ? 'gen-frame-failed' : 'gen-frame-pending';
|
||||
const poly = L.polygon(t.corners.map(c => [c[0], c[1]]), {
|
||||
pane: 'genFrames', className: cls, weight: 2, fillOpacity: 0.15,
|
||||
interactive: false,
|
||||
}).addTo(map);
|
||||
genFrames.set(t.name || JSON.stringify(t.corners[0]), poly);
|
||||
}
|
||||
}
|
||||
|
||||
// Fin de run : la carte se rafraîchit (nouvelles couches/dalles, stamp de
|
||||
// tuile neuf) et la maintenance de pyramide est relancée côté serveur.
|
||||
function genAfterRun() {
|
||||
fetch('api/map/meta').then(r => r.json()).then(meta => {
|
||||
META = meta;
|
||||
buildLayers();
|
||||
renderPanel();
|
||||
renderBaseRow();
|
||||
applyLayers();
|
||||
}).catch(() => {});
|
||||
fetch('api/map/background', { method: 'POST' }).catch(() => {});
|
||||
}
|
||||
|
||||
function genUpdateStatus(d) {
|
||||
const queued = (d.queue || []).length;
|
||||
if (d.running || queued) {
|
||||
el('genActions').hidden = true;
|
||||
el('genOpts').hidden = true;
|
||||
el('genRun').hidden = false;
|
||||
const tiles = d.tiles || [];
|
||||
const done = tiles.filter(t => t.state === 'done').length;
|
||||
const failed = tiles.filter(t => t.state === 'failed').length;
|
||||
let txt = d.running ? ('Run en cours — ' + done + '/' + tiles.length + ' dalle(s)')
|
||||
: 'Démarrage du run suivant…';
|
||||
if (failed) txt += ' • ' + failed + ' échec(s)';
|
||||
if (queued) txt += ' • ' + queued + ' demande(s) en file';
|
||||
el('genProgress').textContent = txt;
|
||||
el('genLog').textContent = (d.log || []).slice(-12).join('\n');
|
||||
genDrawFrames(tiles);
|
||||
GEN.wasRunning = true;
|
||||
} else {
|
||||
el('genRun').hidden = true;
|
||||
el('genActions').hidden = !GEN.available;
|
||||
el('genOpts').hidden = false;
|
||||
genDrawFrames(null);
|
||||
if (GEN.wasRunning) {
|
||||
GEN.wasRunning = false;
|
||||
toast(d.returncode === 0 ? 'Génération terminée'
|
||||
: 'Génération interrompue (code ' + d.returncode + ')', 6000);
|
||||
genAfterRun();
|
||||
}
|
||||
}
|
||||
el('genBadge').textContent = d.running ? '● en cours'
|
||||
: queued ? ('⏱ ' + queued) : '';
|
||||
el('btnZone').classList.toggle('busy', !!d.running || !!queued);
|
||||
}
|
||||
|
||||
function genPollNow() {
|
||||
if (!GEN.available) return;
|
||||
fetch('api/status').then(r => r.ok ? r.json() : null).then(d => {
|
||||
if (d) genUpdateStatus(d);
|
||||
genPollSoon();
|
||||
}).catch(() => genPollSoon());
|
||||
}
|
||||
|
||||
function genPollSoon() {
|
||||
clearTimeout(GEN.timer);
|
||||
GEN.timer = setTimeout(genPollNow, GEN.wasRunning ? 3000 : 20000);
|
||||
}
|
||||
|
||||
el('btnZone').addEventListener('click', () => genSetDrawing(!genDrawing));
|
||||
el('btnComplete').addEventListener('click', genComplete);
|
||||
el('genClose').addEventListener('click', () => {
|
||||
el('gencard').hidden = true;
|
||||
if (genDrawing) genSetDrawing(false);
|
||||
});
|
||||
el('genClear').addEventListener('click', () => {
|
||||
genSel.clear();
|
||||
genCorners.clear();
|
||||
genRefreshSel();
|
||||
genOpenCard('Sélection effacée. Dessinez une zone (+) ou complétez (⤒).');
|
||||
});
|
||||
el('genGo').addEventListener('click', genLaunch);
|
||||
el('genStop').addEventListener('click', () =>
|
||||
genApi('POST', 'api/stop')
|
||||
.then(() => toast('Arrêt demandé'))
|
||||
.catch(msg => toast('Échec : ' + msg.message, 6000)));
|
||||
el('genQueueClear').addEventListener('click', () =>
|
||||
genApi('POST', 'api/queue/clear')
|
||||
.then(d => toast((d.retirées || 0) + ' demande(s) retirée(s)'))
|
||||
.catch(msg => toast('Échec : ' + msg.message, 6000)));
|
||||
|
||||
// --- démarrage ------------------------------------------------------------
|
||||
function fitData() {
|
||||
if (META.bounds) {
|
||||
@ -746,6 +1114,19 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
|
||||
buildLayers();
|
||||
renderPanel();
|
||||
renderBaseRow();
|
||||
genFillViz();
|
||||
// Droits de génération : boutons affichés si le serveur peut générer
|
||||
// (pipeline local ou LIDAR_GENERATION_URL) ET que le navigateur vient du
|
||||
// réseau autorisé (LIDAR_REGEN_CIDR) — sinon masqués silencieusement.
|
||||
fetch('api/status').then(r => r.ok ? r.json() : null).then(d => {
|
||||
if (!d) return;
|
||||
GEN.available = !!d.available;
|
||||
GEN.allowed = d.regen_allowed !== false;
|
||||
const show = GEN.available && GEN.allowed;
|
||||
el('btnZone').hidden = !show;
|
||||
el('btnComplete').hidden = !show;
|
||||
if (show) { genUpdateStatus(d); genPollNow(); }
|
||||
}).catch(() => {});
|
||||
}).catch(() => toast('Serveur de tuiles injoignable.', 10000));
|
||||
|
||||
let applyingHash = false;
|
||||
@ -766,8 +1147,10 @@ window.addEventListener('hashchange', () => {
|
||||
|
||||
map.on('moveend zoomend', saveHashSoon);
|
||||
map.on('click', (e) => {
|
||||
if (genDrawing || Date.now() < genIgnoreClickUntil) return;
|
||||
fetch('api/map/tile?lat=' + e.latlng.lat + '&lng=' + e.latlng.lng)
|
||||
.then(r => r.ok ? r.json() : null).then(showTileInfo).catch(() => {});
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => showTileInfo(d, e.latlng)).catch(() => {});
|
||||
});
|
||||
|
||||
el('cardToggle').addEventListener('click', () =>
|
||||
@ -851,6 +1234,9 @@ el('gps').addEventListener('click', () => {
|
||||
}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 });
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') { el('tilecard').hidden = true; el('usecard').hidden = true; }
|
||||
if (e.key === 'Escape') {
|
||||
el('tilecard').hidden = true; el('usecard').hidden = true;
|
||||
if (genDrawing) genSetDrawing(false);
|
||||
}
|
||||
});
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user