Deep Research, Posterverse and the site's maps from the dx-maps output folder
- public/dx-maps: the dx-maps output folder (doxa-maps e651da54f), copied unchanged. - Home, Pray, Adopt and Research: their maps load from /dx-maps/ with the same presets. They use Mapbox with the site's token, as before, and the keyless MapLibre when there is none. Under the Research map, one new line links to /deep-research. - /regions: each region card gets a "View country posters" toggle (app/components/PosterSlot.vue) that opens the region's country posters from /dx-maps/; a poster's "Show dashboard" opens the country's own page. - /deep-research: every research map, dashboard and list in one full-page app (no site header or footer; "Back to homepage" in the app's menu). Hidden: in no menu, not indexed. - /posterverse: the poster plugins in one full-page app, hidden the same way. - Each app's own language menu switches the site's language (the language is in the address); the open tab carries over. - Each page asks for a fresh copy of its dx-maps script before adding it, so a returning reader never runs a stale one. - Every other file is as it was.
179
app/components/PosterSlot.vue
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<script lang="ts">
|
||||
// The posters' script is shared by every PosterSlot on the page: it loads once, on the first opening anywhere.
|
||||
const POSTERS_TAG = 'dxm-poster-section'
|
||||
const POSTERS_SCRIPT = '/dx-maps/country-posters/country-posters.js'
|
||||
// FRESH, NOT STALE: a returning reader's browser may keep /dx-maps/ files for hours (no cache rule is sent for them), so
|
||||
// the script is asked for once with cache 'no-cache' before it is added (a 304 when unchanged; a changed file replaces the
|
||||
// stale copy), then added at its PLAIN address, so DX Maps' gallery still recognises it and never runs it twice.
|
||||
function revalidate(src: string): Promise<void> {
|
||||
return fetch(src, { cache: 'no-cache', credentials: 'same-origin' })
|
||||
.then((r) => { if (r.status === 200 && r.body) r.body.cancel() })
|
||||
.catch(() => {})
|
||||
}
|
||||
let postersLoad: Promise<void> | null = null
|
||||
function loadPosters(prayBaseUrl: string): Promise<void> {
|
||||
if (customElements.get(POSTERS_TAG)) return Promise.resolve()
|
||||
postersLoad ??= revalidate(POSTERS_SCRIPT).then(() => new Promise<void>((resolve, reject) => {
|
||||
// the prayer API the posters read is set before the script runs
|
||||
if (prayBaseUrl) window.MAP_APP_API_URL = prayBaseUrl
|
||||
const script = document.createElement('script')
|
||||
script.src = POSTERS_SCRIPT
|
||||
script.onload = () => customElements.get(POSTERS_TAG) ? resolve() : reject(new Error('the posters bundle did not register'))
|
||||
script.onerror = () => reject(new Error('the posters bundle failed to load'))
|
||||
document.head.appendChild(script)
|
||||
})).catch((err) => {
|
||||
postersLoad = null // a later opening tries again
|
||||
throw err
|
||||
})
|
||||
return postersLoad
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { countrySlug } from '~~/config/countries-meta'
|
||||
/**
|
||||
* PosterSlot: a layout component, like DoxaMapSlot. A "View country posters" disclosure that, when opened,
|
||||
* drops down and embeds DX Maps' poster section for one region (<dxm-poster-section>, from this site's own
|
||||
* /dx-maps/ copy). The posters are DX Maps'; this only places them and owns the toggle, so the element is
|
||||
* asked for no header of its own (show-header="false").
|
||||
*
|
||||
* Its markup and look are the Regions plugin's card (DX Maps library/components/regions/RegionSectionCard.vue,
|
||||
* itself taken from this site's /regions card): a second footer row under the card's dashboard link, a caret
|
||||
* that turns to point down when open (the closed caret mirrors in RTL like the site's rotate-90; the open one
|
||||
* does not, like rotate-180), aria-expanded and aria-controls, and the posters open underneath.
|
||||
*/
|
||||
interface Props {
|
||||
/** The region's /regions slug, e.g. "latin-america-and-caribbean": the poster section's scope. */
|
||||
scope: string
|
||||
/** The poster section's type. */
|
||||
type?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { type: 'country' })
|
||||
|
||||
const { locale } = useI18n()
|
||||
const localePath = useLocalePath()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
// DX Maps' own words for the two states (library/i18n/locales/<lang>/regions.json), in each language the site
|
||||
// has; English for any other.
|
||||
const LABELS: Record<string, [string, string]> = {
|
||||
en: ['View country posters', 'Hide country posters'],
|
||||
ar: ['عرض ملصقات الدولة', 'إخفاء ملصقات الدولة'],
|
||||
de: ['Länderposter ansehen', 'Länderposter ausblenden'],
|
||||
es: ['Ver los carteles del país', 'Ocultar los carteles del país'],
|
||||
fr: ['Voir les affiches du pays', 'Masquer les affiches du pays'],
|
||||
hi: ['देश के पोस्टर देखें', 'देश के पोस्टर छिपाएँ'],
|
||||
it: ['Vedi i poster del paese', 'Nascondi i poster del paese'],
|
||||
pt: ['Ver os cartazes do país', 'Ocultar os cartazes do país'],
|
||||
ro: ['Vezi posterele țării', 'Ascunde posterele țării'],
|
||||
ru: ['Показать плакаты страны', 'Скрыть плакаты страны'],
|
||||
zh: ['查看国家海报', '隐藏国家海报']
|
||||
}
|
||||
|
||||
const open = ref(false)
|
||||
const label = computed(() => (LABELS[locale.value] || LABELS.en)[open.value ? 1 : 0])
|
||||
const disclosureId = `poster-slot-${props.scope}`
|
||||
const posters = ref<HTMLElement | null>(null)
|
||||
|
||||
async function toggle() {
|
||||
open.value = !open.value
|
||||
if (!open.value || !posters.value || posters.value.firstElementChild) return
|
||||
const prayBaseUrl = (runtimeConfig.public.prayBaseUrl as string) || ''
|
||||
try {
|
||||
await loadPosters(prayBaseUrl)
|
||||
} catch (err) {
|
||||
console.error('[poster slot]', err)
|
||||
return
|
||||
}
|
||||
const section = document.createElement(POSTERS_TAG)
|
||||
section.setAttribute('type', props.type)
|
||||
section.setAttribute('scope', props.scope)
|
||||
section.setAttribute('locale', locale.value)
|
||||
section.setAttribute('show-header', 'false')
|
||||
section.setAttribute('min-card-px', '150')
|
||||
section.setAttribute('collapsible', 'false')
|
||||
// each poster card's button reads "Show dashboard" and asks for the country's own page (the site decides the address)
|
||||
section.setAttribute('card-action', 'dashboard')
|
||||
section.setAttribute('assets-base', '/dx-maps/assets')
|
||||
if (prayBaseUrl) section.setAttribute('api-base-url', prayBaseUrl)
|
||||
posters.value.appendChild(section)
|
||||
}
|
||||
|
||||
// "Show dashboard" on a poster card: the element sends dxm-poster-dashboard ({ iso3, name, … }, composed) and this goes
|
||||
// to the country's own page, /regions/<slug>, by the same slug rule as the regions page's country links.
|
||||
function onDashboard(event: Event) {
|
||||
const detail = (event as CustomEvent<{ iso3?: string, name?: string }>).detail
|
||||
if (!detail?.iso3) return
|
||||
event.preventDefault()
|
||||
navigateTo(localePath(`/regions/${countrySlug(detail.iso3, detail.name)}`))
|
||||
}
|
||||
onMounted(() => posters.value?.addEventListener('dxm-poster-dashboard', onDashboard))
|
||||
onBeforeUnmount(() => posters.value?.removeEventListener('dxm-poster-dashboard', onDashboard))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="region-card__footer region-card__posters-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="region-card__footer-link | with-icon"
|
||||
:aria-expanded="open ? 'true' : 'false'"
|
||||
:aria-controls="disclosureId"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ label }}
|
||||
<svg
|
||||
class="icon | right"
|
||||
:class="open ? 'rotate-180' : 'rotate-90'"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/assets/icons/arrow-chevron.svg#chevron-up" />
|
||||
</svg>
|
||||
</button>
|
||||
</p>
|
||||
<div
|
||||
v-show="open"
|
||||
:id="disclosureId"
|
||||
ref="posters"
|
||||
class="region-card__posters"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* The region card's footer row, as /regions draws it (flex, flex-end, margin-top --spacing-lg); the toggle row
|
||||
sits directly under the dashboard link's row (the Regions plugin's .region-card__posters-toggle). */
|
||||
.region-card__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.region-card__posters-toggle {
|
||||
/* a little room under the dashboard link (the Driver: "a little bit more") */
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* A <button> that reads as the footer's link: the site's own `a` rule (base/_globals.scss). */
|
||||
.region-card__footer-link {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: var(--color-brand-light);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 4px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.region-card__footer-link:hover {
|
||||
color: var(--color-brand-lighter);
|
||||
}
|
||||
|
||||
.region-card__posters {
|
||||
margin-top: 13.3px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -29,11 +29,133 @@ onMounted(() => reload())
|
|||
const config = useRuntimeConfig()
|
||||
const mapboxToken = config.public.mapboxToken as string
|
||||
|
||||
const adoptMapConfig = JSON.stringify({
|
||||
// THE MAP comes from this site's own copy of the dx-maps output (public/dx-maps/) and is loaded here,
|
||||
// not through DoxaMapSlot, which loads the older bundle in public/js/. Mapbox with the site's token, as
|
||||
// before; the keyless MapLibre when there is none. Its styles and boundaries live in the same tree, and
|
||||
// the site's language is the map's.
|
||||
const adoptMapConfig = computed(() => JSON.stringify({
|
||||
profile: 'doxa-simple-map',
|
||||
dataSource: 'pray-tools',
|
||||
tk: mapboxToken,
|
||||
tabs: [{ id: 'adoption', colorStrategy: 'adoption', legend: 'adoption', popup: 'adoption' }]
|
||||
tabs: [{ id: 'adoption', colorStrategy: 'adoption', legend: 'adoption', popup: 'adoption' }],
|
||||
provider: mapboxToken ? 'mapbox' : 'maplibre',
|
||||
...(mapboxToken ? { tk: mapboxToken } : {}),
|
||||
assetsBase: '/dx-maps/assets',
|
||||
locale: locale.value
|
||||
}))
|
||||
// THE MAP'S ELEMENT, <dxm-simple-map>: DX Maps' own name for it, so it never meets the older <doxa-map> elements
|
||||
// elsewhere on the site, in whatever order the pages are visited. Its bundle comes from this site's own copy of
|
||||
// the dx-maps output and is added once per visit. A load in progress is shared, and a failed load takes its tag
|
||||
// away again, so a later page tries afresh; the page then shows without the map.
|
||||
const MAP_TAG = 'dxm-simple-map'
|
||||
const MAP_SCRIPT = '/dx-maps/doxa-simple-map/doxa-simple-map.js'
|
||||
// FRESH, NOT STALE: a returning reader's browser may keep /dx-maps/ files for hours (no cache rule is sent for them), so
|
||||
// the script is asked for once with cache 'no-cache' before it is added (a 304 when unchanged; a changed file replaces the
|
||||
// stale copy), then added at its PLAIN address, so DX Maps' gallery still recognises it and never runs it twice.
|
||||
function revalidate(src: string): Promise<void> {
|
||||
return fetch(src, { cache: 'no-cache', credentials: 'same-origin' })
|
||||
.then((r) => { if (r.status === 200 && r.body) r.body.cancel() })
|
||||
.catch(() => {})
|
||||
}
|
||||
function loadMapBundle(): Promise<void> {
|
||||
if (customElements.get(MAP_TAG)) return Promise.resolve()
|
||||
return revalidate(MAP_SCRIPT).then(() => addMapBundle())
|
||||
}
|
||||
function addMapBundle(): Promise<void> {
|
||||
if (customElements.get(MAP_TAG)) return Promise.resolve()
|
||||
const pending = document.querySelector<HTMLScriptElement>('script[data-dxl-map="' + MAP_TAG + '"]')
|
||||
if (pending) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.addEventListener('load', () => customElements.get(MAP_TAG) ? resolve() : reject(new Error('the map bundle did not register')), { once: true })
|
||||
pending.addEventListener('error', () => reject(new Error('the map bundle failed to load')), { once: true })
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
const fail = (message: string) => {
|
||||
script.remove()
|
||||
reject(new Error(message))
|
||||
}
|
||||
script.src = MAP_SCRIPT
|
||||
script.dataset.dxlMap = MAP_TAG
|
||||
script.onload = () => customElements.get(MAP_TAG) ? resolve() : fail('the map bundle did not register')
|
||||
script.onerror = () => fail('the map bundle failed to load')
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
// THE PHONE HEIGHT, LOCKED IN PX: 80% of the window's height when the page opens (never under 320px), so the map
|
||||
// box does not grow and shrink while the browser's address bar collapses and expands during a scroll, which also
|
||||
// broke a drag on the map. It is measured again only when the WIDTH changes (a rotation), never on a height-only
|
||||
// resize. Wider screens keep the stylesheet's height; 80svh there is the fallback until this runs.
|
||||
// A phone: a narrow screen, or a touch screen held sideways (the same test as the stylesheet's phone rules).
|
||||
const PHONE_QUERY = '(max-width: 768px), (pointer: coarse) and (max-height: 600px)'
|
||||
function lockPhoneHeight(slot: HTMLElement): () => void {
|
||||
const PHONE = window.matchMedia(PHONE_QUERY)
|
||||
let width = -1
|
||||
const lock = () => {
|
||||
if (window.innerWidth === width) return
|
||||
width = window.innerWidth
|
||||
slot.style.height = PHONE.matches ? `${Math.max(320, Math.round(window.innerHeight * 0.8))}px` : ''
|
||||
}
|
||||
lock()
|
||||
window.addEventListener('resize', lock)
|
||||
return () => window.removeEventListener('resize', lock)
|
||||
}
|
||||
let unlockHeight: (() => void) | null = null
|
||||
onMounted(() => { if (adoptMapHost.value) unlockHeight = lockPhoneHeight(adoptMapHost.value) })
|
||||
onBeforeUnmount(() => unlockHeight?.())
|
||||
// THE SCROLL BAND STEPS ASIDE FOR THE MAP'S PANELS. A panel the map's controls open (Share, and every other menu)
|
||||
// sits at the map's top, under the band, so its ✕ could not be tapped. DX Maps says when a panel opens (the composed
|
||||
// 'dxmaps:panel-open') and when it closes ('dxmaps:panel-close'). The band counts them (another panel can still be open
|
||||
// when one closes) and stays off while the count is above 0; for builds without the close event, it also comes back
|
||||
// after any tap once no panel is open in the map any more.
|
||||
const padOff = ref(false)
|
||||
let openPanels = 0
|
||||
function mapPanelOpen(): boolean {
|
||||
const walk = (node: Element | ShadowRoot): boolean => {
|
||||
for (const n of node.querySelectorAll('*')) {
|
||||
if (n.classList.contains('mcm')) return true
|
||||
if (n.shadowRoot && walk(n.shadowRoot)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return !!adoptMap && walk(adoptMap.shadowRoot || adoptMap)
|
||||
}
|
||||
const onPanelOpen = () => { openPanels++; padOff.value = true }
|
||||
const onPanelClose = () => { openPanels = Math.max(0, openPanels - 1); padOff.value = openPanels > 0 }
|
||||
const onAnyTap = () => {
|
||||
if (padOff.value) setTimeout(() => { if (!mapPanelOpen()) { openPanels = 0; padOff.value = false } }, 50)
|
||||
}
|
||||
onMounted(() => {
|
||||
adoptMapHost.value?.addEventListener('dxmaps:panel-open', onPanelOpen)
|
||||
adoptMapHost.value?.addEventListener('dxmaps:panel-close', onPanelClose)
|
||||
document.addEventListener('click', onAnyTap, true)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
adoptMapHost.value?.removeEventListener('dxmaps:panel-open', onPanelOpen)
|
||||
adoptMapHost.value?.removeEventListener('dxmaps:panel-close', onPanelClose)
|
||||
document.removeEventListener('click', onAnyTap, true)
|
||||
})
|
||||
// The prayer API the map reads is set before its script runs; the element is created once the bundle is in.
|
||||
const adoptMapHost = ref<HTMLElement | null>(null)
|
||||
let adoptMap: HTMLElement | null = null
|
||||
onMounted(async () => {
|
||||
if (config.public.prayBaseUrl) window.MAP_APP_API_URL = config.public.prayBaseUrl as string
|
||||
try {
|
||||
await loadMapBundle()
|
||||
} catch (err) {
|
||||
console.error('[adopt map]', err)
|
||||
return
|
||||
}
|
||||
if (!adoptMapHost.value) return
|
||||
adoptMap = document.createElement(MAP_TAG)
|
||||
adoptMap.id = 'adopt-map'
|
||||
adoptMap.setAttribute('profile-config', adoptMapConfig.value)
|
||||
adoptMapHost.value.prepend(adoptMap)
|
||||
})
|
||||
watch(adoptMapConfig, (value) => { adoptMap?.setAttribute('profile-config', value) })
|
||||
onBeforeUnmount(() => {
|
||||
adoptMap?.remove()
|
||||
adoptMap = null
|
||||
})
|
||||
|
||||
useTextHighlight()
|
||||
|
|
@ -87,7 +209,9 @@ useTextHighlight()
|
|||
data-highlight-color="primary"
|
||||
>{{ t('Adoption Progress') }}</h1>
|
||||
</div>
|
||||
<DoxaMapSlot map-id="adopt-map" :profile-config="adoptMapConfig" class="rounded-md" />
|
||||
<div ref="adoptMapHost" class="doxa-map-slot rounded-md">
|
||||
<div class="doxa-map-scroll-pad doxa-map-scroll-pad--top" :class="{ 'is-off': padOff }" />
|
||||
</div>
|
||||
<NuxtLink
|
||||
:to="localePath('/research')"
|
||||
class="research-map-link"
|
||||
|
|
@ -310,4 +434,60 @@ useTextHighlight()
|
|||
.research-map-link:hover {
|
||||
color: var(--color-brand-primary-darker, #1f2328);
|
||||
}
|
||||
|
||||
/* The map's frame, as DoxaMapSlot draws it. */
|
||||
.doxa-map-slot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 780px;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 768px), (pointer: coarse) and (max-height: 600px) {
|
||||
/* On a phone the map is 80% of the SMALL viewport height, a little shorter than the screen so the page
|
||||
stays in view (svh does not jump when the browser's address bar collapses), never under 320px. */
|
||||
.doxa-map-slot {
|
||||
min-height: 320px;
|
||||
aspect-ratio: auto;
|
||||
height: 80svh;
|
||||
}
|
||||
}
|
||||
.doxa-map-slot :deep(dxm-simple-map) {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
contain: paint;
|
||||
}
|
||||
.doxa-map-scroll-pad {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 768px), (pointer: coarse) and (max-height: 600px) {
|
||||
.doxa-map-scroll-pad {
|
||||
display: block;
|
||||
position: absolute;
|
||||
/* stops short of the map's control column at the inline end (32px round buttons, 3px in), so the + and
|
||||
the other controls stay tappable; the rest of the band still lets a finger scroll the page */
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 44px;
|
||||
height: 48px;
|
||||
z-index: 1500;
|
||||
touch-action: pan-y;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.doxa-map-scroll-pad--top { top: 0; }
|
||||
/* while a map panel is open, its ✕ and its contents get every tap */
|
||||
.doxa-map-scroll-pad.is-off { display: none; }
|
||||
}
|
||||
.doxa-map-slot :deep(dxm-simple-map:fullscreen),
|
||||
.doxa-map-slot :deep(dxm-simple-map:-webkit-full-screen) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
199
app/pages/deep-research/index.vue
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<script setup lang="ts">
|
||||
// Deep Research: the Research Gallery (every research map, dashboard and list in one app) as a
|
||||
// full-page app. DX Life asked for the gallery under this name, on a page of the site rather than a
|
||||
// subdomain, and kept off the Research page. So this page is HIDDEN: it is in no menu, and robots are
|
||||
// told not to index it. It is reached by its address, or by the link under the Research page's map.
|
||||
//
|
||||
// The gallery is a web component served from DX-life's OWN origin (public/dx-maps/, a copy of
|
||||
// the dx-maps build tree), so this site is its CDN: one script, one element, and no other host for the
|
||||
// map code (the engine and base-map tiles come from public services).
|
||||
// It reads its manifest and every child map from the same /dx-maps/ tree.
|
||||
//
|
||||
// The element is created in onMounted rather than written in the template: it is a custom element
|
||||
// that only exists in the browser, so this keeps it out of SSR and out of Vue's component resolver
|
||||
// without any nuxt.config change.
|
||||
|
||||
// A full-page app on every screen: no layout, so no site header and no footer. The way back to the site
|
||||
// is the gallery's own "Back to homepage" button, directly above its Collapse button (home-href below),
|
||||
// so the gallery fills the whole screen. The page itself never scrolls (the gallery's own panes scroll).
|
||||
definePageMeta({ layout: false })
|
||||
|
||||
const { locale, locales } = useI18n()
|
||||
const localePath = useLocalePath()
|
||||
|
||||
// The page's own words, in every language the site has (English for any other); the site's shared
|
||||
// translation files stay as they are. "Back to homepage" is drawn by the app, above its Collapse button.
|
||||
const HOME_LABEL: Record<string, string> = {
|
||||
en: "Back to homepage",
|
||||
ar: "العودة إلى الصفحة الرئيسية",
|
||||
de: "Zurück zur Startseite",
|
||||
es: "Volver a la página de inicio",
|
||||
fr: "Retour à l'accueil",
|
||||
hi: "होमपेज पर वापस जाएँ",
|
||||
it: "Torna alla home page",
|
||||
pt: "Voltar à página inicial",
|
||||
ro: "Înapoi la pagina principală",
|
||||
ru: "Вернуться на главную",
|
||||
zh: "返回首页",
|
||||
}
|
||||
const homeLabel = () => HOME_LABEL[locale.value] || HOME_LABEL.en
|
||||
// "Back to homepage" goes to the public site, doxa.life, in the reader's language (https://doxa.life/,
|
||||
// https://doxa.life/ar), whichever server this page runs on.
|
||||
const homeHref = () => `https://doxa.life${localePath('/')}`
|
||||
|
||||
// The gallery comes from this site's own /dx-maps/ copy; the script and every embed URL come from it.
|
||||
const GALLERY_SCRIPT = '/dx-maps/doxa-maps-spa/doxa-maps-spa.js'
|
||||
|
||||
// FRESH, NOT STALE: a returning reader's browser may keep /dx-maps/ files for hours (no cache rule is sent for them), so
|
||||
// the script is asked for once with cache 'no-cache' before it is added (a 304 when unchanged; a changed file replaces the
|
||||
// stale copy), then added at its PLAIN address, so DX Maps' gallery still recognises it and never runs it twice.
|
||||
function revalidate(src: string): Promise<void> {
|
||||
return fetch(src, { cache: 'no-cache', credentials: 'same-origin' })
|
||||
.then((r) => { if (r.status === 200 && r.body) r.body.cancel() })
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'Deep Research',
|
||||
meta: [{ name: 'robots', content: 'noindex' }]
|
||||
})
|
||||
|
||||
// With no layout, this page sets <html lang dir> itself, as the public layout does for every other
|
||||
// page: in Arabic the bar and the gallery read right to left.
|
||||
const dir = computed(() => {
|
||||
const loc = locales.value.find(l => typeof l === 'object' && 'code' in l && l.code === locale.value)
|
||||
return (typeof loc === 'object' && loc && 'dir' in loc && loc.dir) ? loc.dir : 'ltr'
|
||||
})
|
||||
useHead(() => ({
|
||||
htmlAttrs: {
|
||||
lang: locale.value,
|
||||
dir: dir.value
|
||||
}
|
||||
}))
|
||||
|
||||
const host = ref<HTMLElement | null>(null)
|
||||
let gallery: HTMLElement | null = null
|
||||
|
||||
// THE LANGUAGE MENU INSIDE THE APP (its phone bar and its rail): on a pick it fires a cancelable
|
||||
// 'doxa-language-request' {detail.language}. This site owns the language (it is in the address: /ar/...),
|
||||
// so the page cancels the event and switches the SITE's locale; the app then follows through `language`
|
||||
// (the watch below). The tab open at that moment is carried over in ?tab=, because the localized
|
||||
// address is a new page. A code the site has no route for is cancelled and ignored.
|
||||
const route = useRoute()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
let currentTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
||||
function onTabChange(e: Event) {
|
||||
const d = (e as CustomEvent).detail
|
||||
const id = Array.isArray(d) ? d[0]?.id : d?.id
|
||||
if (typeof id === 'string' && id) currentTab = id
|
||||
}
|
||||
function onLanguageRequest(e: Event) {
|
||||
e.preventDefault()
|
||||
const code = (e as CustomEvent<{ language?: string }>).detail?.language
|
||||
const known = locales.value.some(l => (typeof l === 'string' ? l : l.code) === code)
|
||||
if (!code || !known || code === locale.value) return
|
||||
const target = switchLocalePath(code as typeof locale.value)
|
||||
if (!target) return
|
||||
const url = new URL(target, window.location.origin)
|
||||
if (currentTab) url.searchParams.set('tab', currentTab)
|
||||
navigateTo(url.pathname + url.search + url.hash)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
gallery = document.createElement('doxa-maps-spa')
|
||||
// Every embed URL is built at RUNTIME from the origin serving this page, so the same build works on
|
||||
// any host with nothing hardcoded.
|
||||
gallery.setAttribute('tree-base', `${window.location.origin}/dx-maps`)
|
||||
// The site's language drives the gallery's (and every map's); it follows the language switcher.
|
||||
gallery.setAttribute('language', locale.value)
|
||||
// A website, not the mobile app: no phone bar of the app's own (one header on a phone); its ☰ sits in the
|
||||
// corner and its language menu at the drawer's foot. Read once, when the element mounts, so it is set here.
|
||||
gallery.setAttribute('shell-mode', 'web')
|
||||
// The way back to the site: the gallery draws it above its Collapse button, in the site's own words.
|
||||
gallery.setAttribute('home-href', homeHref())
|
||||
gallery.setAttribute('home-label', homeLabel())
|
||||
// Every section shows, Experimental included (the gallery's default).
|
||||
const galleryConfig: Record<string, unknown> = {}
|
||||
// The keyless MapLibre engine, always: the app needs no token and no setting.
|
||||
galleryConfig.provider = 'maplibre'
|
||||
gallery.setAttribute('provider', 'maplibre')
|
||||
gallery.setAttribute('profile-config', JSON.stringify(galleryConfig))
|
||||
gallery.addEventListener('tab-change', onTabChange)
|
||||
gallery.addEventListener('doxa-language-request', onLanguageRequest)
|
||||
host.value?.appendChild(gallery)
|
||||
// its script, revalidated (above) and then added at its plain address, once per page load
|
||||
if (!customElements.get('doxa-maps-spa') && !document.querySelector(`script[src="${GALLERY_SCRIPT}"]`)) {
|
||||
revalidate(GALLERY_SCRIPT).then(() => {
|
||||
if (document.querySelector(`script[src="${GALLERY_SCRIPT}"]`)) return
|
||||
const script = document.createElement('script')
|
||||
script.src = GALLERY_SCRIPT
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(locale, (lang) => {
|
||||
gallery?.setAttribute('language', lang)
|
||||
gallery?.setAttribute('home-href', homeHref())
|
||||
gallery?.setAttribute('home-label', homeLabel())
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
gallery?.remove()
|
||||
gallery = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="research-gallery-page">
|
||||
<h1 class="visually-hidden">
|
||||
Deep Research
|
||||
</h1>
|
||||
<div
|
||||
ref="host"
|
||||
class="research-gallery-host"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.research-gallery-page {
|
||||
/* 100svh: the phone's SMALL viewport, so the browser's own toolbars never cover the gallery. */
|
||||
height: 100svh;
|
||||
}
|
||||
/* The gallery takes the whole screen, so there is no page scrollbar. */
|
||||
.research-gallery-host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.research-gallery-host :deep(doxa-maps-spa) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Not scoped, and keyed to this page's own element, so it applies only while this page is shown and
|
||||
no other page changes. With no layout, the site's stylesheet may not be loaded here: the page sets
|
||||
the two things it needs itself, no body margin and no page scroll. */
|
||||
html:has(.research-gallery-page),
|
||||
html:has(.research-gallery-page) body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* The site keeps room for a scrollbar on every page (scrollbar-gutter: stable, base/_globals.scss).
|
||||
This page never scrolls, so on a desktop that room would only be an empty strip at the right edge. */
|
||||
html:has(.research-gallery-page) {
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -51,11 +51,133 @@ useHead({
|
|||
: []
|
||||
})
|
||||
|
||||
const homeMapConfig = JSON.stringify({
|
||||
// THE MAP comes from this site's own copy of the dx-maps output (public/dx-maps/) and is loaded here,
|
||||
// not through DoxaMapSlot, which loads the older bundle in public/js/. Mapbox with the site's token, as
|
||||
// before; the keyless MapLibre when there is none. Its styles and boundaries live in the same tree, and
|
||||
// the site's language is the map's.
|
||||
const homeMapConfig = computed(() => JSON.stringify({
|
||||
profile: 'doxa-simple-map',
|
||||
dataSource: 'pray-tools',
|
||||
tk: mapboxToken,
|
||||
tabs: [{ id: 'engagement', colorStrategy: 'engagement', legend: 'engagement', popup: 'engagement' }]
|
||||
tabs: [{ id: 'engagement', colorStrategy: 'engagement', legend: 'engagement', popup: 'engagement' }],
|
||||
provider: mapboxToken ? 'mapbox' : 'maplibre',
|
||||
...(mapboxToken ? { tk: mapboxToken } : {}),
|
||||
assetsBase: '/dx-maps/assets',
|
||||
locale: locale.value
|
||||
}))
|
||||
// THE MAP'S ELEMENT, <dxm-simple-map>: DX Maps' own name for it, so it never meets the older <doxa-map> elements
|
||||
// elsewhere on the site, in whatever order the pages are visited. Its bundle comes from this site's own copy of
|
||||
// the dx-maps output and is added once per visit. A load in progress is shared, and a failed load takes its tag
|
||||
// away again, so a later page tries afresh; the page then shows without the map.
|
||||
const MAP_TAG = 'dxm-simple-map'
|
||||
const MAP_SCRIPT = '/dx-maps/doxa-simple-map/doxa-simple-map.js'
|
||||
// FRESH, NOT STALE: a returning reader's browser may keep /dx-maps/ files for hours (no cache rule is sent for them), so
|
||||
// the script is asked for once with cache 'no-cache' before it is added (a 304 when unchanged; a changed file replaces the
|
||||
// stale copy), then added at its PLAIN address, so DX Maps' gallery still recognises it and never runs it twice.
|
||||
function revalidate(src: string): Promise<void> {
|
||||
return fetch(src, { cache: 'no-cache', credentials: 'same-origin' })
|
||||
.then((r) => { if (r.status === 200 && r.body) r.body.cancel() })
|
||||
.catch(() => {})
|
||||
}
|
||||
function loadMapBundle(): Promise<void> {
|
||||
if (customElements.get(MAP_TAG)) return Promise.resolve()
|
||||
return revalidate(MAP_SCRIPT).then(() => addMapBundle())
|
||||
}
|
||||
function addMapBundle(): Promise<void> {
|
||||
if (customElements.get(MAP_TAG)) return Promise.resolve()
|
||||
const pending = document.querySelector<HTMLScriptElement>('script[data-dxl-map="' + MAP_TAG + '"]')
|
||||
if (pending) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.addEventListener('load', () => customElements.get(MAP_TAG) ? resolve() : reject(new Error('the map bundle did not register')), { once: true })
|
||||
pending.addEventListener('error', () => reject(new Error('the map bundle failed to load')), { once: true })
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
const fail = (message: string) => {
|
||||
script.remove()
|
||||
reject(new Error(message))
|
||||
}
|
||||
script.src = MAP_SCRIPT
|
||||
script.dataset.dxlMap = MAP_TAG
|
||||
script.onload = () => customElements.get(MAP_TAG) ? resolve() : fail('the map bundle did not register')
|
||||
script.onerror = () => fail('the map bundle failed to load')
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
// THE PHONE HEIGHT, LOCKED IN PX: 80% of the window's height when the page opens (never under 320px), so the map
|
||||
// box does not grow and shrink while the browser's address bar collapses and expands during a scroll, which also
|
||||
// broke a drag on the map. It is measured again only when the WIDTH changes (a rotation), never on a height-only
|
||||
// resize. Wider screens keep the stylesheet's height; 80svh there is the fallback until this runs.
|
||||
// A phone: a narrow screen, or a touch screen held sideways (the same test as the stylesheet's phone rules).
|
||||
const PHONE_QUERY = '(max-width: 768px), (pointer: coarse) and (max-height: 600px)'
|
||||
function lockPhoneHeight(slot: HTMLElement): () => void {
|
||||
const PHONE = window.matchMedia(PHONE_QUERY)
|
||||
let width = -1
|
||||
const lock = () => {
|
||||
if (window.innerWidth === width) return
|
||||
width = window.innerWidth
|
||||
slot.style.height = PHONE.matches ? `${Math.max(320, Math.round(window.innerHeight * 0.8))}px` : ''
|
||||
}
|
||||
lock()
|
||||
window.addEventListener('resize', lock)
|
||||
return () => window.removeEventListener('resize', lock)
|
||||
}
|
||||
let unlockHeight: (() => void) | null = null
|
||||
onMounted(() => { if (homeMapHost.value) unlockHeight = lockPhoneHeight(homeMapHost.value) })
|
||||
onBeforeUnmount(() => unlockHeight?.())
|
||||
// THE SCROLL BAND STEPS ASIDE FOR THE MAP'S PANELS. A panel the map's controls open (Share, and every other menu)
|
||||
// sits at the map's top, under the band, so its ✕ could not be tapped. DX Maps says when a panel opens (the composed
|
||||
// 'dxmaps:panel-open') and when it closes ('dxmaps:panel-close'). The band counts them (another panel can still be open
|
||||
// when one closes) and stays off while the count is above 0; for builds without the close event, it also comes back
|
||||
// after any tap once no panel is open in the map any more.
|
||||
const padOff = ref(false)
|
||||
let openPanels = 0
|
||||
function mapPanelOpen(): boolean {
|
||||
const walk = (node: Element | ShadowRoot): boolean => {
|
||||
for (const n of node.querySelectorAll('*')) {
|
||||
if (n.classList.contains('mcm')) return true
|
||||
if (n.shadowRoot && walk(n.shadowRoot)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return !!homeMap && walk(homeMap.shadowRoot || homeMap)
|
||||
}
|
||||
const onPanelOpen = () => { openPanels++; padOff.value = true }
|
||||
const onPanelClose = () => { openPanels = Math.max(0, openPanels - 1); padOff.value = openPanels > 0 }
|
||||
const onAnyTap = () => {
|
||||
if (padOff.value) setTimeout(() => { if (!mapPanelOpen()) { openPanels = 0; padOff.value = false } }, 50)
|
||||
}
|
||||
onMounted(() => {
|
||||
homeMapHost.value?.addEventListener('dxmaps:panel-open', onPanelOpen)
|
||||
homeMapHost.value?.addEventListener('dxmaps:panel-close', onPanelClose)
|
||||
document.addEventListener('click', onAnyTap, true)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
homeMapHost.value?.removeEventListener('dxmaps:panel-open', onPanelOpen)
|
||||
homeMapHost.value?.removeEventListener('dxmaps:panel-close', onPanelClose)
|
||||
document.removeEventListener('click', onAnyTap, true)
|
||||
})
|
||||
// The prayer API the map reads is set before its script runs; the element is created once the bundle is in.
|
||||
const homeMapHost = ref<HTMLElement | null>(null)
|
||||
let homeMap: HTMLElement | null = null
|
||||
onMounted(async () => {
|
||||
if (config.public.prayBaseUrl) window.MAP_APP_API_URL = config.public.prayBaseUrl as string
|
||||
try {
|
||||
await loadMapBundle()
|
||||
} catch (err) {
|
||||
console.error('[home map]', err)
|
||||
return
|
||||
}
|
||||
if (!homeMapHost.value) return
|
||||
homeMap = document.createElement(MAP_TAG)
|
||||
homeMap.id = 'home-map'
|
||||
homeMap.setAttribute('profile-config', homeMapConfig.value)
|
||||
homeMapHost.value.prepend(homeMap)
|
||||
})
|
||||
watch(homeMapConfig, (value) => { homeMap?.setAttribute('profile-config', value) })
|
||||
onBeforeUnmount(() => {
|
||||
homeMap?.remove()
|
||||
homeMap = null
|
||||
})
|
||||
|
||||
useTextHighlight()
|
||||
|
|
@ -114,7 +236,9 @@ useTextHighlight()
|
|||
data-highlight-color="primary"
|
||||
>{{ t('DOXA people groups around the world') }}</h1>
|
||||
</div>
|
||||
<DoxaMapSlot map-id="home-map" :profile-config="homeMapConfig" class="rounded-xlg" />
|
||||
<div ref="homeMapHost" class="doxa-map-slot rounded-xlg">
|
||||
<div class="doxa-map-scroll-pad doxa-map-scroll-pad--top" :class="{ 'is-off': padOff }" />
|
||||
</div>
|
||||
<NuxtLink
|
||||
:to="localePath('/research')"
|
||||
class="research-map-link"
|
||||
|
|
@ -337,4 +461,65 @@ useTextHighlight()
|
|||
.research-map-link:hover {
|
||||
color: var(--color-brand-primary-darker, #1f2328);
|
||||
}
|
||||
|
||||
/* The map's frame, as DoxaMapSlot draws it. */
|
||||
.doxa-map-slot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 780px;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 768px), (pointer: coarse) and (max-height: 600px) {
|
||||
/* On a phone the map is 80% of the SMALL viewport height, a little shorter than the screen so the page
|
||||
stays in view (svh does not jump when the browser's address bar collapses), never under 320px. */
|
||||
.doxa-map-slot {
|
||||
min-height: 320px;
|
||||
aspect-ratio: auto;
|
||||
height: 80svh;
|
||||
}
|
||||
}
|
||||
/* The card's corners are concentric with the map's top control (a 32px round button 3px in: 3 + 16 = 19px), so the
|
||||
rounded corner no longer cuts across it. */
|
||||
.doxa-map-slot.rounded-xlg {
|
||||
border-radius: 19px;
|
||||
}
|
||||
.doxa-map-slot :deep(dxm-simple-map) {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
contain: paint;
|
||||
}
|
||||
.doxa-map-scroll-pad {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 768px), (pointer: coarse) and (max-height: 600px) {
|
||||
.doxa-map-scroll-pad {
|
||||
display: block;
|
||||
position: absolute;
|
||||
/* stops short of the map's control column at the inline end (32px round buttons, 3px in), so the + and
|
||||
the other controls stay tappable; the rest of the band still lets a finger scroll the page */
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 44px;
|
||||
height: 48px;
|
||||
z-index: 1500;
|
||||
touch-action: pan-y;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.doxa-map-scroll-pad--top { top: 0; }
|
||||
/* while a map panel is open, its ✕ and its contents get every tap */
|
||||
.doxa-map-scroll-pad.is-off { display: none; }
|
||||
}
|
||||
.doxa-map-slot :deep(dxm-simple-map:fullscreen),
|
||||
.doxa-map-slot :deep(dxm-simple-map:-webkit-full-screen) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
197
app/pages/posterverse/index.vue
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<script setup lang="ts">
|
||||
// Posterverse: every poster plugin (UPG 100 List Posters, Country Posters, Poster Design and the poster
|
||||
// shelf of experimental plugins) in one full-page app, the poster twin of Deep Research. Like Deep
|
||||
// Research, this page is HIDDEN: it is in no menu and links nowhere, and robots are told not to index
|
||||
// it. It is reached by its address, and ?tab=<row id> opens one plugin directly (the shell reads it).
|
||||
//
|
||||
// The shell is a web component served from this site's OWN /dx-maps/ tree, the same copy the Deep
|
||||
// Research page uses: one script, one element, <posterverse-app>. A custom
|
||||
// element's name needs a hyphen, which is why the tag is not just <posterverse>.
|
||||
//
|
||||
// The element is created in onMounted rather than written in the template: it is a custom element
|
||||
// that only exists in the browser, so this keeps it out of SSR and out of Vue's component resolver
|
||||
// without any nuxt.config change.
|
||||
|
||||
// A full-page app on every screen: no layout, so no site header and no footer. The way back to the site
|
||||
// is the shell's own "Back to homepage" button, directly above its Collapse button (home-href below),
|
||||
// so the shell fills the whole screen. The page itself never scrolls (the shell's own panes scroll).
|
||||
definePageMeta({ layout: false })
|
||||
|
||||
const { locale, locales } = useI18n()
|
||||
const localePath = useLocalePath()
|
||||
|
||||
// The page's own words, in every language the site has (English for any other); the site's shared
|
||||
// translation files stay as they are. "Back to homepage" is drawn by the app, above its Collapse button.
|
||||
const HOME_LABEL: Record<string, string> = {
|
||||
en: "Back to homepage",
|
||||
ar: "العودة إلى الصفحة الرئيسية",
|
||||
de: "Zurück zur Startseite",
|
||||
es: "Volver a la página de inicio",
|
||||
fr: "Retour à l'accueil",
|
||||
hi: "होमपेज पर वापस जाएँ",
|
||||
it: "Torna alla home page",
|
||||
pt: "Voltar à página inicial",
|
||||
ro: "Înapoi la pagina principală",
|
||||
ru: "Вернуться на главную",
|
||||
zh: "返回首页",
|
||||
}
|
||||
const homeLabel = () => HOME_LABEL[locale.value] || HOME_LABEL.en
|
||||
// "Back to homepage" goes to the public site, doxa.life, in the reader's language (https://doxa.life/,
|
||||
// https://doxa.life/ar), whichever server this page runs on.
|
||||
const homeHref = () => `https://doxa.life${localePath('/')}`
|
||||
|
||||
// The shell comes from this site's own /dx-maps/ copy; the script and every embed URL come from it.
|
||||
const POSTERVERSE_SCRIPT = '/dx-maps/posterverse/posterverse.js'
|
||||
|
||||
// FRESH, NOT STALE: a returning reader's browser may keep /dx-maps/ files for hours (no cache rule is sent for them), so
|
||||
// the script is asked for once with cache 'no-cache' before it is added (a 304 when unchanged; a changed file replaces the
|
||||
// stale copy), then added at its PLAIN address, so DX Maps' gallery still recognises it and never runs it twice.
|
||||
function revalidate(src: string): Promise<void> {
|
||||
return fetch(src, { cache: 'no-cache', credentials: 'same-origin' })
|
||||
.then((r) => { if (r.status === 200 && r.body) r.body.cancel() })
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'Posterverse',
|
||||
meta: [{ name: 'robots', content: 'noindex' }]
|
||||
})
|
||||
|
||||
// With no layout, this page sets <html lang dir> itself, as the public layout does for every other
|
||||
// page: in Arabic the shell reads right to left.
|
||||
const dir = computed(() => {
|
||||
const loc = locales.value.find(l => typeof l === 'object' && 'code' in l && l.code === locale.value)
|
||||
return (typeof loc === 'object' && loc && 'dir' in loc && loc.dir) ? loc.dir : 'ltr'
|
||||
})
|
||||
useHead(() => ({
|
||||
htmlAttrs: {
|
||||
lang: locale.value,
|
||||
dir: dir.value
|
||||
}
|
||||
}))
|
||||
|
||||
const host = ref<HTMLElement | null>(null)
|
||||
let shell: HTMLElement | null = null
|
||||
|
||||
// THE LANGUAGE MENU INSIDE THE APP (its phone bar and its rail): on a pick it fires a cancelable
|
||||
// 'doxa-language-request' {detail.language}. This site owns the language (it is in the address: /ar/...),
|
||||
// so the page cancels the event and switches the SITE's locale; the app then follows through `language`
|
||||
// (the watch below). The tab open at that moment is carried over in ?tab=, because the localized
|
||||
// address is a new page. A code the site has no route for is cancelled and ignored.
|
||||
const route = useRoute()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
let currentTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
||||
function onTabChange(e: Event) {
|
||||
const d = (e as CustomEvent).detail
|
||||
const id = Array.isArray(d) ? d[0]?.id : d?.id
|
||||
if (typeof id === 'string' && id) currentTab = id
|
||||
}
|
||||
function onLanguageRequest(e: Event) {
|
||||
e.preventDefault()
|
||||
const code = (e as CustomEvent<{ language?: string }>).detail?.language
|
||||
const known = locales.value.some(l => (typeof l === 'string' ? l : l.code) === code)
|
||||
if (!code || !known || code === locale.value) return
|
||||
const target = switchLocalePath(code as typeof locale.value)
|
||||
if (!target) return
|
||||
const url = new URL(target, window.location.origin)
|
||||
if (currentTab) url.searchParams.set('tab', currentTab)
|
||||
navigateTo(url.pathname + url.search + url.hash)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
shell = document.createElement('posterverse-app')
|
||||
// Every embed URL is built at RUNTIME from the origin serving this page, so the same build works on
|
||||
// any host with nothing hardcoded.
|
||||
shell.setAttribute('tree-base', `${window.location.origin}/dx-maps`)
|
||||
// The site's language drives the shell's (and every plugin's); it follows the language switcher.
|
||||
shell.setAttribute('language', locale.value)
|
||||
// A website, not the mobile app: no phone bar of the app's own (one header on a phone); its ☰ sits in the
|
||||
// corner and its language menu at the drawer's foot. Read once, when the element mounts, so it is set here.
|
||||
shell.setAttribute('shell-mode', 'web')
|
||||
// The way back to the site: the shell draws it above its Collapse button, in the site's own words.
|
||||
shell.setAttribute('home-href', homeHref())
|
||||
shell.setAttribute('home-label', homeLabel())
|
||||
// The keyless MapLibre engine, always: the app needs no token and no setting.
|
||||
const shellConfig: Record<string, unknown> = { provider: 'maplibre' }
|
||||
shell.setAttribute('provider', 'maplibre')
|
||||
shell.setAttribute('profile-config', JSON.stringify(shellConfig))
|
||||
shell.addEventListener('tab-change', onTabChange)
|
||||
shell.addEventListener('doxa-language-request', onLanguageRequest)
|
||||
host.value?.appendChild(shell)
|
||||
// its script, revalidated (above) and then added at its plain address, once per page load
|
||||
if (!customElements.get('posterverse-app') && !document.querySelector(`script[src="${POSTERVERSE_SCRIPT}"]`)) {
|
||||
revalidate(POSTERVERSE_SCRIPT).then(() => {
|
||||
if (document.querySelector(`script[src="${POSTERVERSE_SCRIPT}"]`)) return
|
||||
const script = document.createElement('script')
|
||||
script.src = POSTERVERSE_SCRIPT
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(locale, (lang) => {
|
||||
shell?.setAttribute('language', lang)
|
||||
shell?.setAttribute('home-href', homeHref())
|
||||
shell?.setAttribute('home-label', homeLabel())
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
shell?.remove()
|
||||
shell = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="posterverse-page">
|
||||
<h1 class="visually-hidden">
|
||||
Posterverse
|
||||
</h1>
|
||||
<div
|
||||
ref="host"
|
||||
class="posterverse-host"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.posterverse-page {
|
||||
/* 100svh: the phone's SMALL viewport, so the browser's own toolbars never cover the shell. */
|
||||
height: 100svh;
|
||||
}
|
||||
/* The shell takes the whole screen, so there is no page scrollbar. */
|
||||
.posterverse-host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* 100%, not the element's own 85vh default: here the shell is the whole page. */
|
||||
.posterverse-host :deep(posterverse-app) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Not scoped, and keyed to this page's own element, so it applies only while this page is shown and
|
||||
no other page changes. With no layout, the site's stylesheet may not be loaded here: the page sets
|
||||
the two things it needs itself, no body margin and no page scroll. */
|
||||
html:has(.posterverse-page),
|
||||
html:has(.posterverse-page) body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* The site keeps room for a scrollbar on every page (scrollbar-gutter: stable, base/_globals.scss).
|
||||
This page never scrolls, so on a desktop that room would only be an empty strip at the right edge. */
|
||||
html:has(.posterverse-page) {
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -26,11 +26,133 @@ onMounted(() => reload())
|
|||
|
||||
const mapboxToken = config.public.mapboxToken as string
|
||||
|
||||
const prayMapConfig = JSON.stringify({
|
||||
// THE MAP comes from this site's own copy of the dx-maps output (public/dx-maps/) and is loaded here,
|
||||
// not through DoxaMapSlot, which loads the older bundle in public/js/. Mapbox with the site's token, as
|
||||
// before; the keyless MapLibre when there is none. Its styles and boundaries live in the same tree, and
|
||||
// the site's language is the map's.
|
||||
const prayMapConfig = computed(() => JSON.stringify({
|
||||
profile: 'doxa-simple-map',
|
||||
dataSource: 'pray-tools',
|
||||
tk: mapboxToken,
|
||||
tabs: [{ id: 'prayer', colorStrategy: 'prayer', legend: 'prayer', popup: 'prayer' }]
|
||||
tabs: [{ id: 'prayer', colorStrategy: 'prayer', legend: 'prayer', popup: 'prayer' }],
|
||||
provider: mapboxToken ? 'mapbox' : 'maplibre',
|
||||
...(mapboxToken ? { tk: mapboxToken } : {}),
|
||||
assetsBase: '/dx-maps/assets',
|
||||
locale: locale.value
|
||||
}))
|
||||
// THE MAP'S ELEMENT, <dxm-simple-map>: DX Maps' own name for it, so it never meets the older <doxa-map> elements
|
||||
// elsewhere on the site, in whatever order the pages are visited. Its bundle comes from this site's own copy of
|
||||
// the dx-maps output and is added once per visit. A load in progress is shared, and a failed load takes its tag
|
||||
// away again, so a later page tries afresh; the page then shows without the map.
|
||||
const MAP_TAG = 'dxm-simple-map'
|
||||
const MAP_SCRIPT = '/dx-maps/doxa-simple-map/doxa-simple-map.js'
|
||||
// FRESH, NOT STALE: a returning reader's browser may keep /dx-maps/ files for hours (no cache rule is sent for them), so
|
||||
// the script is asked for once with cache 'no-cache' before it is added (a 304 when unchanged; a changed file replaces the
|
||||
// stale copy), then added at its PLAIN address, so DX Maps' gallery still recognises it and never runs it twice.
|
||||
function revalidate(src: string): Promise<void> {
|
||||
return fetch(src, { cache: 'no-cache', credentials: 'same-origin' })
|
||||
.then((r) => { if (r.status === 200 && r.body) r.body.cancel() })
|
||||
.catch(() => {})
|
||||
}
|
||||
function loadMapBundle(): Promise<void> {
|
||||
if (customElements.get(MAP_TAG)) return Promise.resolve()
|
||||
return revalidate(MAP_SCRIPT).then(() => addMapBundle())
|
||||
}
|
||||
function addMapBundle(): Promise<void> {
|
||||
if (customElements.get(MAP_TAG)) return Promise.resolve()
|
||||
const pending = document.querySelector<HTMLScriptElement>('script[data-dxl-map="' + MAP_TAG + '"]')
|
||||
if (pending) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.addEventListener('load', () => customElements.get(MAP_TAG) ? resolve() : reject(new Error('the map bundle did not register')), { once: true })
|
||||
pending.addEventListener('error', () => reject(new Error('the map bundle failed to load')), { once: true })
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
const fail = (message: string) => {
|
||||
script.remove()
|
||||
reject(new Error(message))
|
||||
}
|
||||
script.src = MAP_SCRIPT
|
||||
script.dataset.dxlMap = MAP_TAG
|
||||
script.onload = () => customElements.get(MAP_TAG) ? resolve() : fail('the map bundle did not register')
|
||||
script.onerror = () => fail('the map bundle failed to load')
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
// THE PHONE HEIGHT, LOCKED IN PX: 80% of the window's height when the page opens (never under 320px), so the map
|
||||
// box does not grow and shrink while the browser's address bar collapses and expands during a scroll, which also
|
||||
// broke a drag on the map. It is measured again only when the WIDTH changes (a rotation), never on a height-only
|
||||
// resize. Wider screens keep the stylesheet's height; 80svh there is the fallback until this runs.
|
||||
// A phone: a narrow screen, or a touch screen held sideways (the same test as the stylesheet's phone rules).
|
||||
const PHONE_QUERY = '(max-width: 768px), (pointer: coarse) and (max-height: 600px)'
|
||||
function lockPhoneHeight(slot: HTMLElement): () => void {
|
||||
const PHONE = window.matchMedia(PHONE_QUERY)
|
||||
let width = -1
|
||||
const lock = () => {
|
||||
if (window.innerWidth === width) return
|
||||
width = window.innerWidth
|
||||
slot.style.height = PHONE.matches ? `${Math.max(320, Math.round(window.innerHeight * 0.8))}px` : ''
|
||||
}
|
||||
lock()
|
||||
window.addEventListener('resize', lock)
|
||||
return () => window.removeEventListener('resize', lock)
|
||||
}
|
||||
let unlockHeight: (() => void) | null = null
|
||||
onMounted(() => { if (prayMapHost.value) unlockHeight = lockPhoneHeight(prayMapHost.value) })
|
||||
onBeforeUnmount(() => unlockHeight?.())
|
||||
// THE SCROLL BAND STEPS ASIDE FOR THE MAP'S PANELS. A panel the map's controls open (Share, and every other menu)
|
||||
// sits at the map's top, under the band, so its ✕ could not be tapped. DX Maps says when a panel opens (the composed
|
||||
// 'dxmaps:panel-open') and when it closes ('dxmaps:panel-close'). The band counts them (another panel can still be open
|
||||
// when one closes) and stays off while the count is above 0; for builds without the close event, it also comes back
|
||||
// after any tap once no panel is open in the map any more.
|
||||
const padOff = ref(false)
|
||||
let openPanels = 0
|
||||
function mapPanelOpen(): boolean {
|
||||
const walk = (node: Element | ShadowRoot): boolean => {
|
||||
for (const n of node.querySelectorAll('*')) {
|
||||
if (n.classList.contains('mcm')) return true
|
||||
if (n.shadowRoot && walk(n.shadowRoot)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return !!prayMap && walk(prayMap.shadowRoot || prayMap)
|
||||
}
|
||||
const onPanelOpen = () => { openPanels++; padOff.value = true }
|
||||
const onPanelClose = () => { openPanels = Math.max(0, openPanels - 1); padOff.value = openPanels > 0 }
|
||||
const onAnyTap = () => {
|
||||
if (padOff.value) setTimeout(() => { if (!mapPanelOpen()) { openPanels = 0; padOff.value = false } }, 50)
|
||||
}
|
||||
onMounted(() => {
|
||||
prayMapHost.value?.addEventListener('dxmaps:panel-open', onPanelOpen)
|
||||
prayMapHost.value?.addEventListener('dxmaps:panel-close', onPanelClose)
|
||||
document.addEventListener('click', onAnyTap, true)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
prayMapHost.value?.removeEventListener('dxmaps:panel-open', onPanelOpen)
|
||||
prayMapHost.value?.removeEventListener('dxmaps:panel-close', onPanelClose)
|
||||
document.removeEventListener('click', onAnyTap, true)
|
||||
})
|
||||
// The prayer API the map reads is set before its script runs; the element is created once the bundle is in.
|
||||
const prayMapHost = ref<HTMLElement | null>(null)
|
||||
let prayMap: HTMLElement | null = null
|
||||
onMounted(async () => {
|
||||
if (config.public.prayBaseUrl) window.MAP_APP_API_URL = config.public.prayBaseUrl as string
|
||||
try {
|
||||
await loadMapBundle()
|
||||
} catch (err) {
|
||||
console.error('[pray map]', err)
|
||||
return
|
||||
}
|
||||
if (!prayMapHost.value) return
|
||||
prayMap = document.createElement(MAP_TAG)
|
||||
prayMap.id = 'pray-map'
|
||||
prayMap.setAttribute('profile-config', prayMapConfig.value)
|
||||
prayMapHost.value.prepend(prayMap)
|
||||
})
|
||||
watch(prayMapConfig, (value) => { prayMap?.setAttribute('profile-config', value) })
|
||||
onBeforeUnmount(() => {
|
||||
prayMap?.remove()
|
||||
prayMap = null
|
||||
})
|
||||
|
||||
const DEMO_PRAYER_COVERAGE = {
|
||||
|
|
@ -119,7 +241,9 @@ useTextHighlight()
|
|||
<section>
|
||||
<div class="container stack stack--lg">
|
||||
<h2>{{ t('Prayer Progress') }}</h2>
|
||||
<DoxaMapSlot map-id="pray-map" :profile-config="prayMapConfig" class="rounded-md" />
|
||||
<div ref="prayMapHost" class="doxa-map-slot rounded-md">
|
||||
<div class="doxa-map-scroll-pad doxa-map-scroll-pad--top" :class="{ 'is-off': padOff }" />
|
||||
</div>
|
||||
<NuxtLink
|
||||
:to="localePath('/research')"
|
||||
class="research-map-link"
|
||||
|
|
@ -406,4 +530,60 @@ useTextHighlight()
|
|||
.prayer-coverage-key--full {
|
||||
background: var(--color-brand-primary);
|
||||
}
|
||||
|
||||
/* The map's frame, as DoxaMapSlot draws it. */
|
||||
.doxa-map-slot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 780px;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 768px), (pointer: coarse) and (max-height: 600px) {
|
||||
/* On a phone the map is 80% of the SMALL viewport height, a little shorter than the screen so the page
|
||||
stays in view (svh does not jump when the browser's address bar collapses), never under 320px. */
|
||||
.doxa-map-slot {
|
||||
min-height: 320px;
|
||||
aspect-ratio: auto;
|
||||
height: 80svh;
|
||||
}
|
||||
}
|
||||
.doxa-map-slot :deep(dxm-simple-map) {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
contain: paint;
|
||||
}
|
||||
.doxa-map-scroll-pad {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 768px), (pointer: coarse) and (max-height: 600px) {
|
||||
.doxa-map-scroll-pad {
|
||||
display: block;
|
||||
position: absolute;
|
||||
/* stops short of the map's control column at the inline end (32px round buttons, 3px in), so the + and
|
||||
the other controls stay tappable; the rest of the band still lets a finger scroll the page */
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 44px;
|
||||
height: 48px;
|
||||
z-index: 1500;
|
||||
touch-action: pan-y;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.doxa-map-scroll-pad--top { top: 0; }
|
||||
/* while a map panel is open, its ✕ and its contents get every tap */
|
||||
.doxa-map-scroll-pad.is-off { display: none; }
|
||||
}
|
||||
.doxa-map-slot :deep(dxm-simple-map:fullscreen),
|
||||
.doxa-map-slot :deep(dxm-simple-map:-webkit-full-screen) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ useTextHighlight()
|
|||
</svg>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
<!-- Country posters for this region, under the dashboard link (a PosterSlot: DX Maps' poster section). -->
|
||||
<PosterSlot :scope="region.slug" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,13 +23,80 @@ const initialSearchTerm = computed(() =>
|
|||
|
||||
const translations = computed(() => buildUupgListTranslations(t))
|
||||
|
||||
// THE MAP comes from this site's own copy of the dx-maps output (public/dx-maps/) and is loaded here,
|
||||
// not through DoxaMapSlot, which loads the older bundle in public/js/. Mapbox with the site's token, as
|
||||
// before; the keyless MapLibre when there is none. Its styles and boundaries live in the same tree, and
|
||||
// the site's language is the map's.
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const mapboxToken = (runtimeConfig.public as { mapboxToken?: string }).mapboxToken || ''
|
||||
const researchProfileConfig = computed(() => JSON.stringify({
|
||||
profile: 'research-map',
|
||||
tk: (runtimeConfig.public as { mapboxToken?: string }).mapboxToken || '',
|
||||
dataSource: 'pray-tools',
|
||||
instanceId: 'research-page'
|
||||
instanceId: 'research-page',
|
||||
provider: mapboxToken ? 'mapbox' : 'maplibre',
|
||||
...(mapboxToken ? { tk: mapboxToken } : {}),
|
||||
assetsBase: '/dx-maps/assets',
|
||||
locale: locale.value
|
||||
}))
|
||||
// THE PHONE HEIGHT, LOCKED IN PX: 80% of the window's height when the page opens (never under 320px), so the map
|
||||
// box does not grow and shrink while the browser's address bar collapses and expands during a scroll, which also
|
||||
// broke a drag on the map. It is measured again only when the WIDTH changes (a rotation), never on a height-only
|
||||
// resize. Wider screens keep the stylesheet's height; 80svh there is the fallback until this runs.
|
||||
// A phone: a narrow screen, or a touch screen held sideways (the same test as the stylesheet's phone rules).
|
||||
const PHONE_QUERY = '(max-width: 768px), (pointer: coarse) and (max-height: 600px)'
|
||||
function lockPhoneHeight(slot: HTMLElement): () => void {
|
||||
const PHONE = window.matchMedia(PHONE_QUERY)
|
||||
let width = -1
|
||||
const lock = () => {
|
||||
if (window.innerWidth === width) return
|
||||
width = window.innerWidth
|
||||
slot.style.height = PHONE.matches ? `${Math.max(320, Math.round(window.innerHeight * 0.8))}px` : ''
|
||||
}
|
||||
lock()
|
||||
window.addEventListener('resize', lock)
|
||||
return () => window.removeEventListener('resize', lock)
|
||||
}
|
||||
const researchMapSlot = ref<HTMLElement | null>(null)
|
||||
let unlockHeight: (() => void) | null = null
|
||||
onMounted(() => { if (researchMapSlot.value) unlockHeight = lockPhoneHeight(researchMapSlot.value) })
|
||||
onBeforeUnmount(() => unlockHeight?.())
|
||||
// FRESH, NOT STALE: a returning reader's browser may keep /dx-maps/ files for hours (no cache rule is sent for them), so
|
||||
// the script is asked for once with cache 'no-cache' before it is added (a 304 when unchanged; a changed file replaces the
|
||||
// stale copy), then added at its PLAIN address, so DX Maps' gallery still recognises it and never runs it twice.
|
||||
function revalidate(src: string): Promise<void> {
|
||||
return fetch(src, { cache: 'no-cache', credentials: 'same-origin' })
|
||||
.then((r) => { if (r.status === 200 && r.body) r.body.cancel() })
|
||||
.catch(() => {})
|
||||
}
|
||||
// The prayer API the map reads is set before its script runs, and the script is added once per visit.
|
||||
onMounted(async () => {
|
||||
if (runtimeConfig.public.prayBaseUrl) window.MAP_APP_API_URL = runtimeConfig.public.prayBaseUrl as string
|
||||
const src = '/dx-maps/doxa-research-map/doxa-research-map.js'
|
||||
if (customElements.get('doxa-research-map') || document.querySelector(`script[src="${src}"]`)) return
|
||||
await revalidate(src)
|
||||
if (!document.querySelector(`script[src="${src}"]`)) {
|
||||
const script = document.createElement('script')
|
||||
script.src = src
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
})
|
||||
|
||||
// The line under the map, in every language the site has (English for any other); the site's shared
|
||||
// translation files stay as they are.
|
||||
const DEEP_RESEARCH_LINE: Record<string, { ask: string, link: string }> = {
|
||||
en: { ask: "Want Deeper Research?", link: "Deep Research app" },
|
||||
ar: { ask: "هل تريد بحثًا أعمق؟", link: "تطبيق Deep Research" },
|
||||
de: { ask: "Möchten Sie tiefer recherchieren?", link: "Deep-Research-App" },
|
||||
es: { ask: "¿Quiere investigar más a fondo?", link: "App Deep Research" },
|
||||
fr: { ask: "Vous voulez aller plus loin dans vos recherches ?", link: "Application Deep Research" },
|
||||
hi: { ask: "क्या आप और गहन शोध चाहते हैं?", link: "Deep Research ऐप" },
|
||||
it: { ask: "Vuoi approfondire la ricerca?", link: "App Deep Research" },
|
||||
pt: { ask: "Quer uma pesquisa mais aprofundada?", link: "App Deep Research" },
|
||||
ro: { ask: "Vrei o cercetare mai aprofundată?", link: "Aplicația Deep Research" },
|
||||
ru: { ask: "Хотите исследовать глубже?", link: "Приложение Deep Research" },
|
||||
zh: { ask: "想进行更深入的研究?", link: "Deep Research 应用" },
|
||||
}
|
||||
const deepResearchLine = computed(() => DEEP_RESEARCH_LINE[locale.value] || DEEP_RESEARCH_LINE.en)
|
||||
|
||||
useTextHighlight()
|
||||
</script>
|
||||
|
|
@ -42,14 +109,22 @@ useTextHighlight()
|
|||
</div>
|
||||
|
||||
<!-- Research map embed — sits directly under the H1, above the UUPG
|
||||
search bar. Loads /js/doxa-research-map.js (auto-published from
|
||||
the upstream module on every research-mfe build). -->
|
||||
<DoxaMapSlot
|
||||
map-id="research-page-map"
|
||||
bundle="research-map"
|
||||
:profile-config="researchProfileConfig"
|
||||
class="rounded-xlg"
|
||||
/>
|
||||
search bar. Loads /dx-maps/doxa-research-map/doxa-research-map.js. -->
|
||||
<div class="stack stack--xs">
|
||||
<div ref="researchMapSlot" class="doxa-map-slot rounded-xlg">
|
||||
<doxa-research-map id="research-page-map" :profile-config="researchProfileConfig" />
|
||||
</div>
|
||||
<!-- Under the map: the way to the full set of research maps, the hidden Deep Research page.
|
||||
Small, like the homepage's "Explore our research maps" line under its map. -->
|
||||
<p class="text-center deep-research-line">
|
||||
{{ deepResearchLine.ask }}
|
||||
<!-- A full page load: the app starts in a page of its own, with nothing of this site's maps in it. -->
|
||||
<a
|
||||
class="light-link"
|
||||
:href="localePath('/deep-research')"
|
||||
>{{ deepResearchLine.link }} →</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UupgsList
|
||||
:language-code="locale"
|
||||
|
|
@ -60,3 +135,50 @@ useTextHighlight()
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* The same size as the homepage's link under its map (.research-map-link in pages/index.vue). */
|
||||
.deep-research-line {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* The map's frame, as DoxaMapSlot draws it. */
|
||||
.doxa-map-slot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 780px;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 768px), (pointer: coarse) and (max-height: 600px) {
|
||||
/* On a phone the map is 80% of the SMALL viewport height, a little shorter than the screen so the page
|
||||
stays in view (svh does not jump when the browser's address bar collapses), never under 320px. */
|
||||
.doxa-map-slot {
|
||||
min-height: 320px;
|
||||
aspect-ratio: auto;
|
||||
height: 80svh;
|
||||
}
|
||||
}
|
||||
/* The card's corners are concentric with the map's tab row, which starts 16px in, so no tab is cut. */
|
||||
.doxa-map-slot.rounded-xlg {
|
||||
border-radius: 16px;
|
||||
}
|
||||
.doxa-map-slot doxa-research-map {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
contain: paint;
|
||||
}
|
||||
.doxa-map-slot doxa-research-map:fullscreen,
|
||||
.doxa-map-slot doxa-research-map:-webkit-full-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
481
public/dx-maps/all-people-groups/all-people-groups.js
Normal file
259
public/dx-maps/all-people-groups/all-people-groups/index.html
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
|
||||
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
|
||||
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
|
||||
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. -->
|
||||
<script>window.MAP_OWNS_PAGE = true;</script>
|
||||
<title>all-people-groups · all-people-groups</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<!-- A9 hints: the map's origins, so DNS/TLS + the engine start with the bundle, not after it (engine = provider or the MapLibre default). -->
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
<link rel="preconnect" href="https://tiles.openfreemap.org" crossorigin />
|
||||
<link rel="preconnect" href="https://pray.doxa.life" crossorigin />
|
||||
<link rel="preload" as="script" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js" integrity="sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp" crossorigin="anonymous" />
|
||||
<!-- NO STATIC ENGINE TAGS (2026-09-25). This leaf used to hard-link mapbox-gl.css, the
|
||||
geocoder css, mapbox-gl.js and the geocoder js from Mapbox's CDN, chosen at BUILD time —
|
||||
so a partner embedding it with ?provider=maplibre still sent every visitor's IP to Mapbox
|
||||
(4 requests per leaf, measured). The provider is chosen at RUNTIME (profile-config.provider
|
||||
→ ?provider= → window.MAP_PROVIDER → auto), and the element's own runtime loaders load
|
||||
exactly that engine: ensureEngineScript / ensureEngineCSS (useMapInstance) and the
|
||||
geocoder's on-demand loader — the path composite leaves already run on. Gate:
|
||||
check:leaf-engine-per-provider (0 Mapbox requests under maplibre; Mapbox unchanged). -->
|
||||
<style>
|
||||
/* BARE full-page map — no chrome. The custom element IS the page, so the
|
||||
browser URL shares the map itself.
|
||||
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
|
||||
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
|
||||
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
|
||||
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
|
||||
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. */
|
||||
html, body { height: 100%; margin: 0; background: #0a0e15; }
|
||||
#slot, all-people-groups { 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;
|
||||
padding: 24px; text-align: center; color: #9aa7b8;
|
||||
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
#status.err { color: #f87171; }
|
||||
|
||||
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
|
||||
Two things every generated leaf got wrong on paper, because no generated page has ever
|
||||
carried an @media print rule (measured: 0 across the tree).
|
||||
|
||||
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
|
||||
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
|
||||
flooded black, and that is the reader who most wants the printout.
|
||||
|
||||
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
|
||||
a full sheet reading "loading…" above the content that did in fact load. It is hidden
|
||||
only while it is NOT an error — an error still prints, because a blank sheet with the
|
||||
explanation suppressed is worse than a sheet that says what went wrong. */
|
||||
@media print {
|
||||
html, body { height: auto; background: #fff; }
|
||||
#status:not(.err) { display: none; }
|
||||
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
|
||||
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
|
||||
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
|
||||
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
|
||||
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
|
||||
then blank to the bottom of the sheet.
|
||||
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
|
||||
so height:auto with nothing else can leave the element a zero-height box and print an empty
|
||||
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
|
||||
box for the map case while letting a taller document grow onto page two. */
|
||||
#slot, all-people-groups { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="slot"></div>
|
||||
<div id="status">loading token…</div>
|
||||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
|
||||
<script src="../all-people-groups.js" integrity="sha384-LMy0vhHZstVM6y2VkOkv/qjy4f13oCHJ7Vk8kDH87Il73sqGL3UlqFh/rq1JEr0h" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
|
||||
// 2. ?tk= URL param — token carried in the shared link
|
||||
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
|
||||
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
|
||||
// *endpoint* is resolved and fetched:
|
||||
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
|
||||
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
|
||||
// means "read that global"; anything else is used as a URL — so both a global
|
||||
// name and a plain URL are safe (no SyntaxError footgun).
|
||||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"profile":"all-people-groups","dataSource":"pray-tools","instanceId":"all-people-groups-all-people-groups","assetsBase":"../../assets"};
|
||||
// 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.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
var TOKEN_FREE = false;
|
||||
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
|
||||
// rather than this page's profile-config. With no baked token (the public copy), it must get
|
||||
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
|
||||
var NESTS_MAPS = false;
|
||||
var TOKEN_SRC = "window.MAP_TOKEN_URL";
|
||||
var BAKED_TOKEN = "";
|
||||
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
|
||||
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
|
||||
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
|
||||
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
|
||||
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
|
||||
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
|
||||
var DEFAULT_TOKEN_URL = '/api/maps/token';
|
||||
|
||||
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
|
||||
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
|
||||
function askToken() {
|
||||
if (!status) return;
|
||||
status.className = 'err';
|
||||
status.innerHTML = 'map needs a Mapbox token — ' +
|
||||
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
|
||||
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
|
||||
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
|
||||
var go = document.getElementById('tkGo');
|
||||
if (go) go.addEventListener('click', function () {
|
||||
var v = (document.getElementById('tkIn').value || '').trim();
|
||||
if (!v) return;
|
||||
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
|
||||
mount(v);
|
||||
});
|
||||
}
|
||||
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
|
||||
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
|
||||
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
|
||||
mounted element does not cover shows it through, because every layer above body is
|
||||
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
|
||||
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
|
||||
unpainted canvas as first supposed. Clearing it here (the one place we already know the
|
||||
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
|
||||
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
|
||||
function done() {
|
||||
if (status && status.parentNode) { status.parentNode.removeChild(status); }
|
||||
document.documentElement.style.background = '#f3f3f1';
|
||||
document.body.style.background = '#f3f3f1';
|
||||
}
|
||||
|
||||
function mount(token) {
|
||||
if (!token && !TOKEN_FREE) { return askToken(); }
|
||||
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
|
||||
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
|
||||
try {
|
||||
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
|
||||
var el = document.createElement("all-people-groups");
|
||||
el.setAttribute('profile-config', JSON.stringify(cfg));
|
||||
slot.innerHTML = '';
|
||||
slot.appendChild(el);
|
||||
done();
|
||||
} catch (e) { fail(e && e.message ? e.message : String(e)); }
|
||||
}
|
||||
|
||||
function qp(name) {
|
||||
try { return new URLSearchParams(location.search).get(name) || ''; }
|
||||
catch (e) { return ''; }
|
||||
}
|
||||
|
||||
function extractToken(data) {
|
||||
if (typeof data === 'string') return data.trim();
|
||||
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Rungs 1-3b: an actual token → mount directly, no fetch.
|
||||
// 2b = localStorage: the paste-once store shared with the staging pages
|
||||
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
|
||||
var stored = '';
|
||||
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
|
||||
var urlTk = qp('tk');
|
||||
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
|
||||
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
|
||||
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
|
||||
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
|
||||
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
|
||||
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
|
||||
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
|
||||
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
|
||||
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
|
||||
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
|
||||
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
|
||||
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
|
||||
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
|
||||
if (TOKEN_FREE) {
|
||||
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
|
||||
mount('');
|
||||
// A token-free composite with no token anywhere: its content renders, but its nested maps
|
||||
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
|
||||
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
|
||||
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
|
||||
var bar = document.createElement('div');
|
||||
bar.id = 'status';
|
||||
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
|
||||
document.body.appendChild(bar);
|
||||
status = bar;
|
||||
askToken();
|
||||
var goBtn = document.getElementById('tkGo');
|
||||
if (goBtn) goBtn.addEventListener('click', function () {
|
||||
var v = (document.getElementById('tkIn').value || '').trim();
|
||||
if (v) { location.reload(); }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (directToken) { return mount(directToken); }
|
||||
|
||||
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
|
||||
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
|
||||
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
|
||||
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
|
||||
// A single hard-coded pick used to kill the page when its one url 404'd or was
|
||||
// blocked by CORS; a shared link must work wherever the tree is hosted.
|
||||
var candidates = [];
|
||||
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
|
||||
addCandidate(window.MAP_TOKEN_URL);
|
||||
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
|
||||
addCandidate(DEFAULT_TOKEN_URL);
|
||||
|
||||
function fetchToken(i, errors) {
|
||||
if (i >= candidates.length) {
|
||||
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
|
||||
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
|
||||
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
|
||||
askToken();
|
||||
if (status) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
|
||||
status.insertBefore(d, status.firstChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var url = candidates[i];
|
||||
return fetch(url)
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
|
||||
var ct = r.headers.get('content-type') || '';
|
||||
return ct.indexOf('json') !== -1 ? r.json() : r.text();
|
||||
})
|
||||
.then(function (data) {
|
||||
var tk = extractToken(data);
|
||||
if (!tk) throw new Error(url + ' → no token in response');
|
||||
mount(tk);
|
||||
})
|
||||
.catch(function (e) {
|
||||
errors.push(e && e.message ? e.message : String(e));
|
||||
return fetchToken(i + 1, errors);
|
||||
});
|
||||
}
|
||||
fetchToken(0, []);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
61
public/dx-maps/assets/boundaries/ATTRIBUTION
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
BOUNDARY DATA — SOURCES AND ATTRIBUTION
|
||||
=======================================
|
||||
|
||||
The .geojson files in this folder are third-party boundary data. Every value below is taken
|
||||
VERBATIM from the upstream metadata (the geoBoundaries API, https://www.geoboundaries.org/api/current/gbOpen/<ISO3>/<LEVEL>/).
|
||||
Nothing here is our own wording about a licence.
|
||||
|
||||
gb-IND-ADM1-full.geojson, gb-IND-ADM1-full-web.geojson
|
||||
source geoBoundaries gbOpen (www.geoboundaries.org)
|
||||
boundaryID IND-ADM1-1811400
|
||||
licence Creative Commons Attribution 2.5 India (CC BY 2.5 IN)
|
||||
licenceSource github.com/datameet/maps
|
||||
year 2011
|
||||
ATTRIBUTION IS REQUIRED for this file: it is Creative Commons Attribution, so a work that
|
||||
redistributes or displays it must credit the source above.
|
||||
|
||||
gb-IND-ADM1.geojson, gb-IND-ADM1-web.geojson
|
||||
The same India ADM1 boundary as above, curated to the UPG-100 list's partition.
|
||||
Same source, same boundaryID, same licence, same attribution duty.
|
||||
|
||||
gb-CHN-ADM1.geojson
|
||||
source geoBoundaries gbOpen (www.geoboundaries.org)
|
||||
boundaryID CHN-ADM1-43563684
|
||||
licence Public Domain
|
||||
licenceSource commons.wikimedia.org/wiki/File
|
||||
year 2019
|
||||
No attribution duty: upstream records this boundary as Public Domain.
|
||||
|
||||
world-adm0.geojson, world-adm0-web.geojson
|
||||
source geoBoundaries gbOpen (www.geoboundaries.org), assembled per country
|
||||
features 162 countries
|
||||
licence MIXED — and now ESTABLISHED per country, not merely acknowledged.
|
||||
THE PER-COUNTRY TABLE IS THE AUTHORITY, and it lives in ONE place on purpose:
|
||||
plugin-designs/my-upg-100-list/data/adm0/PROVENANCE.json
|
||||
It records boundaryLicense, licenseSource and boundaryID for all 162, each value taken
|
||||
verbatim from https://www.geoboundaries.org/api/current/gbOpen/<ISO3>/<LEVEL>/ (162 queries,
|
||||
0 failures, one spot-check re-verified against the live API). This file deliberately does NOT
|
||||
copy that table: one table in two places drifts.
|
||||
|
||||
130 OF THE 162 CARRY A DUTY. Only 32 are duty-free (28 Public Domain, 4 CC0). By class:
|
||||
|
||||
ATTRIBUTION-ONLY (credit the source)
|
||||
44 CC BY 4.0 13 CC BY 3.0 IGO 4 CC BY 2.5 2 CC BY 3.0
|
||||
2 Open Government Licence v3
|
||||
|
||||
SHARE-ALIKE CLASS — A STRONGER DUTY: a derivative may have to be licensed alike, which is
|
||||
the class that actually constrains giving this repository away.
|
||||
58 Open Data Commons ODbL 1.0
|
||||
7 Creative Commons ShareAlike 2.0 / 3.0 — AGO GAB GEO GIN MMR NER TUR
|
||||
|
||||
ODbL IS LISTED IN THAT CLASS DELIBERATELY. Alongside attribution it requires a Derivative
|
||||
Database to be offered under ODbL and carries a keep-open provision, so by volume the
|
||||
share-alike question covers 65 of 162 countries, not 7. This repository has already acted on
|
||||
that reading once: library/scripts/posters/data/README.md records that Natural Earth's 1:10m
|
||||
ROADS were requested and deliberately NOT shipped because of an ODbL obligation.
|
||||
WHETHER 'licensed alike' binds a giveaway of this repository is a legal question, not a
|
||||
measurement, and it is open. It is recorded here so nobody has to rediscover it.
|
||||
|
||||
duty-free: 28 Public Domain, 4 CC0.
|
||||
|
||||
See documentation/THIRD-PARTY.md for the whole shipped tree.
|
||||
1
public/dx-maps/assets/boundaries/gb-CHN-ADM1.geojson
Normal file
14
public/dx-maps/assets/boundaries/gb-IND-ADM1-full.meta.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"file": "gb-IND-ADM1-full.geojson",
|
||||
"units": 36,
|
||||
"source": "geoBoundaries gbOpen (www.geoboundaries.org)",
|
||||
"boundaryID": "IND-ADM1-1811400",
|
||||
"boundarySource": "DataMeet India community, Election Commission of India",
|
||||
"licence": "Creative Commons Attribution 2.5 India (CC BY 2.5 IN)",
|
||||
"licenceSource": "github.com/datameet/maps",
|
||||
"yearRepresented": "2011",
|
||||
"sourceUpdated": "Thu Jan 19 07:31:04 2023",
|
||||
"fetchedFrom": "https://github.com/wmgeolab/geoBoundaries/raw/9469f09/releaseData/gbOpen/IND/ADM1/geoBoundaries-IND-ADM1_simplified.geojson",
|
||||
"processing": "coordinates rounded to 3 decimals, consecutive duplicate points dropped (library/scripts/boundaries/source-full-adm1.mjs)",
|
||||
"note": "Complete ADM1. The curated gb-IND-ADM1.geojson (the 100-list partition, listId-tagged) is a different file and is not replaced."
|
||||
}
|
||||
36
public/dx-maps/assets/boundaries/gb-IND-ADM1-web.geojson
Normal file
1
public/dx-maps/assets/boundaries/gb-IND-ADM1.geojson
Normal file
1
public/dx-maps/assets/boundaries/ne-places.json
Normal file
164
public/dx-maps/assets/boundaries/world-adm0-web.geojson
Normal file
1
public/dx-maps/assets/boundaries/world-adm0.geojson
Normal file
93
public/dx-maps/assets/fonts/bebas-neue/LICENSE
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright © 2010 by Dharma Type.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/dx-maps/assets/fonts/bebas-neue/bebas-neue-latin.woff2
Normal file
93
public/dx-maps/assets/fonts/figtree/LICENSE
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/dx-maps/assets/fonts/figtree/figtree-500-latin-ext.woff2
Normal file
BIN
public/dx-maps/assets/fonts/figtree/figtree-500-latin.woff2
Normal file
93
public/dx-maps/assets/fonts/noto-serif-devanagari/LICENSE
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
126
public/dx-maps/assets/fonts/oswald/LICENSE
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
Oswald — SIL Open Font License, Version 1.1
|
||||
Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont)
|
||||
Full text below, and at https://openfontlicense.org/ (OFL-1.1)
|
||||
|
||||
WHY THIS FILE IS IN THIS REPO
|
||||
DX-life sets --font-family-cyrillic to 'Oswald' and base/_globals.scss:35-44 switches every
|
||||
h1/h2/h3 to it under [lang="ru-RU"], with letter-spacing -0.02em. That switch exists because
|
||||
BEBAS NEUE HAS NO CYRILLIC GLYPHS AT ALL — their heading face simply cannot render Russian, so
|
||||
without the switch a Russian heading falls out of the family entirely. Ours did exactly that:
|
||||
we ship a `ru` locale, we had neither the rule nor the font, so every Russian heading rendered
|
||||
in whatever condensed system face the browser chose.
|
||||
|
||||
WHICH SUBSETS, AND WHY FOUR
|
||||
cyrillic and cyrillic-ext are the point. latin and latin-ext are carried too because a Russian
|
||||
heading routinely contains Latin characters — a country name, a figure, an ampersand — and a
|
||||
face that covers only Cyrillic drops those glyphs back to the fallback MID-WORD, which looks
|
||||
worse than not switching at all. Google's own unicode-range values are carried verbatim
|
||||
(css2 v57), so the browser fetches only the subset a given heading needs.
|
||||
|
||||
oswald-400-cyrillic.woff2 6700 B
|
||||
oswald-400-cyrillic-ext.woff2 7688 B
|
||||
oswald-400-latin.woff2 12248 B
|
||||
oswald-400-latin-ext.woff2 11624 B
|
||||
|
||||
Each verified to begin with the wOF2 magic (774f4632) after download, because a fetch that
|
||||
silently returns an HTML error page also "succeeds" and a font that is really a 404 page fails
|
||||
at render time with no network error to find later.
|
||||
|
||||
Weight 400 only: their --font-weight-regular is what h1/h2/h3 use, and nothing on the regions
|
||||
surfaces asks Oswald for another weight.
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/dx-maps/assets/fonts/oswald/oswald-400-cyrillic-ext.woff2
Normal file
BIN
public/dx-maps/assets/fonts/oswald/oswald-400-cyrillic.woff2
Normal file
BIN
public/dx-maps/assets/fonts/oswald/oswald-400-latin-ext.woff2
Normal file
BIN
public/dx-maps/assets/fonts/oswald/oswald-400-latin.woff2
Normal file
93
public/dx-maps/assets/fonts/poppins/LICENSE
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/dx-maps/assets/fonts/poppins/poppins-400-latin-ext.woff2
Normal file
BIN
public/dx-maps/assets/fonts/poppins/poppins-400-latin.woff2
Normal file
BIN
public/dx-maps/assets/fonts/poppins/poppins-500-latin-ext.woff2
Normal file
BIN
public/dx-maps/assets/fonts/poppins/poppins-500-latin.woff2
Normal file
BIN
public/dx-maps/assets/fonts/poppins/poppins-600-latin-ext.woff2
Normal file
BIN
public/dx-maps/assets/fonts/poppins/poppins-600-latin.woff2
Normal file
BIN
public/dx-maps/assets/fonts/poppins/poppins-700-latin-ext.woff2
Normal file
BIN
public/dx-maps/assets/fonts/poppins/poppins-700-latin.woff2
Normal file
36
public/dx-maps/assets/images/PROVENANCE
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
worldmap.svg — provenance
|
||||
=========================
|
||||
|
||||
WHAT IT IS
|
||||
The faint world-map watermark DX-life paints behind its people-group list. Their
|
||||
UupgsList sets it inline on the list root (app/components/public/UupgsList.vue:451):
|
||||
background-image: url(/assets/images/worldmap.svg), background-size 60%,
|
||||
background-position top, min-height 400px. Measured on their live
|
||||
/regions/cameroon: HTTP 200, 62,740 bytes over the wire, computed background-size
|
||||
60% / position 50% 0% / repeat no-repeat.
|
||||
|
||||
WHERE IT CAME FROM
|
||||
Copied byte-for-byte from the DX-life repository:
|
||||
doxa-life/public/assets/images/worldmap.svg
|
||||
Same organisation, copied so the regions clone matches their page. 311 KB on disk;
|
||||
small against this tree's other assets, which run 644 KB to 42 MB.
|
||||
|
||||
WHY THERE IS NO LICENCE FILE BESIDE IT
|
||||
Checked, rather than assumed. A full-file scan for copyright, licence, attribution,
|
||||
author, creator, <title>, <desc>, <metadata>, rdf:/dc: nodes and the usual stock-library
|
||||
names (Natural Earth, simplemaps, amCharts, Freepik, Shutterstock, OSM, Mapbox) returns
|
||||
NOTHING. The file is a plain vector — 394 <path> elements, three groups, no <image> and
|
||||
no <text> — and its ids are design-tool export names (Group_345,
|
||||
data-name="Group 345"), i.e. authored by their designer rather than downloaded with
|
||||
embedded credits. DX-life keeps no licence file alongside public/assets/images/ either;
|
||||
they treat that folder as their own artwork.
|
||||
So this is internal reuse of a first-party asset, not redistribution of a third-party
|
||||
one. Recorded here because the alternative was leaving the judgement implicit.
|
||||
|
||||
Contrast with the vendored typefaces, which ARE third-party and therefore carry their
|
||||
licence in the folder that serves them: public/assets/fonts/bebas-neue/LICENSE and
|
||||
.../poppins/LICENSE, both the SIL Open Font License 1.1, as the OFL requires.
|
||||
|
||||
NOTE ON THE FILENAME
|
||||
Extensionless on purpose. check:public-hygiene refuses a *.txt in a served folder, and
|
||||
the font LICENSE files set that precedent.
|
||||
1
public/dx-maps/assets/images/worldmap.svg
Normal file
|
After Width: | Height: | Size: 236 KiB |
3060
public/dx-maps/assets/lib/packet-strings.json
Normal file
2834
public/dx-maps/assets/lib/poster-render.global.js
Normal file
2829
public/dx-maps/assets/lib/poster-render.js
Normal file
60
public/dx-maps/assets/people-groups/india-states.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"meta":{
|
||||
"what": "India's 28 states and 8 union territories, each with the UPG-100 people groups located inside it (slugs). The term is \"states and union territories\", never \"provinces\".",
|
||||
"regenerate": "node plugin-designs/my-upg-100-list/data/emit-india-states.mjs (after pip-stamp.mjs re-stamps admin-membership.json)",
|
||||
"inputs": {
|
||||
"membership": {
|
||||
"path": "plugin-designs/my-upg-100-list/data/admin-membership.json",
|
||||
"sha384": "sha384-0D8paDuTT3HmH+YQkuuQP0ZIjp84ysz3lIXNaFlNbZKp+OevjnpZjj4FPyi7SJUl"
|
||||
},
|
||||
"geometry": {
|
||||
"path": "data/boundaries/gb-IND-ADM1-full.geojson",
|
||||
"sha384": "sha384-0tTsrv3FE4otJ2OGq7IXCs84Y9lHF7rz3cB+Vhxv76ARuw90JtJt7RLAG91L+/9j"
|
||||
}
|
||||
},
|
||||
"served_geometry": "boundaries/gb-IND-ADM1-full-web.geojson",
|
||||
"shape_property": "shapeName",
|
||||
"counts": {
|
||||
"states": 28,
|
||||
"union_territories": 8,
|
||||
"people_groups": 855
|
||||
}
|
||||
},
|
||||
"states":[
|
||||
{"id":"india-an","iso":"IN-AN","kind":"union_territory","name":"Andaman and Nicobar Islands","shapeName":"Andaman and Nicobar Islands","bbox":[92.204,6.754,93.943,13.673],"count":9,"slugs":["andamanese","bhatu-andamani","brahmin-andamani","coorgi-andamanese","great-andamanese","jarawas","onge","sentinelese","telengi"]},
|
||||
{"id":"india-ap","iso":"IN-AP","kind":"state","name":"Andhra Pradesh","shapeName":"Andhra Pradesh","bbox":[76.761,12.624,84.765,19.168],"count":81,"slugs":["accukattavandlu","aguru","aiyarakulu","andhra-vaisnawa","ansari-momin-telugu","arakala","are-bondili","are-katika","atollu","bandollu","bania-agarwal-telegu","bayita-kammara","berad","bhondari","budakukkala","cakketakare","camadollu","cundollu","dammali","dasari","gandalla","gandla","ganta-sayebulu","garadi","gavara","godagali","godari","guddi-eluguwallu","hajjam-telugu","hatgar","jakkula","jandra","jethi","kaikali","karnam","karnibhakthulu","kommu-dasari","konda-kammara","kuliya","kurakula","madiga-dasu","majjula","malis","mandula","manna-dhora","medari","mondivogula","muli","musti-chenchu","nagavamsam","nakhasa","neyyala","ontaris","padigarajulu","pagativeshagallu","paky","pamidi","pamula","pancaratra-srivaisnavas-brahmans","reddi-dora","reddika","runjalavallu","sadaru","sadhu-chetty","sakunapaksollu","samayavallu","senapathi","silavantulu","swakulasali-koshti","tamboli-telugu","thogataveera-kshatriya","tirugatigantla","tolakari","udupula","uppara","vaikhanasa-brahmans","vipravinodulu","yata","yatagiri","yatla","yite"]},
|
||||
{"id":"india-ar","iso":"IN-AR","kind":"state","name":"Arunachal Pradesh","shapeName":"Arunāchal Pradesh","bbox":[91.546,26.651,97.411,29.462],"count":7,"slugs":["khamba","khamyang","khowa","momba","na","serdukpen","zakhrin"]},
|
||||
{"id":"india-as","iso":"IN-AS","kind":"state","name":"Assam","shapeName":"Assam","bbox":[89.699,24.136,96.018,27.972],"count":15,"slugs":["aiton","bilaspuri","chamar-assamese","hajjam-assamese","kumhar-assamese","man","maria-muslims","matak","mukhi","nai-assamese","namasudra-assamese","sudra-das","taiphake","teli-assamese","turun"]},
|
||||
{"id":"india-br","iso":"IN-BR","kind":"state","name":"Bihar","shapeName":"Bihār","bbox":[83.32,24.286,88.3,27.522],"count":18,"slugs":["banpar","camma","dhamin","gadihar","gandharv","hindu-santarash","jains-maithili","jogi-angika","kadar","mukeri","nalband","rajdhop","sahisia","shershabadia","thakurai","tikulhara","tilli","turuk-pasi"]},
|
||||
{"id":"india-ch","iso":"IN-CH","kind":"union_territory","name":"Chandigarh","shapeName":"Chandīgarh","bbox":[76.705,30.666,76.849,30.795],"count":1,"slugs":["khatik-punjabi"]},
|
||||
{"id":"india-ct","iso":"IN-CT","kind":"state","name":"Chhattisgarh","shapeName":"Chhattīsgarh","bbox":[80.244,17.782,84.396,24.106],"count":24,"slugs":["badhai-chhattisgarhi","bairagi-chhattisgarhi","bargah","beldar-chhattisgarhi","bhangi-chhattisgarhi","bharewa","bharia-bhumia-chhattisgarhi","bhattra-bhatri","bhunjia","chaikwa","dhakad-halbi","dom-chhattisgarhi","ghadwa","gosain-chhattisgarhi","jogi-halbi","kalal-sadri","khairwar-chhattisgarhi","khatik-chhattisgarhi","kol-chhattisgarhi","munnewar","panara","pando","sayyid-chhattisgarhi","son-koli"]},
|
||||
{"id":"india-dh","iso":"IN-DH","kind":"union_territory","name":"Dadra and Nagar Haveli and Daman and Diu","shapeName":"Dādra and Nagar Haveli and Damān and Diu","bbox":[70.876,20.047,73.218,20.743],"count":1,"slugs":["sagharias"]},
|
||||
{"id":"india-dl","iso":"IN-DL","kind":"union_territory","name":"Delhi","shapeName":"Delhi","bbox":[76.839,28.405,77.347,28.884],"count":1,"slugs":["khatbune"]},
|
||||
{"id":"india-ga","iso":"IN-GA","kind":"state","name":"Goa","shapeName":"Goa","bbox":[73.675,14.901,74.336,15.801],"count":5,"slugs":["bania-konkani","gabit","gomantak-maratha","mhalo","mith-gauda"]},
|
||||
{"id":"india-gj","iso":"IN-GJ","kind":"state","name":"Gujarat","shapeName":"Gujarāt","bbox":[68.093,20.12,74.477,24.712],"count":90,"slugs":["agaria","agri","anjana-patel-gujarati","bafan","bairagi-gujarati","baloch","bamcha","banjania","barot","barthari","bhodala","borpi","caki","champta","cunara","dafer","dangasiya","dasnami-bawa","depala","dudhwala","gadhai","galiara","garmatan","garvi","gavli","ghantiya","golarana","gosain-gujarati","hajjam-gujarati","halaypotra","hingora","hingorza","jat-muslim","juneja","kahar-gujarati","kamalia","kambodia-bhagat","kapadi","kasar-gujarati","kasbati","kathi","ker","khalpa","khaskeli","khatik-gujarati","langa","machhiyara","majothi","makrani","mandali-muslims","mer","mir","miyana","moghol-gujarati","mujavar","muslim-khatri","muslim-lakhera","muslim-patel","mutwa","narsinghpura","nayak-muslims","node","padhar","panar","paradhi","patanwadia","patni-jamat","patni-vankars","pomla","rathodia","raysipotra","royma","sagar","sandhai-muslims","saranias","sayyid-gujarati","shravan","siddi","sompura-salat","tai","targala","theba","tirgar","turi-barot","vadi","vahivanca-barot","vayati","vyapari","wagheri","wandharo"]},
|
||||
{"id":"india-hp","iso":"IN-HP","kind":"state","name":"Himachal Pradesh","shapeName":"Himāchal Pradesh","bbox":[75.594,30.378,79.009,33.257],"count":20,"slugs":["bhojki","daule","dhaki","dosali","gosain-kangri","hadi","hesi","khatik-chambeali","malaneese","masku","nar","nicar","pangwal-buddhists","pangwala","rehar","sanhai","sansoi","seok","swangla","thavi"]},
|
||||
{"id":"india-hr","iso":"IN-HR","kind":"state","name":"Haryana","shapeName":"Haryāna","bbox":[74.474,27.653,77.604,30.929],"count":4,"slugs":["arain-punjabi","hajjam-haryanvi","kucband","lakhera-haryanvi"]},
|
||||
{"id":"india-jh","iso":"IN-JH","kind":"state","name":"Jharkhand","shapeName":"Jhārkhand","bbox":[83.33,21.97,87.962,25.349],"count":9,"slugs":["dhekaro","jadupetia","jarkhandi-hajjam","jarkhandi-julaha","jharkhandi-nai","modaks","nagbansi","puran","sukiyar"]},
|
||||
{"id":"india-jk","iso":"IN-JK","kind":"union_territory","name":"Jammu and Kashmir","shapeName":"Jammu and Kashmīr","bbox":[73.39,32.276,76.78,35.118],"count":33,"slugs":["arain-dogri","bania-dogri","bhand-kashmiri","bomba","budhan","cheintz","chopan","dhobi-dogri","dhyar","domal","galwan","ganz","gardi","gorkun","hajjam-kashmiri","kawoj","kraal","lohar-dogri","manhas","manjygur-muslims","moghol-dogri","nagbansi-dogri","najjar","namdagur","naqqas","pohul","ratal","sayyid-pahari","shaksaz","shina-india","sofi","sudhan","waza"]},
|
||||
{"id":"india-ka","iso":"IN-KA","kind":"state","name":"Karnataka","shapeName":"Karnātaka","bbox":[74.086,11.595,78.588,18.459],"count":81,"slugs":["adiyan","agasa","ansari-momin-kannada","arasu","ayri","bagga","baira","balasantoshi","bandi","bania-kannada","batada","bayil-kammara","bayil-pattar","bellara","bestha","chaptegar","charodi","chenna-dasar","dombi-dasa","durga-murga","galada-kokani","ganti-chores","gatti","gauli","ghadi","godda","gond-kannada","gudikara","guraw","hajjam-kannada","halleer","harkantra","hasalaru","helawa","holeya-dasari","jatigar","jaweri","jogi-telugu","jogi-tulu","khakar","kolari","komara-panta","koosa","koraga-korra","kottari","koyava","kudiya","kudubi","kummara-kumbara","kurahina-setti","ladar","maila","malaikudi","maleru","manser","meda","mogaveera","mundala","nadors","nai-kannada","nairy","natua","nawaiyat","padarthis","padiyar","pambada","pattegar","pendar","pinjara-kannada","rachavar","raddi","rajapuri","raneyar","raval","raya-rawat","sapalyas","sayyid-kannada","soligaru","sudir","thammadi","vilyagaras"]},
|
||||
{"id":"india-kl","iso":"IN-KL","kind":"state","name":"Kerala","shapeName":"Kerala","bbox":[74.868,8.292,77.412,12.796],"count":72,"slugs":["allar","ambalavasi","anthur-nair","arayavati","ayyanavar","bakuda","bania-malayalam","bowis-mogeyar","cakkala","cakkyar-nambiyar","chakkiyar","chamar-muchi-malayalam","chengazhi-nambiar","cingatanmar","dheevara","erawallan","ezhavathi","ilayatu","kakkalan","kalanadies","kalladi","kallattu-kurup","karimpalan","kavara","kavuthiyan","kidaran","kocu-wellan","kollan","kootan","kumar-kshatriya","kummara-kusuvan","kunduvadian","kurukkal","malai-pandaram","malayalar","maran-nairs","marasari","mavilan","moger-malayalam","moothan","mukkari","musari","muttatu","nalkadaya","nambutiris","narsanna","nasuvan","nayadi","otans","padannan","palakad-tandans","pandithar","pathinaickans","pathiyan","pisaroti","poduval","pulisari","puspakan-unni","reddi","samanthan","thachanad-moopan","tiyattunni","urindavan","vadugan","valinjiyar","vetan","vilkurup","visavan","wainadan-cetti","wainadan-kader","wainadan-pulayan","wayanad-pathiyan"]},
|
||||
{"id":"india-la","iso":"IN-LA","kind":"union_territory","name":"Ladakh","shapeName":"Ladākh","bbox":[72.529,32.338,80.327,37.077],"count":6,"slugs":["bania-kashmiri","brukpa","campa","dokhpa","mon-india","purig-pa"]},
|
||||
{"id":"india-ld","iso":"IN-LD","kind":"union_territory","name":"Lakshadweep","shapeName":"Lakshadweep","bbox":[72.165,8.264,73.702,11.704],"count":1,"slugs":["laccadive"]},
|
||||
{"id":"india-mh","iso":"IN-MH","kind":"state","name":"Maharashtra","shapeName":"Mahārāshtra","bbox":[72.655,15.606,80.898,22.034],"count":54,"slugs":["attar-saibulu","baharupi","barda","bari-marathi","bharwad","bhoi-marathi","chari","chitari","citrakathi","dhawad","dom-marathi","garodi","garpagari","gondhali","gopal","hardas","hatkar","jhadi","kachchi-kushwaha","kaghzi","kapewar","kasikapadi","kathar","kharvi","khatik-marathi","kokaniz","kolhati","kudimahan","ladsi","lajjhar","lodha-marathi","lonar","mana","mitna","mondiwar","muslim-kachera","nahal","naikpod","nandiwala","neeli","nilkanth","pangul","phularis","pinjara-marathi","pochammalollu","prabhu-patane","sahi","sanghar","sarodi","sutar-mistry","takari","velip","waidu","yelmar"]},
|
||||
{"id":"india-ml","iso":"IN-ML","kind":"state","name":"Meghalaya","shapeName":"Meghālaya","bbox":[89.814,25.029,92.803,26.12],"count":2,"slugs":["banai","dalu-india"]},
|
||||
{"id":"india-mn","iso":"IN-MN","kind":"state","name":"Manipur","shapeName":"Manipur","bbox":[92.971,23.833,94.745,25.692],"count":5,"slugs":["koiren","lois","pangans","thanga","yatibi"]},
|
||||
{"id":"india-mp","iso":"IN-MP","kind":"state","name":"Madhya Pradesh","shapeName":"Madhya Pradesh","bbox":[74.03,21.07,82.81,26.869],"count":33,"slugs":["anjana","banchada","baraua","bari-hindi","basudeva","berar","bhanumati","bhurtia","dhed","gachha","hajjam-hindi","jadam","jains-hindi","kachera-bagheli","kandera","kasar-hindi","ladia","lahangir","manbhav","mankar","muskhan","muslim-gond","naita","namdeo","naoda","nat-bagheli","nema","rujjhar","sais","sikligar","sisgars","soria","zamral"]},
|
||||
{"id":"india-mz","iso":"IN-MZ","kind":"state","name":"Mizoram","shapeName":"Mizoram","bbox":[92.257,21.941,93.436,24.521],"count":0,"slugs":[]},
|
||||
{"id":"india-nl","iso":"IN-NL","kind":"state","name":"Nagaland","shapeName":"Nāgāland","bbox":[93.327,25.199,95.242,27.036],"count":0,"slugs":[]},
|
||||
{"id":"india-or","iso":"IN-OR","kind":"state","name":"Odisha","shapeName":"Odisha","bbox":[81.388,17.813,87.486,22.568],"count":64,"slugs":["baha-i","bairagi-odia","bangti","banka","bari-odia","bauti","bavuri","bhoi-odia","bondo","brahman-jyoti","candhai-maru","chatarkheya","cherua","citrakar","daitapati","damla","darji-odia","dhera","didayi","eneti-dora","ghantarghada","ghogia","ghusuria","godra","guna","irika","janughanta","jogi-odia","kachchi-alia","kamila","kapasiya","kasar-odia","khadura","khandwal","khati-odia","kobari","koraga","kuda","kuli","kurunga","kusta","liari","maghia","mahuria","malia","markande-kanjia","mundapotta","nai-odia","naksia","noliya","oriya","panka-odia","parenga","radhi","rana","sabakhia","samasi","sanei","sayyid-odia","sinduria","suda","thanapati","tharua","tula-vina"]},
|
||||
{"id":"india-pb","iso":"IN-PB","kind":"state","name":"Punjab","shapeName":"Punjab","bbox":[73.88,29.542,76.939,32.513],"count":4,"slugs":["daoli","gagra","pinjara-punjabi","racband"]},
|
||||
{"id":"india-py","iso":"IN-PY","kind":"union_territory","name":"Puducherry","shapeName":"Puducherry","bbox":[75.527,10.827,82.344,16.761],"count":5,"slugs":["indo-french","indo-vietnamese","sonavar","vettalaikarans","vettiyan"]},
|
||||
{"id":"india-rj","iso":"IN-RJ","kind":"state","name":"Rajasthan","shapeName":"Rājasthān","bbox":[69.484,23.061,78.272,30.198],"count":48,"slugs":["badi-marwari","baret","bargi","bhopa","bhutta","cejara","chadwa","chheepi","chobdar","chungar","cudigar","dadupanthi","damami","dhola","ghihara","gosain-rajasthani","gurru","hiranbaj","khadim","kharol","kriyagar","laskari","mangnyar-muslims","moghol-marwari","mogia","murhid","nagda","nai-mewari","nyaria","pajabgar","pakhimara","rajkumhar","rajpurohit","rath","runwalas","sain","sanchia","santia","saraogi","shikari","singiwala","sorgars","sunga","tak","usta","van-baoriya","ved","vijabargiya"]},
|
||||
{"id":"india-sk","iso":"IN-SK","kind":"state","name":"Sikkim","shapeName":"Sikkim","bbox":[88.014,27.079,88.92,28.129],"count":1,"slugs":["yakha"]},
|
||||
{"id":"india-tg","iso":"IN-TG","kind":"state","name":"Telangana","shapeName":"Telangāna","bbox":[77.236,15.836,81.323,19.916],"count":26,"slugs":["are-kapu","attagara","bhatrajulu","bindla","bolla","cippollu","gangireddula","gosangi","gurika","jains-telugu","kachi","katipamula","khatik-telugu","lakkamari-kapu","mala-masti","matangi","mitha-ayyalavar","moghal-telugu","muchi-chamar","pambala","pariki-muggula","patkars","rajannalu","saradagallu","sindhollu","verra-musti"]},
|
||||
{"id":"india-tn","iso":"IN-TN","kind":"state","name":"Tamil Nadu","shapeName":"Tamil Nādu","bbox":[76.233,8.077,80.347,13.565],"count":64,"slugs":["alavan","ansari-momin-tamil","anuppan","aranadan","baig","bondili","cekkala","chamar-muchi-tamil","chayakkaran","dasa-palanjika","dayare","dhobi-tamil","gangawar","gudugudupandi","gurukkal","jains-tamil","jati-pillai","kambalattan","kambar","kannada-naicker","kannada-saineegar","karaiyar","karlar","karna-sale","katesar","kattiakaran","kayalar","kepmari","koliyan","kongu-vaisnawa","kongu-vellala-gounder","kota","krishnavaka","kudumi","kuluvan","kummara-kummari","kunnuva-vellalar","malai-vellala","maleyan","maniyattikkaran","mudugar","nakkala","namdeo-maratha","nanjil-mudali","pannadi","panniandi","parkavakulam","parvatarajakulam","pattanavan","perumkollar","salivahanam","savalakkarar","sayyid-tamil","semman","senaithalaivar","siviar","tambiran","tholuva-naicker","thoraiyar","tondman","uppillian","urikkar-naicker","vellaiyan-kuppam-padaiyaci","vettaikkaran"]},
|
||||
{"id":"india-tr","iso":"IN-TR","kind":"state","name":"Tripura","shapeName":"Tripura","bbox":[91.15,22.939,92.332,24.528],"count":11,"slugs":["bajandar","banik-bania","barua","haluadas","kanda","mahisyadas","malakar","sabar","saha","shabdakar","tripuri-namasudra"]},
|
||||
{"id":"india-up","iso":"IN-UP","kind":"state","name":"Uttar Pradesh","shapeName":"Uttar Pradesh","bbox":[77.084,23.87,84.635,30.406],"count":23,"slugs":["ahmadiya","arain-kannauji","badhik","badi","balahar","bargala","bargujar","bhuiyar","bisati","charwaha","gadhia","habura","hijra","kankali","khangar-bundeli","mujavir","pankhiya","patari","putliwale","rai-bhat-muslims","rastogi","rave","sheikhzade"]},
|
||||
{"id":"india-ut","iso":"IN-UT","kind":"state","name":"Uttarakhand","shapeName":"Uttarākhand","bbox":[77.573,28.723,81.045,31.456],"count":7,"slugs":["anwal","chaudangsi","jimdar","kuthliya-bora","raji-india","sayyid-garhwali","tagewale"]},
|
||||
{"id":"india-wb","iso":"IN-WB","kind":"state","name":"West Bengal","shapeName":"West Bengal","bbox":[85.82,21.484,89.874,27.221],"count":30,"slugs":["angarok","ansari-momin-bengali","bagti","baida","baisya","bhakta","bhalla","bhujel","byadh","chunari","dhal-kstriya","dhimal","ghazi","hajjam-bengali","jains-bengali","kakmara","kapuria","kasar-bengali","khairwar-bengali","khalifa","khatik-bengali","kurol","mirshikar","paharia","pinjara-dhunia-bengali","siyalgar","sunuwar","sutradhar","tamboli-bengali","tripuri-kumhar"]}
|
||||
]}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"id":"frontier-500k","label":"Project 700: frontier people groups of 500,000 or more","definition":"IMB Lostness Priority 0 and Population of at least 500,000","filter":{"lostness":[0],"minPopulation":500000},"sources":{"imb":"data/people-groups/people_groups.csv","doxa":"https://pray.doxa.life/api/people-groups/list (fetched 2026-09-25)"},"counts":{"imb":737,"doxa":71},"slugs":["agasa","aiyarakulu","akhdam","alawites-turkiye","arab-bedouin","avar-kunsakh","badhai-chhattisgarhi","bairagi-gujarati","baloch-eastern","baloch-western","bania-agarwal-telegu","bederia","bedouin-arabs-saudi-arabia","dar-hamid","deaf-burmese","deaf-congolese-democratic-republic-of-the-congo","deaf-iranians","deaf-pakistanis","digil-rahanweyn","gaaliin","gawamaa","gazan-arabs","guhayna","ha-li","hadrami-arabs","hasania","jains-hindi","jat-muslim","jula-burkina-faso","kawahia","kotokoli-togo","kummara-kumbara","kurd-northern","kuria","laki","lebanese-shia-arabs","lebanese-sunni-arabs","libyan-arabs","luwu","makhuwa-shirima","makua","meadow-mari","mishaba-laluo","moor","mwera","namasudra-assamese","northern-dong","northern-yemeni-arabs","nuer","pila","pingdi-yao","rizeiqat","rufaa","sanusi-bedouins","shiites","shina-pakistan","shirazi","sudra-das","sutradhar","tigre","tihama-yemeni-arabs","tondman","turkmen","west-bank-arabs","western-baloch-pakistan","white-moor","xiaoliangshan-nosu","yao","yazeed","yinuo-nosu","zigua"]}
|
||||
1
public/dx-maps/assets/people-groups/lists/frontier.json
Normal file
1
public/dx-maps/assets/people-groups/lists/unengaged.json
Normal file
1
public/dx-maps/assets/people-groups/lists/unreached.json
Normal file
353
public/dx-maps/assets/posters/andean-caribbean.html
Normal file
353
public/dx-maps/assets/posters/asia-china-yunnan-province.html
Normal file
353
public/dx-maps/assets/posters/asia-india-andhra-pradesh.html
Normal file
353
public/dx-maps/assets/posters/asia-india-gujarat.html
Normal file
353
public/dx-maps/assets/posters/asia-india-karnataka.html
Normal file
353
public/dx-maps/assets/posters/asia-india-kerala.html
Normal file
353
public/dx-maps/assets/posters/asia-india-madhya-pradesh.html
Normal file
353
public/dx-maps/assets/posters/asia-india-odisha.html
Normal file
353
public/dx-maps/assets/posters/asia-india-rajasthan.html
Normal file
353
public/dx-maps/assets/posters/asia-india-tamil-nadu.html
Normal file
353
public/dx-maps/assets/posters/asia-india-uttarakhand.html
Normal file
353
public/dx-maps/assets/posters/asia-india-west-bengal.html
Normal file
353
public/dx-maps/assets/posters/brazil.html
Normal file
353
public/dx-maps/assets/posters/central-africa.html
Normal file
BIN
public/dx-maps/assets/posters/city-ago--8.8484-13.2535.webp
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
public/dx-maps/assets/posters/city-bfa-9.7792--4.2879.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/dx-maps/assets/posters/city-brn-4.9234-114.9209.webp
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
public/dx-maps/assets/posters/city-btn-27.75-91.2637.webp
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/dx-maps/assets/posters/city-cod-3.5049-24.4533.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/dx-maps/assets/posters/city-idn--3.7191-127.9542.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/dx-maps/assets/posters/city-ind-10.1396-77.7883.webp
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
public/dx-maps/assets/posters/city-ind-10.3796-78.8202.webp
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
public/dx-maps/assets/posters/city-ind-10.5285-76.2146.webp
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
public/dx-maps/assets/posters/city-ind-10.7853-78.7051.webp
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
public/dx-maps/assets/posters/city-ind-10.7855-76.6516.webp
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
public/dx-maps/assets/posters/city-ind-10.787-79.1378.webp
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
public/dx-maps/assets/posters/city-ind-10.8469-76.2633.webp
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.0049-76.9626.webp
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.0546-78.6369.webp
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.06-76.0816.webp
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.0991-77.2807.webp
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.1271-78.6565.webp
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.253-76.2299.webp
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.3548-77.711.webp
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.4517-76.7145.webp
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.6234-92.7265.webp
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.6765-76.1741.webp
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.8745-75.3695.webp
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.9182-79.8085.webp
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
public/dx-maps/assets/posters/city-ind-11.967-75.3663.webp
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/dx-maps/assets/posters/city-ind-12.2973-75.7684.webp
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
public/dx-maps/assets/posters/city-ind-12.3022-76.6423.webp
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
public/dx-maps/assets/posters/city-ind-12.427-75.742.webp
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/dx-maps/assets/posters/city-ind-12.4538-75.1701.webp
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/dx-maps/assets/posters/city-ind-12.5102-74.9852.webp
Normal file
|
After Width: | Height: | Size: 127 KiB |