Ajouter le bouton d'export PDF, ses réglages et le cadre imprimable à la carte

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 16:21:25 +02:00
parent 7f590ddecf
commit 1f8f13dd13
2 changed files with 138 additions and 0 deletions

View File

@ -121,6 +121,28 @@ _MAP_HTML = """<!DOCTYPE html>
<div class="card-body" id="useBody"></div>
</div>
<div id="printcard" class="card" hidden>
<div class="card-head">
<span class="card-title">Export PDF</span>
<span id="printInfo" class="card-stat"></span>
<button id="printClose" class="icon-btn" title="Fermer">×</button>
</div>
<div class="card-body">
<div class="use-row"><select id="printPaper" title="Format papier">
<option value="A4" selected>A4</option><option value="A3">A3</option></select>
<select id="printOrient" title="Orientation">
<option value="paysage" selected>Paysage</option><option value="portrait">Portrait</option></select>
<select id="printScale" title="Échelle d'impression">
<option value="1000">1:1 000</option><option value="2000" selected>1:2 000</option>
<option value="5000">1:5 000</option><option value="10000">1:10 000</option></select></div>
<div class="use-row"><input id="printTitle" type="text" maxlength="120"
placeholder="Titre (optionnel)" title="Titre imprimé dans le cartouche"></div>
<p class="print-hint">Déplacez la carte sous le cadre jaune : il montre la zone imprimée.</p>
<div class="card-foot"><button id="printGo">Exporter le PDF</button>
<span id="printMsg" class="print-msg"></span></div>
</div>
</div>
<div id="gencard" class="card" hidden>
<div class="card-head">
<span class="card-title">Génération</span>
@ -160,6 +182,7 @@ _MAP_HTML = """<!DOCTYPE html>
<button id="zoomOut" title="Dézoomer">−</button>
<button id="zoomFit" title="Recadrer sur les données">⤢</button>
<button id="gps" title="Centrer sur ma position">⌖</button>
<button id="btnPrint" title="Exporter la zone en PDF (impression terrain)">⎙</button>
</nav>
<div id="toast" class="toast" hidden></div>
<div id="tileload" class="tileload" hidden aria-live="polite">
@ -322,6 +345,11 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius
/* --- génération de tuiles ------------------------------------------------ */
#gencard { right: 12px; top: 56px; width: 340px; max-width: calc(100vw - 24px); }
#printcard { right: 60px; bottom: 12px; width: 340px; max-width: calc(100vw - 84px); }
#printTitle { flex: 1; min-width: 0; background: rgba(255,255,255,0.06); color: var(--text);
border: 1px solid rgba(255,255,255,0.15); border-radius: 6px; padding: 4px 6px; }
.print-hint { margin: 4px 0; font-size: 12px; opacity: 0.75; }
.print-msg { font-size: 12px; margin-left: 8px; }
body.drawing { cursor: crosshair !important; }
body.drawing .leaflet-container { cursor: crosshair !important; }
/* Barre de génération : en HAUT à droite de l'écran, boutons accentués —
@ -363,6 +391,7 @@ button.primary:hover { filter: brightness(1.12); }
#layercard { width: calc(100vw - 24px); }
#tilecard { width: calc(100vw - 24px); }
#gencard { width: calc(100vw - 24px); }
#printcard { width: calc(100vw - 84px); }
}
"""
@ -1258,6 +1287,100 @@ el('genQueueClear').addEventListener('click', () =>
.then(d => toast((d.retirées || 0) + ' demande(s) retirée(s)'))
.catch(msg => toast('Échec : ' + msg.message, 6000)));
// --- export PDF (planche d'impression terrain) ---
// Cadre = zone imprimée, toujours centré sur la vue : on déplace la carte
// dessous. Géométrie calculée par le serveur (projection Lambert 93).
const PRINT_LS = 'lidar-print';
const PRINT = { on: false, rect: null, seq: 0 };
map.createPane('printFrame').style.zIndex = '690';
function printSettings() {
return { paper: el('printPaper').value, orient: el('printOrient').value,
scale: el('printScale').value, title: el('printTitle').value.trim() };
}
function printQuery(s, withTitle) {
const c = map.getCenter();
const q = new URLSearchParams({ lat: c.lat.toFixed(6), lon: c.lng.toFixed(6),
paper: s.paper, orient: s.orient, scale: s.scale });
if (withTitle && s.title) q.set('title', s.title);
return q.toString();
}
function printSave() {
try { localStorage.setItem(PRINT_LS, JSON.stringify(printSettings())); } catch (e) { /* privé */ }
}
function printLoad() {
try {
const s = JSON.parse(localStorage.getItem(PRINT_LS) || '{}');
for (const [k, id] of [['paper', 'printPaper'], ['orient', 'printOrient'],
['scale', 'printScale'], ['title', 'printTitle']]) {
if (s[k] != null) el(id).value = s[k];
}
} catch (e) { /* privé ou illisible : réglages par défaut */ }
}
function printRefreshFrame() {
if (!PRINT.on) return;
const seq = ++PRINT.seq;
fetch('api/export/frame?' + printQuery(printSettings(), false))
.then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)))
.then(d => {
if (seq !== PRINT.seq || !PRINT.on) return;
if (PRINT.rect) PRINT.rect.setLatLngs(d.corners);
else PRINT.rect = L.polygon(d.corners, { pane: 'printFrame', color: '#ffd166', weight: 2,
dashArray: '8 5', fill: false, interactive: false }).addTo(map);
el('printInfo').textContent = d.width_m + ' × ' + d.height_m + ' m';
})
.catch(() => { if (seq === PRINT.seq) el('printInfo').textContent = ''; });
}
function printSetOpen(on) {
PRINT.on = on;
el('printcard').hidden = !on;
el('btnPrint').classList.toggle('accent', on);
if (on) printRefreshFrame();
else if (PRINT.rect) { PRINT.rect.remove(); PRINT.rect = null; }
}
function printExport() {
const btn = el('printGo');
btn.disabled = true;
el('printMsg').textContent = 'Génération du PDF…';
fetch('api/export/pdf?' + printQuery(printSettings(), true))
.then(r => {
if (!r.ok) {
return r.json().catch(() => ({})).then(d => {
throw new Error((d && d.detail) || ('HTTP ' + r.status));
});
}
const m = /filename="([^"]+)"/.exec(r.headers.get('Content-Disposition') || '');
return r.blob().then(b => ({ b: b, name: m ? m[1] : 'relief.pdf' }));
})
.then(({ b, name }) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(b);
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 10000);
el('printMsg').textContent = 'Téléchargé : ' + name;
})
.catch(e => { el('printMsg').textContent = 'Échec : ' + e.message; })
.finally(() => { btn.disabled = false; });
}
el('btnPrint').addEventListener('click', () => printSetOpen(!PRINT.on));
el('printClose').addEventListener('click', () => printSetOpen(false));
el('printGo').addEventListener('click', printExport);
for (const id of ['printPaper', 'printOrient', 'printScale', 'printTitle']) {
el(id).addEventListener('change', () => { printSave(); printRefreshFrame(); });
}
map.on('moveend', printRefreshFrame);
printLoad();
// --- démarrage ------------------------------------------------------------
function fitData() {
if (META.bounds) {