All People Groups: no DOXA group is drawn twice; the base is re-sent slim without them

The feature-state approach left an invisible twin per DOXA group (2,616 circles drawn at alpha 0, and counted by queryRenderedFeatures). When the DOXA set arrives the base is re-sent once, slim (1.25 MB, not the old 3.7 MB), without the DOXA rows; the black pins stay on their own top layer. Popup fields and counts unchanged.
This commit is contained in:
Doxa Maps 2026-09-29 02:52:15 +00:00
parent fd118fa2b5
commit cf962e4065

View file

@ -193,8 +193,8 @@ const mapData = useMapData({
// row's index) and one category number. Every name, country, language and religion used to ride on
// all 12,379 features, 3.7 MB of GeoJSON built and re-tiled TWICE (first paint, then the DOXA recolour);
// slim, it is 1.25 MB, built once. The popup reads the row by id. The DOXA recolour no longer rebuilds
// the 12,379: DOXA pins are a small source of their own drawn on top, and their base duplicates are
// hidden by feature-state.
// the 12,379 with all their fields: DOXA pins are a small source of their own drawn on top, and the base
// is re-sent once, slim, WITHOUT them (an invisible twin per DOXA group still cost a draw: rega measured).
let rows = [] // the decoded rows; a feature's id is its index here
let doxaIds = new Set() // row indexes in the DOXA set (empty until the prayer API answers)
let doxaBySlugRop = new Map() // rop3 → { slug, name } for the pray link
@ -240,7 +240,7 @@ const slimPoint = (i, c) => ({ type: 'Feature', id: i, geometry: { type: 'Point'
// Every pin by GSEC, from the file alone: built ONCE. The DOXA overlay (applyDoxa) comes later and never blocks.
function buildBase() {
const feats = []
for (let i = 0; i < rows.length; i++) if (placed(rows[i])) feats.push(slimPoint(i, GSEC_CAT.indexOf(catOf(i))))
for (let i = 0; i < rows.length; i++) if (placed(rows[i]) && !doxaIds.has(i)) feats.push(slimPoint(i, GSEC_CAT.indexOf(catOf(i))))
recount()
return { type: 'FeatureCollection', features: feats }
}
@ -249,17 +249,16 @@ function recount() {
for (let i = 0; i < rows.length; i++) if (placed(rows[i])) c[catOf(i)]++
counts.value = c
}
// The DOXA set arrived: its pins go on the top layer (a ~2,100-point source) and their base duplicates
// are hidden by feature-state. The 12,379-feature source is never rebuilt.
// The DOXA set arrived: its pins go on the top layer (a ~2,100-point source) and the base is re-sent,
// slim, without them, so no DOXA group is drawn twice.
function applyDoxa(doxaSet) {
const m = map.value
if (!m || !m.getSource('apg')) return
const ids = new Set()
for (let i = 0; i < rows.length; i++) if (placed(rows[i]) && doxaSet.has(String(rows[i].rop3 || ''))) ids.add(i)
doxaIds = ids
for (const i of ids) m.setFeatureState({ source: 'apg', id: i }, { doxa: true })
m.getSource('apg').setData(buildBase())
m.getSource('apg-doxa').setData({ type: 'FeatureCollection', features: [...ids].map((i) => slimPoint(i, 0)) })
recount()
}
// The live DOXA set (rop3 = peoplegroups.org ROP3Code). Resolves to an empty set on failure.
@ -284,8 +283,6 @@ async function loadDoxaSet() {
// Pin size and ring, shared by the base and the DOXA layer.
const PIN_RADIUS = ['interpolate', ['linear'], ['zoom'], 0, 2.2, 4, 3.4, 8, 6, 12, 9]
const PIN_STROKE_WIDTH = ['interpolate', ['linear'], ['zoom'], 0, 0.3, 6, 0.6, 12, 1]
// A base pin whose group is DOXA is hidden: its black twin is drawn on the layer above.
const HIDDEN_IF_DOXA = (v) => ['case', ['boolean', ['feature-state', 'doxa'], false], 0, v]
function addLayer(fc) {
const m = map.value
@ -312,9 +309,9 @@ function addLayer(fc) {
// green legible on near-white (a white ring was right only on the old dark map).
'circle-stroke-color': '#1f2937',
'circle-stroke-width': PIN_STROKE_WIDTH,
'circle-stroke-opacity': HIDDEN_IF_DOXA(0.55),
'circle-stroke-opacity': 0.55,
'circle-radius': PIN_RADIUS,
'circle-opacity': HIDDEN_IF_DOXA(0.92),
'circle-opacity': 0.92,
},
})
// DOXA: the assignment, black, above everything else.
@ -330,10 +327,10 @@ function addLayer(fc) {
m.on('mouseenter', id, () => { m.getCanvas().style.cursor = 'pointer' })
m.on('mouseleave', id, () => { m.getCanvas().style.cursor = '' })
}
// ONE click handler: a DOXA pin (top layer) wins over its hidden base twin at the same point.
// ONE click handler, top layer first: a DOXA pin wins over a base pin under it.
m.on('click', (e) => {
const hits = m.queryRenderedFeatures(e.point, { layers: ['apg-doxa-pins', 'apg-pins'].filter((id) => m.getLayer(id)) })
const f = hits.find((h) => h.layer.id === 'apg-doxa-pins' || !doxaIds.has(Number(h.id)))
const f = hits[0]
if (!f || rows[Number(f.id)] == null) return
const i = Number(f.id), row = rows[i]
const category = catOf(i)
@ -373,8 +370,8 @@ function selectCat(id) {
return
}
activeCat.value = id
// DOXA lives on its own layer; the other three are the base layer's category numbers (a DOXA group's
// base twin stays hidden by feature-state, so a GSEC filter never shows it twice).
// DOXA lives on its own layer; the other three are the base layer's category numbers (the base holds
// no DOXA group, so a GSEC filter never shows one twice).
if (id === 'doxa') m.setFilter('apg-pins', ['==', ['get', 'c'], -1])
else m.setFilter('apg-pins', ['==', ['get', 'c'], GSEC_CAT.indexOf(id)])
m.setLayoutProperty('apg-doxa-pins', 'visibility', id === 'doxa' ? 'visible' : 'none')