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:
@ -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) {
|
||||
|
||||
@ -1321,3 +1321,18 @@ def test_export_pdf_never_delegated(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(mapserve, "GENERATION_URL", "http://injoignable.invalid")
|
||||
lat, lon = _center()
|
||||
assert mapserve.export_pdf_file(lat, lon, "A4", "paysage", 2000, None).body.startswith(b"%PDF")
|
||||
|
||||
|
||||
def test_map_ui_has_pdf_export():
|
||||
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
|
||||
for id_ in ("btnPrint", "printcard", "printPaper", "printOrient", "printScale",
|
||||
"printTitle", "printGo", "printMsg", "printClose", "printInfo"):
|
||||
assert f'id="{id_}"' in _MAP_HTML, id_
|
||||
for scale in ("1000", "2000", "5000", "10000"):
|
||||
assert f'value="{scale}"' in _MAP_HTML
|
||||
assert "api/export/frame?" in _MAP_JS and "api/export/pdf?" in _MAP_JS
|
||||
assert "createPane('printFrame')" in _MAP_JS
|
||||
assert "map.on('moveend', printRefreshFrame)" in _MAP_JS
|
||||
assert "#printcard" in _MAP_CSS
|
||||
# bouton toujours visible : indépendant de /api/status.available
|
||||
assert 'id="btnPrint"' in _MAP_HTML and 'id="btnPrint" hidden' not in _MAP_HTML
|
||||
|
||||
Reference in New Issue
Block a user