Country posters: the packet prints through the shared renderer instead of downloading a PDF
The map's packet built a jsPDF file and downloaded packet.pdf while its poster already
printed. It now goes through usePosterPrints.printPacket, the same path as Simple
Research (8457b5dc): the live map rides in as an image data URL, and an explicit
adapter maps the rows, reading religionName and countryName so a card never prints a
religion code ("MSN") under a people group's name.
This commit is contained in:
parent
d8c67ca28a
commit
d8ee9e9787
1 changed files with 72 additions and 34 deletions
|
|
@ -81,11 +81,6 @@ import { useGeocoderSearch } from '@map/composables/useGeocoderSearch.js'
|
|||
// the import until that happens — see `openPoster()` below.
|
||||
let _posterPromise = null
|
||||
|
||||
// PERF: useMapPacket builds a multi-page PDF (map page + one detail card per
|
||||
// people group). It reuses useMapPoster's pipeline, so it's at least as heavy.
|
||||
// EXPERIMENTAL: dormant behind the shared kill-switch — only fires from the
|
||||
// SemanticTreeLegend Export → Packet menu item. Lazy-loaded once via openPacket().
|
||||
let _packetPromise = null
|
||||
|
||||
// ── Geocoder search logic moved to @map/composables/useGeocoderSearch.js ──────
|
||||
// The geocoder-clear guard (_geoBeingCleared + clearGeocoderProgrammatic) and the
|
||||
|
|
@ -1240,52 +1235,95 @@ function onExport(kind) {
|
|||
if (kind === 'packet') return openPacket()
|
||||
}
|
||||
|
||||
// ─── Packet export (multi-page PDF: map + one card per people group) ─────────
|
||||
// PERF: useMapPacket reuses useMapPoster's render pipeline — heavy. Lazy-load
|
||||
// once, mirroring loadPoster()/_posterPromise. Defensive throughout: never
|
||||
// throw uncaught into the UI; guard against double-run.
|
||||
// ─── Packet (printed through the shared renderer, like Simple Research's) ─────
|
||||
// Defensive throughout: never throw uncaught into the UI; guard against double-run.
|
||||
let _packetBusy = false
|
||||
function _downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
}
|
||||
async function openPacket() {
|
||||
if (_packetBusy) return
|
||||
_packetBusy = true
|
||||
try {
|
||||
if (!_packetPromise) {
|
||||
_packetPromise = import('@map/composables/useMapPacket.js')
|
||||
.then(m => m.useMapPacket(() => map.value))
|
||||
}
|
||||
const packet = await _packetPromise
|
||||
// ★ THE PACKET PRINTS, AND IT PRINTS THROUGH THE SHARED RENDERER (Driver-lens audit card 2, and
|
||||
// the Driver's own rule: an export button opens the print dialog, never hands over a file). This
|
||||
// built a jsPDF document and downloaded packet.pdf while its poster sibling, two functions up,
|
||||
// already printed. The fix is NOT to wrap that PDF in a dialog — a PDF blob cannot go in the
|
||||
// <img> the in-place print frame uses, and every workaround (embed, object, the browser's own
|
||||
// viewer) prints on some engines and silently does nothing on others, which is the dead-button
|
||||
// class this file has already paid for twice.
|
||||
//
|
||||
// library/composables/usePosterPrints.js ALREADY has printPacket: the same pages, authored as HTML
|
||||
// with @page rules, printed in place. This map was running a private second implementation of a
|
||||
// shared product feature. Routing to it deletes the duplicate, drops jspdf from this bundle, and
|
||||
// the packet gains the pages the shared renderer has that the local one never did (the life
|
||||
// calendar, the funnel, the prayer wheel — see packet-render.buildPacket's page order).
|
||||
const [{ usePosterPrints }, { defaultPosterSpec }, { useMapPoster }] = await Promise.all([
|
||||
import('@map/composables/usePosterPrints.js'),
|
||||
import('@map/constants/posterDefaults.js'),
|
||||
import('@map/composables/poster/useMapPoster.js'),
|
||||
])
|
||||
|
||||
// Gather the people groups for the current selection. No reliable
|
||||
// descendant filter exists across all tabs, so use the currently
|
||||
// normalized groups. CAP at 50 for overnight safety.
|
||||
// Same 50-group cap as before: an overnight safety limit, not a design choice.
|
||||
let groups = mapData.normalizedPeopleGroups.value || []
|
||||
if (groups.length > 50) {
|
||||
console.warn(`[research-map] packet export truncated ${groups.length} → 50 people groups`)
|
||||
console.warn(`[country-posters] packet export truncated ${groups.length} → 50 people groups`)
|
||||
groups = groups.slice(0, 50)
|
||||
}
|
||||
|
||||
const { defaultPosterSpec } = await import('@map/constants/posterDefaults.js')
|
||||
// ★ AN EXPLICIT ADAPTER, because the two halves name the same fields differently and a mismatch
|
||||
// here is SILENT — the packet would print with blank names and empty photos rather than failing.
|
||||
// The renderer reads slug / name / population / religion / country_code, and usePosterPrints reads
|
||||
// image_url; this map's normalized rows carry country and imageUrl in camelCase. Mapped once, here,
|
||||
// where the difference is visible, rather than renaming either side's vocabulary.
|
||||
const records = groups.map((g) => ({
|
||||
slug: String(g.slug || g.uniqueId || g.id || ''),
|
||||
name: g.name,
|
||||
population: g.population,
|
||||
// ★ THE DISPLAY NAME, NOT THE CODE, and the order is the whole point. The card prints these two
|
||||
// straight into the page — flat(g.religion) and flat(g.country_code) — so they are read by a
|
||||
// person, not matched by a machine. DataSourceManager normalizes `religion` to the CODE and puts
|
||||
// the readable form in `religionName` (it does the same for country: countryIso is the code,
|
||||
// countryName the label). Reading religion first would have printed MSN under a people group's
|
||||
// name, and the field name country_code invites exactly that mistake on the country line.
|
||||
// Checked against the normalizer rather than guessed: library/api/DataSourceManager.js:357-358.
|
||||
religion: g.religionName || g.religionLabel || g.religion || '',
|
||||
country_code: g.countryName || g.countryIsoLabel || g.country || g.countryIso || '',
|
||||
image_url: g.imageUrl || g.image_url || '',
|
||||
}))
|
||||
|
||||
// Page 1 keeps the LIVE map rather than a fetched poster: this half is the one that knows what is
|
||||
// currently on screen. The shared renderer interpolates `mapSvg` as raw markup, so the PNG this map
|
||||
// already knows how to produce rides in as an <img> — a data URL, because the print frame is a
|
||||
// separate document and an object URL created here would be revoked out from under it.
|
||||
const spec = defaultPosterSpec({ widthIn: 8.27, heightIn: 11.69, dpi: 150, orientation: 'portrait' })
|
||||
spec.slots.title = { ...spec.slots.title, text: legendTitle.value || 'Doxa Map' }
|
||||
spec.slots.legend = { ...spec.slots.legend, rows: legend?.rows?.value || [] }
|
||||
let mapSvg = ''
|
||||
try {
|
||||
const mapBlob = await useMapPoster(() => map.value).render(spec, 'png')
|
||||
const dataUrl = await new Promise((resolve, reject) => {
|
||||
const r = new FileReader()
|
||||
r.onload = () => resolve(String(r.result))
|
||||
r.onerror = reject
|
||||
r.readAsDataURL(mapBlob)
|
||||
})
|
||||
mapSvg = `<img src="${dataUrl}" alt="" style="width:100%;height:100%;object-fit:contain">`
|
||||
} catch (e) {
|
||||
// A cover without the map beats no packet — the same call the shared composable documents.
|
||||
console.warn('[country-posters] packet map page skipped:', e?.message || e)
|
||||
}
|
||||
|
||||
const blob = await packet.renderPacket(spec, groups, {
|
||||
source: 'pray.doxa.life',
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
const zone = {
|
||||
id: String(activeTab.value?.id || 'research-map'),
|
||||
label: legendTitle.value || tabLabel(activeTab.value) || 'Doxa Map',
|
||||
zoneKey: String(activeTab.value?.id || 'research-map'),
|
||||
}
|
||||
const prints = usePosterPrints({
|
||||
records: () => records,
|
||||
lang: () => String(uiLocale.value || 'en').split('-')[0],
|
||||
label: () => zone.label,
|
||||
})
|
||||
_downloadBlob(blob, 'packet.pdf')
|
||||
await prints.printPacket(zone, { mapSvg })
|
||||
} catch (err) {
|
||||
console.error('[research-map] packet export failed:', err)
|
||||
console.error('[country-posters] packet export failed:', err)
|
||||
} finally {
|
||||
_packetBusy = false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue