PPLR data maps: every view loads under MapLibre, and the legend has accessible names
Under provider=maplibre (the keyless gallery) four of the seven map views never finished loading. They built `new mapboxgl.NavigationControl` in the load handler, and that global does not exist under MapLibre, so they sat on "Loading…". Popups and the pin marker had the same fault. Every such call now goes through glFor(map), the namespace of the engine that built the map. The views' styles and the Dark/Satellite/Light switch now use registry names (ink, hybrid, paper), which are the same Mapbox styles under Mapbox and OpenFreeMap under MapLibre. The search box loads its geocoder by engine, as GeocoderComponent does. Under MapLibre it is MaplibreGeocoder over this map's own local index, so the keyless gallery makes no request to api.mapbox.com at all. The legend's row caret, clear chip and collapse/reopen buttons carry accessible names and aria-expanded from legend.a11y.* and aria.* (optional i18n, English fallback), the keys the library legend uses. check:shared-reached now counts pplr-data-maps as carrying legend-a11y-keys. window.__pplrMap() reports what the active view's map did (engine, load fired, style layers, this bundle's data sources), so "loaded" is asserted, not inferred. Measured on a served built tree, host page on another origin: all ten views loaded under MapLibre and under Mapbox (load fired, style layers and data sources present, canvas not blank, 0 errors), with 0 api.mapbox.com requests under MapLibre. The same check on the parent build fails four map views under MapLibre. A control with no provider makes 13 Mapbox requests.
This commit is contained in:
parent
3cd3e7a55f
commit
17931ed031
15 changed files with 190 additions and 84 deletions
|
|
@ -18,8 +18,9 @@
|
|||
*
|
||||
* The `/` keyboard shortcut from the original PplrGeocoder is preserved.
|
||||
*/
|
||||
import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||
import { ensureGeocoder } from '../utils/pplrData.js'
|
||||
import { ref, watch, onMounted, onBeforeUnmount, computed, inject } from 'vue'
|
||||
import { ensureGeocoder, GEOCODER } from '../utils/pplrData.js'
|
||||
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
||||
import { useInstance } from '../composables/usePplrInstance.js'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -44,6 +45,10 @@ const wrapEl = ref(null)
|
|||
const slotEl = ref(null)
|
||||
|
||||
let geocoder = null
|
||||
// Same engine the map uses. Under maplibre (the keyless gallery) search runs on MaplibreGeocoder
|
||||
// over this map's own local index only, so it makes no Mapbox request at all.
|
||||
const provider = resolveMapProvider(inject('profileConfig', null))
|
||||
const isMaplibre = provider === 'maplibre'
|
||||
|
||||
// Build the localGeocoder callback. Mapbox-gl-geocoder calls this on every
|
||||
// keystroke; we return Carmen-GeoJSON-shaped features for matching local
|
||||
|
|
@ -116,7 +121,7 @@ function renderSuggestion(item) {
|
|||
// Shadow DOM is sealed from regular-document styles, so we must inject the same
|
||||
// stylesheet into each shadow root that hosts a geocoder. Idempotent — checks
|
||||
// if already present.
|
||||
const GEOCODER_CSS_URL = 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css'
|
||||
const GEOCODER_CSS_URL = (GEOCODER[provider] || GEOCODER.mapbox).css
|
||||
function ensureGeocoderCss(root) {
|
||||
if (!root || root === document) return
|
||||
if (root.querySelector('link[data-pplr-geocoder-css]')) return
|
||||
|
|
@ -134,6 +139,22 @@ function setup() {
|
|||
// from mounting. Per user 2026-05-02: map went completely blank when
|
||||
// the geocoder threw on shadow-DOM addTo().
|
||||
try {
|
||||
if (isMaplibre) {
|
||||
if (!window.MaplibreGeocoder || !window.maplibregl) {
|
||||
console.warn('[MapboxGeocoder] maplibre-gl-geocoder or maplibre-gl not on window — search disabled')
|
||||
return
|
||||
}
|
||||
// Local-only: the PG/family index this map already holds. No place search, no key.
|
||||
geocoder = new window.MaplibreGeocoder({ forwardGeocode: async () => ({ type: 'FeatureCollection', features: [] }) }, {
|
||||
maplibregl: window.maplibregl,
|
||||
placeholder: props.placeholder,
|
||||
localGeocoder: buildLocalGeocoder(),
|
||||
localGeocoderOnly: true,
|
||||
minLength: props.minChars,
|
||||
showResultsWhileTyping: true,
|
||||
render: renderSuggestion,
|
||||
})
|
||||
} else {
|
||||
if (!window.MapboxGeocoder || !window.mapboxgl) {
|
||||
console.warn('[MapboxGeocoder] mapbox-gl-geocoder or mapbox-gl not on window — check CDN <script> tags')
|
||||
return
|
||||
|
|
@ -152,6 +173,7 @@ function setup() {
|
|||
limit: 5,
|
||||
render: renderSuggestion,
|
||||
})
|
||||
}
|
||||
|
||||
geocoder.on('result', (ev) => {
|
||||
const f = ev.result
|
||||
|
|
@ -222,7 +244,7 @@ function onGlobalKey(e) {
|
|||
const isCmdK = (e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')
|
||||
if (isSlash || isCmdK) {
|
||||
e.preventDefault()
|
||||
const input = slotEl.value?.querySelector('input.mapboxgl-ctrl-geocoder--input')
|
||||
const input = slotEl.value?.querySelector('input.mapboxgl-ctrl-geocoder--input, input.maplibregl-ctrl-geocoder--input')
|
||||
input?.focus()
|
||||
input?.select?.()
|
||||
}
|
||||
|
|
@ -230,7 +252,7 @@ function onGlobalKey(e) {
|
|||
|
||||
onMounted(() => {
|
||||
// setup() swallows its own errors; a failed script load leaves the map working without search.
|
||||
ensureGeocoder().then(setup, (e) => console.warn('[MapboxGeocoder]', e.message))
|
||||
ensureGeocoder(provider).then(setup, (e) => console.warn('[MapboxGeocoder]', e.message))
|
||||
document.addEventListener('keydown', onGlobalKey)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,22 @@
|
|||
* (one search per map cluster, per QA "unified geocoder" decision).
|
||||
*/
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// Accessible names from the shared catalog (legend.a11y.*, aria.*), the keys the library legend
|
||||
// uses, so they are translated in all eleven locales. i18n is OPTIONAL: on a host without it, the
|
||||
// English fallback below is used, never a raw key.
|
||||
const A11Y_EN = {
|
||||
'legend.a11y.expandRow': 'Expand {label}', 'legend.a11y.collapseRow': 'Collapse {label}',
|
||||
'legend.a11y.clearSelection': 'Clear selection',
|
||||
'aria.expandLegend': 'Expand legend', 'aria.collapseLegend': 'Collapse legend',
|
||||
}
|
||||
const fillA11y = (str, p) => String(str).replace(/\{(\w+)\}/g, (m, k) => (p && k in p ? p[k] : m))
|
||||
let ta = (key, p) => fillA11y(A11Y_EN[key] ?? key, p)
|
||||
try {
|
||||
const i18n = useI18n()
|
||||
ta = (key, p) => { const v = i18n.t(key, p || {}); return v === key ? fillA11y(A11Y_EN[key] ?? key, p) : v }
|
||||
} catch (_) { /* no i18n on this host */ }
|
||||
import { useInstance } from '../composables/usePplrInstance.js'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -373,6 +389,8 @@ function rowClasses(row) {
|
|||
class="stl-collapse-btn"
|
||||
:class="{ closed: !panelOpen }"
|
||||
:title="panelOpen ? 'Collapse legend' : 'Expand legend'"
|
||||
:aria-label="panelOpen ? ta('aria.collapseLegend') : ta('aria.expandLegend')"
|
||||
:aria-expanded="String(panelOpen)"
|
||||
@click="panelOpen = !panelOpen">
|
||||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M4.5 3L7.5 6L4.5 9" stroke="currentColor" stroke-width="1.8"
|
||||
|
|
@ -442,6 +460,8 @@ function rowClasses(row) {
|
|||
class="stl-trunk" :class="`stl-trunk-${seg}`"></span>
|
||||
<button v-if="row.node.children?.length"
|
||||
class="stl-caret" :class="{ open: expandedIds.has(row.node.id) }"
|
||||
:aria-expanded="expandedIds.has(row.node.id) ? 'true' : 'false'"
|
||||
:aria-label="ta(expandedIds.has(row.node.id) ? 'legend.a11y.collapseRow' : 'legend.a11y.expandRow', { label: row.node.label })"
|
||||
@click.stop="toggleExpand(row.node.id, $event)">
|
||||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M4.5 3L7.5 6L4.5 9" stroke="currentColor" stroke-width="1.6"
|
||||
|
|
@ -456,7 +476,7 @@ function rowClasses(row) {
|
|||
<span v-if="hasPop" class="stl-num">{{ fmtPop(row.node.pop) }}</span>
|
||||
<span class="stl-x-slot">
|
||||
<button v-if="selectedId === row.node.id"
|
||||
class="stl-x-chip" title="Clear selection"
|
||||
class="stl-x-chip" :title="ta('legend.a11y.clearSelection')" :aria-label="ta('legend.a11y.clearSelection')"
|
||||
@click.stop="deselect($event)">
|
||||
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2 2L10 10M10 2L2 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
|
|
@ -485,7 +505,9 @@ function rowClasses(row) {
|
|||
<button v-if="!panelOpen"
|
||||
class="stl-reopen"
|
||||
:data-theme="instance?.theme.value || 'dark'"
|
||||
title="Expand legend"
|
||||
:title="ta('aria.expandLegend')"
|
||||
:aria-label="ta('aria.expandLegend')"
|
||||
aria-expanded="false"
|
||||
@click="panelOpen = true">
|
||||
<svg class="stl-reopen-icon" width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2 4h8M2 6h8M2 8h5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
|
|
|
|||
|
|
@ -33,17 +33,58 @@ export function loadNames() {
|
|||
return namesPromise
|
||||
}
|
||||
|
||||
// mapbox-gl-geocoder is a page global too (the original page carried a <script> tag). Load it
|
||||
// once, on first need, so the search box works wherever the bundle is embedded.
|
||||
const GEOCODER_JS = 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js'
|
||||
let geocoderPromise = null
|
||||
export function ensureGeocoder() {
|
||||
if (typeof window === 'undefined' || window.MapboxGeocoder) return Promise.resolve()
|
||||
if (!geocoderPromise) geocoderPromise = new Promise((res, rej) => {
|
||||
// The geocoder is a page global too (the original page carried a <script> tag). Load it once, on
|
||||
// first need, and pick it BY ENGINE, as library/components/map-controls/GeocoderComponent.vue does:
|
||||
// the Mapbox plugin under provider=mapbox, MapLibre's under provider=maplibre. A hard-coded Mapbox
|
||||
// script made this the only request to api.mapbox.com in the keyless (maplibre) gallery.
|
||||
export const GEOCODER = {
|
||||
mapbox: { js: 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js',
|
||||
css: 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css',
|
||||
global: 'MapboxGeocoder' },
|
||||
maplibre: { js: 'https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1/dist/maplibre-gl-geocoder.min.js',
|
||||
css: 'https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1/dist/maplibre-gl-geocoder.css',
|
||||
global: 'MaplibreGeocoder' },
|
||||
}
|
||||
const geocoderPromises = {}
|
||||
export function ensureGeocoder(provider = 'mapbox') {
|
||||
const g = GEOCODER[provider] || GEOCODER.mapbox
|
||||
if (typeof window === 'undefined' || window[g.global]) return Promise.resolve()
|
||||
if (!geocoderPromises[provider]) geocoderPromises[provider] = new Promise((res, rej) => {
|
||||
const s = document.createElement('script')
|
||||
s.src = GEOCODER_JS
|
||||
s.onload = () => res(); s.onerror = () => { geocoderPromise = null; rej(new Error('geocoder failed to load')) }
|
||||
s.src = g.js
|
||||
s.onload = () => res(); s.onerror = () => { delete geocoderPromises[provider]; rej(new Error('geocoder failed to load')) }
|
||||
document.head.appendChild(s)
|
||||
})
|
||||
return geocoderPromise
|
||||
return geocoderPromises[provider]
|
||||
}
|
||||
|
||||
// The engine namespace that built THIS map (Popup, Marker, NavigationControl…). The views were
|
||||
// written against the Mapbox global; under provider=maplibre that global does not exist and a
|
||||
// `new mapboxgl.Popup` throws on the first pin click.
|
||||
export function glFor(map) {
|
||||
if (typeof window === 'undefined') return undefined
|
||||
const ml = window.maplibregl
|
||||
if (ml && map && ml.Map && map instanceof ml.Map) return ml
|
||||
return window.mapboxgl || ml
|
||||
}
|
||||
|
||||
// Verification handle (the gospel-resources __grState contract): what the ACTIVE view's map
|
||||
// actually did, so a check can assert "loaded" rather than inferring it from missing requests.
|
||||
// window.__pplrMap() -> { engine, loaded, styleLayers, pplrLayers }
|
||||
export function exposeMap(map) {
|
||||
if (typeof window === 'undefined' || !map) return
|
||||
let loaded = false
|
||||
map.once('load', () => { loaded = true })
|
||||
window.__pplrMap = () => {
|
||||
let layers = []
|
||||
try { layers = map.getStyle()?.layers || [] } catch (_) {}
|
||||
return {
|
||||
engine: window.maplibregl && window.maplibregl.Map && map instanceof window.maplibregl.Map ? 'maplibre' : 'mapbox',
|
||||
// 'load' may already have fired before this listener was attached; a loaded style says so too
|
||||
loaded: loaded || !!(map.isStyleLoaded && map.isStyleLoaded()),
|
||||
styleLayers: layers.length,
|
||||
// sources this bundle added (not the basemap's): proof the view's own data reached the map
|
||||
dataSources: (() => { try { return Object.keys(map.getStyle()?.sources || {}).filter((k) => !/^(composite|mapbox|openmaptiles|natural_earth|ne2_shaded|maplibre)/i.test(k)).length } catch (_) { return 0 } })(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const LOCALE_NAMES = {
|
|||
}
|
||||
|
||||
// ── PPLR FeatureServer — country polygons (live, not bundled) ────────────────
|
||||
import { dataUrl } from './pplrData.js'
|
||||
import { dataUrl, glFor } from './pplrData.js'
|
||||
|
||||
export const COUNTRY_FS_URL = 'https://services2.arcgis.com/S4ydGgujXcif36k3/arcgis/rest/services/aCountry/FeatureServer/0/query'
|
||||
export const COUNTRY_FS_FIELDS = ['GENCalpha3', 'ISOalpha2', 'CtryNm', 'ctryNmDisp']
|
||||
|
|
@ -282,7 +282,7 @@ export function makePinIndicator() {
|
|||
if (!map || !lngLat) return
|
||||
ensureEl(color)
|
||||
if (!marker) {
|
||||
marker = new mapboxgl.Marker({ element: el, anchor: 'bottom' })
|
||||
marker = new (glFor(map)).Marker({ element: el, anchor: 'bottom' })
|
||||
}
|
||||
marker.setLngLat(lngLat).addTo(map)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-coverage.vue — Country choropleth: avg # of locales filled per PG in that country.
|
||||
*
|
||||
|
|
@ -31,7 +31,7 @@ function reload() { window.location.reload() }
|
|||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer,
|
||||
accessToken: mapboxToken.value,
|
||||
style: 'mapbox://styles/mapbox/dark-v11',
|
||||
style: 'ink', // named: dark-v11 under Mapbox, OpenFreeMap dark under MapLibre
|
||||
center: [25, 18],
|
||||
zoom: 1.6
|
||||
})
|
||||
|
|
@ -50,6 +50,7 @@ async function streamCoverage() {
|
|||
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
exposeMap(map.value)
|
||||
map.value.on('load', async () => {
|
||||
try {
|
||||
status.value = 'Loading per-country coverage…'
|
||||
|
|
@ -105,7 +106,7 @@ onMounted(async () => {
|
|||
<b>${p.avgFilled != null ? p.avgFilled.toFixed(1) : '—'}</b> / 17</td></tr>
|
||||
</table>
|
||||
</div>`
|
||||
new mapboxgl.Popup({ maxWidth: '300px' })
|
||||
new (glFor(map.value)).Popup({ maxWidth: '300px' })
|
||||
.setLngLat(e.lngLat).setHTML(html).addTo(map.value)
|
||||
})
|
||||
|
||||
|
|
@ -113,7 +114,7 @@ onMounted(async () => {
|
|||
const { pinLayerId } = await addPGPoints(map.value, { circleColor: '#fef3c7' })
|
||||
map.value.on('click', pinLayerId, e => {
|
||||
const f = e.features[0]
|
||||
new mapboxgl.Popup({ maxWidth: '320px' })
|
||||
new (glFor(map.value)).Popup({ maxWidth: '320px' })
|
||||
.setLngLat(f.geometry.coordinates).setHTML(popupForPG(f.properties)).addTo(map.value)
|
||||
})
|
||||
status.value = `Ready · ${countryStats.size} countries colored by avg translation locales / 17 (all ${[...countryStats.values()].reduce((n, c) => n + c.pgCount, 0).toLocaleString('en-US')} PG-in-country rows)`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-language-families.vue — PG pins colored by PPLR ethnolinguistic family.
|
||||
*
|
||||
|
|
@ -12,6 +12,8 @@ import { dataUrl } from '../utils/pplrData.js'
|
|||
* Each depth level = one tab. Filter expressions are pre-built into each tree node.
|
||||
*/
|
||||
import { inject, onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'
|
||||
import { resolveNamedStyle } from '@map/styles/registry.js'
|
||||
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, POPUP_CSS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick } from '../utils/pplrMap.js'
|
||||
import SemanticTreeLegend from '../components/SemanticTreeLegend.vue'
|
||||
|
|
@ -23,6 +25,8 @@ import { createPplrInstance, provideInstance } from '../composables/usePplrInsta
|
|||
const pplrInstance = createPplrInstance('language-families')
|
||||
provideInstance(pplrInstance)
|
||||
|
||||
// Registry names resolve per engine, so the Dark/Satellite/Light switch works under MapLibre too.
|
||||
const mapProvider = resolveMapProvider(inject('profileConfig', null))
|
||||
const mapboxToken = inject('mapboxToken')
|
||||
const mapContainer = ref(null)
|
||||
const profileRoot = ref(null) // FullscreenControl target — fullscreens .profile so chrome stays visible
|
||||
|
|
@ -32,7 +36,7 @@ const PIN_LAYER = 'lf-pins'
|
|||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer,
|
||||
accessToken: mapboxToken.value,
|
||||
style: 'mapbox://styles/mapbox/dark-v11',
|
||||
style: 'ink', // named: dark-v11 under Mapbox, OpenFreeMap dark under MapLibre
|
||||
center: [25, 18],
|
||||
zoom: 1.6,
|
||||
projection: 'mercator' // pinned at construct time — no globe→mercator reproject flash
|
||||
|
|
@ -100,9 +104,9 @@ const projection = ref('mercator') // default mercator per user spec
|
|||
const showBasemap = ref(false)
|
||||
const currentBasemap = ref('dark')
|
||||
const BASEMAPS = [
|
||||
{ key: 'dark', label: 'Dark', url: 'mapbox://styles/mapbox/dark-v11' },
|
||||
{ key: 'satellite', label: 'Satellite', url: 'mapbox://styles/mapbox/satellite-streets-v12' },
|
||||
{ key: 'light', label: 'Light', url: 'mapbox://styles/mapbox/light-v11' },
|
||||
{ key: 'dark', label: 'Dark', url: 'ink' },
|
||||
{ key: 'satellite', label: 'Satellite', url: 'hybrid' },
|
||||
{ key: 'light', label: 'Light', url: 'paper' },
|
||||
]
|
||||
const pgData = ref(null)
|
||||
|
||||
|
|
@ -370,7 +374,7 @@ function setBasemap(key) {
|
|||
map.value.once('style.load', () => { setupLayers(); map.value.setProjection(projection.value) })
|
||||
// diff:false suppresses Mapbox's "Unimplemented: setSprite" warning between
|
||||
// dark-v11 and light-v11. We re-add layers in setupLayers anyway.
|
||||
map.value.setStyle(style, { diff: false })
|
||||
map.value.setStyle(resolveNamedStyle(style, mapProvider).style, { diff: false })
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
|
|
@ -422,8 +426,9 @@ function popupHtml(p) {
|
|||
// ── Lifecycle ─────────────────────────────────────────────────────────────────
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
exposeMap(map.value)
|
||||
if (map.value) {
|
||||
map.value.addControl(new mapboxgl.NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new (glFor(map.value)).NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new FullscreenToggleControl(profileRoot.value), 'bottom-right')
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
* Individual pins at all zoom levels; click pin for full Rosetta popup; hover country for highlight.
|
||||
*/
|
||||
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { glFor, exposeMap } from '../utils/pplrData.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, addPGPoints, popupForPG, POPUP_CSS } from '../utils/pplrMap.js'
|
||||
import LoadingGlobe from '../components/LoadingGlobe.vue'
|
||||
|
|
@ -26,13 +27,14 @@ const DATA_SOURCES = [
|
|||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer,
|
||||
accessToken: mapboxToken.value,
|
||||
style: 'mapbox://styles/mapbox/dark-v11',
|
||||
style: 'ink', // named: dark-v11 under Mapbox, OpenFreeMap dark under MapLibre
|
||||
center: [25, 18],
|
||||
zoom: 1.6
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
exposeMap(map.value)
|
||||
map.value.on('load', async () => {
|
||||
try {
|
||||
status.value = 'Adding country polygons…'
|
||||
|
|
@ -42,7 +44,7 @@ onMounted(async () => {
|
|||
|
||||
map.value.on('click', pinLayerId, e => {
|
||||
const f = e.features[0]
|
||||
new mapboxgl.Popup({ maxWidth: '340px' })
|
||||
new (glFor(map.value)).Popup({ maxWidth: '340px' })
|
||||
.setLngLat(f.geometry.coordinates)
|
||||
.setHTML(popupForPG(f.properties))
|
||||
.addTo(map.value)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-religion.vue — PG pins colored by primary religion.
|
||||
*
|
||||
|
|
@ -8,6 +8,8 @@ import { dataUrl } from '../utils/pplrData.js'
|
|||
* Uses the `rg` field pre-computed in pg-points.geojson.
|
||||
*/
|
||||
import { inject, onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'
|
||||
import { resolveNamedStyle } from '@map/styles/registry.js'
|
||||
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, POPUP_CSS, COUNTRY_FS_URL, COUNTRY_FS_FIELDS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick } from '../utils/pplrMap.js'
|
||||
import MapboxGeocoder from '../components/MapboxGeocoder.vue'
|
||||
|
|
@ -19,6 +21,8 @@ import { createPplrInstance, provideInstance } from '../composables/usePplrInsta
|
|||
const pplrInstance = createPplrInstance('religion')
|
||||
provideInstance(pplrInstance)
|
||||
|
||||
// Registry names resolve per engine, so the Dark/Satellite/Light switch works under MapLibre too.
|
||||
const mapProvider = resolveMapProvider(inject('profileConfig', null))
|
||||
const mapboxToken = inject('mapboxToken')
|
||||
const mapContainer = ref(null)
|
||||
const profileRoot = ref(null) // FullscreenControl target — fullscreens .profile so chrome stays visible
|
||||
|
|
@ -30,9 +34,9 @@ const projection = ref('mercator')
|
|||
const showBasemap = ref(false)
|
||||
const currentBasemap = ref('dark')
|
||||
const BASEMAPS = [
|
||||
{ key: 'dark', label: 'Dark', url: 'mapbox://styles/mapbox/dark-v11' },
|
||||
{ key: 'satellite', label: 'Satellite', url: 'mapbox://styles/mapbox/satellite-streets-v12' },
|
||||
{ key: 'light', label: 'Light', url: 'mapbox://styles/mapbox/light-v11' },
|
||||
{ key: 'dark', label: 'Dark', url: 'ink' },
|
||||
{ key: 'satellite', label: 'Satellite', url: 'hybrid' },
|
||||
{ key: 'light', label: 'Light', url: 'paper' },
|
||||
]
|
||||
const pgData = ref(null)
|
||||
const dataReady = ref(false)
|
||||
|
|
@ -57,7 +61,7 @@ function setBasemap(key) {
|
|||
map.value.once('style.load', () => { setupLayers(); map.value.setProjection(projection.value) })
|
||||
// diff:false suppresses Mapbox's "Unimplemented: setSprite" warning between
|
||||
// dark-v11 and light-v11. We re-add layers in setupLayers anyway.
|
||||
map.value.setStyle(style, { diff: false })
|
||||
map.value.setStyle(resolveNamedStyle(style, mapProvider).style, { diff: false })
|
||||
}
|
||||
|
||||
function setupLayers() {
|
||||
|
|
@ -108,7 +112,7 @@ const DATA_SOURCES = [
|
|||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer,
|
||||
accessToken: mapboxToken.value,
|
||||
style: 'mapbox://styles/mapbox/dark-v11',
|
||||
style: 'ink', // named: dark-v11 under Mapbox, OpenFreeMap dark under MapLibre
|
||||
center: [25, 18],
|
||||
zoom: 1.6,
|
||||
projection: 'mercator' // pinned at construct time — no globe→mercator reproject flash
|
||||
|
|
@ -240,8 +244,9 @@ function popupHtml(p) {
|
|||
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
exposeMap(map.value)
|
||||
if (map.value) {
|
||||
map.value.addControl(new mapboxgl.NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new (glFor(map.value)).NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new FullscreenToggleControl(profileRoot.value), 'bottom-right')
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-rop-tree.vue — ROP hierarchy: Affinity Bloc → Cluster → People Group → PGIC.
|
||||
*
|
||||
|
|
@ -8,6 +8,8 @@ import { dataUrl } from '../utils/pplrData.js'
|
|||
* Pins are colored by Affinity Bloc. Each level sorts by pin count desc.
|
||||
*/
|
||||
import { inject, onMounted, onBeforeUnmount, ref, computed, shallowRef, watch } from 'vue'
|
||||
import { resolveNamedStyle } from '@map/styles/registry.js'
|
||||
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, setCountryPolygonTheme, POPUP_CSS, COUNTRY_FS_URL, COUNTRY_FS_FIELDS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick } from '../utils/pplrMap.js'
|
||||
import SemanticTreeLegend from '../components/SemanticTreeLegend.vue'
|
||||
|
|
@ -20,6 +22,8 @@ import { createPplrInstance, provideInstance } from '../composables/usePplrInsta
|
|||
const pplrInstance = createPplrInstance('rop-tree')
|
||||
provideInstance(pplrInstance)
|
||||
|
||||
// Registry names resolve per engine, so the Dark/Satellite/Light switch works under MapLibre too.
|
||||
const mapProvider = resolveMapProvider(inject('profileConfig', null))
|
||||
const mapboxToken = inject('mapboxToken')
|
||||
const mapContainer = ref(null)
|
||||
const profileRoot = ref(null) // FullscreenControl target — fullscreens the .profile so chrome stays visible
|
||||
|
|
@ -29,7 +33,7 @@ const PIN_LAYER = 'rop-pins'
|
|||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer,
|
||||
accessToken: mapboxToken.value,
|
||||
style: 'mapbox://styles/mapbox/dark-v11',
|
||||
style: 'ink', // named: dark-v11 under Mapbox, OpenFreeMap dark under MapLibre
|
||||
center: [25, 18],
|
||||
zoom: 1.6,
|
||||
projection: 'mercator' // pinned at construct time — see useMapInstance projection arg
|
||||
|
|
@ -78,9 +82,9 @@ const projection = ref('mercator') // default mercator per user spec
|
|||
const showBasemap = ref(false)
|
||||
const currentBasemap = ref('dark')
|
||||
const BASEMAPS = [
|
||||
{ key: 'dark', label: 'Dark', url: 'mapbox://styles/mapbox/dark-v11' },
|
||||
{ key: 'satellite', label: 'Satellite', url: 'mapbox://styles/mapbox/satellite-streets-v12' },
|
||||
{ key: 'light', label: 'Light', url: 'mapbox://styles/mapbox/light-v11' },
|
||||
{ key: 'dark', label: 'Dark', url: 'ink' },
|
||||
{ key: 'satellite', label: 'Satellite', url: 'hybrid' },
|
||||
{ key: 'light', label: 'Light', url: 'paper' },
|
||||
]
|
||||
|
||||
// ── ROP tree (4 generations: Affinity Bloc → People Cluster → ROP25 → ROP3byCountry)
|
||||
|
|
@ -309,7 +313,7 @@ function setBasemap(key) {
|
|||
// "Unimplemented: setSprite" warning whenever sprite sheets differ between
|
||||
// dark-v11 and light-v11. We re-add all our layers in setupLayers anyway,
|
||||
// so a full rebuild is correct and avoids the noisy warning.
|
||||
map.value.setStyle(style, { diff: false })
|
||||
map.value.setStyle(resolveNamedStyle(style, mapProvider).style, { diff: false })
|
||||
}
|
||||
|
||||
// Quick light/dark toggle — flips between dark-v11 and light-v11 directly
|
||||
|
|
@ -389,12 +393,13 @@ function popupHtml(p) {
|
|||
// ── Lifecycle ─────────────────────────────────────────────────────────────────
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
exposeMap(map.value)
|
||||
|
||||
// Navigation: zoom +/- (Mapbox built-in) + custom fullscreen toggle.
|
||||
// See FullscreenToggleControl in pplrMap.js for why we don't use
|
||||
// mapboxgl.FullscreenControl with a non-map container.
|
||||
if (map.value) {
|
||||
map.value.addControl(new mapboxgl.NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new (glFor(map.value)).NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new FullscreenToggleControl(profileRoot.value), 'bottom-right')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
* Click any pin → popup with full Rosetta table.
|
||||
*/
|
||||
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { glFor, exposeMap } from '../utils/pplrData.js'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { addCountryPolygons, loadOnce, popupForPG, POPUP_CSS } from '../utils/pplrMap.js'
|
||||
import LoadingGlobe from '../components/LoadingGlobe.vue'
|
||||
|
|
@ -34,13 +35,14 @@ function reload() { window.location.reload() }
|
|||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer,
|
||||
accessToken: mapboxToken.value,
|
||||
style: 'mapbox://styles/mapbox/dark-v11',
|
||||
style: 'ink', // named: dark-v11 under Mapbox, OpenFreeMap dark under MapLibre
|
||||
center: [50, 22],
|
||||
zoom: 1.8
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
exposeMap(map.value)
|
||||
map.value.on('load', async () => {
|
||||
try {
|
||||
status.value = 'Loading PG data…'
|
||||
|
|
@ -126,7 +128,7 @@ onMounted(async () => {
|
|||
${missingIds.join(', ')} ID${missingIds.length > 1 ? 's' : ''} not yet cross-mapped in PPLR — empty rows are a data gap, not an error.
|
||||
</td></tr>`
|
||||
: ''
|
||||
new mapboxgl.Popup({ maxWidth: '360px' })
|
||||
new (glFor(map.value)).Popup({ maxWidth: '360px' })
|
||||
.setLngLat(f.geometry.coordinates)
|
||||
.setHTML(popupForPG(p, rosettaRow + missingNote))
|
||||
.addTo(map.value)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-translations.vue — PG name in all 17 locales.
|
||||
*
|
||||
|
|
@ -26,7 +26,7 @@ const PIN_LAYER = 'trans-pins'
|
|||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer,
|
||||
accessToken: mapboxToken.value,
|
||||
style: 'mapbox://styles/mapbox/dark-v11',
|
||||
style: 'ink', // named: dark-v11 under Mapbox, OpenFreeMap dark under MapLibre
|
||||
center: [70, 22],
|
||||
zoom: 2,
|
||||
projection: 'mercator',
|
||||
|
|
@ -56,8 +56,9 @@ const DATA_SOURCES = [
|
|||
|
||||
onMounted(async () => {
|
||||
await initializeMap()
|
||||
exposeMap(map.value)
|
||||
if (map.value) {
|
||||
map.value.addControl(new mapboxgl.NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new (glFor(map.value)).NavigationControl({ showCompass: false, visualizePitch: false }), 'bottom-right')
|
||||
map.value.addControl(new FullscreenToggleControl(profileRoot.value), 'bottom-right')
|
||||
}
|
||||
map.value.on('load', async () => {
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
4
public/doxa-maps/manifest.json
generated
4
public/doxa-maps/manifest.json
generated
|
|
@ -837,7 +837,7 @@
|
|||
"bundle": "pplr-data-maps",
|
||||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"output": "doxa-maps/pplr-data-maps/pplr-data-maps.js",
|
||||
"integrity": "sha384-u04GVRZqNXyBqfkyvCtm9IT6/RWbJTNTxTpv6ScsnE/5za+UIlTjAoZKIn9k2Zp3",
|
||||
"integrity": "sha384-RHtStBg9uYgRi/QxaFbnrR9Q3wKUebRcm+gi2eza6tSYsI3t7YsjayWiCwU4fCKU",
|
||||
"elements": [
|
||||
"pplr-data-maps"
|
||||
],
|
||||
|
|
@ -853,7 +853,7 @@
|
|||
"scriptUrl": "./pplr-data-maps/pplr-data-maps.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./pplr-data-maps/pplr-data-maps/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-u04GVRZqNXyBqfkyvCtm9IT6/RWbJTNTxTpv6ScsnE/5za+UIlTjAoZKIn9k2Zp3\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./pplr-data-maps/pplr-data-maps.js\" integrity=\"sha384-RHtStBg9uYgRi/QxaFbnrR9Q3wKUebRcm+gi2eza6tSYsI3t7YsjayWiCwU4fCKU\" crossorigin=\"anonymous\"></script>\n<pplr-data-maps profile-config='{\"profile\":\"pplr-data-maps\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></pplr-data-maps>"
|
||||
},
|
||||
"bundle": "pplr-data-maps"
|
||||
}
|
||||
|
|
|
|||
58
public/doxa-maps/pplr-data-maps/pplr-data-maps.js
generated
58
public/doxa-maps/pplr-data-maps/pplr-data-maps.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <pplr-data-maps> custom element. -->
|
||||
<script src="../pplr-data-maps.js" integrity="sha384-u04GVRZqNXyBqfkyvCtm9IT6/RWbJTNTxTpv6ScsnE/5za+UIlTjAoZKIn9k2Zp3" crossorigin="anonymous"></script>
|
||||
<script src="../pplr-data-maps.js" integrity="sha384-RHtStBg9uYgRi/QxaFbnrR9Q3wKUebRcm+gi2eza6tSYsI3t7YsjayWiCwU4fCKU" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue