Keep map pins locked to the map while panning - #68
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 AndroidViewparented to theMapViewand repositioned by hand once per rendered frame (MLRNMapView.kt:713→MarkerViewManager.updateMarkers()). The map renders into aGLSurfaceView, 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.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-keyreplaces the manual array reordering that floated the selected marker to the end.icon-sizereplaces the grow-on-select transform.Test plan
tsc --noEmit,biome check, and all 28 tests pass.🤖 Generated with Claude Code