Fit to the pins when a narrow screen cuts them out of the frame
A country page frames by a fixed center and zoom, and a fixed zoom shows less on
a narrow screen. Measured on the drop-in: /regions/niger at 390 wide put the
camera over southern Niger and northern Nigeria with the Agadez outline cut off
at the top and 0 of the page's 2 pins in view, while the same page at 1440
showed both.
Inherited rather than introduced: their CountryDetail passes only center and
zoom to CountryMap and never a box, so the live site frames the same way.
chooserZoom cannot save it either -- its first line returns props.zoom unchanged
unless colorMode is 'region', and a country page is 'prayer', so the
width-and-height-aware solver below it never runs.
The fit is CONDITIONAL, which is the part that keeps this a clone. fitTheirWay
ends in easeTo({center, zoom}), so it moves the centre as well as the zoom;
fitting unconditionally would re-frame every page that is already correct. So
the configured camera is applied and kept, and the pins' box is fitted only when
a pin actually falls outside it. Asked on moveend, because getBounds reads the
current camera and flyTo is animated, so asking sooner would test the camera
being left behind.
The cap is a ceiling: maxZoom = props.zoom lets the fit zoom OUT, which is what
the narrow case needs, while forbidding it from zooming in past the camera the
page asked for. Capping it the other way would have made this a no-op on the one
case it exists for.
The pins' box is the honest input available here. The country outline comes from
the countries tile source, so its extent is only as good as the tiles that
happen to be loaded; reading a box off it would measure the viewport, not the
country. A box supplied by the page would be better still, since pins can
cluster in one corner.
Probe in _qa-cycle lifts pinsBox and fitIfPinsAreOutOfView out of this file by
source and tests both sides: a well-framed page takes zero fits, and the narrow
case fits the pins' box capped at props.zoom. Mutation-tested both ways --
removing the already-framed early return, and capping with minZoom instead of
maxZoom, each make it fail.
This commit is contained in:
parent
7b06dc95a7
commit
5b35257f7b
1 changed files with 66 additions and 4 deletions
|
|
@ -589,12 +589,12 @@ function chooserZoom(m) {
|
|||
// Mapbox takes the bare string; MapLibre v5 takes { type }. Writing the wrong shape is not a
|
||||
// no-op — maplibre reads e?.type and would set undefined.
|
||||
|
||||
function fitTheirWay(m, bounds) {
|
||||
function fitTheirWay(m, bounds, opts = FIT_OPTS) {
|
||||
let cam = null
|
||||
const proj = projectionOf(m)
|
||||
try {
|
||||
if (proj && proj !== 'mercator') setProjectionOn(m, 'mercator')
|
||||
cam = m.cameraForBounds(bounds, FIT_OPTS)
|
||||
cam = m.cameraForBounds(bounds, opts)
|
||||
} catch (e) {
|
||||
console.warn('[RegionCountryMap] mercator fit unavailable, falling back to fitBounds:', e && e.message)
|
||||
} finally {
|
||||
|
|
@ -602,7 +602,62 @@ function fitTheirWay(m, bounds) {
|
|||
if (proj && proj !== 'mercator') setProjectionOn(m, proj)
|
||||
}
|
||||
if (cam && cam.center && typeof cam.zoom === 'number') m.easeTo({ center: cam.center, zoom: cam.zoom, duration: 600 })
|
||||
else m.fitBounds(bounds, { ...FIT_OPTS, duration: 600 })
|
||||
else m.fitBounds(bounds, { ...opts, duration: 600 })
|
||||
}
|
||||
|
||||
/**
|
||||
* The box the drawn pins occupy, or null when there are none to bound.
|
||||
* [[west, south], [east, north]] — the same shape props.bounds arrives in.
|
||||
*/
|
||||
function pinsBox() {
|
||||
let w = Infinity, s = Infinity, e = -Infinity, n = -Infinity
|
||||
for (const p of props.points) {
|
||||
if (!Number.isFinite(p.lng) || !Number.isFinite(p.lat)) continue
|
||||
if (p.lng < w) w = p.lng
|
||||
if (p.lng > e) e = p.lng
|
||||
if (p.lat < s) s = p.lat
|
||||
if (p.lat > n) n = p.lat
|
||||
}
|
||||
return Number.isFinite(w) && Number.isFinite(s) ? [[w, s], [e, n]] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ A FIXED ZOOM SHOWS LESS ON A NARROW SCREEN, so a tall country loses its edges.
|
||||
*
|
||||
* MEASURED by rega on the drop-in: /regions/niger at 390 wide put the camera over southern Niger
|
||||
* and northern Nigeria with the Agadez outline cut off at the top, and 0 of the page's 2 pins in
|
||||
* view, while the same page at 1440 showed both. INHERITED, not introduced here: their
|
||||
* CountryDetail.vue passes only :center and :zoom to CountryMap and never a box, so the live site
|
||||
* frames the same way. chooserZoom cannot save it either — its first line returns props.zoom
|
||||
* unchanged unless colorMode is 'region', and a country page is 'prayer'.
|
||||
*
|
||||
* ★ WHY THIS IS CONDITIONAL RATHER THAN ALWAYS FITTING. fitTheirWay ends in easeTo({center, zoom}),
|
||||
* so it moves the CENTRE as well as the zoom. Fitting unconditionally would therefore shift the
|
||||
* framing of every already-correct page, which is exactly what a clone must not do. So the
|
||||
* configured camera is applied first and kept, and the fit runs only when a pin actually falls
|
||||
* outside it.
|
||||
*
|
||||
* ★ AND THE CAP IS A CEILING, NOT A FLOOR. maxZoom: props.zoom lets the fit zoom OUT — which is the
|
||||
* whole point, since the narrow case needs a LOWER zoom to fit — while forbidding it from zooming
|
||||
* IN past the camera the page asked for. (An earlier version of this idea capped it the other way,
|
||||
* which would have made the fix a no-op on the one case it exists for.)
|
||||
*
|
||||
* The pins' box is the honest input available here. The country outline comes from the countries
|
||||
* tile source, so its extent is only as good as the tiles that happen to be loaded — reading a box
|
||||
* off it would measure the viewport, not the country. A real country box would have to come from
|
||||
* the page, and would be better, because pins can cluster in one corner.
|
||||
*/
|
||||
function fitIfPinsAreOutOfView(m) {
|
||||
const box = pinsBox()
|
||||
if (!box) return
|
||||
let view = null
|
||||
try { view = m.getBounds() } catch (_) { return }
|
||||
if (!view || typeof view.contains !== 'function') return
|
||||
const inView = (lng, lat) => { try { return view.contains([lng, lat]) } catch (_) { return true } }
|
||||
const allIn = inView(box[0][0], box[0][1]) && inView(box[1][0], box[1][1])
|
||||
&& inView(box[0][0], box[1][1]) && inView(box[1][0], box[0][1])
|
||||
if (allIn) return // well framed already: do not touch the camera
|
||||
fitTheirWay(m, box, { ...FIT_OPTS, maxZoom: props.zoom })
|
||||
}
|
||||
|
||||
function applySubject() {
|
||||
|
|
@ -641,7 +696,14 @@ function applySubject() {
|
|||
// the same box and tell 'the fit is wrong' apart from 'something overrode the fit'.
|
||||
if (props.bounds && typeof window !== 'undefined') window.__rcmBounds = props.bounds
|
||||
if (props.bounds) fitTheirWay(m, props.bounds)
|
||||
else m.flyTo({ center: props.center, zoom: chooserZoom(m), duration: 600 })
|
||||
else {
|
||||
const zoom = chooserZoom(m)
|
||||
m.flyTo({ center: props.center, zoom, duration: 600 })
|
||||
// getBounds() reads the CURRENT camera, and flyTo is animated, so asking now would test the
|
||||
// camera we are leaving. Ask once the move has landed instead. `once` so repeated re-frames
|
||||
// cannot stack listeners, and the check is a no-op on a page that is already framed right.
|
||||
m.once('moveend', () => fitIfPinsAreOutOfView(m))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue