Skip to content

Keep map pins locked to the map while panning - #68

Merged
dfalling merged 1 commit into
mainfrom
fix-map-lag
Jul 31, 2026
Merged

Keep map pins locked to the map while panning#68
dfalling merged 1 commit into
mainfrom
fix-map-lag

Conversation

@dfalling

Copy link
Copy Markdown
Owner

Summary

Map pins visibly trailed behind the map during a pan, as if their layer refreshed slower than the map's.

The cause is that <Marker> isn't part of the map on Android — it's an Android View parented to the MapView and repositioned by hand once per rendered frame (MLRNMapView.kt:713MarkerViewManager.updateMarkers()). The map renders into a GLSurfaceView, so its content is composited independently of the view hierarchy holding the pins, and the pins land a frame or more behind the map content they're pinned to. That happens with one pin; a viewport full of them just makes it obvious, since the per-frame work is O(pins). The library's own docs point at a symbol layer for this reason.

Clustering would only have reduced the pin count without touching the desync, so it wasn't the right fix.

Pins are now a <Layer type="symbol"> over a <GeoJSONSource>. Symbols render inside the map's own GL frame, so they're pixel-locked to it and the count barely matters.

  • Each distinct element emoji is rasterised off-screen once (react-native-view-shot) and registered as a map image. Emoji can't come from the style's glyphs — those are single-channel SDF and OpenFreeMap's Noto Sans set has no emoji ranges — so an image is the only route.
  • symbol-sort-key replaces the manual array reordering that floated the selected marker to the end.
  • icon-size replaces the grow-on-select transform.
  • The client-side viewport cull is gone; MapLibre culls per tile, which also stops pins popping in at the screen edges.
  • Because icon offsets scale with icon size, a selected pin's tip now stays exactly on its coordinate instead of dipping ~2pt below it.

Test plan

  • tsc --noEmit, biome check, and all 28 tests pass.
  • Built and ran on an emulator. Verified pins draw as the correct red teardrops with the emoji upright, the no-icon fallback pin renders, the selected pin is larger and drawn above its neighbours, the drop shadow survives the off-screen capture uncropped, and tapping a pin reports the right element id. Checked in both light and dark themes.
  • Not verified: that the judder is actually gone at real pin counts — that needs a real device with a real dataset, which the emulator and an unauthenticated session can't provide. Worth a look before merging.

🤖 Generated with Claude Code

On Android, <Marker> is not part of the map at all: it's an Android View
parented to the MapView and repositioned by hand once per rendered frame.
Because the map renders into a GLSurfaceView, its content is composited
separately from the view hierarchy holding the pins, so the pins land a frame
or more behind the map they're pinned to — visible as the whole pin layer
lagging behind a pan. The cost is also O(pins) per frame, which is what made
it obvious once a viewport held a lot of them.

Draw the pins as a symbol layer over a GeoJSON source instead. Symbols are
rendered inside the map's own GL frame, so they're pixel-locked to it and
their count barely matters. A symbol can only draw a registered image and our
pins carry a per-element emoji, so each distinct icon is rasterised off-screen
once and registered as a map image; emoji can't come from the style's glyphs,
which are SDF and have no emoji ranges.

Three workarounds go away with it: the manual reordering that floated the
selected marker to the end of the array is now symbol-sort-key, its grow-on-
select style is icon-size, and the client-side viewport cull is gone entirely
since MapLibre culls per tile — which also stops pins popping in at the screen
edges. Because icon offsets scale with icon size, a selected pin's tip now
stays exactly on its coordinate rather than dipping just below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dfalling
dfalling merged commit 8c37e97 into main Jul 31, 2026
2 checks passed
@dfalling
dfalling deleted the fix-map-lag branch July 31, 2026 09:16
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.

1 participant