// Export the shop-floor map (current filtered assets on the facility blueprint) // to a PDF, entirely client-side. The blueprint image is drawn full-size and // each visible marker is placed at its scaled coordinate, so the PDF is a crisp // vector-over-raster page rather than a screen capture. import { jsPDF } from 'jspdf' import { resolveMarkerColor, getSubtypeId, getAssetTypeColor } from './mapColors' function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image() img.crossOrigin = 'anonymous' img.onload = () => resolve(img) img.onerror = () => reject(new Error('Failed to load blueprint image: ' + url)) img.src = url }) } function hexToRgb(hex) { const h = (hex || '#BDBDBD').replace('#', '') const v = h.length === 3 ? h.split('').map(c => c + c).join('') : h return [parseInt(v.slice(0, 2), 16), parseInt(v.slice(2, 4), 16), parseInt(v.slice(4, 6), 16)] } // Build the legend entries (color + label) for the assets present, matching the // active coloring mode. function buildLegend(assets, { selectedType, subtypeColors, subtypeNames }) { const seen = new Map() for (const a of assets) { let key, label, color if (selectedType) { const id = getSubtypeId(a) key = id != null ? String(id) : 'none' label = (id != null && subtypeNames[id]) || 'Unspecified' color = (id != null && subtypeColors[id]) || '#BDBDBD' } else { key = (a.assettype || 'unknown').toLowerCase() label = a.assettype || 'Unknown' color = getAssetTypeColor(a.assettype) } if (!seen.has(key)) seen.set(key, { label, color, count: 0 }) seen.get(key).count += 1 } return [...seen.values()].sort((a, b) => b.count - a.count) } export async function exportMapPdf(opts) { const { assets = [], blueprintUrl, mapWidth = 3300, mapHeight = 2550, selectedType = '', subtypeColors = {}, subtypeNames = {}, filters = [], facility = '', title = 'Shop Floor Map' } = opts const img = await loadImage(blueprintUrl) const doc = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4', compress: true }) const pageW = doc.internal.pageSize.getWidth() const pageH = doc.internal.pageSize.getHeight() const margin = 28 // ---- Header ---- doc.setTextColor('#111111') doc.setFont('helvetica', 'bold') doc.setFontSize(16) doc.text(title, margin, margin + 6) doc.setFont('helvetica', 'normal') doc.setFontSize(9) doc.setTextColor('#555555') const stamp = new Date().toLocaleString() const subParts = [facility, stamp, `${assets.length} asset${assets.length === 1 ? '' : 's'}`].filter(Boolean) doc.text(subParts.join(' | '), margin, margin + 22) const filterText = filters.length ? 'Filters: ' + filters.join(' ') : 'Filters: none (all assets)' doc.text(filterText, margin, margin + 35) // ---- Legend (wraps under the header, pushes the map down) ---- const legend = buildLegend(assets, { selectedType, subtypeColors, subtypeNames }) const swatch = 8 const legendY0 = margin + 50 const lineH = 15 let lx = margin let ly = legendY0 doc.setFontSize(8.5) for (const entry of legend) { const label = `${entry.label} (${entry.count})` const w = swatch + 4 + doc.getTextWidth(label) + 16 if (lx + w > pageW - margin) { lx = margin; ly += lineH } const [r, g, b] = hexToRgb(entry.color) doc.setFillColor(r, g, b) doc.setDrawColor('#ffffff'); doc.setLineWidth(0.4) doc.rect(lx, ly - swatch + 1, swatch, swatch, 'F') doc.setTextColor('#333333') doc.text(label, lx + swatch + 4, ly) lx += w } const legendBottom = legend.length ? ly + 6 : legendY0 // ---- Blueprint image, fit into the area below the legend ---- const aspect = mapWidth / mapHeight const contentTop = legendBottom + 8 const availW = pageW - margin * 2 const availH = pageH - contentTop - margin let imgW = availW let imgH = availW / aspect if (imgH > availH) { imgH = availH; imgW = availH * aspect } const imgX = margin + (availW - imgW) / 2 const imgY = contentTop + (availH - imgH) / 2 doc.addImage(img, 'PNG', imgX, imgY, imgW, imgH, undefined, 'FAST') doc.setDrawColor('#cccccc'); doc.setLineWidth(0.5) doc.rect(imgX, imgY, imgW, imgH) // ---- Markers (database Y is top-down, same origin as the drawn image) ---- const radius = 3.2 for (const a of assets) { if (a.mapx == null || a.mapy == null) continue const x = imgX + (a.mapx / mapWidth) * imgW const y = imgY + (a.mapy / mapHeight) * imgH if (x < imgX || x > imgX + imgW || y < imgY || y > imgY + imgH) continue const [r, g, b] = hexToRgb(resolveMarkerColor(a, { selectedType, subtypeColors })) doc.setFillColor(r, g, b) doc.setDrawColor('#ffffff'); doc.setLineWidth(0.6) doc.circle(x, y, radius, 'FD') } const dateSlug = new Date().toISOString().slice(0, 10) doc.save(`shopfloor-map-${dateSlug}.pdf`) }