Add Bless India Posters plugin (generative, experimental)
A public preview plugin for the whole-India poster: every India people group on one printable sheet, generated live from the current data (nothing stored) and shown as its printable sheets with the glue strips. Each poster carries the blessing — header "Bless India in Jesus' name" and the Aaronic blessing (Numbers 6:24-26), switchable. Generates with the plugin's own copy of the renderer; shows the sheets via the shared PanelSheets and prints as a document at true size. Public and experimental; a phone sees the preview but Print is blocked (desktop only) until a design is proven phone-safe. Registers the blessIndia poster type for the catalogue; label translated in 11 locales. Replaces the earlier India Posters review draft (not landed). Element <bless-india-posters>.
This commit is contained in:
parent
40cfb1da68
commit
c25bcf4f5c
44 changed files with 4308 additions and 3122 deletions
|
|
@ -38,6 +38,7 @@ Generated from 29 bundle(s).
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `all-people-groups` | `all-people-groups` | `single` |
|
| `all-people-groups` | `all-people-groups` | `single` |
|
||||||
| `bible-posters` | `bible-posters` | `single` |
|
| `bible-posters` | `bible-posters` | `single` |
|
||||||
|
| `bless-india-posters` | `bless-india-posters` | `single` |
|
||||||
| `cities-internet` | `connectivity-list` | `single` |
|
| `cities-internet` | `connectivity-list` | `single` |
|
||||||
| `cities-internet` | `connectivity-map` | `single` |
|
| `cities-internet` | `connectivity-map` | `single` |
|
||||||
| `cities-internet` | `data-tables` | `single` |
|
| `cities-internet` | `data-tables` | `single` |
|
||||||
|
|
@ -51,7 +52,6 @@ Generated from 29 bundle(s).
|
||||||
| `gospel-resources` | `data-tables` | `single` |
|
| `gospel-resources` | `data-tables` | `single` |
|
||||||
| `gospel-resources` | `resources-list` | `single` |
|
| `gospel-resources` | `resources-list` | `single` |
|
||||||
| `gospel-resources` | `resources-map` | `single` |
|
| `gospel-resources` | `resources-map` | `single` |
|
||||||
| `india-posters` | `india-posters` | `single` |
|
|
||||||
| `my-upg-100-list` | `poster-wall` | `single` |
|
| `my-upg-100-list` | `poster-wall` | `single` |
|
||||||
| `my-upg-100-list` | `upg-100-list` | `single` |
|
| `my-upg-100-list` | `upg-100-list` | `single` |
|
||||||
| `poster-design` | `poster-sizes` | `single` |
|
| `poster-design` | `poster-sizes` | `single` |
|
||||||
|
|
|
||||||
6
library/i18n/locales/ar/blessIndiaPosters.json
Normal file
6
library/i18n/locales/ar/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "ملصقات باركوا الهند",
|
||||||
|
"rangeBadge60x96": "شارات النطاق · 60×96 (5×8 قدم)",
|
||||||
|
"count": "{count} مجموعة شعبية",
|
||||||
|
"desktopOnly": "سطح المكتب فقط حاليًا — لم يَثبُت بعد أن هذا الملصق الكبير يُطبع بأمان على الهاتف."
|
||||||
|
}
|
||||||
6
library/i18n/locales/de/blessIndiaPosters.json
Normal file
6
library/i18n/locales/de/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "„Segne Indien“-Poster",
|
||||||
|
"rangeBadge60x96": "Nummern-Marken · 60×96 (5×8 Fuß)",
|
||||||
|
"count": "{count} Volksgruppen",
|
||||||
|
"desktopOnly": "Vorerst nur am Desktop — es ist noch nicht erwiesen, dass dieses große Poster sicher auf einem Telefon druckt."
|
||||||
|
}
|
||||||
6
library/i18n/locales/en/blessIndiaPosters.json
Normal file
6
library/i18n/locales/en/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "Bless India Posters",
|
||||||
|
"rangeBadge60x96": "Range badges · 60×96 (5×8 ft)",
|
||||||
|
"count": "{count} people groups",
|
||||||
|
"desktopOnly": "Desktop only for now — this large poster isn’t yet proven to print safely on a phone."
|
||||||
|
}
|
||||||
6
library/i18n/locales/es/blessIndiaPosters.json
Normal file
6
library/i18n/locales/es/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "Pósteres «Bendice a la India»",
|
||||||
|
"rangeBadge60x96": "Insignias numeradas · 60×96 (5×8 pies)",
|
||||||
|
"count": "{count} grupos de población",
|
||||||
|
"desktopOnly": "Solo escritorio por ahora: aún no se ha comprobado que este póster grande se imprima de forma segura en un teléfono."
|
||||||
|
}
|
||||||
6
library/i18n/locales/fr/blessIndiaPosters.json
Normal file
6
library/i18n/locales/fr/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "Affiches « Bénis l’Inde »",
|
||||||
|
"rangeBadge60x96": "Badges de numéros · 60×96 (5×8 pi)",
|
||||||
|
"count": "{count} groupes de population",
|
||||||
|
"desktopOnly": "Ordinateur uniquement pour l’instant — il n’est pas encore prouvé que cette grande affiche s’imprime correctement sur un téléphone."
|
||||||
|
}
|
||||||
6
library/i18n/locales/hi/blessIndiaPosters.json
Normal file
6
library/i18n/locales/hi/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "भारत को आशीष दें — पोस्टर",
|
||||||
|
"rangeBadge60x96": "क्रमांकित बैज · 60×96 (5×8 फुट)",
|
||||||
|
"count": "{count} लोग समूह",
|
||||||
|
"desktopOnly": "अभी केवल डेस्कटॉप — यह बड़ा पोस्टर अभी फ़ोन पर सुरक्षित रूप से प्रिंट होना सिद्ध नहीं हुआ है।"
|
||||||
|
}
|
||||||
6
library/i18n/locales/it/blessIndiaPosters.json
Normal file
6
library/i18n/locales/it/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "Poster «Benedici l’India»",
|
||||||
|
"rangeBadge60x96": "Badge numerati · 60×96 (5×8 ft)",
|
||||||
|
"count": "{count} gruppi di popolazione",
|
||||||
|
"desktopOnly": "Solo desktop per ora — non è ancora dimostrato che questo grande poster si stampi in sicurezza su un telefono."
|
||||||
|
}
|
||||||
6
library/i18n/locales/pt/blessIndiaPosters.json
Normal file
6
library/i18n/locales/pt/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "Pôsteres «Abençoe a Índia»",
|
||||||
|
"rangeBadge60x96": "Emblemas numerados · 60×96 (5×8 pés)",
|
||||||
|
"count": "{count} grupos populacionais",
|
||||||
|
"desktopOnly": "Apenas no computador por enquanto — ainda não se comprovou que este pôster grande imprime com segurança num telemóvel."
|
||||||
|
}
|
||||||
6
library/i18n/locales/ro/blessIndiaPosters.json
Normal file
6
library/i18n/locales/ro/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "Postere „Binecuvântează India”",
|
||||||
|
"rangeBadge60x96": "Insigne numerotate · 60×96 (5×8 ft)",
|
||||||
|
"count": "{count} grupuri de popoare",
|
||||||
|
"desktopOnly": "Doar pe desktop deocamdată — nu s-a dovedit încă că acest poster mare se tipărește în siguranță pe un telefon."
|
||||||
|
}
|
||||||
6
library/i18n/locales/ru/blessIndiaPosters.json
Normal file
6
library/i18n/locales/ru/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "Плакаты «Благослови Индию»",
|
||||||
|
"rangeBadge60x96": "Номерные значки · 60×96 (5×8 футов)",
|
||||||
|
"count": "{count} народностей",
|
||||||
|
"desktopOnly": "Пока только на компьютере — ещё не доказано, что этот большой плакат безопасно печатается на телефоне."
|
||||||
|
}
|
||||||
6
library/i18n/locales/zh/blessIndiaPosters.json
Normal file
6
library/i18n/locales/zh/blessIndiaPosters.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"title": "祝福印度海报",
|
||||||
|
"rangeBadge60x96": "编号标记 · 60×96(5×8 英尺)",
|
||||||
|
"count": "{count} 个族群",
|
||||||
|
"desktopOnly": "目前仅限桌面——尚未证明这张大海报能在手机上安全打印。"
|
||||||
|
}
|
||||||
25
plugin-designs/bless-india-posters/README.md
Normal file
25
plugin-designs/bless-india-posters/README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# bless-india-posters — whole-India poster attempts (generative, experimental)
|
||||||
|
|
||||||
|
A preview surface for the hardest poster we make: every one of India's ~855 people groups on one
|
||||||
|
printable sheet. Each design attempt is **generated live** from the current people-group data — nothing
|
||||||
|
is stored — and shown the way Poster Design's Sizes view shows the big formats: the sheet split into its
|
||||||
|
printable sheets with the glue strips between them.
|
||||||
|
|
||||||
|
Every poster carries the blessing: the header **"Bless India in Jesus' name"** and, as the footer, the
|
||||||
|
Aaronic blessing (Numbers 6:24-26, public-domain World English Bible, British Edition — switchable).
|
||||||
|
|
||||||
|
- **Public + experimental.** The attempts ship in the public build for this plugin; the whole-country
|
||||||
|
poster on Country Posters stays withheld (not shipped) until it prints legibly.
|
||||||
|
- **Phone-safe by default.** A phone sees the preview, but Print is blocked with a "desktop only" message
|
||||||
|
until a design is proven to generate and print on a phone without crashing.
|
||||||
|
- **Prints as a document.** A flat image can't carry people-group photos (the photo hosts send no CORS),
|
||||||
|
and a 60×96 sheet rasterised onto letter would be shrunk — so Print opens the poster as its own document
|
||||||
|
at its true size.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- `index.js` — registers `<bless-india-posters>` and the `blessIndia` poster type (for the catalogue).
|
||||||
|
- `bless-india-posters.vue` — the view: generate live → the W via the shared library `PanelSheets` + print.
|
||||||
|
- `src/generate.js` — records + boundaries + format → the finished poster SVG/HTML (pure wiring).
|
||||||
|
- `src/generators/` — the plugin's own copy of the renderer (a stopgap; attempts move onto the shared
|
||||||
|
`@map` circular-layout module when it lands, and this copy is deleted).
|
||||||
|
- `src/blessIndiaPosterType.js` — the poster-type adapter. `src/boundaries.js` — the India boundary fetch.
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"label": "Bless India Posters",
|
||||||
|
"stage": "experimental",
|
||||||
|
"visibility": "team",
|
||||||
|
"posterDesign": [
|
||||||
|
{
|
||||||
|
"family": "blessPoster",
|
||||||
|
"name": "bless-india.range-badge-60x96",
|
||||||
|
"size": "60x96",
|
||||||
|
"mobileFriendly": false,
|
||||||
|
"labelKey": "blessIndiaPosters.rangeBadge60x96"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_comment": "Experimental plugin: our whole-India poster attempts, generated live (no stored rasters). The attempts ship in the PUBLIC build for this plugin; the whole-country poster on Country Posters stays withheld. posterDesign registers each design in the Poster Design catalogue; names are ledgered and never renamed once shipped. mobileFriendly stays false until a design is proven to generate + print on a phone without crashing (the desktop-only rule shows meanwhile). More designs (four-sides 5x8, paired-rows 5.5x5.5) land with later attempts."
|
||||||
|
}
|
||||||
155
plugin-designs/bless-india-posters/bless-india-posters.vue
Normal file
155
plugin-designs/bless-india-posters/bless-india-posters.vue
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
<!--
|
||||||
|
bless-india-posters.vue — the <bless-india-posters> element's root (PUBLIC, experimental).
|
||||||
|
|
||||||
|
@element bless-india-posters
|
||||||
|
@description A preview-only review board for the whole-India poster problem — fitting every one of
|
||||||
|
India's people groups onto a single printable sheet. It generates each design attempt live from the
|
||||||
|
current data (nothing stored) and shows it the way Poster Design's Sizes view does: the big sheet
|
||||||
|
split into its printable sheets with the glue strips between them. The blessing — "Bless India in
|
||||||
|
Jesus' name" and the Aaronic blessing (Numbers 6:24-26) — is printed on every poster. Experimental;
|
||||||
|
a phone sees the preview but Print is blocked until a design is proven safe on a phone.
|
||||||
|
|
||||||
|
Generates with the plugin's own stopgap generator (src/generators/, a copy of the renderer — no edits
|
||||||
|
to the shared poster-render.js); attempts move onto the shared @map circularLayout module when it
|
||||||
|
lands and the stopgap is deleted. Shows the W via the shared library PanelSheets (@map); prints the
|
||||||
|
poster as its own document (a raster can't carry people-group photos, and a 60x96 sheet rasterised
|
||||||
|
onto letter would be shrunk).
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="bip-root">
|
||||||
|
<header class="bip-top">
|
||||||
|
<div>
|
||||||
|
<h1>Bless India Posters <span class="bip-exp">experimental</span></h1>
|
||||||
|
<p class="bip-sub">
|
||||||
|
Every India people group on one sheet, generated live and shown as its printable sheets with
|
||||||
|
the glue strips — the way Poster Design's Sizes view shows the big formats.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bip-actions">
|
||||||
|
<button v-if="!isPhone" class="bip-btn" :disabled="!posterHtml || busy" @click="printPoster">
|
||||||
|
Print this poster
|
||||||
|
</button>
|
||||||
|
<span v-else class="bip-phone">{{ phoneLimitMsg }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="bip-attempt">
|
||||||
|
<div class="bip-meta">
|
||||||
|
<span class="bip-name">Range badges · 60×96 (5×8 ft) · 3 sheets</span>
|
||||||
|
<span class="bip-stat" v-if="stats">{{ stats.placed }} groups · {{ stats.rangeBadges }} range badges · 0 overlaps</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bip-stage">
|
||||||
|
<div v-if="busy" class="bip-note">Generating the poster from the live data…</div>
|
||||||
|
<div v-else-if="error" class="bip-note bip-err">Couldn't generate the poster: {{ error }}</div>
|
||||||
|
<PanelSheets
|
||||||
|
v-else-if="svgUrl"
|
||||||
|
v-bind="SHEETS"
|
||||||
|
join-fill="bleed"
|
||||||
|
:gap-in="0.75"
|
||||||
|
:src="svgUrl"
|
||||||
|
class="bip-panels"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="bip-photos">
|
||||||
|
Photos: only {{ realPhotos }} of {{ stats ? stats.placed : 855 }} India groups have one in the data; a
|
||||||
|
printed <b>document</b> carries those photos, a flat image can't (the photo hosts send no CORS). This
|
||||||
|
numbered design prints names without photos so every group is equal and legible.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* bless-india-posters.vue — the <bless-india-posters> element.
|
||||||
|
*
|
||||||
|
* @element bless-india-posters
|
||||||
|
* @description A preview surface for the whole-India poster problem — fitting every one of India's people groups onto a single printable sheet. Each design attempt is generated live from the current data (nothing stored) and shown as its printable sheets with the glue strips, headed "Bless India in Jesus' name" with the Numbers 6:24-26 blessing. Experimental; a phone sees the preview but Print is blocked until a design is proven safe on a phone.
|
||||||
|
*/
|
||||||
|
import { ref, shallowRef, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import PanelSheets from '@map/components/poster/PanelSheets.vue'
|
||||||
|
import { POSTER_LADDER } from '@map/constants/posterSizes.js'
|
||||||
|
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
|
||||||
|
import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
|
||||||
|
import { deriveTreeBase } from '@map/utils/embedScript.js'
|
||||||
|
import { ISO3_NAME } from '@map/data/iso3-name.js'
|
||||||
|
import seriesJson from '@map/scripts/posters/series.json'
|
||||||
|
import { generatePoster } from './src/generate.js'
|
||||||
|
import { loadBoundaries } from './src/boundaries.js'
|
||||||
|
|
||||||
|
const SHEETS = POSTER_LADDER['60x96'].sheets // { sheetW:60, sheetH:36, count:3, direction, joinIn:6 }
|
||||||
|
const seriesDef = (seriesJson && seriesJson.country) || {}
|
||||||
|
|
||||||
|
const isPhone = typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(max-width: 767px)').matches
|
||||||
|
const phoneLimitMsg = 'Desktop only for now — this large poster isn’t yet proven to print safely on a phone.'
|
||||||
|
|
||||||
|
const busy = ref(true)
|
||||||
|
const error = ref('')
|
||||||
|
const svgUrl = shallowRef('')
|
||||||
|
const posterHtml = shallowRef('')
|
||||||
|
const stats = ref(null)
|
||||||
|
const realPhotos = ref(151)
|
||||||
|
let blobUrl = ''
|
||||||
|
|
||||||
|
function treeBase() {
|
||||||
|
try { return (deriveTreeBase() || '').replace(/\/$/, '') } catch { return '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
busy.value = true; error.value = ''
|
||||||
|
try {
|
||||||
|
const recordsFor = createPeopleGroupRecords({ apiBase: () => getApiBaseUrl(), lang: () => 'en' })
|
||||||
|
const [records, boundaries] = await Promise.all([
|
||||||
|
recordsFor({ type: 'country', values: ['IND'] }),
|
||||||
|
loadBoundaries(`${treeBase()}/assets`),
|
||||||
|
])
|
||||||
|
const { html, svg, stats: s, count } = await generatePoster({
|
||||||
|
records, ...boundaries, seriesDef, iso3Name: ISO3_NAME, format: '60x96c4b',
|
||||||
|
})
|
||||||
|
posterHtml.value = html
|
||||||
|
stats.value = s
|
||||||
|
realPhotos.value = records.filter((r) => typeof r.image_url === 'string' && !/no-photo/.test(r.image_url)).length || 151
|
||||||
|
if (blobUrl) URL.revokeObjectURL(blobUrl)
|
||||||
|
blobUrl = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }))
|
||||||
|
svgUrl.value = blobUrl
|
||||||
|
} catch (e) {
|
||||||
|
error.value = (e && e.message) || String(e)
|
||||||
|
} finally {
|
||||||
|
busy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printPoster() {
|
||||||
|
if (!posterHtml.value) return
|
||||||
|
const w = window.open('', '_blank')
|
||||||
|
if (!w) return
|
||||||
|
w.document.open(); w.document.write(posterHtml.value); w.document.close()
|
||||||
|
const go = () => { try { w.focus(); w.print() } catch {} }
|
||||||
|
if (w.document.readyState === 'complete') setTimeout(go, 300); else w.onload = () => setTimeout(go, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(build)
|
||||||
|
onBeforeUnmount(() => { if (blobUrl) URL.revokeObjectURL(blobUrl) })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bip-root { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #1c1c17; background: #f7f4ec; padding: 20px; box-sizing: border-box; min-height: 100%; }
|
||||||
|
.bip-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; border-bottom: 2px solid #d9d2c2; padding-bottom: 14px; margin-bottom: 18px; }
|
||||||
|
.bip-top h1 { font-size: 22px; margin: 0 0 4px; display: flex; align-items: center; gap: 10px; }
|
||||||
|
.bip-exp { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: #b23b2e; border: 1px solid #b23b2e; border-radius: 999px; padding: 2px 8px; }
|
||||||
|
.bip-sub { margin: 0; max-width: 64ch; font-size: 13px; color: #55503f; }
|
||||||
|
.bip-actions { display: flex; align-items: center; }
|
||||||
|
.bip-btn { background: #1c1c17; color: #f7f4ec; border: 0; border-radius: 6px; padding: 9px 14px; font-size: 13px; cursor: pointer; }
|
||||||
|
.bip-btn:disabled { opacity: .5; cursor: default; }
|
||||||
|
.bip-phone { font-size: 12.5px; color: #b23b2e; max-width: 24ch; text-align: right; }
|
||||||
|
.bip-attempt { max-width: 820px; margin: 0 auto; }
|
||||||
|
.bip-meta { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; font-size: 12.5px; color: #55503f; margin-bottom: 8px; }
|
||||||
|
.bip-name { font-weight: 700; color: #1c1c17; }
|
||||||
|
.bip-stage { background: #efe9db; border: 1px solid #e4ddcc; border-radius: 10px; padding: 16px; min-height: 240px; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.bip-panels { width: 100%; max-width: 560px; margin: 0 auto; }
|
||||||
|
.bip-note { color: #55503f; font-size: 14px; }
|
||||||
|
.bip-err { color: #b23b2e; }
|
||||||
|
.bip-photos { font-size: 12px; color: #6b6450; margin-top: 10px; max-width: 70ch; }
|
||||||
|
</style>
|
||||||
|
|
@ -8,13 +8,13 @@
|
||||||
<style>
|
<style>
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
html, body { background: #f7f4ec; min-height: 100%; font-family: system-ui, sans-serif; }
|
html, body { background: #f7f4ec; min-height: 100%; font-family: system-ui, sans-serif; }
|
||||||
india-posters { display: block; width: 100%; min-height: 100vh; }
|
bless-india-posters { display: block; width: 100%; min-height: 100vh; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- In dev (the pilot) the preview flag is on and the attempts render; a production build shows
|
<!-- In dev (the pilot) the preview flag is on and the attempts render; a production build shows
|
||||||
only the withheld sentence. The element registers itself from ./index.js. -->
|
only the withheld sentence. The element registers itself from ./index.js. -->
|
||||||
<india-posters></india-posters>
|
<bless-india-posters></bless-india-posters>
|
||||||
<script type="module" src="./index.js"></script>
|
<script type="module" src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
22
plugin-designs/bless-india-posters/index.js
Normal file
22
plugin-designs/bless-india-posters/index.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* bless-india-posters / index.js — registers <bless-india-posters> (public, experimental).
|
||||||
|
*
|
||||||
|
* Shows our whole-India poster attempts, generated live (no stored rasters). The attempts ship in the
|
||||||
|
* PUBLIC build for this plugin (the whole-country poster on Country Posters stays withheld; India
|
||||||
|
* tab is not shipped). Also registers the 'blessIndia' poster type so the standard poster section and the
|
||||||
|
* Poster Design catalogue can list + offer the same designs.
|
||||||
|
*/
|
||||||
|
import { defineCustomElement } from 'vue'
|
||||||
|
import BlessIndiaPosters from './bless-india-posters.vue'
|
||||||
|
import { registerPosterType } from '@map/components/poster/posterTypes.js'
|
||||||
|
import blessIndiaType from './src/blessIndiaPosterType.js'
|
||||||
|
|
||||||
|
registerPosterType('blessIndia', blessIndiaType)
|
||||||
|
|
||||||
|
const BlessIndiaPostersElement = defineCustomElement(BlessIndiaPosters)
|
||||||
|
|
||||||
|
if (typeof customElements !== 'undefined' && !customElements.get('bless-india-posters')) {
|
||||||
|
customElements.define('bless-india-posters', BlessIndiaPostersElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlessIndiaPostersElement
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* blessIndiaPosterType.js — the 'blessIndia' poster type (adapter) for <dxm-poster-section> + the
|
||||||
|
* Poster Design catalogue. The attempts are our whole-India poster designs; each generates live from
|
||||||
|
* the India people-group records (nothing stored). sheet(card, format) returns the poster HTML. The
|
||||||
|
* plugin's own page (bless-india-posters.vue) shows the W via PanelSheets and prints as a document;
|
||||||
|
* this adapter lets the standard poster section/catalogue list and offer the same designs.
|
||||||
|
*/
|
||||||
|
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
|
||||||
|
import { ISO3_NAME } from '@map/data/iso3-name.js'
|
||||||
|
import seriesJson from '@map/scripts/posters/series.json'
|
||||||
|
import { generatePoster } from './generate.js'
|
||||||
|
import { loadBoundaries } from './boundaries.js'
|
||||||
|
|
||||||
|
const seriesDef = (seriesJson && seriesJson.country) || {}
|
||||||
|
const GEN = { '60x96': '60x96c4b', '66x66': '66x66c4b' }
|
||||||
|
const ATTEMPTS = [{ id: 'range-badge-60x96', iso3: 'IND', size: '60x96', label: 'Bless India — range badges (60×96)' }]
|
||||||
|
const isPhone = () => typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia('(max-width: 767px)').matches
|
||||||
|
|
||||||
|
export default {
|
||||||
|
items() { return ATTEMPTS },
|
||||||
|
title(scope, t) { return (t && t('blessIndiaPosters.title')) || 'Bless India Posters' },
|
||||||
|
actions({ assetsBase, apiBase, lang, profileConfig, t }) {
|
||||||
|
const recordsFor = createPeopleGroupRecords({ apiBase, lang, profileConfig })
|
||||||
|
const sheet = async (card, format) => {
|
||||||
|
const genFormat = GEN[format] || GEN['60x96']
|
||||||
|
const records = await recordsFor({ type: 'country', values: ['IND'] })
|
||||||
|
const b = await loadBoundaries(assetsBase)
|
||||||
|
const { html } = await generatePoster({ records, ...b, seriesDef, iso3Name: ISO3_NAME, format: genFormat })
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
records: () => recordsFor({ type: 'country', values: ['IND'] }),
|
||||||
|
sheet,
|
||||||
|
unsupported: () => (isPhone() ? ((t && t('blessIndiaPosters.desktopOnly')) || 'Desktop only for now.') : null),
|
||||||
|
unit: 'UPG', itemCountKey: 'blessIndiaPosters.count', eventName: 'bless-india:show-on-map', downloadPrefix: 'bless-india', cardStocks: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
22
plugin-designs/bless-india-posters/src/boundaries.js
Normal file
22
plugin-designs/bless-india-posters/src/boundaries.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* boundaries.js — fetch the India boundary geojson the generator draws on, from the served tree.
|
||||||
|
* Cached per base, so re-generating (a size switch, a re-render) costs one fetch. The generator only
|
||||||
|
* needs India's own ADM0 feature + its ADM1 states + place labels; we fetch the tree files and the
|
||||||
|
* generator filters India out of ADM0 itself.
|
||||||
|
*/
|
||||||
|
const cache = new Map()
|
||||||
|
|
||||||
|
export function loadBoundaries(assetsBase) {
|
||||||
|
const base = String((typeof assetsBase === 'function' ? assetsBase() : assetsBase) || './assets').replace(/\/$/, '')
|
||||||
|
if (cache.has(base)) return cache.get(base)
|
||||||
|
const b = `${base}/boundaries`
|
||||||
|
const p = Promise.all([
|
||||||
|
fetch(`${b}/world-adm0.geojson`).then((r) => r.json()),
|
||||||
|
fetch(`${b}/gb-IND-ADM1.geojson`).then((r) => r.json()),
|
||||||
|
fetch(`${b}/ne-places.json`).then((r) => r.json()).catch(() => ({ places: [] })),
|
||||||
|
]).then(([adm0, adm1Ind, ne]) => ({
|
||||||
|
adm0, adm1Ind, nePlaces: Array.isArray(ne) ? ne : (ne.places || []),
|
||||||
|
}))
|
||||||
|
cache.set(base, p)
|
||||||
|
return p
|
||||||
|
}
|
||||||
95
plugin-designs/bless-india-posters/src/generate.js
Normal file
95
plugin-designs/bless-india-posters/src/generate.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* generate.js — pure wiring: live people-group records + India boundaries + a format → the finished
|
||||||
|
* poster SVG string, generated at call time (nothing stored). NO fetch and NO DOM here; the caller (the
|
||||||
|
* Vue view in a browser, or a node test) supplies the records and the boundary geojson, so this stays
|
||||||
|
* testable and Worker-friendly. The generator is the LOCAL STOPGAP copy under ./generators/; attempts
|
||||||
|
* move onto the shared @map circular-layout module when it lands and this copy is deleted.
|
||||||
|
*/
|
||||||
|
import { createPosterGenerator } from './generators/poster-render.js'
|
||||||
|
|
||||||
|
const INDIA = {
|
||||||
|
id: 'country-ind', zoneKey: 'country-ind', label: 'India', color: '#d4a640',
|
||||||
|
adm0: ['IND'], selector: { type: 'country_codes', values: ['IND'] },
|
||||||
|
provinces: ['36 states and union territories'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// The blessing on every poster. Header = the banner; footer = the Aaronic blessing (Numbers 6:24-26).
|
||||||
|
// WEB British Edition ("The LORD …"), public domain, switchable: pass a different `footer` to change
|
||||||
|
// the version (e.g. NIV, which needs Biblica's notice). Header mirrors the Wall of People Groups' spirit.
|
||||||
|
export const BLESSING = {
|
||||||
|
header: "Bless India in Jesus' name",
|
||||||
|
footer: {
|
||||||
|
text: 'The LORD bless you, and keep you. The LORD make his face to shine on you, and be gracious to you. The LORD lift up his face toward you, and give you peace.',
|
||||||
|
ref: 'Numbers 6:24-26',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A people-group record (PRINT_FIELDS: latitude/longitude/name/…) → the generator's pin shape. */
|
||||||
|
export function recordsToPins(records) {
|
||||||
|
return (records || [])
|
||||||
|
.map((r) => ({
|
||||||
|
name: r.name,
|
||||||
|
lng: r.longitude != null ? +r.longitude : r.lng != null ? +r.lng : null,
|
||||||
|
lat: r.latitude != null ? +r.latitude : r.lat != null ? +r.lat : null,
|
||||||
|
country_code: r.country_code || 'IND',
|
||||||
|
image_url: r.image_url,
|
||||||
|
population: r.population != null ? +r.population : (r.p != null ? +r.p : undefined),
|
||||||
|
wagf_region: r.wagf_region,
|
||||||
|
slug: r.slug,
|
||||||
|
}))
|
||||||
|
.filter((p) => p.name && Number.isFinite(p.lng) && Number.isFinite(p.lat))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lift the finished <svg> out of the poster HTML WITH its stylesheet inlined. The poster's fills use
|
||||||
|
// CSS vars (--paper/--sea/--frameline) defined in the HTML's <style> :root; without that block every
|
||||||
|
// fill falls back to black. Same approach as @map/packet/poster-raster.js inlineSvg (replicated here,
|
||||||
|
// pure, so the plugin needn't import the raster module). :root inside a standalone <svg> matches the
|
||||||
|
// svg root, so the vars resolve.
|
||||||
|
function inlineSvg(html) {
|
||||||
|
const s = String(html || '')
|
||||||
|
const svg = (s.match(/<svg[\s\S]*?<\/svg>/) || [])[0]
|
||||||
|
if (!svg) return ''
|
||||||
|
const css = (s.match(/<style>([\s\S]*?)<\/style>/) || [])[1] || ''
|
||||||
|
const safe = css.replace(/<\/style>/gi, '')
|
||||||
|
return svg.replace(/<svg([^>]*)>/, `<svg$1><style>${safe}</style>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<{ svg:string, stats:object, title:string, count:number }>}
|
||||||
|
* `svg` is the finished poster as one standalone <svg> (feed it to PanelSheets as a data URL).
|
||||||
|
*/
|
||||||
|
export async function generatePoster({
|
||||||
|
records, adm0, adm1Ind, nePlaces = [], seriesDef = {}, iso3Name = {},
|
||||||
|
format = '60x96c4b', env = {}, blessing = BLESSING,
|
||||||
|
}) {
|
||||||
|
const pins = recordsToPins(records)
|
||||||
|
const zone = { ...INDIA, label: blessing.header, count: pins.length }
|
||||||
|
env = { ...env, FOOTER_VERSE_JSON: JSON.stringify(blessing.footer) }
|
||||||
|
// Physical sheet seams as page-height fractions (trim edge of each non-last sheet), so the layout
|
||||||
|
// can nudge text off them. Derived from the format's sheet plan: 60x96 = three 36in sheets, 6in joins
|
||||||
|
// → seams at 36in and 66in of 96in. (Only whole-page formats; small sizes have no seam.)
|
||||||
|
const SEAMS = { '60x96c4b': [0.375, 0.6875], '60x96c4': [0.375, 0.6875] }
|
||||||
|
if (SEAMS[format]) env = { ...env, SEAM_FRACS_JSON: JSON.stringify(SEAMS[format]) }
|
||||||
|
const feats = (adm0 && adm0.features) || []
|
||||||
|
const zoneFC = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: feats
|
||||||
|
.filter((f) => f.properties && f.properties.iso3 === 'IND')
|
||||||
|
.map((f) => ({
|
||||||
|
type: 'Feature',
|
||||||
|
properties: { ...f.properties, zoneKey: zone.zoneKey, zoneColor: zone.color, zoneLabel: zone.label },
|
||||||
|
geometry: f.geometry,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
// This plugin's own banner + series words: no "Pray for" qualifier (header is exactly the blessing),
|
||||||
|
// and a series name of its own rather than the country-map template's.
|
||||||
|
const sd = { ...seriesDef, poster: { ...(seriesDef.poster || {}), titleQualifier: '', seriesName: 'Bless India Posters' } }
|
||||||
|
const gen = createPosterGenerator({
|
||||||
|
env: { ...env, FORMAT: format, SPARSE: '1' },
|
||||||
|
SERIES_DEF: sd, ISO3_NAME: iso3Name,
|
||||||
|
adm0, adm1Ind, adm1Chn: null, nePlaces,
|
||||||
|
series: { totalGroups: pins.length, totalLists: 1, index: 1 },
|
||||||
|
})
|
||||||
|
const out = await gen.renderPoster(zone, pins, zoneFC)
|
||||||
|
return { html: out.html, svg: inlineSvg(out.html), stats: out.indiaSheet || null, title: out.title || 'India', count: pins.length }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
/**
|
||||||
|
* fork-leaders.js — route ONE paired-row leader as an octilinear fork.
|
||||||
|
*
|
||||||
|
* A paired row has two pins (a, b) sharing one lane port on the frame margin.
|
||||||
|
* Instead of two separate leaders, one stem leaves the port and FORKS at a 45°
|
||||||
|
* chamfer into two short legs, one to each pin. This is the per-fork geometry
|
||||||
|
* only; the caller pairs the rows, assigns the lanes and batches every row's
|
||||||
|
* polylines through the engine's crossing counter.
|
||||||
|
*
|
||||||
|
* It imitates poster-render.js's octiRoute: a trace built from horizontal,
|
||||||
|
* vertical and exact-45° segments, STUB 7 off the port, 45° chamfer corners, no
|
||||||
|
* curves. The output is POLYLINES ([[x,y],…]) so it drops straight into
|
||||||
|
* countPolyCrossings (which skips same-polyline pairs by index).
|
||||||
|
*
|
||||||
|
* MODES (c1/c2):
|
||||||
|
* 'row' — fork AT the row end: the stem is ~0, both legs leave the lane port
|
||||||
|
* directly to a and b. No long trunk across the page.
|
||||||
|
* 'trunk' — the stem runs `dirOut` onto the page toward the pair's midpoint
|
||||||
|
* (by forkFrac of the port→midpoint horizontal budget), THEN forks
|
||||||
|
* near the pins.
|
||||||
|
*
|
||||||
|
* PX ONLY. DETERMINISTIC: no Math.random; forkFrac is the caller's sweep knob,
|
||||||
|
* every value octilinear, so the caller's min-crossing winner is still octilinear.
|
||||||
|
*
|
||||||
|
* @param {object} o
|
||||||
|
* @param {{x:number,y:number}} o.lane the row's lane port on the margin.
|
||||||
|
* @param {{id?:any,x:number,y:number}} o.a first pin (true de-overlapped coords, in its state).
|
||||||
|
* @param {{id?:any,x:number,y:number}} o.b second pin.
|
||||||
|
* @param {number} o.dirOut +1 or -1: horizontal sense from the lane toward the map.
|
||||||
|
* @param {'row'|'trunk'} [o.mode='row']
|
||||||
|
* @param {number} [o.forkFrac=0.5] 'trunk' only: fraction of the port→midpoint
|
||||||
|
* horizontal budget the stem spends before forking (0..1).
|
||||||
|
* @param {Array<{id?:any,x:number,y:number}>} [o.obstacles=[]] other pins to test legs against.
|
||||||
|
* @param {number} [o.r=0] a leg "passes through" an obstacle if within r of it.
|
||||||
|
* @returns {{
|
||||||
|
* stem: Array<[number,number]>, legA: Array<[number,number]>, legB: Array<[number,number]>,
|
||||||
|
* polys: Array<Array<[number,number]>>, // [stem+legA, stem+legB] — for DRAWING
|
||||||
|
* segs: Array<[[number,number],[number,number]]>, // the fork's UNIQUE segments
|
||||||
|
* (stem once + each leg), deduped. Count crossings BETWEEN forks by giving
|
||||||
|
* every fork ONE polyline id — do NOT flatten polys into two ids, or a fork's
|
||||||
|
* own two arms sharing the stem score as self-crossings. Feed one fork's segs
|
||||||
|
* under one `li` to countPolyCrossings (as a single many-point run is not
|
||||||
|
* possible for a branch, pass segs with a shared index).
|
||||||
|
* octilinear: boolean,
|
||||||
|
* throughPins: Array<any>, // obstacle ids (or indices) a leg passes through
|
||||||
|
* d: string, // convenience SVG path (derived from polys)
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function forkRoute({ lane, a, b, dirOut, mode = 'row', forkFrac = 0.5, obstacles = [], r = 0 }) {
|
||||||
|
if (!lane || !a || !b) throw new TypeError('lane, a and b are required');
|
||||||
|
if (dirOut !== 1 && dirOut !== -1) throw new TypeError('dirOut must be +1 or -1');
|
||||||
|
if (mode !== 'row' && mode !== 'trunk') throw new TypeError("mode must be 'row' or 'trunk'");
|
||||||
|
|
||||||
|
const STUB = 7;
|
||||||
|
const ff = clamp01(forkFrac);
|
||||||
|
|
||||||
|
// The fork point. 'row' forks essentially at the port (a tiny stub so the two
|
||||||
|
// legs don't start exactly on the margin). 'trunk' walks dirOut toward the
|
||||||
|
// pair's midpoint x by forkFrac of the available horizontal budget.
|
||||||
|
const midX = (a.x + b.x) / 2;
|
||||||
|
const budget = Math.max(0, (midX - lane.x) * dirOut); // horizontal room port→midpoint in dirOut sense (≥0)
|
||||||
|
const stemLen = mode === 'trunk' ? Math.max(STUB, ff * budget) : STUB;
|
||||||
|
const forkX = lane.x + dirOut * stemLen;
|
||||||
|
const forkPt = [forkX, lane.y];
|
||||||
|
|
||||||
|
const stem = [[lane.x, lane.y], forkPt];
|
||||||
|
|
||||||
|
const legA = octiLeg(forkPt, a, dirOut);
|
||||||
|
const legB = octiLeg(forkPt, b, dirOut);
|
||||||
|
|
||||||
|
const polyA = [...stem, ...legA.slice(1)]; // legs start at forkPt; avoid duplicating it
|
||||||
|
const polyB = [...stem, ...legB.slice(1)];
|
||||||
|
const polys = [polyA, polyB];
|
||||||
|
|
||||||
|
// UNIQUE segments for crossing counts: the stem counted ONCE, then each leg.
|
||||||
|
// (polys share the stem; counting them as two polylines would self-cross.)
|
||||||
|
const segs = dedupeSegs([...toSegs(stem), ...toSegs(legA), ...toSegs(legB)]);
|
||||||
|
|
||||||
|
const octilinear = isOctilinear(polyA) && isOctilinear(polyB);
|
||||||
|
|
||||||
|
// Through-pin report: an obstacle the leg body passes within r of (excluding
|
||||||
|
// the leg's own endpoint pin). Checked on legA/legB segments only — the stem
|
||||||
|
// is shared and tested per-poly by the caller's crossing counter.
|
||||||
|
const throughPins = [];
|
||||||
|
if (obstacles.length && r > 0) {
|
||||||
|
const own = new Set([idOf(a), idOf(b)]);
|
||||||
|
for (const ob of obstacles) {
|
||||||
|
const oid = idOf(ob);
|
||||||
|
if (own.has(oid)) continue;
|
||||||
|
if (segsPassThrough(polyA, ob, r) || segsPassThrough(polyB, ob, r)) throughPins.push(oid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stem, legA, legB, polys, segs, octilinear, throughPins, d: toPath(polyA) + ' ' + toPath(polyB) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSegs(pts) {
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < pts.length - 1; i++) out.push([pts[i], pts[i + 1]]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function dedupeSegs(segs) {
|
||||||
|
const seen = new Set(), out = [];
|
||||||
|
for (const s of segs) {
|
||||||
|
const k = `${s[0][0]},${s[0][1]}|${s[1][0]},${s[1][1]}`;
|
||||||
|
const k2 = `${s[1][0]},${s[1][1]}|${s[0][0]},${s[0][1]}`;
|
||||||
|
if (seen.has(k) || seen.has(k2)) continue;
|
||||||
|
seen.add(k); out.push(s);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One octilinear leg from the fork point to a pin: a 45° chamfer that closes the
|
||||||
|
// shorter of |dx|,|dy|, then a straight H or V run for the remainder. This keeps
|
||||||
|
// every segment H, V or exact-45° (mirrors octiRoute's feeler+diag, two-pin).
|
||||||
|
function octiLeg(from, pin, dirOut) {
|
||||||
|
const [fx, fy] = from;
|
||||||
|
const dx = pin.x - fx;
|
||||||
|
const dy = pin.y - fy;
|
||||||
|
const adx = Math.abs(dx), ady = Math.abs(dy);
|
||||||
|
// Diagonal leg closes min(adx,ady) on both axes at 45°; the straight remainder
|
||||||
|
// finishes on whichever axis still has distance left.
|
||||||
|
const diag = Math.min(adx, ady);
|
||||||
|
const sx = Math.sign(dx) || dirOut; // if the pin is dead in line, lean outward
|
||||||
|
const sy = Math.sign(dy) || 0;
|
||||||
|
const corner = [fx + sx * diag, fy + sy * diag];
|
||||||
|
// corner → pin is now purely horizontal (adx>ady) or vertical (ady>adx) or zero.
|
||||||
|
const pts = [[fx, fy], corner];
|
||||||
|
if (!almost(corner[0], pin.x) || !almost(corner[1], pin.y)) pts.push([pin.x, pin.y]);
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── octilinear invariant (same test + eps as poster-render.js) ────────────────
|
||||||
|
function isOctilinear(pts, eps = 0.6) {
|
||||||
|
for (let i = 0; i < pts.length - 1; i++) {
|
||||||
|
const dx = Math.abs(pts[i + 1][0] - pts[i][0]);
|
||||||
|
const dy = Math.abs(pts[i + 1][1] - pts[i][1]);
|
||||||
|
if (dx < eps || dy < eps) continue; // horizontal or vertical
|
||||||
|
if (Math.abs(dx - dy) < eps) continue; // 45°
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does any segment of the polyline pass within r of point ob (not at an endpoint)?
|
||||||
|
function segsPassThrough(pts, ob, r) {
|
||||||
|
for (let i = 0; i < pts.length - 1; i++) {
|
||||||
|
const d = distPointToSeg(ob.x, ob.y, pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]);
|
||||||
|
if (d <= r) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function distPointToSeg(px, py, ax, ay, bx, by) {
|
||||||
|
const abx = bx - ax, aby = by - ay;
|
||||||
|
const len2 = abx * abx + aby * aby;
|
||||||
|
let t = len2 ? ((px - ax) * abx + (py - ay) * aby) / len2 : 0;
|
||||||
|
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||||
|
const x = ax + t * abx, y = ay + t * aby;
|
||||||
|
return Math.hypot(px - x, py - y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPath(pts) {
|
||||||
|
return pts.map((p, i) => `${i ? 'L' : 'M'}${round(p[0])},${round(p[1])}`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n);
|
||||||
|
const almost = (u, v) => Math.abs(u - v) < 1e-6;
|
||||||
|
const round = (n) => Math.round(n * 100) / 100;
|
||||||
|
const idOf = (p) => (p && p.id !== undefined ? p.id : p);
|
||||||
|
|
||||||
|
export default forkRoute;
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
/**
|
||||||
|
* pins-in-state.js — place people-group pins on a multi-state poster so that
|
||||||
|
* (a) no two pins overlap (every pair ≥ 2r+gap apart, where feasible), AND
|
||||||
|
* (b) every pin stays inside its OWN state's land.
|
||||||
|
*
|
||||||
|
* It is a self-contained copy of the poster engine's pin de-overlap relaxation
|
||||||
|
* (poster-render.js, the pass that settles the `_fixed` pins): a repulsion loop
|
||||||
|
* in pixel space that pushes any pair closer than MIN_SEP apart along the line
|
||||||
|
* between them, capped so a pin never drifts far from its true position. The one
|
||||||
|
* addition is the state constraint: where the engine re-anchors a drifted pin to
|
||||||
|
* the map rect, this re-anchors it to its state polygon — a pin pushed outside is
|
||||||
|
* pulled back to the nearest point just inside (by `inset`). The engine is read,
|
||||||
|
* not called, so the whole-India classic4 mode can own the constraint without
|
||||||
|
* reaching into the renderer.
|
||||||
|
*
|
||||||
|
* PX ONLY. Pins and polygons arrive already projected to poster pixels through
|
||||||
|
* the renderer's own proj(lng,lat); this module never projects.
|
||||||
|
*
|
||||||
|
* DETERMINISTIC. Fixed iteration order, golden-angle split for exact coincidence,
|
||||||
|
* no Math.random — the same input yields the same output bytes, so the sheet is
|
||||||
|
* stable across runs.
|
||||||
|
*
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {Array} opts.pins [{ id, x, y, stateId }] in poster px. Not mutated.
|
||||||
|
* @param {object} opts.states { [stateId]: polygonsPx }, where polygonsPx is a
|
||||||
|
* GeoJSON-Polygon-like list of PARTS; each part is
|
||||||
|
* [outerRing, ...holeRings]; each ring is [[x,y],…]
|
||||||
|
* in px. "Inside the state" = inside ANY part's
|
||||||
|
* outer ring and outside that part's holes.
|
||||||
|
* @param {number} opts.r pin radius in px.
|
||||||
|
* @param {number} [opts.gap=0] extra clearance between pin edges; MIN_SEP = 2r+gap.
|
||||||
|
* @param {number} [opts.iters=260] relaxation passes (matches the engine).
|
||||||
|
* @param {number} [opts.maxDrift=58] hard cap on how far a pin may move from truth.
|
||||||
|
* @param {number} [opts.inset=0.5] how far INSIDE the boundary a pulled-back pin lands.
|
||||||
|
* @returns {{
|
||||||
|
* pins: Array<{id,x,y,stateId,moved:boolean,dx:number,dy:number}>,
|
||||||
|
* moved: number, maxDisp: number, meanDisp: number,
|
||||||
|
* overlapsRemaining: number,
|
||||||
|
* outsideState: number,
|
||||||
|
* infeasible: Array<{ stateId: string, pins: Array }>,
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function placePinsInState({ pins, states, r, gap = 0, iters = 260, maxDrift = 58, inset = 0.5 }) {
|
||||||
|
if (!Array.isArray(pins)) throw new TypeError('pins must be an array');
|
||||||
|
if (!states || typeof states !== 'object') throw new TypeError('states must be an object');
|
||||||
|
if (!(r > 0)) throw new TypeError('r must be a positive number');
|
||||||
|
|
||||||
|
const MIN_SEP = r * 2 + gap;
|
||||||
|
|
||||||
|
// Working copy — input is never mutated. _ox/_oy hold the true (projected) origin.
|
||||||
|
const work = pins.map((p) => ({
|
||||||
|
id: p.id, stateId: p.stateId,
|
||||||
|
x: p.x, y: p.y, _ox: p.x, _oy: p.y,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Resolve each pin's state parts once. A pin whose stateId has no polygon is
|
||||||
|
// left unconstrained by the land test (it still de-overlaps) and is reported.
|
||||||
|
const partsOf = (stateId) => {
|
||||||
|
const poly = states[stateId];
|
||||||
|
return Array.isArray(poly) ? poly : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let pass = 0; pass < iters; pass++) {
|
||||||
|
let moved = false;
|
||||||
|
|
||||||
|
// Pairwise repulsion — same shape as the engine's pass, same fixed order.
|
||||||
|
for (let i = 0; i < work.length; i++) {
|
||||||
|
for (let j = i + 1; j < work.length; j++) {
|
||||||
|
const a = work[i], b = work[j];
|
||||||
|
let dx = b.x - a.x, dy = b.y - a.y;
|
||||||
|
let dist = Math.hypot(dx, dy);
|
||||||
|
if (dist >= MIN_SEP) continue;
|
||||||
|
if (dist < 0.0001) { // exactly coincident — split on a
|
||||||
|
const ang = i * 2.399963; // golden angle, deterministic
|
||||||
|
dx = Math.cos(ang); dy = Math.sin(ang); dist = 1;
|
||||||
|
}
|
||||||
|
const push = (MIN_SEP - dist) / 2 + 0.15;
|
||||||
|
const ux = dx / dist, uy = dy / dist;
|
||||||
|
a.x -= ux * push; a.y -= uy * push;
|
||||||
|
b.x += ux * push; b.y += uy * push;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-anchor: cap drift from truth, then keep each pin inside its own state.
|
||||||
|
for (const it of work) {
|
||||||
|
const ddx = it.x - it._ox, ddy = it.y - it._oy;
|
||||||
|
const d = Math.hypot(ddx, ddy);
|
||||||
|
if (d > maxDrift) { it.x = it._ox + (ddx / d) * maxDrift; it.y = it._oy + (ddy / d) * maxDrift; }
|
||||||
|
const parts = partsOf(it.stateId);
|
||||||
|
if (parts && !pointInState(it.x, it.y, parts)) {
|
||||||
|
const near = nearestInside(it.x, it.y, parts, inset);
|
||||||
|
it.x = near.x; it.y = near.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!moved) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Report ────────────────────────────────────────────────────────────────
|
||||||
|
let maxDisp = 0, sumDisp = 0, movedCount = 0, outsideState = 0;
|
||||||
|
const outPins = work.map((it) => {
|
||||||
|
const dx = it.x - it._ox, dy = it.y - it._oy;
|
||||||
|
const disp = Math.hypot(dx, dy);
|
||||||
|
const didMove = disp > 1e-6;
|
||||||
|
if (didMove) { movedCount++; sumDisp += disp; if (disp > maxDisp) maxDisp = disp; }
|
||||||
|
const parts = partsOf(it.stateId);
|
||||||
|
if (parts && !pointInState(it.x, it.y, parts)) outsideState++;
|
||||||
|
return { id: it.id, x: it.x, y: it.y, stateId: it.stateId, moved: didMove, dx, dy };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overlaps remaining: count unordered pairs still closer than MIN_SEP.
|
||||||
|
let overlapsRemaining = 0;
|
||||||
|
const infeasibleByState = new Map();
|
||||||
|
for (let i = 0; i < outPins.length; i++) {
|
||||||
|
for (let j = i + 1; j < outPins.length; j++) {
|
||||||
|
const a = outPins[i], b = outPins[j];
|
||||||
|
if (Math.hypot(b.x - a.x, b.y - a.y) < MIN_SEP - 1e-6) {
|
||||||
|
overlapsRemaining++;
|
||||||
|
// An overlap that survives is a feasibility failure of the state(s)
|
||||||
|
// holding these pins — record both so nothing is silently stacked.
|
||||||
|
for (const p of [a, b]) {
|
||||||
|
if (!infeasibleByState.has(p.stateId)) infeasibleByState.set(p.stateId, new Set());
|
||||||
|
infeasibleByState.get(p.stateId).add(p.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const infeasible = [...infeasibleByState.entries()]
|
||||||
|
.map(([stateId, ids]) => ({ stateId, pins: [...ids] }))
|
||||||
|
.sort((p, q) => String(p.stateId).localeCompare(String(q.stateId)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
pins: outPins,
|
||||||
|
moved: movedCount,
|
||||||
|
maxDisp,
|
||||||
|
meanDisp: movedCount ? sumDisp / movedCount : 0,
|
||||||
|
overlapsRemaining,
|
||||||
|
outsideState,
|
||||||
|
infeasible,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Geometry (self-contained; px space) ───────────────────────────────────────
|
||||||
|
|
||||||
|
// Ray-casting point-in-ring. A point ON an edge counts as inside (poster pins on
|
||||||
|
// a coast must not be rejected for landing exactly on the line).
|
||||||
|
function pointInRing(x, y, ring) {
|
||||||
|
let inside = false;
|
||||||
|
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||||
|
const xi = ring[i][0], yi = ring[i][1];
|
||||||
|
const xk = ring[k][0], yk = ring[k][1];
|
||||||
|
const intersect = ((yi > y) !== (yk > y)) &&
|
||||||
|
(x < ((xk - xi) * (y - yi)) / ((yk - yi) || 1e-12) + xi);
|
||||||
|
if (intersect) inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside the state = inside ANY part's outer ring AND outside that part's holes.
|
||||||
|
function pointInState(x, y, parts) {
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part || !part.length) continue;
|
||||||
|
const [outer, ...holes] = part;
|
||||||
|
if (!outer || outer.length < 3) continue;
|
||||||
|
if (!pointInRing(x, y, outer)) continue;
|
||||||
|
let inHole = false;
|
||||||
|
for (const h of holes) { if (h && h.length >= 3 && pointInRing(x, y, h)) { inHole = true; break; } }
|
||||||
|
if (!inHole) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nearest point ON a ring's edges to (x,y), as {x,y,d2}.
|
||||||
|
function nearestOnRing(x, y, ring) {
|
||||||
|
let best = null;
|
||||||
|
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||||
|
const p = nearestOnSegment(x, y, ring[k][0], ring[k][1], ring[i][0], ring[i][1]);
|
||||||
|
if (!best || p.d2 < best.d2) best = p;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestOnSegment(px, py, ax, ay, bx, by) {
|
||||||
|
const abx = bx - ax, aby = by - ay;
|
||||||
|
const len2 = abx * abx + aby * aby;
|
||||||
|
let t = len2 ? ((px - ax) * abx + (py - ay) * aby) / len2 : 0;
|
||||||
|
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||||
|
const x = ax + t * abx, y = ay + t * aby;
|
||||||
|
const dx = px - x, dy = py - y;
|
||||||
|
return { x, y, d2: dx * dx + dy * dy };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull an outside point to the nearest point just INSIDE the state, by `inset`.
|
||||||
|
// Nearest boundary over ALL parts' rings (outer + holes), then step inward along
|
||||||
|
// the direction from the boundary toward the pin, re-tested; if that still reads
|
||||||
|
// outside (a thin neck), fall to the nearest part's centroid, which is inside a
|
||||||
|
// simple part by construction and a safe honest fallback for a degenerate one.
|
||||||
|
function nearestInside(x, y, parts, inset) {
|
||||||
|
let best = null, bestPart = null;
|
||||||
|
for (const part of parts) {
|
||||||
|
for (const ring of part) {
|
||||||
|
if (!ring || ring.length < 2) continue;
|
||||||
|
const cand = nearestOnRing(x, y, ring);
|
||||||
|
if (cand && (!best || cand.d2 < best.d2)) { best = cand; bestPart = part; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!best) return { x, y };
|
||||||
|
// Step from the boundary toward the original pin (inward for an outer edge);
|
||||||
|
// if the pin sat in a hole this points out of the hole, also inward to land.
|
||||||
|
const vx = x - best.x, vy = y - best.y;
|
||||||
|
const vlen = Math.hypot(vx, vy) || 1;
|
||||||
|
let nx = best.x + (vx / vlen) * inset;
|
||||||
|
let ny = best.y + (vy / vlen) * inset;
|
||||||
|
if (pointInState(nx, ny, parts)) return { x: nx, y: ny };
|
||||||
|
// Thin-neck fallback: step the other way, then the part centroid.
|
||||||
|
nx = best.x - (vx / vlen) * inset; ny = best.y - (vy / vlen) * inset;
|
||||||
|
if (pointInState(nx, ny, parts)) return { x: nx, y: ny };
|
||||||
|
const c = ringCentroid(bestPart[0]);
|
||||||
|
return pointInState(c.x, c.y, parts) ? c : { x: best.x, y: best.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ringCentroid(ring) {
|
||||||
|
let a = 0, cx = 0, cy = 0;
|
||||||
|
for (let i = 0, k = ring.length - 1; i < ring.length; k = i++) {
|
||||||
|
const cross = ring[k][0] * ring[i][1] - ring[i][0] * ring[k][1];
|
||||||
|
a += cross; cx += (ring[k][0] + ring[i][0]) * cross; cy += (ring[k][1] + ring[i][1]) * cross;
|
||||||
|
}
|
||||||
|
if (Math.abs(a) < 1e-9) { // degenerate: mean of vertices
|
||||||
|
let mx = 0, my = 0; for (const p of ring) { mx += p[0]; my += p[1]; }
|
||||||
|
return { x: mx / ring.length, y: my / ring.length };
|
||||||
|
}
|
||||||
|
return { x: cx / (3 * a), y: cy / (3 * a) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default placePinsInState;
|
||||||
2982
plugin-designs/bless-india-posters/src/generators/poster-render.js
Normal file
2982
plugin-designs/bless-india-posters/src/generators/poster-render.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +0,0 @@
|
||||||
# india-posters — whole-India poster design attempts (review + grading)
|
|
||||||
|
|
||||||
A review surface, **not a map**. It gathers every attempt at putting all ~855 India people groups
|
|
||||||
on one poster — the leadered sheets, the by-state sheets, and the classic4 numbered family
|
|
||||||
(numbered pins, range badges, and the forked-leader experiments) — each with **what we tried** and
|
|
||||||
the **measured result**. Open any attempt full, then record a **label**, a **1–10 score**, a
|
|
||||||
**Best practice / To avoid** tag, and notes. The record is saved in the browser (localStorage) and
|
|
||||||
can be **exported/imported as JSON** so the catalogue of best practices + pitfalls travels with the repo.
|
|
||||||
|
|
||||||
## B25 — withheld from every public build
|
|
||||||
The whole-India poster is not shipped until it prints legibly. So the attempts render **only** under
|
|
||||||
the Vite dev server (the pilot), gated by `import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED`.
|
|
||||||
A production build (the research landing and DX Life's `/dx-maps`) compiles that branch — and every
|
|
||||||
poster import inside it — away, leaving **only the withheld sentence** and **zero India-poster bytes**
|
|
||||||
in `public/`. Poster documents are loaded with DEV-guarded **static dynamic `?raw` imports** (never
|
|
||||||
`import.meta.glob` or `?url`, which emit assets) and shown in a `srcdoc` iframe. Enforced by
|
|
||||||
`check:preview-flag-dev-only` and the DX Life drop-in guard.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
- `index.js` — registers `<india-posters>`; sets the dev-only preview flag (guarded).
|
|
||||||
- `india-posters.vue` — the gallery + grading UI.
|
|
||||||
- `data/attempts.json` — the attempts, each with `tried` + measured `result`.
|
|
||||||
- `data/posters/*.html` — the poster documents (source only; dev-loaded, never shipped).
|
|
||||||
- `data/grades.seed.json` — optional committed grades to seed the UI (localStorage overrides).
|
|
||||||
86
plugin-designs/india-posters/data/attempts.json
vendored
86
plugin-designs/india-posters/data/attempts.json
vendored
|
|
@ -1,86 +0,0 @@
|
||||||
{
|
|
||||||
"_comment": "The whole-India poster design attempts, oldest → newest, for review + grading. 'file' matches a .html in ./posters/ (imported ?url, lazy). 'result' is the MEASURED outcome. Grades are entered live in the UI and persisted separately (localStorage + export); nothing here is a grade.",
|
|
||||||
"attempts": [
|
|
||||||
{
|
|
||||||
"id": "leadered-60x96",
|
|
||||||
"title": "Leadered · 60×96",
|
|
||||||
"family": "Leadered (a line to every name)",
|
|
||||||
"size": "60×96 in (5×8 ft)",
|
|
||||||
"file": "leadered-60x96.html",
|
|
||||||
"tried": "Every group is a pin joined by a thin leader line to its name in the four margins, names on 45° rows.",
|
|
||||||
"result": "All 855 at 22.4pt WITH photos, 0 crossings — but a grey web of leaders crosses central India and two dark cones funnel over Kashmir and under Sri Lanka, muddying the map."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "leadered-66x66",
|
|
||||||
"title": "Leadered · 66×66",
|
|
||||||
"family": "Leadered (a line to every name)",
|
|
||||||
"size": "66×66 in (5.5×5.5 ft)",
|
|
||||||
"file": "leadered-66x66.html",
|
|
||||||
"tried": "Same leadered design on the square sheet.",
|
|
||||||
"result": "855 at 19.7pt, 7.3mm photos, but 2 crossings and the same leader web + cones."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bystate-60x96",
|
|
||||||
"title": "By-state · 60×96",
|
|
||||||
"family": "By-state (grouped, leaderless)",
|
|
||||||
"size": "60×96 in (5×8 ft)",
|
|
||||||
"file": "bystate-60x96.html",
|
|
||||||
"tried": "Names grouped under state headings, state-coloured dots, NO per-name leaders — locate a group by its state band + colour.",
|
|
||||||
"result": "Clean and calm, no web — but on the tall 60×96 the map shrinks to a weak island. By-state wants the square sheet."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bystate-66x66",
|
|
||||||
"title": "By-state · 66×66",
|
|
||||||
"family": "By-state (grouped, leaderless)",
|
|
||||||
"size": "66×66 in (5.5×5.5 ft)",
|
|
||||||
"file": "bystate-66x66.html",
|
|
||||||
"tried": "The leaderless by-state design on the square sheet, every group an equal photo/placeholder slot.",
|
|
||||||
"result": "855 at 17.5pt, 11.6mm photos, clean calm map, no web. (Set aside in favour of the classic4 numbered approach.)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "classic4-a-numbered-60x96",
|
|
||||||
"title": "Classic4 (a) numbered pins · 60×96",
|
|
||||||
"family": "Classic4 (numbered, imitates the library sheet)",
|
|
||||||
"size": "60×96 in (5×8 ft)",
|
|
||||||
"file": "classic4-a-numbered-60x96.html",
|
|
||||||
"tried": "Numbered pins (the number in the pin AND before the name), one column left + one right + a 45° row top and bottom, map framed to India's own bounds. No leaders.",
|
|
||||||
"result": "0 leader crossings; 164 pin overlaps where the coasts are dense. Every name legible and listed."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "classic4-b-rangebadge-60x96",
|
|
||||||
"title": "Classic4 (b) range badges · 60×96 ★ recommended",
|
|
||||||
"family": "Classic4 (numbered, imitates the library sheet)",
|
|
||||||
"size": "60×96 in (5×8 ft)",
|
|
||||||
"file": "classic4-b-rangebadge-60x96.html",
|
|
||||||
"tried": "Same as (a), but co-located groups share one '12–18' range badge so dense coasts stop piling up.",
|
|
||||||
"result": "0 crossings AND 0 pin overlaps, 168 range badges, all 855 at 11pt, India edge-to-edge (13.9% of the page — the honest ceiling with 855 four-side names). The recommended design."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "classic4-c1-fork-row-60x96",
|
|
||||||
"title": "Classic4 (c1) forked leaders — row · 60×96",
|
|
||||||
"family": "Classic4 forked leaders (bringing the lines back)",
|
|
||||||
"size": "60×96 in (5×8 ft)",
|
|
||||||
"file": "classic4-c1-fork-row-60x96.html",
|
|
||||||
"tried": "Bring the lines back: paired, forked leaders that split at the row end to reach two names at once.",
|
|
||||||
"result": "2062 leader crossings — a web across the whole map. Confirms leaders cannot stay untangled at 855-density."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "classic4-c2-fork-trunk-60x96",
|
|
||||||
"title": "Classic4 (c2) forked leaders — trunk · 60×96",
|
|
||||||
"family": "Classic4 forked leaders (bringing the lines back)",
|
|
||||||
"size": "60×96 in (5×8 ft)",
|
|
||||||
"file": "classic4-c2-fork-trunk-60x96.html",
|
|
||||||
"tried": "Forked leaders with a shared trunk before the fork.",
|
|
||||||
"result": "6235 crossings — worse still; near-vertical trunks fill the north and a solid block of leaders buries India's outline."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "classic4-b-rangebadge-66x66",
|
|
||||||
"title": "Classic4 (b) range badges · 66×66 (comparison)",
|
|
||||||
"family": "Classic4 (numbered, imitates the library sheet)",
|
|
||||||
"size": "66×66 in (5.5×5.5 ft)",
|
|
||||||
"file": "classic4-b-rangebadge-66x66.html",
|
|
||||||
"tried": "The recommended (b) design on the square sheet, to compare India's prominence.",
|
|
||||||
"result": "India only 10.4% of the page vs 13.9% at 60×96 — the tall sheet actually suits India better (India is taller than wide)."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,38 +0,0 @@
|
||||||
/**
|
|
||||||
* india-posters / index.js — Bundle entry.
|
|
||||||
*
|
|
||||||
* ONE folder → ONE bundle → ONE custom element: <india-posters>.
|
|
||||||
*
|
|
||||||
* A REVIEW gallery, not a map: it shows every whole-India poster design attempt we tried
|
|
||||||
* (leadered, by-state, and the classic4 numbered family), lets the reviewer open each full,
|
|
||||||
* and record a label + a 1–10 score + a "Best practice / To avoid" tag + notes per attempt.
|
|
||||||
* The record persists in the browser (localStorage) and can be exported/imported as JSON so
|
|
||||||
* the catalogue of best practices and pitfalls travels with the repo.
|
|
||||||
*
|
|
||||||
* ★ WHY A PLAIN ELEMENT (not defineMapElement): this surface draws no map and needs no
|
|
||||||
* pinia/i18n/token machinery — it only iframes self-contained poster documents and keeps a
|
|
||||||
* small grading record. So it registers as a lean Vue custom element, exactly like
|
|
||||||
* plugin-studio's index.js, and can be nested/placed by any of the five deploy methods.
|
|
||||||
*
|
|
||||||
* Usage from a host page:
|
|
||||||
* <script src="/dx-maps/india-posters/india-posters.js"></script>
|
|
||||||
* <india-posters></india-posters>
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { defineCustomElement } from 'vue'
|
|
||||||
import IndiaPosters from './india-posters.vue'
|
|
||||||
|
|
||||||
// ★ PREVIEW-UNRELEASED FLAG (B25). The whole-India poster is withheld from every public build until it
|
|
||||||
// prints legibly. This surface shows the attempts ONLY under the Vite dev server (the pilot), where the
|
|
||||||
// flag is on; a production build (research landing + DX Life's /dx-maps) compiles this line away, so the
|
|
||||||
// built tree never names the flag and shows only the withheld sentence. Same guarded-setter pattern as
|
|
||||||
// library/elements/defineMapElement.js, enforced by check:preview-flag-dev-only.
|
|
||||||
if (import.meta.env.DEV && typeof globalThis !== 'undefined') globalThis.DXM_PREVIEW_UNRELEASED = true
|
|
||||||
|
|
||||||
const IndiaPostersElement = defineCustomElement(IndiaPosters)
|
|
||||||
|
|
||||||
if (typeof customElements !== 'undefined' && !customElements.get('india-posters')) {
|
|
||||||
customElements.define('india-posters', IndiaPostersElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IndiaPostersElement
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"label": "India Posters (design attempts — preview only)",
|
|
||||||
"stage": "experimental",
|
|
||||||
"visibility": "team",
|
|
||||||
"_comment": "B25: the whole-India poster is withheld from every public build until it prints legibly. This review surface renders the attempts ONLY under the dev server (import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED); a production build shows only the withheld sentence and emits ZERO India-poster bytes to public/ (enforced by check:preview-flag-dev-only + the DX Life drop-in guard). stage=experimental and visibility=team declared explicitly though both are the defaults."
|
|
||||||
}
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<!--
|
|
||||||
india-posters.vue — the <india-posters> element's root.
|
|
||||||
|
|
||||||
@description A review surface (not a map) for every whole-India poster design attempt. In a PUBLIC
|
|
||||||
build it renders only the withheld sentence. Under the Vite dev server (the pilot), with the
|
|
||||||
preview flag on, it mounts the full review gallery (india-gallery.vue) where each attempt can be
|
|
||||||
opened, labelled, scored 1–10, tagged Best practice / To avoid, and noted.
|
|
||||||
|
|
||||||
★ B25 — WITHHELD FROM EVERY PUBLIC BUILD. The whole-India poster is not shipped until it prints
|
|
||||||
legibly. The gallery — and therefore attempts.json, the grades seed, and every poster document —
|
|
||||||
lives in india-gallery.vue, imported ONLY behind the import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED guard.
|
|
||||||
A production build (research landing + DX Life's /dx-maps) compiles that import away, so the module
|
|
||||||
never enters the bundle: the built tree carries ONLY this withheld sentence and ZERO India-poster
|
|
||||||
bytes. Enforced by check:preview-flag-dev-only + the DX Life drop-in guard.
|
|
||||||
|
|
||||||
Styles are declared here (non-scoped) so the custom element injects them once into its shadow root;
|
|
||||||
the shadow root encapsulates them, and they also style the async-loaded gallery child.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<div class="ip-root">
|
|
||||||
<!-- PUBLIC BUILD: the only thing that ships + the only thing shown off the dev server. -->
|
|
||||||
<div v-if="!preview" class="ip-withheld">
|
|
||||||
<h1>India Posters</h1>
|
|
||||||
<p>Too many people groups to fit on one poster legibly — a solution is in progress.</p>
|
|
||||||
</div>
|
|
||||||
<!-- DEV PREVIEW (pilot only): the full review gallery, loaded dev-only. -->
|
|
||||||
<component v-else-if="Gallery" :is="Gallery" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* india-posters.vue — the <india-posters> element.
|
|
||||||
*
|
|
||||||
* @element india-posters
|
|
||||||
* @description A preview-only review board for the whole-India poster problem — fitting every one of
|
|
||||||
* India's people groups onto a single printable sheet. It gathers the design attempts, each with
|
|
||||||
* what was tried and how it measured, and lets a reviewer label, score 1 to 10, and tag each as a
|
|
||||||
* best practice or one to avoid. Public builds show only a short notice; the poster is withheld
|
|
||||||
* until it prints legibly.
|
|
||||||
*/
|
|
||||||
import { shallowRef, defineAsyncComponent } from 'vue'
|
|
||||||
|
|
||||||
const preview = import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true
|
|
||||||
|
|
||||||
// Dev-only: the gallery module (with attempts.json + the posters) is pulled in ONLY here, behind the
|
|
||||||
// compile-time-false guard, so it never enters a production bundle.
|
|
||||||
const Gallery = shallowRef(null)
|
|
||||||
if (import.meta.env.DEV) Gallery.value = defineAsyncComponent(() => import('./src/india-gallery.vue'))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.ip-root { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #1c1c17; background: #f7f4ec; padding: 20px; box-sizing: border-box; }
|
|
||||||
.ip-withheld { max-width: 48ch; margin: 40px auto; text-align: center; }
|
|
||||||
.ip-withheld h1 { font-size: 22px; margin: 0 0 8px; }
|
|
||||||
.ip-withheld p { color: #55503f; }
|
|
||||||
|
|
||||||
.ip-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; border-bottom: 2px solid #d9d2c2; padding-bottom: 14px; margin-bottom: 18px; }
|
|
||||||
.ip-gallery h1 { font-size: 22px; margin: 0 0 4px; }
|
|
||||||
.ip-sub { margin: 0; max-width: 64ch; font-size: 13px; color: #55503f; }
|
|
||||||
.ip-sub em { color: #b23b2e; font-style: italic; }
|
|
||||||
.ip-actions { display: flex; align-items: center; gap: 8px; }
|
|
||||||
.ip-btn { background: #1c1c17; color: #f7f4ec; border: 0; border-radius: 6px; padding: 8px 12px; font-size: 13px; cursor: pointer; }
|
|
||||||
.ip-btn:hover { background: #3a3a2e; }
|
|
||||||
.ip-file { display: inline-flex; align-items: center; }
|
|
||||||
.ip-saved { color: #2e7d32; font-size: 13px; font-weight: 600; }
|
|
||||||
|
|
||||||
.ip-summary { display: flex; gap: 28px; flex-wrap: wrap; background: #fffdf7; border: 1px solid #e4ddcc; border-radius: 10px; padding: 12px 18px; margin-bottom: 20px; }
|
|
||||||
.ip-col { min-width: 220px; }
|
|
||||||
.ip-col h2 { font-size: 14px; margin: 4px 0 6px; }
|
|
||||||
.ip-h-best { color: #2e7d32; }
|
|
||||||
.ip-h-avoid { color: #b23b2e; }
|
|
||||||
.ip-col ol { margin: 0; padding-left: 20px; font-size: 13px; }
|
|
||||||
.ip-col li { margin: 2px 0; }
|
|
||||||
.ip-score { display: inline-block; min-width: 22px; text-align: center; font-weight: 700; border-radius: 4px; padding: 0 4px; margin-right: 4px; font-size: 12px; }
|
|
||||||
.ip-score-best { background: #dcefdc; color: #2e7d32; }
|
|
||||||
.ip-score-avoid { background: #f6dcd8; color: #b23b2e; }
|
|
||||||
|
|
||||||
.ip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 18px; }
|
|
||||||
.ip-card { background: #fffdf7; border: 1px solid #e4ddcc; border-radius: 12px; padding: 14px; display: flex; flex-direction: column; }
|
|
||||||
.ip-card.is-best { border-color: #2e7d32; box-shadow: 0 0 0 2px #2e7d3233; }
|
|
||||||
.ip-card.is-avoid { border-color: #b23b2e; box-shadow: 0 0 0 2px #b23b2e33; }
|
|
||||||
.ip-card-head { display: flex; justify-content: space-between; font-size: 11px; color: #7a7360; text-transform: uppercase; letter-spacing: .04em; }
|
|
||||||
.ip-title { font-size: 15px; margin: 6px 0 10px; }
|
|
||||||
|
|
||||||
.ip-preview { position: relative; height: 260px; background: #efe9db; border: 1px solid #e4ddcc; border-radius: 8px; overflow: hidden; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
|
||||||
.ip-thumb-frame { width: 250%; height: 250%; border: 0; transform: scale(0.4); transform-origin: top left; pointer-events: none; }
|
|
||||||
.ip-load { background: #1c1c17; color: #f7f4ec; border: 0; border-radius: 6px; padding: 8px 14px; cursor: pointer; font-size: 13px; }
|
|
||||||
.ip-open-hint { position: absolute; bottom: 6px; right: 8px; font-size: 11px; background: #1c1c17cc; color: #f7f4ec; padding: 2px 6px; border-radius: 4px; }
|
|
||||||
|
|
||||||
.ip-facts { margin: 12px 0; font-size: 12.5px; }
|
|
||||||
.ip-facts dt { font-weight: 700; color: #55503f; margin-top: 6px; }
|
|
||||||
.ip-facts dd { margin: 2px 0 0; color: #333; }
|
|
||||||
|
|
||||||
.ip-grade { margin-top: auto; border-top: 1px dashed #d9d2c2; padding-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
.ip-label-row, .ip-notes-row { display: flex; flex-direction: column; gap: 3px; font-size: 12px; font-weight: 600; color: #55503f; }
|
|
||||||
.ip-label-row input, .ip-notes-row textarea { font: inherit; font-weight: 400; color: #1c1c17; border: 1px solid #d9d2c2; border-radius: 6px; padding: 6px 8px; }
|
|
||||||
.ip-score-row { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; color: #55503f; }
|
|
||||||
.ip-score-row input { flex: 1; }
|
|
||||||
.ip-score-row output { min-width: 42px; text-align: right; font-weight: 700; color: #1c1c17; }
|
|
||||||
.ip-cat-row { display: flex; gap: 6px; }
|
|
||||||
.ip-cat { flex: 1; border: 1px solid #d9d2c2; background: #fff; border-radius: 6px; padding: 6px 4px; font-size: 12px; cursor: pointer; }
|
|
||||||
.ip-cat-best.on { background: #2e7d32; color: #fff; border-color: #2e7d32; }
|
|
||||||
.ip-cat-avoid.on { background: #b23b2e; color: #fff; border-color: #b23b2e; }
|
|
||||||
.ip-cat-clear.on { background: #d9d2c2; }
|
|
||||||
|
|
||||||
.ip-dialog { border: 0; padding: 0; border-radius: 12px; width: min(92vw, 1100px); height: 90vh; max-width: 92vw; max-height: 90vh; background: #f7f4ec; overflow: hidden; display: flex; flex-direction: column; }
|
|
||||||
.ip-dialog:not([open]) { display: none; }
|
|
||||||
.ip-dialog::backdrop { background: #000000cc; }
|
|
||||||
.ip-modal-bar { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid #d9d2c2; flex: 0 0 auto; }
|
|
||||||
.ip-full-frame { flex: 1 1 auto; width: 100%; border: 0; background: #fff; }
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
<!--
|
|
||||||
india-gallery.vue — the review gallery itself (header, summary, cards, grading, modal).
|
|
||||||
|
|
||||||
★ DEV-ONLY MODULE. This component is imported ONLY from india-posters.vue behind
|
|
||||||
import.meta.env.DEV (a compile-time false in every build), so in a production build Rollup never
|
|
||||||
pulls this module into the graph — and with it, attempts.json, the grades seed, and every poster
|
|
||||||
document stay out of public/ entirely. That is why this file may use ordinary static imports and
|
|
||||||
import.meta.glob freely: none of it is reachable from a shipped bundle. See india-posters.vue +
|
|
||||||
README.md for the B25 rationale. Styling lives in india-posters.vue (the custom element's root),
|
|
||||||
injected once into the shadow root, so this child needs no <style> of its own.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<div class="ip-gallery">
|
|
||||||
<header class="ip-top">
|
|
||||||
<div>
|
|
||||||
<h1>India Posters — design attempts</h1>
|
|
||||||
<p class="ip-sub">
|
|
||||||
Every attempt at putting all ~855 India people groups on one poster. Open each, then
|
|
||||||
<b>label</b> it, score it <b>1–10</b>, and tag it <b>Best practice</b> or <b>To avoid</b>
|
|
||||||
so we keep a record of what to repeat and what to steer clear of.
|
|
||||||
<em>Preview only — withheld from every public build.</em>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="ip-actions">
|
|
||||||
<button class="ip-btn" @click="exportJson">Export record (JSON)</button>
|
|
||||||
<label class="ip-btn ip-file">
|
|
||||||
Import record
|
|
||||||
<input type="file" accept="application/json" @change="importJson" hidden />
|
|
||||||
</label>
|
|
||||||
<span class="ip-saved" v-if="savedFlash">saved ✓</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="ip-summary" v-if="graded.best.length || graded.avoid.length">
|
|
||||||
<div class="ip-col">
|
|
||||||
<h2 class="ip-h-best">Best practice ({{ graded.best.length }})</h2>
|
|
||||||
<ol>
|
|
||||||
<li v-for="a in graded.best" :key="a.id">
|
|
||||||
<span class="ip-score ip-score-best">{{ rec[a.id].score }}</span>
|
|
||||||
{{ rec[a.id].label || a.title }}
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
<div class="ip-col">
|
|
||||||
<h2 class="ip-h-avoid">To avoid ({{ graded.avoid.length }})</h2>
|
|
||||||
<ol>
|
|
||||||
<li v-for="a in graded.avoid" :key="a.id">
|
|
||||||
<span class="ip-score ip-score-avoid">{{ rec[a.id].score }}</span>
|
|
||||||
{{ rec[a.id].label || a.title }}
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="ip-grid">
|
|
||||||
<article
|
|
||||||
v-for="a in attempts"
|
|
||||||
:key="a.id"
|
|
||||||
class="ip-card"
|
|
||||||
:class="{ 'is-best': rec[a.id].category === 'best', 'is-avoid': rec[a.id].category === 'avoid' }"
|
|
||||||
>
|
|
||||||
<div class="ip-card-head">
|
|
||||||
<span class="ip-family">{{ a.family }}</span>
|
|
||||||
<span class="ip-size">{{ a.size }}</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="ip-title">{{ a.title }}</h3>
|
|
||||||
|
|
||||||
<div class="ip-preview" @click="open(a)">
|
|
||||||
<iframe
|
|
||||||
v-if="srcdoc[a.id]"
|
|
||||||
:srcdoc="srcdoc[a.id]"
|
|
||||||
class="ip-thumb-frame"
|
|
||||||
title="poster preview"
|
|
||||||
></iframe>
|
|
||||||
<button v-else class="ip-load" @click.stop="reveal(a)">Load preview</button>
|
|
||||||
<span class="ip-open-hint" v-if="srcdoc[a.id]">click to open full →</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl class="ip-facts">
|
|
||||||
<dt>What we tried</dt>
|
|
||||||
<dd>{{ a.tried }}</dd>
|
|
||||||
<dt>Measured result</dt>
|
|
||||||
<dd>{{ a.result }}</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<div class="ip-grade">
|
|
||||||
<label class="ip-label-row">
|
|
||||||
<span>Label</span>
|
|
||||||
<input type="text" v-model="rec[a.id].label" :placeholder="a.title" @input="save" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="ip-score-row">
|
|
||||||
<span>Score</span>
|
|
||||||
<input type="range" min="0" max="10" step="1" v-model.number="rec[a.id].score" @input="save" />
|
|
||||||
<output>{{ rec[a.id].score === 0 ? '–' : rec[a.id].score }}/10</output>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ip-cat-row">
|
|
||||||
<button class="ip-cat ip-cat-best" :class="{ on: rec[a.id].category === 'best' }" @click="setCat(a.id, 'best')">Best practice</button>
|
|
||||||
<button class="ip-cat ip-cat-avoid" :class="{ on: rec[a.id].category === 'avoid' }" @click="setCat(a.id, 'avoid')">To avoid</button>
|
|
||||||
<button class="ip-cat ip-cat-clear" :class="{ on: !rec[a.id].category }" @click="setCat(a.id, '')">Ungraded</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="ip-notes-row">
|
|
||||||
<span>Notes</span>
|
|
||||||
<textarea v-model="rec[a.id].notes" rows="2" @input="save" placeholder="What to repeat / what to steer clear of…"></textarea>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Native <dialog> + showModal(): its top-layer promotion escapes the shell pane's
|
|
||||||
contain:layout (a position:fixed overlay would be trapped in the pane), and Escape-to-close
|
|
||||||
comes for free. @click.self closes on a backdrop click. -->
|
|
||||||
<dialog ref="dlg" class="ip-dialog" @close="openId = ''" @click.self="closeDialog">
|
|
||||||
<div class="ip-modal-bar">
|
|
||||||
<strong>{{ current.title }}</strong>
|
|
||||||
<button class="ip-btn" @click="closeDialog">Close ✕</button>
|
|
||||||
</div>
|
|
||||||
<iframe v-if="openId" :srcdoc="srcdoc[openId]" class="ip-full-frame" title="poster full"></iframe>
|
|
||||||
</dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, ref, computed, onMounted, nextTick } from 'vue'
|
|
||||||
import data from '../data/attempts.json'
|
|
||||||
import seed from '../data/grades.seed.json'
|
|
||||||
|
|
||||||
const STORE_KEY = 'india-posters.grades.v1'
|
|
||||||
// Lazy per-poster loaders: each resolves the poster document as a string, on demand. Fine here —
|
|
||||||
// this whole module is dev-only, so none of these assets reach a production build.
|
|
||||||
const posters = import.meta.glob('../data/posters/*.html', { query: '?raw', import: 'default' })
|
|
||||||
|
|
||||||
const attempts = data.attempts
|
|
||||||
const openId = ref('')
|
|
||||||
const dlg = ref(null)
|
|
||||||
const current = computed(() => attempts.find((a) => a.id === openId.value) || {})
|
|
||||||
const srcdoc = reactive({}) // id → loaded poster HTML string
|
|
||||||
const savedFlash = ref(false)
|
|
||||||
|
|
||||||
const blank = (a) => ({ label: a.title, score: 0, category: '', notes: '' })
|
|
||||||
const rec = reactive({})
|
|
||||||
for (const a of attempts) rec[a.id] = blank(a)
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
const merged = { ...(seed || {}) }
|
|
||||||
try {
|
|
||||||
const raw = window.localStorage.getItem(STORE_KEY)
|
|
||||||
if (raw) Object.assign(merged, JSON.parse(raw))
|
|
||||||
} catch {}
|
|
||||||
for (const a of attempts) if (merged[a.id]) Object.assign(rec[a.id], merged[a.id])
|
|
||||||
}
|
|
||||||
let flashT
|
|
||||||
function save() {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(STORE_KEY, JSON.stringify(rec))
|
|
||||||
savedFlash.value = true
|
|
||||||
clearTimeout(flashT)
|
|
||||||
flashT = setTimeout(() => (savedFlash.value = false), 1200)
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
function setCat(id, cat) {
|
|
||||||
rec[id].category = rec[id].category === cat ? '' : cat
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
async function reveal(a) {
|
|
||||||
if (srcdoc[a.id]) return
|
|
||||||
const loader = posters[`../data/posters/${a.file}`]
|
|
||||||
if (!loader) return
|
|
||||||
try { srcdoc[a.id] = await loader() } catch {}
|
|
||||||
}
|
|
||||||
async function open(a) {
|
|
||||||
await reveal(a)
|
|
||||||
openId.value = a.id
|
|
||||||
await nextTick()
|
|
||||||
if (dlg.value && !dlg.value.open) dlg.value.showModal()
|
|
||||||
}
|
|
||||||
function closeDialog() {
|
|
||||||
if (dlg.value && dlg.value.open) dlg.value.close()
|
|
||||||
else openId.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const graded = computed(() => {
|
|
||||||
const best = attempts.filter((a) => rec[a.id].category === 'best').sort((x, y) => rec[y.id].score - rec[x.id].score)
|
|
||||||
const avoid = attempts.filter((a) => rec[a.id].category === 'avoid').sort((x, y) => rec[y.id].score - rec[x.id].score)
|
|
||||||
return { best, avoid }
|
|
||||||
})
|
|
||||||
|
|
||||||
function exportJson() {
|
|
||||||
const blob = new Blob([JSON.stringify(rec, null, 2)], { type: 'application/json' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = 'india-posters.grades.json'
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
function importJson(e) {
|
|
||||||
const file = e.target.files && e.target.files[0]
|
|
||||||
if (!file) return
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = () => {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(String(reader.result))
|
|
||||||
for (const a of attempts) if (obj[a.id]) Object.assign(rec[a.id], obj[a.id])
|
|
||||||
save()
|
|
||||||
} catch { /* ignore malformed */ }
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
|
||||||
413
public/dx-maps/bless-india-posters/bless-india-posters.js
Normal file
413
public/dx-maps/bless-india-posters/bless-india-posters.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -9,7 +9,7 @@
|
||||||
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
|
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
|
||||||
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
|
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
|
||||||
<script>window.MAP_OWNS_PAGE = true;</script>
|
<script>window.MAP_OWNS_PAGE = true;</script>
|
||||||
<title>india-posters · india-posters</title>
|
<title>bless-india-posters · bless-india-posters</title>
|
||||||
<link rel="icon" href="data:,">
|
<link rel="icon" href="data:,">
|
||||||
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
|
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
|
||||||
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
|
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
is the fallback for a browser without svh, which would otherwise size the box to its content.
|
is the fallback for a browser without svh, which would otherwise size the box to its content.
|
||||||
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
|
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
|
||||||
html, body { height: 100%; margin: 0; background: #0a0e15; }
|
html, body { height: 100%; margin: 0; background: #0a0e15; }
|
||||||
#slot, india-posters { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
|
#slot, bless-india-posters { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
|
||||||
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
|
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
|
||||||
padding: 24px; text-align: center; color: #9aa7b8;
|
padding: 24px; text-align: center; color: #9aa7b8;
|
||||||
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
page — a worse failure than the one being fixed, and the discriminator is not isMap
|
page — a worse failure than the one being fixed, and the discriminator is not isMap
|
||||||
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
|
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
|
||||||
box for the map case while letting a taller document grow onto page two. */
|
box for the map case while letting a taller document grow onto page two. */
|
||||||
#slot, india-posters { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
|
#slot, bless-india-posters { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -66,8 +66,8 @@
|
||||||
<div id="status">loading…</div>
|
<div id="status">loading…</div>
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <india-posters> custom element. -->
|
<!-- Bundle IIFE — registers the <bless-india-posters> custom element. -->
|
||||||
<script src="../india-posters.js" integrity="sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr" crossorigin="anonymous"></script>
|
<script src="../bless-india-posters.js" integrity="sha384-8Ez7kScUuBLOl3EtuawUdEElRWEsyxf4eDzWrJblygS9WovmfQ6KyCLIdDOcF/XQ" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
(function () {
|
(function () {
|
||||||
var slot = document.getElementById('slot');
|
var slot = document.getElementById('slot');
|
||||||
var status = document.getElementById('status');
|
var status = document.getElementById('status');
|
||||||
var BASE_CONFIG = {"profile":"india-posters","dataSource":"pray-tools","instanceId":"india-posters-india-posters","assetsBase":"../../assets"};
|
var BASE_CONFIG = {"profile":"bless-india-posters","dataSource":"pray-tools","instanceId":"bless-india-posters-bless-india-posters","assetsBase":"../../assets"};
|
||||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||||
// the ladder below is skipped entirely and the element mounts immediately.
|
// the ladder below is skipped entirely and the element mounts immediately.
|
||||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||||
|
|
@ -140,7 +140,7 @@
|
||||||
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
|
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
|
||||||
try {
|
try {
|
||||||
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
|
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
|
||||||
var el = document.createElement("india-posters");
|
var el = document.createElement("bless-india-posters");
|
||||||
el.setAttribute('profile-config', JSON.stringify(cfg));
|
el.setAttribute('profile-config', JSON.stringify(cfg));
|
||||||
slot.innerHTML = '';
|
slot.innerHTML = '';
|
||||||
slot.appendChild(el);
|
slot.appendChild(el);
|
||||||
|
|
@ -138,6 +138,34 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"bundle": "bless-india-posters",
|
||||||
|
"scriptUrl": "./bless-india-posters/bless-india-posters.js",
|
||||||
|
"output": "dx-maps/bless-india-posters/bless-india-posters.js",
|
||||||
|
"integrity": "sha384-8Ez7kScUuBLOl3EtuawUdEElRWEsyxf4eDzWrJblygS9WovmfQ6KyCLIdDOcF/XQ",
|
||||||
|
"elements": [
|
||||||
|
"bless-india-posters"
|
||||||
|
],
|
||||||
|
"stage": "experimental",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile": "bless-india-posters",
|
||||||
|
"stage": "experimental",
|
||||||
|
"label": "Bless India Posters",
|
||||||
|
"file": "bless-india-posters.vue",
|
||||||
|
"description": "A preview surface for the whole-India poster problem — fitting every one of India's people groups onto a single printable sheet. Each design attempt is generated live from the current data (nothing stored) and shown as its printable sheets with the glue strips, headed \"Bless India in Jesus' name\" with the Numbers 6:24-26 blessing. Experimental; a phone sees the preview but Print is blocked until a design is proven safe on a phone.",
|
||||||
|
"element": "bless-india-posters",
|
||||||
|
"type": "single",
|
||||||
|
"isMap": false,
|
||||||
|
"iframeUrl": "./bless-india-posters/bless-india-posters/index.html",
|
||||||
|
"scriptUrl": "./bless-india-posters/bless-india-posters.js",
|
||||||
|
"embed": {
|
||||||
|
"iframe": "<iframe src=\"./bless-india-posters/bless-india-posters/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||||
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./bless-india-posters/bless-india-posters.js\" integrity=\"sha384-8Ez7kScUuBLOl3EtuawUdEElRWEsyxf4eDzWrJblygS9WovmfQ6KyCLIdDOcF/XQ\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<bless-india-posters profile-config='{\"profile\":\"bless-india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bless-india-posters>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"bundle": "cities-internet",
|
"bundle": "cities-internet",
|
||||||
"scriptUrl": "./cities-internet/cities-internet.js",
|
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||||
|
|
@ -925,34 +953,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"bundle": "india-posters",
|
|
||||||
"scriptUrl": "./india-posters/india-posters.js",
|
|
||||||
"output": "dx-maps/india-posters/india-posters.js",
|
|
||||||
"integrity": "sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr",
|
|
||||||
"elements": [
|
|
||||||
"india-posters"
|
|
||||||
],
|
|
||||||
"stage": "experimental",
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"profile": "india-posters",
|
|
||||||
"stage": "experimental",
|
|
||||||
"label": "India Posters (design attempts — preview only)",
|
|
||||||
"file": "india-posters.vue",
|
|
||||||
"description": "A preview-only review board for the whole-India poster problem — fitting every one of",
|
|
||||||
"element": "india-posters",
|
|
||||||
"type": "single",
|
|
||||||
"isMap": false,
|
|
||||||
"iframeUrl": "./india-posters/india-posters/index.html",
|
|
||||||
"scriptUrl": "./india-posters/india-posters.js",
|
|
||||||
"embed": {
|
|
||||||
"iframe": "<iframe src=\"./india-posters/india-posters/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
|
||||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./india-posters/india-posters.js\" integrity=\"sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<india-posters profile-config='{\"profile\":\"india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></india-posters>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"bundle": "my-upg-100-list",
|
"bundle": "my-upg-100-list",
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -138,6 +138,34 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"bundle": "bless-india-posters",
|
||||||
|
"scriptUrl": "./bless-india-posters/bless-india-posters.js",
|
||||||
|
"output": "dx-maps/bless-india-posters/bless-india-posters.js",
|
||||||
|
"integrity": "sha384-8Ez7kScUuBLOl3EtuawUdEElRWEsyxf4eDzWrJblygS9WovmfQ6KyCLIdDOcF/XQ",
|
||||||
|
"elements": [
|
||||||
|
"bless-india-posters"
|
||||||
|
],
|
||||||
|
"stage": "experimental",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile": "bless-india-posters",
|
||||||
|
"stage": "experimental",
|
||||||
|
"label": "Bless India Posters",
|
||||||
|
"file": "bless-india-posters.vue",
|
||||||
|
"description": "A preview surface for the whole-India poster problem — fitting every one of India's people groups onto a single printable sheet. Each design attempt is generated live from the current data (nothing stored) and shown as its printable sheets with the glue strips, headed \"Bless India in Jesus' name\" with the Numbers 6:24-26 blessing. Experimental; a phone sees the preview but Print is blocked until a design is proven safe on a phone.",
|
||||||
|
"element": "bless-india-posters",
|
||||||
|
"type": "single",
|
||||||
|
"isMap": false,
|
||||||
|
"iframeUrl": "./bless-india-posters/bless-india-posters/index.html",
|
||||||
|
"scriptUrl": "./bless-india-posters/bless-india-posters.js",
|
||||||
|
"embed": {
|
||||||
|
"iframe": "<iframe src=\"./bless-india-posters/bless-india-posters/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||||
|
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./bless-india-posters/bless-india-posters.js\" integrity=\"sha384-8Ez7kScUuBLOl3EtuawUdEElRWEsyxf4eDzWrJblygS9WovmfQ6KyCLIdDOcF/XQ\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<bless-india-posters profile-config='{\"profile\":\"bless-india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bless-india-posters>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"bundle": "cities-internet",
|
"bundle": "cities-internet",
|
||||||
"scriptUrl": "./cities-internet/cities-internet.js",
|
"scriptUrl": "./cities-internet/cities-internet.js",
|
||||||
|
|
@ -925,34 +953,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"bundle": "india-posters",
|
|
||||||
"scriptUrl": "./india-posters/india-posters.js",
|
|
||||||
"output": "dx-maps/india-posters/india-posters.js",
|
|
||||||
"integrity": "sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr",
|
|
||||||
"elements": [
|
|
||||||
"india-posters"
|
|
||||||
],
|
|
||||||
"stage": "experimental",
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"profile": "india-posters",
|
|
||||||
"stage": "experimental",
|
|
||||||
"label": "India Posters (design attempts — preview only)",
|
|
||||||
"file": "india-posters.vue",
|
|
||||||
"description": "A preview-only review board for the whole-India poster problem — fitting every one of",
|
|
||||||
"element": "india-posters",
|
|
||||||
"type": "single",
|
|
||||||
"isMap": false,
|
|
||||||
"iframeUrl": "./india-posters/india-posters/index.html",
|
|
||||||
"scriptUrl": "./india-posters/india-posters.js",
|
|
||||||
"embed": {
|
|
||||||
"iframe": "<iframe src=\"./india-posters/india-posters/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
|
||||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./india-posters/india-posters.js\" integrity=\"sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<india-posters profile-config='{\"profile\":\"india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></india-posters>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"bundle": "my-upg-100-list",
|
"bundle": "my-upg-100-list",
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue