From fc589cef1c1f13b39cb5d24e22e8829d9624df95 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 22:13:28 +0000 Subject: [PATCH] Point the cross the right way up, and let the fit follow its container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cross drew inverted. The projection computes Y = 0.5 - v*K, so a larger v is a HIGHER latitude, and the parts were named on the screen convention where +y goes down — which put the long arm at the top. That is a different symbol, not a cosmetic slip. The parts are named by compass now, and the test asserts in latitude after projection rather than in layout units, which is the only space where the question means anything. The fit was bit-identical through three attempts — more padding, a resize, a refit on idle — and that was the evidence. None of them was wrong; none of them was the moment the box changed size. These panels are v-show, so a map can be built while its container is display:none and therefore zero-sized, and the engine keeps the size it measured then. A fit computed against that lands at the wrong zoom, which is why one layout opened at 0.64 while an almost identically shaped one opened at 0.31. A ResizeObserver is the only thing that knows when the box became real, so the fit follows the container instead of guessing at a moment, and skips while the box is still hidden. The failure is reported rather than swallowed, for the same reason as the cross layer before it. --- .../src/components/GiBoundedSetMap.vue | 26 ++++++++++++++----- .../src/components/GiCenteredSetMap.vue | 26 ++++++++++++++----- .../gospel-internet/src/setChartFeatures.js | 19 +++++++++----- public/dx-maps/doxa-maps-spa/manifest.json | 4 +-- .../gospel-internet/board/board/index.html | 2 +- .../dx-maps/gospel-internet/board/index.html | 2 +- .../board/india-china/index.html | 2 +- .../board/language-tree/index.html | 2 +- .../board/platforms/index.html | 2 +- .../board/project-700/index.html | 2 +- .../gospel-internet/board/zero/index.html | 2 +- .../gospel-internet/gospel-internet.js | 26 +++++++++---------- public/dx-maps/manifest.json | 4 +-- 13 files changed, 76 insertions(+), 43 deletions(-) diff --git a/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue b/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue index 54ae341af..4b8417925 100644 --- a/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue +++ b/plugin-designs/gospel-internet/src/components/GiBoundedSetMap.vue @@ -35,6 +35,7 @@ const board = useBoardState() const el = ref(null) const mapRef = shallowRef(null) const handle = shallowRef(null) +let fitRo = null const failed = ref('') const R = 120, SPREAD = 260 @@ -110,18 +111,31 @@ onMounted(async () => { // unchanged while the others improved. // map.resize() makes the engine re-read the container; fitting on the next frame lets the // flex layout finish first. Both are cheap and neither depends on a timer. + // ★ REFIT WHENEVER THE BOX CHANGES SIZE, because a one-shot fit cannot know when that is. + // These panels are v-show, so a map can be created while its container is display:none and + // therefore zero-sized; the engine keeps whatever size it measured then, and a fit computed + // against it lands at the wrong zoom. That is why one layout opened at 0.64 while an almost + // identically-shaped one opened at 0.31 — and why raising padding, resizing once and + // refitting on idle all left it BIT-IDENTICAL: none of them was the moment the box changed. + // A ResizeObserver is the only thing that knows, so the fit follows the container instead of + // guessing at a moment. const fitTarget = () => { try { + const box = el.value + if (!box || !box.clientWidth || !box.clientHeight) return // still hidden: wait to be shown map.resize() const [w, sLat] = project(-SPREAD, -SPREAD) const [e, nLat] = project(SPREAD, SPREAD) map.fitBounds([[w, sLat], [e, nLat]], { padding: 40, duration: 0 }) - } catch (_) { /* a fit that cannot be computed leaves the default view, which still draws */ } + } catch (e) { + console.warn('[gospel-internet] could not fit the chart to its box:', e && e.message || e) + } } - if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fitTarget) - else fitTarget() - // and again once the engine settles, for a container that was still resolving on that frame - try { map.once('idle', fitTarget) } catch (_) {} + fitTarget() + try { + fitRo = new ResizeObserver(fitTarget) + if (el.value) fitRo.observe(el.value) + } catch (_) { /* no ResizeObserver: the initial fit stands */ } } // ★ CALLED UNCONDITIONALLY, AND THE GUARD THAT USED TO BE HERE IS WHY THIS WAS BROKEN TWICE. // createBoardMap only resolves after `await map.once('load')`, so by here the event HAS fired. @@ -138,7 +152,7 @@ onMounted(async () => { }) watch(data, (d) => { try { mapRef.value?.getSource('gbsm')?.setData(d) } catch (_) {} }) -onBeforeUnmount(() => { try { handle.value?.destroy?.() } catch (_) {} ; try { mapRef.value?.remove?.() } catch (_) {} }) +onBeforeUnmount(() => { try { fitRo?.disconnect() } catch (_) {} ; try { handle.value?.destroy?.() } catch (_) {} ; try { mapRef.value?.remove?.() } catch (_) {} }) useShadowStyles( '.gbsm{position:absolute;inset:0;min-height:0;min-width:0}' + diff --git a/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue index aa2c669e8..0523d3633 100644 --- a/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue +++ b/plugin-designs/gospel-internet/src/components/GiCenteredSetMap.vue @@ -41,6 +41,7 @@ const board = useBoardState() const el = ref(null) const mapRef = shallowRef(null) const handle = shallowRef(null) +let fitRo = null const ready = ref(false) const failed = ref('') @@ -125,18 +126,31 @@ onMounted(async () => { // unchanged while the others improved. // map.resize() makes the engine re-read the container; fitting on the next frame lets the // flex layout finish first. Both are cheap and neither depends on a timer. + // ★ REFIT WHENEVER THE BOX CHANGES SIZE, because a one-shot fit cannot know when that is. + // These panels are v-show, so a map can be created while its container is display:none and + // therefore zero-sized; the engine keeps whatever size it measured then, and a fit computed + // against it lands at the wrong zoom. That is why one layout opened at 0.64 while an almost + // identically-shaped one opened at 0.31 — and why raising padding, resizing once and + // refitting on idle all left it BIT-IDENTICAL: none of them was the moment the box changed. + // A ResizeObserver is the only thing that knows, so the fit follows the container instead of + // guessing at a moment. const fitTarget = () => { try { + const box = el.value + if (!box || !box.clientWidth || !box.clientHeight) return // still hidden: wait to be shown map.resize() const [w, sLat] = project(-EXTENT, -EXTENT) const [e, nLat] = project(EXTENT, EXTENT) map.fitBounds([[w, sLat], [e, nLat]], { padding: 40, duration: 0 }) - } catch (_) { /* a fit that cannot be computed leaves the default view, which still draws */ } + } catch (e) { + console.warn('[gospel-internet] could not fit the chart to its box:', e && e.message || e) + } } - if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fitTarget) - else fitTarget() - // and again once the engine settles, for a container that was still resolving on that frame - try { map.once('idle', fitTarget) } catch (_) {} + fitTarget() + try { + fitRo = new ResizeObserver(fitTarget) + if (el.value) fitRo.observe(el.value) + } catch (_) { /* no ResizeObserver: the initial fit stands */ } ready.value = true } // ★ CALLED UNCONDITIONALLY, AND THE GUARD THAT USED TO BE HERE IS WHY THIS WAS BROKEN TWICE. @@ -166,7 +180,7 @@ watch(opacityExpr, (o) => { try { mapRef.value?.setPaintProperty(id, 'circle-opacity', o) } catch (_) {} }) -onBeforeUnmount(() => { try { handle.value?.destroy?.() } catch (_) {} ; try { mapRef.value?.remove?.() } catch (_) {} }) +onBeforeUnmount(() => { try { fitRo?.disconnect() } catch (_) {} ; try { handle.value?.destroy?.() } catch (_) {} ; try { mapRef.value?.remove?.() } catch (_) {} }) useShadowStyles( '.gcsm{position:absolute;inset:0;min-height:0;min-width:0}' + diff --git a/plugin-designs/gospel-internet/src/setChartFeatures.js b/plugin-designs/gospel-internet/src/setChartFeatures.js index fa78c6f91..35114db9a 100644 --- a/plugin-designs/gospel-internet/src/setChartFeatures.js +++ b/plugin-designs/gospel-internet/src/setChartFeatures.js @@ -157,13 +157,18 @@ export function degreesToPixelRadius(degrees, { minZoom = 0, maxZoom = 22 } = {} * else here. */ export function crossFeature(project, { arm = 13, cx = 0, cy = 0 } = {}) { - // Latin proportions, not a plus. Reported as reading "+ with equal arms": the upper arm was - // 0.65 of the lower, which is a real difference and too small to see at this size. The upper - // arm is now a third of the lower and the bar is narrower than the height, so the shape is - // legible as a cross at the size it is actually drawn. - const upper = arm * 0.55, lower = arm * 1.65, bar = arm * 0.62 - const vert = [[cx, cy - upper], [cx, cy + lower]].map(([u, v]) => project(u, v)) - const horiz = [[cx - bar, cy - upper * 0.05], [cx + bar, cy - upper * 0.05]].map(([u, v]) => project(u, v)) + // ★ +v IS NORTH, NOT DOWN. The projection computes Y = 0.5 - v*K, so a LARGER v is a HIGHER + // latitude. An earlier version named its parts "upper" and "lower" on the screen convention + // where +y goes down, and therefore drew the LONG arm upward — an inverted cross, which is a + // different symbol rather than a cosmetic slip. Named by compass here so the mistake cannot be + // made silently again: `north` is toward the top of the drawing. + const H = arm * 2.2 + const north = cy + H * 0.35 // the short arm, above the bar + const south = cy - H * 0.65 // the long arm, below it + const barV = cy + H * 0.18 // the bar sits high, nearer the top than the middle + const barHalf = arm * 0.62 + const vert = [[cx, north], [cx, south]].map(([u, v]) => project(u, v)) + const horiz = [[cx - barHalf, barV], [cx + barHalf, barV]].map(([u, v]) => project(u, v)) return { type: 'FeatureCollection', features: [ diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json index 0394f00bd..813d9fdfa 100644 --- a/public/dx-maps/doxa-maps-spa/manifest.json +++ b/public/dx-maps/doxa-maps-spa/manifest.json @@ -615,7 +615,7 @@ "bundle": "gospel-internet", "scriptUrl": "./gospel-internet/gospel-internet.js", "output": "dx-maps/gospel-internet/gospel-internet.js", - "integrity": "sha384-6ypksiuOg1UYnG8SJOD9nFoDxMeisCv7Z8ZxvQtGI8E0Yonjw1IkXPHOI2hoHVTn", + "integrity": "sha384-zx0Es4trCFtqdzhsZHh78nh9D+Cpyp8lw7oZiucX7huOJzQlOzmIAcqJfhh5Pesj", "elements": [ "gospel-internet-board" ], @@ -635,7 +635,7 @@ "scriptUrl": "./gospel-internet/gospel-internet.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n" + "script": "\n\n\n\n\n" }, "instances": [ { diff --git a/public/dx-maps/gospel-internet/board/board/index.html b/public/dx-maps/gospel-internet/board/board/index.html index bfa643029..eaf55b380 100644 --- a/public/dx-maps/gospel-internet/board/board/index.html +++ b/public/dx-maps/gospel-internet/board/board/index.html @@ -36,7 +36,7 @@ - + - + - + - + - + - + - + \n\n\n" + "script": "\n\n\n\n\n" }, "instances": [ {