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>
158 lines
4.4 KiB
JavaScript
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);
|
|
}
|
|
}
|