Skip to content

chore(deps): maplibre-gl 5 + vue-maplibre-gl 5 + @maptiler/* 3#581

Closed
JumpLink wants to merge 18 commits into
mainfrom
chore/deps-maplibre-5
Closed

chore(deps): maplibre-gl 5 + vue-maplibre-gl 5 + @maptiler/* 3#581
JumpLink wants to merge 18 commits into
mainfrom
chore/deps-maplibre-5

Conversation

@JumpLink
Copy link
Copy Markdown
Collaborator

@JumpLink JumpLink commented May 7, 2026

Tracking: https://correctivdigital.openproject.com/projects/beabee/work_packages/2196

Summary

Bumps the map stack in apps/frontend:

Package Old New
maplibre-gl ^3.6 ^5.24
vue-maplibre-gl ^3.1 ^5.6
@maptiler/client ^2.2 ^3.0
@maptiler/geocoding-control ^1.2 ^3.0
geojson (transitive) ^0.5 (added)

Code adjustments

  • Map.loadImage is Promise-returning in maplibre-gl 5 — apps/frontend/src/utils/images.ts rewritten to await mapInstance.loadImage(...).then(({ data }) => data).
  • GeoJSONSource.getClusterExpansionZoom is also Promise-returning — apps/frontend/src/pages/crowdnewsroom/[id]/map.vue updated.
  • @maptiler/geocoding-control@3 dropped the DOM-style addEventListener and the standalone style.css export. The control now extends Evented, so the pick listener uses .on('pick', handler) and reads the new event.feature. The CSS is delivered through the Lit shadow DOM, so the previous CSS import is removed.
  • vue-maplibre-gl@5 internally imports geojson at runtime even though it declares it only as a devDependency — added as an explicit dep in apps/frontend.
  • Selection ring — the previous MglCircleLayer against a dedicated SELECTED_RESPONSE GeoJSON source kept silently rendering nothing on maplibre-gl 5: the source held the feature, but no paint reached the canvas (queryRenderedFeatures returned [] even with forced radius: 50, opacity: 1, color: red). Replaced with a raw maplibre Marker with a custom HTML element (red border, transparent fill), driven by a single watch on [selectedFeature, mapLoaded]. Sidesteps the style/source pipeline entirely.
  • queryRenderedFeatures layer filter — maplibre-gl 5 throws on unknown layer ids; v3 silently returned []. The cluster-source sourcedata watcher can fire before the unclustered-points layer has mounted (it's guarded by v-if="mapLoaded" to wait for icons), so findSelectedFeature now filters its layers arg to the ones that actually exist.

Verification

  • yarn check
  • yarn build
  • CI green
  • Manual smoke of the callout-map: cluster expansion ✅, geocoder pick ✅, custom icons ✅, selection ring around the clicked marker ✅

Notes

The selection-ring stayed broken under several earlier attempts (always-mounted source with empty FeatureCollection, markRaw'd feature to dodge Vue reactivity, circle-opacity: 0 instead of alpha-0 color, v-if="mapLoaded" ordering, explicit moveLayer after each selection). None of them produced a rendered ring, which is why this lands as a raw Marker rather than a fix to the source/layer setup.

JumpLink added 8 commits May 7, 2026 14:56
…2 → 3

API changes that reach our code:

- maplibre-gl 5 turned `Map.loadImage` and
  `GeoJSONSource.getClusterExpansionZoom` into Promise-returning
  methods. Updated `apps/frontend/src/utils/images.ts` and
  `apps/frontend/src/pages/crowdnewsroom/[id]/map.vue` accordingly.
- @maptiler/geocoding-control 3 swapped its DOM-event API for an
  `Evented`-style `.on(type, handler)` and exposes the picked feature
  as `event.feature` instead of `event.detail`. CSS is shipped via
  Lit shadow DOM, so the legacy `style.css` import is dropped.
- `vue-maplibre-gl@5` references `geojson` at runtime even though it
  declares it as a devDependency, so add it as an explicit dependency.
maplibre-gl 5 declares `"type": "module"` and ships a native ESM
build. Vite's dep-optimizer (rolldown-based since v8) still wraps
it in a CJS-to-ESM shim, where the re-exported symbol gets renamed
for tree-shaking. The optimized `maplibre-gl.js` and the consumer
chunks (`vue-maplibre-gl.js`, `@maptiler_geocoding-control_*.js`)
are bundled in separate passes and can disagree on the rename — the
result is a runtime `SyntaxError: doesn't provide an export named: 't'`
the moment a callout-map page loads.

Adding `optimizeDeps.exclude: ['maplibre-gl']` lets the browser load
the package directly. The two consumers continue resolving
`from 'maplibre-gl'` against the same native ESM module, so the
import names line up.
@maptiler/geocoding-control v3 split the legacy DOM `pick` event into
two distinct flows:

* `pick`   - fires repeatedly as the user navigates the suggestion
             list (keyboard arrows, hover); `event.feature` reflects
             the currently *previewed* candidate and may be null when
             the user moves away.
* `select` - fires once, when the user actually picks a result
             (mouse click / Enter); `event.feature` is the chosen one.

The map's red selection ring (and the geocode marker that drives it)
should follow the confirmed selection. Listening on `pick` made the
ring blink in/out as the user hovered the suggestions and disappear
the moment they actually clicked, which is what surfaced as "selecting
an address opens the side panel but the marker is gone".
…source

maplibre-gl 5 tightened its worker-IPC serializer. Features returned
by `Map.queryRenderedFeatures` carry an internal prototype that is
not in the worker's class registry, so handing them back into a
`MglGeoJsonSource` (the SELECTED_RESPONSE source on click) explodes
inside the source's `_updateWorkerData` with:

  Error: can't serialize object of unregistered class Xf

Round-trip through JSON.stringify/parse to drop the prototype and
hand the source a plain GeoJSON Feature. This is the same fix
maplibre's own examples now apply for the queryRenderedFeatures →
GeoJSONSource flow.
`optimizeDeps.exclude: ['maplibre-gl']` was the wrong fix: the package
ships UMD inside a `type: module` outer shell (its `dist/package.json`
re-declares `type: commonjs`), so loading it as native ESM in the
browser fails with `doesn't provide an export named: 'default'`.

Switch to `optimizeDeps.include` for the three packages that share
maplibre-gl (`maplibre-gl`, `vue-maplibre-gl`,
`@maptiler/geocoding-control/maplibregl`). Listing them explicitly
forces Vite's dep-optimizer to bundle them in a single pass, so the
rolldown-renamed re-export symbol stays consistent across all three
chunks and the original `doesn't provide an export named: 't'` runtime
error disappears.
The previous JSON.stringify/parse round-trip stripped maplibre's
internal prototype, but it also dropped `geometry` — Mapbox/MapLibre
expose `feature.geometry` via a non-enumerable getter that
JSON.stringify silently skips. Without geometry the SELECTED_RESPONSE
GeoJSON source still mounted (no error, sidebar opened correctly via
properties), but the red-ring circle layer had nothing to draw, so
the selected point looked unmarked.

Build the plain feature explicitly via destructuring; this triggers
the getter for `geometry` and `properties`, gives us a plain GeoJSON
Feature, and avoids the worker-IPC class-registry error.
`findSelectedFeature` resolves the selected response via
`queryRenderedFeatures`, which returns whichever feature is on top —
often the *cluster* the response belongs to, not the response point
itself. The fixed `circle-radius: 20` red ring then sat exactly on
the cluster's filled black circle (which uses the same 20-40 px step
schedule), so the 2 px stroke was hidden inside the cluster's fill
and the user saw no marker at all.

Make the ring data-driven: when the selected feature is a cluster
(`['has', 'point_count']`), use `cluster_radius + 5` so the ring sits
just outside the black circle; otherwise stay at a fixed 22 px around
the unclustered icon. Stroke widened to 3 for parity at higher zooms.

Also add `console.debug('[map] ...')` lines around the selection flow
so it's easier to spot whether `geometry`/`properties` survive the
class-strip and which layer the matched feature came from.
@JumpLink JumpLink force-pushed the chore/deps-maplibre-5 branch from 41d4311 to 64416bf Compare May 7, 2026 12:56
@JumpLink JumpLink marked this pull request as ready for review May 7, 2026 14:32
@JumpLink JumpLink requested a review from wpf500 May 7, 2026 14:32
JumpLink added 2 commits May 7, 2026 16:35
The SELECTED_RESPONSE GeoJSON source was wrapped in v-if="selectedFeature"
and fed a single GeoJSON Feature. With vue-maplibre-gl 5 this combination
intermittently produces no rendered ring even though selectedFeature is
correctly populated and the side panel opens — the source/layer pair gets
torn down and re-mounted on every selection change, and the layer ends
up below the icon stack (or is missed entirely) by the time the next
paint runs.

Switch to the canonical form: a permanently mounted source bound to a
computed FeatureCollection that is empty while nothing is selected. The
circle layer is added once during map mount, lives at the top of the
stack for the lifetime of the page, and just renders zero / one features
depending on selection state. Also widens 'circle-color' from the legacy
'transparent' keyword to an explicit 'rgba(0, 0, 0, 0)' to dodge any
maplibre-5 colour-parser strictness.
@JumpLink JumpLink removed the request for review from wpf500 May 8, 2026 10:35
@JumpLink JumpLink marked this pull request as draft May 8, 2026 10:35
JumpLink added 2 commits May 22, 2026 08:20
The MglCircleLayer approach kept misbehaving on maplibre-gl 5: the
SELECTED_RESPONSE GeoJSONSource accepted and stored the feature
(getSource().getData() returns it), but the GPU pass never rendered
anything. queryRenderedFeatures on the layer returned an empty array
even with forced paint of \`circle-radius: 50\`, \`circle-opacity: 1\`,
\`circle-color: red\` — i.e. nothing landed on the canvas at all.
Looks like a v5 worker-IPC quirk with non-clustered GeoJSON sources
that get their data set dynamically through setData.

Bypass the style stack: create a raw maplibre Marker with a custom
HTML element (red border, transparent fill) at the selected
feature's coordinates. A single watch on [selectedFeature, mapLoaded]
adds / updates / removes the marker. No GeoJSON source, no
CircleLayer, no layer-order / paint-spec worries.

Also fix a stray error from the cluster-source-loaded watcher firing
before the UNCLUSTERED_POINTS layer has been added: filter the
\`layers\` arg of queryRenderedFeatures to layers that actually exist.
maplibre 5 throws on unknown layer ids, where v3 silently returned [].
@JumpLink
Copy link
Copy Markdown
Collaborator Author

red circle is fixed now:
grafik

@JumpLink JumpLink requested a review from wpf500 May 22, 2026 11:01
@JumpLink JumpLink marked this pull request as ready for review May 22, 2026 11:01
Copy link
Copy Markdown
Member

@wpf500 wpf500 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced by the replacement of MglCircleLayer with all this custom code. There must be an underlying reason why it has stopped working?

Unless there is a really good reason to upgrade this as a priority I would leave this change until there is time to look into the underlying reason.

@wpf500
Copy link
Copy Markdown
Member

wpf500 commented Jun 1, 2026

Closing for now until this upgrade becomes urgent or we find a better solution for MglCircleLayer

@wpf500 wpf500 closed this Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants