PPLR data maps: keep the loading globe up until the pins are drawn, not merely added
Six views set dataReady (which removes the LoadingGlobe) straight after addSource(). The engine then still tiles tens of thousands of points in its worker, so the globe vanished and the map sat empty with a full legend. Measured on the DX-life gallery: 0 features for 20 s, then 19,376. whenGeojsonDrawn(map) resolves once every GeoJSON source is loaded AND a frame has rendered (not 'idle', which an animating map may never reach), with a 45 s cap so it can never hang. Religion, pins, coverage, translations, language families and Rosetta cards await it before dataReady. Plugin-local: pplrData.js has no consumer outside pplr-data-maps. Headless proof: a fake-map unit test (already loaded / last source late / never loads -> false at the cap / style mid-swap / no map), 9/9 pass; BUNDLE=pplr-data-maps build + tree + check:sri PASS.
This commit is contained in:
parent
8dcd407e29
commit
0de6a7c2f8
7 changed files with 41 additions and 6 deletions
|
|
@ -63,6 +63,29 @@ export function ensureGeocoder(provider = 'mapbox') {
|
|||
// `new mapboxgl.Popup` throws on the first pin click. Now the library's engineFor (one copy).
|
||||
export { engineFor as glFor } from '@map/composables/useMapProvider.js'
|
||||
|
||||
// Resolves once every GeoJSON source on the map has LOADED and a frame has RENDERED after that,
|
||||
// or after `timeoutMs` (a safety net, never a hang). The views flipped `dataReady` (which hides the
|
||||
// LoadingGlobe) right after addSource(); the engine then still tiles tens of thousands of points in
|
||||
// its worker, so the globe disappeared and the map sat EMPTY. Measured on the DX-life gallery: 0
|
||||
// dots for 20 s, then 19,376 features from 30 s. Not 'idle': an animating map may never go idle.
|
||||
export function whenGeojsonDrawn(map, timeoutMs = 45000) {
|
||||
return new Promise((resolve) => {
|
||||
if (!map) return resolve(false)
|
||||
let done = false
|
||||
const finish = (drawn) => { if (done) return; done = true; clearTimeout(timer); map.off('sourcedata', check); resolve(drawn) }
|
||||
const timer = setTimeout(() => finish(false), timeoutMs)
|
||||
const check = () => {
|
||||
try {
|
||||
const srcs = (map.getStyle() || {}).sources || {}
|
||||
const ids = Object.keys(srcs).filter((k) => srcs[k].type === 'geojson')
|
||||
if (ids.every((id) => map.isSourceLoaded(id))) { map.once('render', () => finish(true)); map.triggerRepaint() }
|
||||
} catch (_) { /* style mid-swap: wait for the next sourcedata */ }
|
||||
}
|
||||
map.on('sourcedata', check)
|
||||
check()
|
||||
})
|
||||
}
|
||||
|
||||
// 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 }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-coverage.vue — Country choropleth: avg # of locales filled per PG in that country.
|
||||
*
|
||||
|
|
@ -118,6 +118,8 @@ onMounted(async () => {
|
|||
.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)`
|
||||
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
|
||||
await whenGeojsonDrawn(map.value)
|
||||
dataReady.value = true
|
||||
} catch (e) {
|
||||
status.value = `Error: ${e.message}`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-language-families.vue — PG pins colored by PPLR ethnolinguistic family.
|
||||
*
|
||||
|
|
@ -490,6 +490,8 @@ onMounted(async () => {
|
|||
|
||||
const coverage = Math.round(100 * matched / fc.features.length)
|
||||
status.value = `Ready · ${fc.features.length.toLocaleString()} pins · ${coverage}% family coverage · top-25 = ${Math.round(100 * inTop25 / fc.features.length)}%`
|
||||
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
|
||||
await whenGeojsonDrawn(map.value)
|
||||
dataReady.value = true
|
||||
} catch (err) {
|
||||
status.value = `Error: ${err.message}`
|
||||
|
|
|
|||
|
|
@ -5,7 +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 { glFor, exposeMap, whenGeojsonDrawn } 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'
|
||||
|
|
@ -50,6 +50,8 @@ onMounted(async () => {
|
|||
.addTo(map.value)
|
||||
})
|
||||
status.value = `Ready · ${fc.features.length.toLocaleString('en-US')} PG-in-country pins · click any pin`
|
||||
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
|
||||
await whenGeojsonDrawn(map.value)
|
||||
dataReady.value = true
|
||||
} catch (e) {
|
||||
status.value = `Error: ${e.message}`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-religion.vue — PG pins colored by primary religion.
|
||||
*
|
||||
|
|
@ -267,6 +267,8 @@ onMounted(async () => {
|
|||
const dominant = RELIGION_GROUPS.reduce((a, b) => (counts[a.key] || 0) > (counts[b.key] || 0) ? a : b)
|
||||
// Per user 2026-05-02: drop the "Ready" prefix — it was unclear and chewed up width.
|
||||
status.value = `${pgData.value.features.length.toLocaleString()} pins · ${dominant.label} most common (${(counts[dominant.key]||0).toLocaleString()})`
|
||||
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
|
||||
await whenGeojsonDrawn(map.value)
|
||||
dataReady.value = true
|
||||
} catch (err) {
|
||||
status.value = `Error: ${err.message}`
|
||||
|
|
|
|||
|
|
@ -13,7 +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 { glFor, exposeMap, whenGeojsonDrawn } 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'
|
||||
|
|
@ -141,6 +141,8 @@ onMounted(async () => {
|
|||
map.value.on('mouseleave', 'rosetta-pin', () => map.value.getCanvas().style.cursor = '')
|
||||
|
||||
status.value = `Ready · ${pgFc.features.length} pins · ${rosKeys.size} Rosetta samples (gold ★)`
|
||||
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
|
||||
await whenGeojsonDrawn(map.value)
|
||||
dataReady.value = true
|
||||
} catch (e) {
|
||||
status.value = `Error: ${e.message}`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
|
||||
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
|
||||
/**
|
||||
* pplr-translations.vue — PG name in all 17 locales.
|
||||
*
|
||||
|
|
@ -92,6 +92,8 @@ onMounted(async () => {
|
|||
})
|
||||
|
||||
status.value = `${fc.features.length.toLocaleString()} pins · click any pin to see all 17 locale names`
|
||||
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
|
||||
await whenGeojsonDrawn(map.value)
|
||||
dataReady.value = true
|
||||
} catch (err) {
|
||||
status.value = `Error: ${err.message}`
|
||||
|
|
|
|||
Loading…
Reference in a new issue