From 0f839f1071b55e26d3d1c31a1a277df69dca5c97 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 26 Sep 2026 00:19:26 +0000 Subject: [PATCH] MapLibre: reading the projection threw, so a region dashboard never framed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver, on MapLibre: opening a region dashboard does not zoom to the region the way Mapbox does. This is why. fitTheirWay read `m.getProjection().name` OUTSIDE its try. The two engines disagree about both halves of that API: Mapbox getProjection() -> { name: 'globe' } setProjection('mercator') MapLibre getProjection() -> { type: 'globe' } | undefined setProjection({ type: 'mercator' }) MEASURED in maplibre-gl@5's own source, which we load from jsDelivr: Map.getProjection() returns `this.stylesheet?.projection`, and the OpenFreeMap style we hand MapLibre declares no projection key at all — its top level is version, sources, sprite, glyphs, layers. So getProjection() is UNDEFINED there and `.name` threw a TypeError. Because the read sat outside the try, the throw escaped fitTheirWay AND escaped applySubject(), of which it is the last statement. The camera was never framed: the map stayed wherever it was instead of flying to the region. The read is now defensive and accepts both shapes, and the WRITE is engine-correct: Mapbox takes the bare string, MapLibre reads e?.type, so handing it a string would set undefined rather than fail loudly. It was wrong in a second way worth recording: on a MapLibre style that DOES set globe, the old read returned null and silently skipped the mercator correction that exists to stop the fit pulling back. So it both crashed on one shape and under-corrected on another. Proven headless per the Playwright policy, with the old expression run against the same stubs as the control: it THROWS TypeError on the real MapLibre shape and returns null where it should see globe, while the new one returns 'globe', 'mercator', 'globe', null, null across the five engine shapes. A check that cannot be shown to fail proves nothing, so the check fails if the old expression ever stops throwing. Needs eyes: MapLibre, region dashboard — does it now fly to the region. --- .../src/RegionCountryMap.vue | 27 +++- public/dx-maps/doxa-maps-spa/manifest.json | 8 +- public/dx-maps/manifest.json | 8 +- .../region-dashboards/index.html | 2 +- .../regions-dashboard/regions-dashboard.js | 120 +++++++++--------- .../regions-dashboard/regions-list/index.html | 2 +- .../regions-dashboard/regions-mfe/index.html | 2 +- 7 files changed, 95 insertions(+), 74 deletions(-) diff --git a/plugin-designs/regions-dashboard/src/RegionCountryMap.vue b/plugin-designs/regions-dashboard/src/RegionCountryMap.vue index 8fb1659b2..c39b78508 100644 --- a/plugin-designs/regions-dashboard/src/RegionCountryMap.vue +++ b/plugin-designs/regions-dashboard/src/RegionCountryMap.vue @@ -506,17 +506,38 @@ function chooserZoom(m) { * Both setProjection calls land in ONE synchronous tick, so no frame renders in mercator; the * probe asserts the projection is still 'globe' afterwards rather than trusting that. */ +// The two engines disagree about BOTH halves of the projection API, and this read used to assume +// Mapbox's: +// Mapbox getProjection() -> { name: 'globe' } setProjection('mercator') +// MapLibre getProjection() -> { type: 'globe' } | undefined +// setProjection({ type: 'mercator' }) +// MEASURED in maplibre-gl@5's own source: Map.getProjection() returns `this.stylesheet?.projection`, +// and the OpenFreeMap style we hand MapLibre declares no `projection` key at all — its top level is +// version, sources, sprite, glyphs, layers. So getProjection() is UNDEFINED there, and reading +// `.name` off it threw a TypeError. That read sat OUTSIDE the try, so the throw escaped +// fitTheirWay, escaped applySubject() — of which it is the last statement — and the camera was +// never framed. Opening a region dashboard under MapLibre therefore left the map wherever it was +// instead of flying to the region, which is exactly what the Driver reported. +const projectionOf = (m) => { + try { const p = m.getProjection && m.getProjection(); return (p && (p.name || p.type)) || null } catch (_) { return null } +} +// 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. +const setProjectionOn = (m, name) => { + try { m.setProjection(isMaplibreProvider ? { type: name } : name) } catch (_) {} +} + function fitTheirWay(m, bounds) { let cam = null - const proj = (m.getProjection && m.getProjection().name) || null + const proj = projectionOf(m) try { - if (proj && proj !== 'mercator') m.setProjection('mercator') + if (proj && proj !== 'mercator') setProjectionOn(m, 'mercator') cam = m.cameraForBounds(bounds, FIT_OPTS) } catch (e) { console.warn('[RegionCountryMap] mercator fit unavailable, falling back to fitBounds:', e && e.message) } finally { // Restore in `finally` so a throw inside cameraForBounds can never leave the map flat. - try { if (proj && proj !== 'mercator') m.setProjection(proj) } catch (_) {} + 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 }) diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json index fb8e8ae7f..c1e4bf7c6 100644 --- a/public/dx-maps/doxa-maps-spa/manifest.json +++ b/public/dx-maps/doxa-maps-spa/manifest.json @@ -888,7 +888,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-0VXlux60qvoDiJCNedP6W33hhrWeghLzYSBkR8zK7bblXC74i4XL14Gsw+b/Z0la", + "integrity": "sha384-lpzd66s+yTDop/akosXlKIXuiOCv3r2RMUOWBcj217m3HvxSQ2UtPHIHqx4xwVAK", "elements": [ "regions-dashboard" ], @@ -908,7 +908,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } }, { @@ -922,7 +922,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } }, { @@ -936,7 +936,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index fb8e8ae7f..c1e4bf7c6 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -888,7 +888,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-0VXlux60qvoDiJCNedP6W33hhrWeghLzYSBkR8zK7bblXC74i4XL14Gsw+b/Z0la", + "integrity": "sha384-lpzd66s+yTDop/akosXlKIXuiOCv3r2RMUOWBcj217m3HvxSQ2UtPHIHqx4xwVAK", "elements": [ "regions-dashboard" ], @@ -908,7 +908,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } }, { @@ -922,7 +922,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } }, { @@ -936,7 +936,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } } ] diff --git a/public/dx-maps/regions-dashboard/region-dashboards/index.html b/public/dx-maps/regions-dashboard/region-dashboards/index.html index c022be9ac..f476ea78d 100644 --- a/public/dx-maps/regions-dashboard/region-dashboards/index.html +++ b/public/dx-maps/regions-dashboard/region-dashboards/index.html @@ -30,7 +30,7 @@ - + - + - +