From 2d6e8e69127b7767f4e6fd03549a44be38ac5f9c Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 19:12:36 +0000 Subject: [PATCH] UPG Neighbor: a green group's unreached neighbours one E-level at a time, closest first, and green dots on Campaigns open them --- plugin-designs/upg-neighbor/src/campaigns.vue | 13 ++++++-- plugin-designs/upg-neighbor/src/mobilize.vue | 31 +++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/plugin-designs/upg-neighbor/src/campaigns.vue b/plugin-designs/upg-neighbor/src/campaigns.vue index 310adc160..8bc1e0d3e 100644 --- a/plugin-designs/upg-neighbor/src/campaigns.vue +++ b/plugin-designs/upg-neighbor/src/campaigns.vue @@ -134,8 +134,17 @@ onMounted(async () => { const u = active.value?.unreached.find((g) => g.pgid === id) if (u) openGroup(u) }) - m.on('mouseenter', 'cp-u', () => { m.getCanvas().style.cursor = 'pointer' }) - m.on('mouseleave', 'cp-u', () => { m.getCanvas().style.cursor = '' }) + // A GREEN dot opens that reached group in Mobilize: the unreached neighbours its believers can + // reach, the letter's audience (Driver, test round 2: "on the Campaign side you can't click green dots"). + m.on('click', 'cp-r', (ev) => { + const id = ev.features?.[0]?.properties?.id + const r = active.value?.reached.find((g) => g.pgid === id) + if (r) openGroup(r) + }) + for (const id of ['cp-u', 'cp-r']) { + m.on('mouseenter', id, () => { m.getCanvas().style.cursor = 'pointer' }) + m.on('mouseleave', id, () => { m.getCanvas().style.cursor = '' }) + } const byId = (id) => active.value && [...active.value.reached, ...active.value.unreached].find((g) => g.pgid === id) for (const id of ['cp-u', 'cp-r']) { m.on('mousemove', id, (ev) => { diff --git a/plugin-designs/upg-neighbor/src/mobilize.vue b/plugin-designs/upg-neighbor/src/mobilize.vue index 5c880ccc8..0264cdaec 100644 --- a/plugin-designs/upg-neighbor/src/mobilize.vue +++ b/plugin-designs/upg-neighbor/src/mobilize.vue @@ -51,11 +51,30 @@ const { map, initializeMap, destroy } = useMapInstance({ // shows the unreached groups its believers are nearest to. One selection, whichever kind it is. const selected = computed(() => data.value?.groups.find((g) => g.pgid === nbState.pgid) || null) const isReachedSel = computed(() => !!selected.value?.reached) -const neighbors = computed(() => { +const allNeighbors = computed(() => { const g = selected.value if (!g) return [] return g.reached ? data.value.unreachedNeighborsOf(g) : data.value.neighborsOf(g) }) +// THE LETTER'S AUDIENCE (Driver, test round 2: UPG Neighbor is a letter generator for GREEN groups). +// A reached group's unreached neighbours are shown ONE E-level at a time: E-1 (the same language) by +// default, E-2 / E-3 on request, closest first. The list, the map lines and the CSV all follow it. +const levelCounts = computed(() => { + const c = { 1: 0, 2: 0, 3: 0 } + for (const n of allNeighbors.value) if (c[n.e] != null) c[n.e]++ + return c +}) +// The reader's pick while that level has neighbours; otherwise the first level that has any (E-1 first). +// Computed, not set on click, because a group opened from a link arrives before its data does. +const eChoice = ref(null) +watch(() => nbState.pgid, () => { eChoice.value = null }) +const eLevel = computed({ + get: () => (eChoice.value && levelCounts.value[eChoice.value]) ? eChoice.value : ([1, 2, 3].find((e) => levelCounts.value[e]) || 1), + set: (v) => { eChoice.value = v }, +}) +const neighbors = computed(() => (isReachedSel.value + ? allNeighbors.value.filter((n) => n.e === eLevel.value).sort((a, b) => a.km - b.km) + : allNeighbors.value)) const byLevel = computed(() => [1, 2, 3].map((e) => ({ e, list: neighbors.value.filter((n) => n.e === e) })).filter((x) => x.list.length)) const believersNear = computed(() => neighbors.value.reduce((a, n) => a + n.g.believers, 0)) const peopleReachable = computed(() => neighbors.value.reduce((a, n) => a + n.g.pop, 0)) @@ -123,7 +142,7 @@ function exportSegments() { re.name, re.country, re.langName, re.lang, re.believers, n.km, re.lat, re.lng, re.bible ? 'yes' : 'no', re.film ? 'yes' : 'no', re.audio ? 'yes' : 'no', re.radio ? 'yes' : 'no', re.pgid] }) - downloadCsv(`near-neighbors-${u.pgid}.csv`, header, rows) + downloadCsv(u.reached ? `unreached-neighbors-${u.pgid}-E${eLevel.value}.csv` : `near-neighbors-${u.pgid}.csv`, header, rows) } function openBridge() { if (selected.value) { nbState.lang = selected.value.lang; nbState.view = 'campaigns' } } const isBridge = computed(() => !!selected.value && !!data.value?.bridges.some((b) => b.lang === selected.value.lang)) @@ -210,7 +229,7 @@ function framePadding() { return w > 760 ? { top: 60, bottom: 60, left: 400, right: 60 } : { top: 40, bottom: Math.round(h * 0.55) + 20, left: 30, right: 30 } } -watch(() => [nbState.pgid, pFilter.value, data.value], () => { repaint(); frame() }) +watch(() => [nbState.pgid, pFilter.value, data.value, eLevel.value], () => { repaint(); frame() }) onMounted(async () => { loadNearData(profileConfig).then((d) => { data.value = d; repaint(); frame() }) @@ -357,6 +376,12 @@ useShadowStyles(` {{ $t('neighbor.mob.reachedLong', { n: fmtNum(selected.believers) }) }}
{{ $t('neighbor.mob.reachCount', { n: neighbors.length.toLocaleString('en-US'), p: fmtNum(peopleReachable) }) }}
+ +
+ +
{{ $t(P_TITLE[selected.p]) }} {{ $t(P_LONG[selected.p]) }}