Skip to content

Stop map pins swapping and truncating each other's images - #81

Merged
dfalling merged 1 commit into
mainfrom
fix-marker-artifacts
Aug 29, 2026
Merged

Stop map pins swapping and truncating each other's images#81
dfalling merged 1 commit into
mainfrom
fix-marker-artifacts

Conversation

@dfalling

Copy link
Copy Markdown
Owner

Summary

A selected element rendered with another element's emoji, cut off by a hard horizontal edge partway down — no teardrop tip, so it also read as too large and off its coordinate. Neither the data nor MapLibre was at fault.

react-native-view-shot's Android module compresses every snapshot through a single static byte[] (ViewShot.java:142) and dispatches captures on Executors.newCachedThreadPool(), with no synchronisation between the two. It then base64-encodes that static field using its own capture's length:

final ReusableByteArrayOutputStream os = new ReusableByteArrayOutputStream(outputBuffer);
captureView(view, os);
outputBuffer = os.innerBuffer();
final String data = Base64.encodeToString(outputBuffer, 0, length, Base64.NO_WRAP);

usePinImages started a capture for every distinct pin emoji in one requestAnimationFrame tick, so they raced through that one array. The URI returned for one pin held another's PNG bytes with its own spliced through the tail, which decodes to the wrong emoji ending where the bytes stopped making sense.

Measured against the reported screenshot: the marker's size and position were correct to within a few pixels of where the element's coordinates put it — only the bottom 40% of the image was missing.

Changes

  • usePinImages captures through a serial queue: at most one snapshot in flight, so no two share the buffer.
  • Ref cleanup deletes a host entry rather than storing null, so the queue can tell "unmounted" from "not yet attached" and re-attempt a pin that comes back.
  • New __tests__/usePinImages.test.tsx: asserts captures never overlap (fails on the previous implementation — 4 concurrent captures), and that each pin is registered under the name of the icon it actually drew.

Trade-off

Pins rasterise one per frame, so a trip with ~20 distinct emoji takes ~20 frames to fully populate. Until a pin's own image lands its element shows the plain teardrop — the fallback that was already designed in.

Upstream

The library is unsafe for any concurrent captureRef on Android, not just this use. Worth reporting separately; this change only fixes our side.

Checks

bunx tsc --noEmit clean · bun run lint clean · bun run test -- --ci 59 passed / 9 suites

react-native-view-shot's Android module compresses every snapshot through
a single static byte buffer and dispatches captures on an unbounded thread
pool, with no synchronisation anywhere between the two. We started a
capture for every distinct pin emoji in one animation frame, so they raced
through that one array: the data URI that came back for a pin held another
pin's PNG bytes with its own spliced through the tail. On the map that
decoded as the wrong emoji, ending in a hard horizontal edge partway down
where the bytes stopped making sense — a bed showing a tree with no
teardrop tip, which reads as a pin that is also too big and off its spot.

Capture through a serial queue instead: at most one snapshot in flight, so
no two ever share the buffer. Pins now rasterise one per frame, and until
one lands its element keeps the plain teardrop that already stands in for
an unrasterised icon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dfalling
dfalling merged commit fb54d35 into main Aug 29, 2026
2 checks passed
@dfalling
dfalling deleted the fix-marker-artifacts branch August 29, 2026 17:37
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