Fill the chooser's frame: solve the globe's zoom instead of reusing a mercator formula
The chooser's zoom came from log2(width / 512), and the comment above it said why: "Mercator's world is 512*2^z wide". But the chooser renders under the globe projection, so that is the same mistake as the region pages framing too wide -- a mercator answer applied to a sphere -- only here it made the frame too SMALL. Found by looking at the screenshot, not by any assertion: a sphere adrift in white space. Measured at 1200x780: the mercator answer of 1.229 put the sphere at 390.61px, 50.1% of the frame's short side, so half the frame was dead space. And the number was inert -- zoom 0.5, 1.0 and 1.229 all render an IDENTICAL 390.61px sphere, because Mapbox clamps the globe's apparent size at low zoom, so the formula's output sat inside the clamp and could have been 0.5 with no visible difference. There is no closed form to swap in. Diameter against a clean doubling is 3.6 to 10.5% off above the clamp, because the globe-to-mercator transition is nonlinear there. What the measurement does establish is monotonicity, which is the only property a solver needs, so the zoom is now SOLVED against the engine: bisect the zoom whose limb projection fills 92% of the frame's short side. project() is pure transform math, every setZoom lands in one synchronous block before a frame renders, the original zoom is restored in a finally, and the old mercator formula remains the fallback because a small globe beats a broken chooser. The first version of the solver measured pole-to-pole and called it the diameter, which holds only when the centre is on the equator. The chooser centres on the mean of every people group, latitude 28, so that was a CHORD: 717.6px where the sphere was really 792-835px across, understating it by 1.104x. The solver over-zoomed and clipped the globe top and bottom -- again visible in the screenshot while every number still looked right. It now samples the limb, points at 90 degrees angular distance, and takes the MAX across twelve bearings, because Mapbox's globe is a perspective camera rather than a true orthographic one and the silhouette is not a perfect screen-space circle: 396px along the meridian against 417.3px east and west at one zoom. Result: the widest axis is 750.6px inside a 780px frame, the whole limb sits in frame, and dead space falls from 49.9% to under 4%. Pins are correspondingly bigger click targets. On a phone the ported 1/2 aspect ratio means the globe fills the WIDTH, which is the right trade -- filling the height would crop it horizontally and lose pins off both edges -- and that leaves half the height empty by geometry, which is carded for a Driver decision rather than resolved by deviating from a clone-faithful rule. The chooser's behaviour is unchanged and re-proven after the camera moved: _probe-chooser 15/15 and the new _probe-chooser-pin-opens-country 8/8.
This commit is contained in:
parent
26892e7274
commit
78a7baaa96
7 changed files with 159 additions and 83 deletions
|
|
@ -373,21 +373,97 @@ function popupHtml(p) {
|
|||
// Re-frame and re-source on a new selection instead of tearing the map down: a rebuilt map
|
||||
// re-downloads tiles and flashes, and the subject changes on every tab-1 click.
|
||||
/**
|
||||
* The chooser's zoom has to follow the CONTAINER WIDTH, not be a constant.
|
||||
* The chooser's zoom has to follow the CONTAINER, and it has to be asked of the GLOBE.
|
||||
*
|
||||
* ★ THIS USED TO BE `log2(width / 512)` — mercator's world is 512*2^z wide, so that is the zoom
|
||||
* which fits one mercator world. But the chooser renders under the GLOBE projection, and the same
|
||||
* mistake that made the region pages frame too wide (a mercator answer applied to a sphere) made
|
||||
* this frame too SMALL. Found by LOOKING at _shot-chooser-map-desktop.png — a sphere adrift in
|
||||
* white space — not by any assertion, and then measured by
|
||||
* _qa-cycle/02-builder-cycle/_probe-chooser-globe-fill.mjs at 1200x780:
|
||||
*
|
||||
* zoom 1.229 (what the mercator formula returned) -> sphere diameter 390.61px = 50.1% of the
|
||||
* frame's short side. HALF THE FRAME WAS DEAD SPACE.
|
||||
*
|
||||
* Worse, the number was inert. That probe measured zoom 0.5, 1.0 and 1.229 all rendering an
|
||||
* IDENTICAL 390.61px sphere, because Mapbox clamps the globe's apparent size at low zoom: the
|
||||
* formula's output sat inside the clamp, so it could have returned 0.5 and changed nothing.
|
||||
*
|
||||
* There is no closed form to replace it with. The same probe measured the diameter against a clean
|
||||
* doubling and found 3.6-10.5% error above the clamp, because the globe-to-mercator transition is
|
||||
* nonlinear there — so `log2` of anything is the wrong shape. What the measurement DID establish
|
||||
* is that the diameter never decreases as zoom rises, and monotonicity is the only property a
|
||||
* solver needs. So SOLVE it against the engine instead of modelling it:
|
||||
* bisect the zoom whose pole-to-pole projection fills FILL of the frame's short side.
|
||||
*
|
||||
* project() is pure transform math — no tiles, no paint, no network — and every setZoom here
|
||||
* lands in ONE synchronous block before any frame renders, so the bisection is invisible; the
|
||||
* original zoom is restored in a `finally` regardless. If any of it throws, the old mercator
|
||||
* formula is the fallback, because a slightly small globe beats a broken chooser.
|
||||
*
|
||||
* MEASURED: a fixed zoom of 1.25 fills a 1200px desktop frame and shows barely a third of the
|
||||
* world at 390px — Asia fell off the right edge, on the one screen whose job is "pick a region".
|
||||
* Mercator's world is 512*2^z wide, so the zoom that fits it is log2(width/512): ~1.23 at 1200px,
|
||||
* ~-0.39 at 390px. Mapbox will not go below 0, so a phone still crops, but ~24% instead of ~67%,
|
||||
* and the map pans. Clamped to [0, 2] so a very wide embed does not zoom past the useful range.
|
||||
* Only the chooser uses this; a selected page is framed by their own bounds/centre/zoom.
|
||||
*/
|
||||
const CHOOSER_FILL = 0.92 // leave a margin so the limb is never clipped by the frame
|
||||
function chooserZoom(m) {
|
||||
if (props.colorMode !== 'region') return props.zoom
|
||||
let w = 0
|
||||
try { w = m.getContainer()?.clientWidth || 0 } catch (_) { w = 0 }
|
||||
if (!w) return props.zoom
|
||||
return Math.max(0, Math.min(2, Math.log2(w / 512)))
|
||||
let W = 0, H = 0
|
||||
try { const el = m.getContainer(); W = el?.clientWidth || 0; H = el?.clientHeight || 0 } catch (_) { W = 0 }
|
||||
if (!W) return props.zoom
|
||||
const mercatorFallback = Math.max(0, Math.min(2, Math.log2(W / 512)))
|
||||
if (!H) return mercatorFallback
|
||||
|
||||
const z0 = m.getZoom()
|
||||
try {
|
||||
const c = m.getCenter()
|
||||
// ★ THE LIMB, NOT THE POLES. The first version of this measured pole-to-pole and called it the
|
||||
// diameter, which is only true when the centre sits on the equator. The chooser centres on the
|
||||
// mean of every people group -- latitude 28 -- so pole-to-pole is a CHORD: measured 717.6px
|
||||
// where the sphere was really 792-835px across, understating it by 1.104x. The solver therefore
|
||||
// over-zoomed and the globe was clipped top and bottom, which the screenshot showed plainly
|
||||
// while every number still looked right.
|
||||
// Under the globe the silhouette is centred on the map centre, so any point at 90 deg ANGULAR
|
||||
// distance lies on the limb. Sample several bearings and take the MAX, because Mapbox's globe
|
||||
// is a perspective camera rather than a true orthographic one, so the silhouette is not a
|
||||
// perfect screen-space circle -- measured radius 396px along the meridian against 417.3px east
|
||||
// and west at the same zoom. Taking the max is what keeps the widest axis inside the frame.
|
||||
const RAD = Math.PI / 180, DEG = 180 / Math.PI
|
||||
const phi0 = c.lat * RAD
|
||||
// Great-circle destination at angular distance 90 deg on bearing t, which reduces to this
|
||||
// because cos(90 deg) is 0 and sin(90 deg) is 1.
|
||||
const limbPoints = []
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const t = (i / 12) * 2 * Math.PI
|
||||
const lat = Math.asin(Math.cos(phi0) * Math.cos(t))
|
||||
const lng = c.lng + Math.atan2(Math.sin(t) * Math.cos(phi0), -Math.sin(phi0) * Math.sin(lat)) * DEG
|
||||
limbPoints.push([lng, lat * DEG])
|
||||
}
|
||||
const diameter = (z) => {
|
||||
m.setZoom(z)
|
||||
const ctr = m.project([c.lng, c.lat])
|
||||
let max = 0
|
||||
for (const pt of limbPoints) {
|
||||
const q = m.project(pt)
|
||||
const d = Math.hypot(q.x - ctr.x, q.y - ctr.y)
|
||||
if (Number.isFinite(d) && d > max) max = d
|
||||
}
|
||||
return max * 2
|
||||
}
|
||||
const want = Math.min(W, H) * CHOOSER_FILL
|
||||
let lo = 0, hi = 5
|
||||
if (diameter(hi) < want) return hi // frame bigger than the globe can fill
|
||||
for (let i = 0; i < 24; i++) { // 24 halvings of [0,5] resolves to ~3e-7 zoom
|
||||
const mid = (lo + hi) / 2
|
||||
if (diameter(mid) < want) lo = mid; else hi = mid
|
||||
}
|
||||
const solved = (lo + hi) / 2
|
||||
return Number.isFinite(solved) ? solved : mercatorFallback
|
||||
} catch (e) {
|
||||
console.warn('[RegionCountryMap] globe fill solve failed, using the mercator fit:', e && e.message)
|
||||
return mercatorFallback
|
||||
} finally {
|
||||
// Never leave the camera parked on a probe value.
|
||||
try { m.setZoom(z0) } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
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
8
public/doxa-maps/manifest.json
generated
8
public/doxa-maps/manifest.json
generated
|
|
@ -879,7 +879,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-yxWIwZyBb6+xve6pVbhxlLEJn4diVUhzV0fZReHWRYgdIb8U0OZUrAi7SEu9DnpI",
|
||||
"integrity": "sha384-MM2p8wtopS2RP5NbieFf+KvoBRT6yHnPCHaL/UQ7hxpJdVVc1o7BpXVwEF9PGPJz",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -899,7 +899,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-yxWIwZyBb6+xve6pVbhxlLEJn4diVUhzV0fZReHWRYgdIb8U0OZUrAi7SEu9DnpI\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MM2p8wtopS2RP5NbieFf+KvoBRT6yHnPCHaL/UQ7hxpJdVVc1o7BpXVwEF9PGPJz\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -913,7 +913,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-yxWIwZyBb6+xve6pVbhxlLEJn4diVUhzV0fZReHWRYgdIb8U0OZUrAi7SEu9DnpI\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MM2p8wtopS2RP5NbieFf+KvoBRT6yHnPCHaL/UQ7hxpJdVVc1o7BpXVwEF9PGPJz\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -927,7 +927,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-yxWIwZyBb6+xve6pVbhxlLEJn4diVUhzV0fZReHWRYgdIb8U0OZUrAi7SEu9DnpI\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MM2p8wtopS2RP5NbieFf+KvoBRT6yHnPCHaL/UQ7hxpJdVVc1o7BpXVwEF9PGPJz\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-yxWIwZyBb6+xve6pVbhxlLEJn4diVUhzV0fZReHWRYgdIb8U0OZUrAi7SEu9DnpI" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-MM2p8wtopS2RP5NbieFf+KvoBRT6yHnPCHaL/UQ7hxpJdVVc1o7BpXVwEF9PGPJz" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
130
public/doxa-maps/regions-dashboard/regions-dashboard.js
generated
130
public/doxa-maps/regions-dashboard/regions-dashboard.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 <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-yxWIwZyBb6+xve6pVbhxlLEJn4diVUhzV0fZReHWRYgdIb8U0OZUrAi7SEu9DnpI" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-MM2p8wtopS2RP5NbieFf+KvoBRT6yHnPCHaL/UQ7hxpJdVVc1o7BpXVwEF9PGPJz" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-yxWIwZyBb6+xve6pVbhxlLEJn4diVUhzV0fZReHWRYgdIb8U0OZUrAi7SEu9DnpI" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-MM2p8wtopS2RP5NbieFf+KvoBRT6yHnPCHaL/UQ7hxpJdVVc1o7BpXVwEF9PGPJz" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue