pwaLUPMIS2/src/pdf-export.js
ekke cfaceb3487 GPS trail recording, SSO auth, account menu, and mobile/UI refinements
Major:
- GPS trail recording: reusable, dependency-free engine in src/geotracker/
  (GeoTracker + geo-utils) with pluggable storage/sync adapters; LUPMIS
  wiring in src/geotracker-lupmis.js. Expandable My Location control
  (Locate Me + Record Trail), live navbar GPS readout, on-map trail/position
  rendering, gps_trails/gps_trail_points SQLocal tables, and store-and-forward
  sync via pushGpsTrail() -> save_gps_trail.php (server side documented, not
  yet built).
- SSO authentication: public/index.php entry point validates the LUSPA SSO
  cookie and injects window.LUPMIS_SESSION; remotedb district_id is now a
  session-resolved getter. Adds public/.htaccess (DirectoryIndex).
- Account menu offcanvas (navbar burger) with sign-in/out states.

UI / fixes:
- LayerSwitcher modernisation; base-map "None" option in picker + settings.
- Mobile drawing toolbar wraps to two rows below 576px and shows only in
  Draw mode; second row right-aligned and clears the Select option bar.
- Safari bottom-dock clipping fixed (app-container 100dvh -> 100svh).
- Rename public/icons -> app-icons to dodge Apache's default /icons/ alias.
- Service Worker bumped to v8 (network-first HTML, per-provider tile clear).

Docs: reusable-mapping and OSM-3D-buildings concept notes; ignore Office
lock files (~$*). Rebuilt dist/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:08:37 +02:00

158 lines
4.4 KiB
JavaScript

/**
* PDF Export Module
*
* Generates branded PDF reports from analysis data.
* Uses jspdf + jspdf-autotable, loaded on demand via dynamic import
* from the calling code so the library is only fetched when needed.
*
* Usage:
* import { exportAnalysisPDF } from '../pdf-export.js';
* await exportAnalysisPDF({ title: 'Circle Analysis', rows: [...] });
*/
import { jsPDF } from 'jspdf';
import { applyPlugin } from 'jspdf-autotable';
applyPlugin(jsPDF);
// Cached logo data URL — fetched once, reused for subsequent exports
let _logoDataUrl = null;
/**
* Load the LUSPA logo, draw it onto a canvas to flatten alpha and
* produce a clean JPEG data URL that jsPDF can embed reliably.
* Caches the result so subsequent calls are instant.
*/
async function getLogoDataUrl() {
if (_logoDataUrl) return _logoDataUrl;
try {
const img = new Image();
img.crossOrigin = 'anonymous';
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = './app-icons/luspa-pdf.jpg';
});
// Draw onto a canvas to get a reliable JPEG data URL
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
// White background to flatten any remaining alpha
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
_logoDataUrl = canvas.toDataURL('image/jpeg', 0.92);
return _logoDataUrl;
} catch (err) {
console.warn('[PDF] Could not load logo:', err);
return null;
}
}
/**
* Generate and open a branded PDF report.
*
* @param {Object} options
* @param {string} options.title - Report title (e.g. "Circle Analysis")
* @param {Array<{label: string, value: string}>} options.rows - Table data
*/
export async function exportAnalysisPDF({ title, rows }) {
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
const pageWidth = doc.internal.pageSize.getWidth();
// Brand colours
const navy = [30, 26, 75]; // #1e1a4b
// ---- Logo ----
const logo = await getLogoDataUrl();
const logoSize = 28;
const marginLeft = 14;
let cursorY = 14;
if (logo) {
doc.addImage(logo, 'JPEG', marginLeft, cursorY, logoSize, logoSize);
}
// ---- Header text (next to logo) ----
const textX = marginLeft + logoSize + 6;
doc.setFont('helvetica', 'bold');
doc.setFontSize(18);
doc.setTextColor(...navy);
doc.text('LUPMIS', textX, cursorY + 11);
doc.setFont('helvetica', 'normal');
doc.setFontSize(12);
doc.text(title, textX, cursorY + 19);
// ---- Date / time ----
const now = new Date();
const dateStr = now.toLocaleDateString(undefined, {
year: 'numeric', month: 'long', day: 'numeric',
});
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
doc.setFontSize(9);
doc.setTextColor(120, 120, 120);
doc.text(`${dateStr} ${timeStr}`, pageWidth - marginLeft, cursorY + 11, { align: 'right' });
// ---- Divider line ----
cursorY += logoSize + 6;
doc.setDrawColor(...navy);
doc.setLineWidth(0.5);
doc.line(marginLeft, cursorY, pageWidth - marginLeft, cursorY);
cursorY += 6;
// ---- Analysis table ----
const tableBody = rows.map(r => [r.label, r.value]);
doc.autoTable({
startY: cursorY,
head: [['Property', 'Value']],
body: tableBody,
margin: { left: marginLeft, right: marginLeft },
styles: {
font: 'helvetica',
fontSize: 10,
cellPadding: 4,
},
headStyles: {
fillColor: navy,
textColor: [255, 255, 255],
fontStyle: 'bold',
},
alternateRowStyles: {
fillColor: [245, 245, 250],
},
columnStyles: {
0: { fontStyle: 'bold', cellWidth: 50 },
},
});
// ---- Footer ----
const finalY = doc.lastAutoTable.finalY + 10;
doc.setFontSize(8);
doc.setTextColor(160, 160, 160);
doc.text('Generated by LUPMIS2 Land Use Planning & Management Information System', marginLeft, finalY);
// ---- Open in browser ----
const blob = doc.output('blob');
const url = URL.createObjectURL(blob);
const win = window.open(url, '_blank');
if (!win) {
// Popup blocked (mobile) — fall back to download
const a = document.createElement('a');
a.href = url;
a.download = `${title.replace(/\s+/g, '_')}_${now.toISOString().slice(0, 10)}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}