From 839b0c453dc1bd63681e7119b688851f43d16c9f Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:57:31 +0000 Subject: [PATCH] A letter-portrait sheet, an honest credit, and a privacy line FORMAT=letter. The same poster on paper a home printer takes. Width stays 1500 for the reason the 3x4 note already gives -- every width-derived measurement (the 330 gutter, the name columns, the header budget) was calibrated there and carries over untouched. Only height moves: 8.5x11 at width 1500 is 1941. Layout, text and spacing are unchanged; the extra height goes to the label columns, which is where 95 names wanted it. PRINTS AS ONE PAGE -- and this fixes the SQUARE sheet too. The stage is sized in CSS pixels while @page declares a physical size, so a browser mapped 1500px to 15.6in and TILED it: measured, the 3x3 sheet printed as FOUR pages. It went unnoticed because these posters were always screenshotted by shoot.mjs, never printed from a browser. A scale computed at generate time (page width in px over stage width) makes one sheet equal one page on every format. Verified via page.pdf: letter 612x792pt x1 page, square 216x216pt x1 page (was 4). ATTRIBUTION CORRECTED. The credit said "(c) Mapbox (c) OpenStreetMap". This file's own header says NO Mapbox, NO token, NO external fetch at render time, and the basemap is bundled GeoJSON -- so it credited a vendor contributing nothing, which is the kind of line that makes a later reader add a token "to comply". The real source is geoBoundaries (the adm0/adm1 files carry its shapeGroup/shapeName fields), CC BY 4.0, which REQUIRES attribution. OSM is named only when OSM tiles are actually fetched (BASEMAP=1). PRIVACY LINE on the sheet. People-group coordinates arrive already generalised from the publisher -- deliberately not precise locations. A printed sheet travels past the person who made it, so it now carries that fact itself. Worded as provenance, not as a claim about processing we do not perform. It gets its own row: sharing the credit row was a measured collision (516-984px against a stamp ending at 543), and it is a sentence to READ, not a provenance stamp. Also: a backtick inside a CSS comment inside a JS template literal broke generation outright -- the trap check:template-literal exists for. --- library/scripts/posters/generate.mjs | 70 ++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/library/scripts/posters/generate.mjs b/library/scripts/posters/generate.mjs index ed57ec9a7..f655ad341 100644 --- a/library/scripts/posters/generate.mjs +++ b/library/scripts/posters/generate.mjs @@ -255,7 +255,7 @@ const FORMATS = { // and those already derive from MAP_BREATH/HEADER rather than hardcoded offsets. // Serves China (154 groups) and is the sheet the shelved granular 4-column table // (F93) was waiting on — build it once, both are served. - '3x4': { w: 1500, h: 2000, gutter: 330, page: '3in 4in' }, + '3x4': { w: 1500, h: 2000, gutter: 330, page: '3in 4in', pageWidthIn: 3 }, // Square: a 3ft x 3ft sheet. The map loses height relative to 3x4, so the // gutters widen slightly — with fewer label rows per column the names can take // more width, and the map stays close to square rather than becoming a letterbox. @@ -273,7 +273,23 @@ const FORMATS = { // be line wrapped — like 1 and 6 and 34, 49, 44." At 330 the name column is // 222px and only "Vellaiyan Kuppam Padaiyaci" (265px) still wraps, which is a // name that genuinely overruns any sane column. - '3x3': { w: 1500, h: 1500, gutter: 330, page: '3in 3in' }, + '3x3': { w: 1500, h: 1500, gutter: 330, page: '3in 3in', pageWidthIn: 3 }, + // LETTER PORTRAIT — the same sheet, sized for paper a normal printer can take. + // Brief: "keep the layout of the poster the same with the same text, just + // formatted for a vertical piece of paper." + // + // WIDTH STAYS 1500 for the reason the 3x4 note gives above: every width-derived + // measurement (the 330 gutter, the name columns, the header budget) was calibrated + // at 1500 and carries over UNCHANGED. Height is the only variable: 8.5x11 at + // width 1500 -> 1500 * (11/8.5) = 1941. That lands between 3x3 (1500) and 3x4 + // (2000), so the map keeps roughly the 3x3 proportions with ~29% more height for + // the label columns — the sheet gets taller, nothing gets re-tuned. + // + // `page` is what the generated HTML puts in @page{size:...}. Naming the real + // paper size (rather than an inch pair) lets the browser's own Letter preset + // match it, which is what makes an ordinary printer produce a full-bleed-free, + // correctly-scaled sheet instead of "fit to page" guesswork. + letter: { w: 1500, h: 1941, gutter: 330, page: 'letter portrait', pageWidthIn: 8.5 }, }; const FMT = FORMATS[process.env.FORMAT || '3x4'] || FORMATS['3x4']; // Stamped at generation time. Overridable so a rebuild can reproduce an existing @@ -2055,9 +2071,34 @@ async function renderPoster(zone, pins, zoneFC) { const credit = (x, y, anchor, txt) => `${txt}` + `${txt}`; + // ATTRIBUTION CORRECTED 2026-09-23. This said "(c) Mapbox (c) OpenStreetMap", + // which was inherited boilerplate and simply not true of this sheet: the header + // of this file states NO Mapbox, NO token, NO external fetch at render time, and + // the basemap is drawn from GeoJSON bundled in the repo. Crediting a vendor that + // contributes nothing implies a dependency that does not exist \u2014 the kind of line + // that makes a later reader add a token "to comply". + // The real source is geoBoundaries (the adm0/adm1 files carry its shapeGroup / + // shapeName fields), CC BY 4.0, which REQUIRES attribution \u2014 so the credit stays, + // it just names who actually earned it. OpenStreetMap is named only when OSM + // street tiles are actually fetched (BASEMAP=1, city sheets). + const basemapCredit = BASEMAP + ? '\u00a9 geoBoundaries \u00b7 \u00a9 OpenStreetMap' + : '\u00a9 geoBoundaries (CC BY 4.0)'; + // \u2605 PRIVACY LINE. The people-group coordinates arrive already generalised from + // the data publisher \u2014 they are deliberately NOT precise locations. A printed + // sheet travels far beyond the person who generated it, so it has to carry that + // fact itself rather than rely on the reader knowing it. + // Worded as provenance ("generalised at the source"), not as a claim about + // processing we perform here, because we do no rounding of our own. + const privacyNote = 'Marker positions are approximate \u00b7 people-group locations are generalised at the source for privacy'; + // The privacy line gets its OWN row, 13px above the credits. Sharing the credit + // row put it at 516-984px against a stamp ending at 543 \u2014 a measured collision. + // It is also a different KIND of text: the other two are ~200px provenance + // stamps, this is a sentence a reader must actually READ, so it needs the room. const mapCredits = - credit(FRAME_X + 8, FRAME_Y + FRAME_H - 7, 'start', `Generated ${POSTER_DATE} \u00b7 data from doxa.life`) - + credit(FRAME_X + FRAME_W - 8, FRAME_Y + FRAME_H - 7, 'end', '\u00a9 Mapbox \u00a9 OpenStreetMap'); + credit(FRAME_X + FRAME_W / 2, FRAME_Y + FRAME_H - 20, 'middle', privacyNote) + + credit(FRAME_X + 8, FRAME_Y + FRAME_H - 7, 'start', `Generated ${POSTER_DATE} \u00b7 data from doxa.life`) + + credit(FRAME_X + FRAME_W - 8, FRAME_Y + FRAME_H - 7, 'end', basemapCredit); // FIT THE TITLE TO THE SHEET. Region names vary a lot in length ("West India" // vs "Northeast India + Bangladesh + Bhutan"), and the qualifier adds a fixed @@ -2443,7 +2484,26 @@ async function renderPoster(zone, pins, zoneFC) { /* Haloed so the credits stay legible wherever they land on the map. */ .attrib-halo{font-family:'Segoe UI',system-ui,sans-serif;font-size:9px;fill:none; stroke:#EEF3F4;stroke-width:2.6;stroke-linejoin:round;letter-spacing:.02em} - @media print{@page{size:${FMT.page};margin:0}html,body{background:#fff}.stage{margin:0}} + /* PRINT. The stage is sized in CSS PIXELS (1500 wide) while @page declares a + PHYSICAL size, so a browser maps 1500px -> 96dpi -> 15.6in and TILES that + across the sheet: the 3x3 sheet prints as FOUR pages, not one. That went + unnoticed because these posters were built to be SCREENSHOT by shoot.mjs at + deviceScaleFactor 4, never printed from the browser. + Scaling the stage to the page's own width makes one sheet = one page on any + format, which is what a home printer needs. transform-origin top-left so the + scale collapses toward the page corner rather than the element centre. */ + @media print{ + @page{size:${FMT.page};margin:0} + html,body{background:#fff} + /* A FIXED ratio, computed at generate time — not a vw calc. a vw/px ratio does not + resolve against the PAGE box in print, so the calc silently evaluated to a + no-op and the sheet still tiled. The page width in CSS px is the paper width + in inches x 96; the scale is that over the stage width. */ + .stage{margin:0;transform:scale(${(FMT.pageWidthIn * 96 / FMT.w).toFixed(6)});transform-origin:top left} + /* The scaled stage still reserves its ORIGINAL box, which pushes a blank second + page. Collapse the flow height to the scaled height. */ + html,body{width:${(FMT.pageWidthIn * 96).toFixed(0)}px;height:${(FMT.pageWidthIn * 96 * FMT.h / FMT.w).toFixed(0)}px;overflow:hidden} + }
${svg}
`; return { html, title, octiOK, crossings, diagFrac }; }