Delete useMapPacket.js: nothing imports it since both packets print through the shared HTML renderer
research-map and countries-map moved to the shared printer, leaving the composable with no importer on main. jsPDF stays: useMapPoster.toPdf, posterSizes and my-upg-100-list still reach it. The file map row goes and the library index is regenerated.
This commit is contained in:
parent
6451a2b158
commit
c73d713b43
3 changed files with 0 additions and 303 deletions
|
|
@ -96,7 +96,6 @@ Generated by a per-file scan. The `@map/…` import path mirrors each `library/`
|
|||
| `library/composables/useMapFly.js` | Camera navigation: fly/fitBounds helpers, panel-offset centering, antimeridian-safe legend-row zoom. | zoomToLegendRow reads getSource()._data and re-implements Mapbox filter evaluation in JS — extend _matchesFilter/_resolveExpr for new expression ops; wide spans ease to a world overview instead of fitBounds. |
|
||||
| `library/composables/useMapInstance.js` | One map's lifecycle: shadow-DOM CSS injection, aspect-aware dynamic minZoom floor, edge scroll strips, teardown. Returns map, isMapReady, initializeMap, destroy. | initializeMap() in onMounted with a real DOM element ref (string IDs break in shadow DOM); minZoom is a lower bound raised to a desktop-only no-duplicate-world floor; expects sibling `.map-scroll-edge` strips and global mapboxgl; always call destroy(). |
|
||||
| `library/composables/useMapLayers.js` | Creates/manages all Mapbox layers (pin GeoJSON with family derivation + co-location fan-out, regions fill/border, connection lines, prayer-glow rAF ripple) and theme-aware pin ring widths. | Do NOT add a style.load listener here — the host profile owns post-setStyle reconciliation (flash race). addPrayerGlowLayers is idempotent; keep zoom 'case' inside interpolate OUTPUTS; dark-ring exceptions are config-only, keyed by strategy name. |
|
||||
| `library/composables/useMapPacket.js` | Multi-page "prayer packet" PDF: page 1 = poster map raster, then one jsPDF vector card per people group. Returns renderPacket, progress, isRendering. | Page 1 reuses useMapPoster.render — keep that the single source of truth; jspdf lazy-imported; footer attribution on every page is a ToS requirement; field reads deliberately defensive across 3 object shapes. |
|
||||
| `library/composables/useMapSelection.js` | Applies region/family selection to layer paint props (highlight/fade opacity, connection lines). | Region matching is on iso_3166_1_alpha_3 (tileset has no `region` prop); labels fuzzy-normalized to slugs; selection state lives in the Pinia mapStore. |
|
||||
| `library/composables/useMapTheme.js` | Seamless light/dark toggle by flipping Mapbox Standard's lightPreset/theme config props. Returns bootStyle, applyTheme, swapTheme, isDarkTheme. | Never setStyle to switch themes (reloads map, wipes custom layers); applyTheme must run after style.load and re-asserts once on 'idle' (fast-toggle grey-film race); profiles should watch(uiStore.theme) → applyTheme. |
|
||||
| `library/composables/usePplrInstance.js` | Per-map mediator "instance store" (provide/inject refs) so map, geocoder, legend in one cluster communicate. Exports createPplrInstance, provideInstance, useInstance, selectNode, clearAll. | Not Pinia — plain refs under key 'pplrInstance'; useInstance() returns null when standalone, so consumers must null-guard; profiles bridge instance.selection to their own store. |
|
||||
|
|
|
|||
|
|
@ -163,8 +163,6 @@ and a change here can break other maps.
|
|||
`import … from '@map/composables/useMapInstance.js'`
|
||||
- **useMapLayers** — useMapLayers.js - Map Layers Composable
|
||||
`import … from '@map/composables/useMapLayers.js'`
|
||||
- **useMapPacket** — useMapPacket — render a multi-page "prayer packet" PDF for a map selection.
|
||||
`import … from '@map/composables/useMapPacket.js'`
|
||||
- **useMapProvider** — useMapProvider.js — resolve the active map engine ("provider").
|
||||
`import … from '@map/composables/useMapProvider.js'`
|
||||
- **useMapSelection** — useMapSelection.js - Map Selection Composable
|
||||
|
|
|
|||
|
|
@ -1,300 +0,0 @@
|
|||
/**
|
||||
* useMapPacket — render a multi-page "prayer packet" PDF for a map selection.
|
||||
*
|
||||
* A packet is one cohesive PDF whose:
|
||||
* - Page 1 = the full-bleed map image (reuses useMapPoster.render(spec,'png')
|
||||
* so the map raster, hidden-map spin-up, and Mapbox attribution path are
|
||||
* identical to the poster export — single source of truth).
|
||||
* - Pages 2..N = one clean "detail card" per people group, drawn with jsPDF
|
||||
* vector primitives (setFontSize / text / rect / line) so the cards stay
|
||||
* crisp at any zoom and add almost nothing to the file size.
|
||||
*
|
||||
* People-group field reads are DEFENSIVE: the same object may arrive as a
|
||||
* normalized record from useMapData (top-level `name`, `country`, `population`,
|
||||
* `peoplePraying`, …), as a backward-compat `peopleGroups[]` entry, or as a
|
||||
* GeoJSON-feature `.properties` bag. We probe the known keys in priority order
|
||||
* and fall back to an em-dash so a card never renders blank labels.
|
||||
*
|
||||
* Mapbox / OpenStreetMap attribution is rendered into every page footer —
|
||||
* non-negotiable (Mapbox ToS), mirroring useMapPoster's footer slot.
|
||||
*
|
||||
* @example
|
||||
* const packet = useMapPacket(getMap);
|
||||
* const blob = await packet.renderPacket(spec, peopleGroups, { source: 'Doxa Life' });
|
||||
* download(blob, 'prayer-packet.pdf');
|
||||
*
|
||||
* // Deps (already in package.json): "jspdf": "^4.2.1"
|
||||
*/
|
||||
|
||||
import { ref, readonly } from 'vue';
|
||||
import { useMapPoster } from './poster/useMapPoster.js';
|
||||
|
||||
/**
|
||||
* @param {() => any} getMap function returning the live Mapbox map
|
||||
* @returns {{
|
||||
* renderPacket: (spec: import('./poster/useMapPoster.js').PosterSpec, peopleGroups: Array<Object>, meta?: Object) => Promise<Blob>,
|
||||
* progress: import('vue').Ref<{ phase: string, completed: number, total: number, message: string }>,
|
||||
* isRendering: import('vue').Ref<boolean>
|
||||
* }}
|
||||
*/
|
||||
export function useMapPacket(getMap) {
|
||||
const progress = ref({ phase: 'idle', completed: 0, total: 0, message: '' });
|
||||
const isRendering = ref(false);
|
||||
|
||||
function _setProgress(phase, completed, total, message) {
|
||||
progress.value = { phase, completed, total, message };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the multi-page packet PDF.
|
||||
* @param {import('./poster/useMapPoster.js').PosterSpec} spec same shape useMapPoster uses
|
||||
* @param {Array<Object>} peopleGroups normalized people-group objects
|
||||
* @param {Object} [meta] { source?, date?, attribution? }
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
async function renderPacket(spec, peopleGroups, meta = {}) {
|
||||
if (isRendering.value) throw new Error('useMapPacket: already rendering');
|
||||
|
||||
isRendering.value = true;
|
||||
const groups = Array.isArray(peopleGroups) ? peopleGroups : [];
|
||||
const total = 1 + groups.length; // page 1 = map, then one page per group
|
||||
|
||||
try {
|
||||
_setProgress('map', 0, total, 'rendering map page');
|
||||
|
||||
// Page 1 raster — reuse the poster renderer so the map image, attribution,
|
||||
// and hidden-map machinery are identical to the poster export path.
|
||||
const mapBlob = await useMapPoster(getMap).render(spec, 'png');
|
||||
const mapDataUrl = await _blobToDataUrl(mapBlob);
|
||||
|
||||
_setProgress('pdf-init', 1, total, 'building PDF');
|
||||
|
||||
// Lazy ESM import — keep jspdf out of the main bundle until a packet is
|
||||
// actually requested. Mirrors useMapPoster._toPdfBlob exactly.
|
||||
let jsPDFCtor;
|
||||
try {
|
||||
const mod = await import('jspdf');
|
||||
jsPDFCtor = mod.jsPDF || mod.default;
|
||||
} catch (_) {
|
||||
throw new Error('useMapPacket: jspdf not installed (npm i jspdf)');
|
||||
}
|
||||
|
||||
const doc = new jsPDFCtor({
|
||||
orientation: spec.widthIn > spec.heightIn ? 'l' : 'p',
|
||||
unit: 'in',
|
||||
format: [spec.widthIn, spec.heightIn],
|
||||
});
|
||||
|
||||
const pageW = spec.widthIn;
|
||||
const pageH = spec.heightIn;
|
||||
|
||||
const footer = _footerText(meta);
|
||||
|
||||
// ─── Page 1: full-page map image ──────────────────────────────────────
|
||||
doc.addImage(mapDataUrl, 'PNG', 0, 0, pageW, pageH);
|
||||
_drawFooter(doc, pageW, pageH, footer);
|
||||
|
||||
// ─── Pages 2..N: one detail card per people group ────────────────────
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const pg = groups[i] || {};
|
||||
doc.addPage();
|
||||
_drawDetailCard(doc, pageW, pageH, pg, footer);
|
||||
_setProgress('cards', 2 + i, total, `card ${i + 1}/${groups.length}`);
|
||||
}
|
||||
|
||||
_setProgress('done', total, total, 'packet complete');
|
||||
return doc.output('blob');
|
||||
} finally {
|
||||
isRendering.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
renderPacket,
|
||||
progress: readonly(progress),
|
||||
isRendering: readonly(isRendering),
|
||||
};
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Detail-card drawing (module-private)
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Draw a single people-group detail card onto the current PDF page.
|
||||
* Uses only jsPDF vector primitives so output is crisp and tiny.
|
||||
*/
|
||||
function _drawDetailCard(doc, pageW, pageH, pg, footer) {
|
||||
const margin = Math.min(pageW, pageH) * 0.08;
|
||||
const x = margin;
|
||||
let y = margin;
|
||||
|
||||
// Heading: people-group name
|
||||
const name = _pgName(pg);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(28);
|
||||
doc.setTextColor(20, 20, 20);
|
||||
doc.text(name, x, y + 0.32);
|
||||
y += 0.7;
|
||||
|
||||
// Underline rule beneath the heading
|
||||
doc.setDrawColor(146, 177, 149); // #92b195 — the Doxa accent green
|
||||
doc.setLineWidth(0.02);
|
||||
doc.line(x, y, pageW - margin, y);
|
||||
y += 0.45;
|
||||
|
||||
// Field rows
|
||||
const rows = [
|
||||
['Country', _pgCountry(pg)],
|
||||
['Population', _pgPopulation(pg)],
|
||||
['% Evangelical', _pgPercent(pg, 'evangelical')],
|
||||
['% Christian', _pgPercent(pg, 'christian')],
|
||||
['Prayer count', _pgPrayerCount(pg)],
|
||||
];
|
||||
|
||||
const labelX = x;
|
||||
const valueX = x + 2.4;
|
||||
const rowH = 0.5;
|
||||
|
||||
doc.setFontSize(14);
|
||||
for (const [label, value] of rows) {
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(110, 110, 110);
|
||||
doc.text(String(label), labelX, y);
|
||||
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(40, 40, 40);
|
||||
doc.text(String(value), valueX, y);
|
||||
|
||||
// hairline separator under each row
|
||||
doc.setDrawColor(225, 225, 225);
|
||||
doc.setLineWidth(0.008);
|
||||
doc.line(labelX, y + 0.14, pageW - margin, y + 0.14);
|
||||
|
||||
y += rowH;
|
||||
}
|
||||
|
||||
_drawFooter(doc, pageW, pageH, footer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Footer line drawn on every page: source • date • Mapbox/OSM attribution.
|
||||
*/
|
||||
function _drawFooter(doc, pageW, pageH, footer) {
|
||||
const margin = Math.min(pageW, pageH) * 0.04;
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(120, 120, 120);
|
||||
doc.text(footer, margin, pageH - margin, { baseline: 'bottom' });
|
||||
}
|
||||
|
||||
function _footerText(meta = {}) {
|
||||
const date = meta.date || new Date().toISOString().slice(0, 10);
|
||||
const attribution = meta.attribution || '© Mapbox © OpenStreetMap';
|
||||
const parts = [];
|
||||
if (meta.source) parts.push(String(meta.source));
|
||||
parts.push(String(date));
|
||||
parts.push(attribution);
|
||||
return parts.join(' • ');
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Defensive people-group field reads
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// People-group objects reach us in three shapes (see useMapData.js +
|
||||
// PeopleGroupDetail.vue): the normalized record (top-level keys), the
|
||||
// backward-compat peopleGroups[] entry, and a GeoJSON feature `.properties`
|
||||
// bag. `_props` flattens those so a single probe order works for all three.
|
||||
|
||||
function _props(pg) {
|
||||
// Prefer an explicit GeoJSON `.properties`, fall back to the object itself.
|
||||
return (pg && pg.properties) ? pg.properties : (pg || {});
|
||||
}
|
||||
|
||||
function _firstDefined(...vals) {
|
||||
for (const v of vals) {
|
||||
if (v !== undefined && v !== null && v !== '') return v;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function _pgName(pg) {
|
||||
const p = _props(pg);
|
||||
return _firstDefined(
|
||||
p.name,
|
||||
p.peopleName,
|
||||
p.PeopNameInCountry,
|
||||
p._raw?.PeopNameInCountry,
|
||||
p._raw?.peo_name,
|
||||
pg.name,
|
||||
) ?? '—';
|
||||
}
|
||||
|
||||
function _pgCountry(pg) {
|
||||
const p = _props(pg);
|
||||
return _firstDefined(
|
||||
p.countryName,
|
||||
p.countryIsoLabel,
|
||||
p.country,
|
||||
p.countryIso,
|
||||
p._raw?.Country,
|
||||
pg.country,
|
||||
) ?? '—';
|
||||
}
|
||||
|
||||
function _pgPopulation(pg) {
|
||||
const p = _props(pg);
|
||||
const raw = _firstDefined(
|
||||
p.population,
|
||||
p._raw?.Population,
|
||||
pg.population,
|
||||
);
|
||||
const n = Number(String(raw ?? '').replace(/,/g, ''));
|
||||
if (raw === undefined || Number.isNaN(n)) return '—';
|
||||
return n.toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Percent fields. The pray-tools / UUPG records expose evangelical &
|
||||
* Christian percentages under several historical keys; probe them all and
|
||||
* format as a percentage with one decimal.
|
||||
*/
|
||||
function _pgPercent(pg, kind) {
|
||||
const p = _props(pg);
|
||||
const candidates = kind === 'evangelical'
|
||||
? [p.percentEvangelical, p.pctEvangelical, p.evangelical, p._raw?.PercentEvangelical, p._raw?.percent_evangelical]
|
||||
: [p.percentChristian, p.pctChristian, p.percentAdherents, p.christian, p._raw?.PercentChristianPC, p._raw?.percent_christian, p._raw?.PercentAdherents];
|
||||
const raw = _firstDefined(...candidates);
|
||||
if (raw === undefined) return '—';
|
||||
const n = Number(raw);
|
||||
if (Number.isNaN(n)) return '—';
|
||||
// Some sources store 0..1 fractions, others 0..100; normalize to percent.
|
||||
const pct = n > 0 && n <= 1 ? n * 100 : n;
|
||||
return `${pct.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function _pgPrayerCount(pg) {
|
||||
const p = _props(pg);
|
||||
const raw = _firstDefined(
|
||||
p.peoplePraying,
|
||||
p._raw?.people_praying,
|
||||
pg.peoplePraying,
|
||||
);
|
||||
if (raw === undefined) return '—';
|
||||
const n = Number(raw);
|
||||
if (Number.isNaN(n)) return '—';
|
||||
return n.toLocaleString();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Blob → dataURL
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _blobToDataUrl(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(new Error('useMapPacket: failed to read map blob'));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue