Skip to content

feat: add custom marker images with anchor, rotation, and flat mode - #25

Merged
piotr-graczyk-dev merged 5 commits into
mainfrom
feat/custom-marker-images
Jun 29, 2026
Merged

feat: add custom marker images with anchor, rotation, and flat mode#25
piotr-graczyk-dev merged 5 commits into
mainfrom
feat/custom-marker-images

Conversation

@jkasprzyk17

Copy link
Copy Markdown
Contributor

Closes #14

Summary

  • Extend MarkerDescriptor / MarkerProps with image, anchor, centerOffset, rotation, flat, and opacity fields
  • Resolve bundled require() images on the JS side into stable Nitro-safe MarkerImage descriptors with LRU caching
  • iOS: MarkerImageLoader, NitroImageAnnotationView, and MapKit adapter updates for custom bitmap markers
  • Android: MarkerIconFactory, anchor/rotation/flat via MarkerOptions, and overlay diffing via image fingerprints
  • Example app demo with bundled marker assets and anchor/rotation/flat toggles
  • README updates and unit tests for image resolution and descriptor normalization

Test plan

  • bun test in package/ (21 tests pass)
  • iOS simulator: custom marker images render with correct anchor
  • Android emulator: custom marker images render with correct anchor
  • Toggle rotation and flat mode in example app
  • Bulk markers prop with custom images
  • Default pin markers without image prop still work
  • Clustering with custom images does not crash

Made with Cursor

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds full custom bitmap marker image support (image, anchor, centerOffset, rotation, flat, opacity) to MarkerProps and MarkerDescriptor. Implements JS-side image resolution and LRU caching, iOS NitroImageAnnotationView and MarkerImageLoader, Android MarkerIconFactory with async HTTP loading, and an example scenario with README documentation.

Changes

Custom Marker Images

Layer / File(s) Summary
Type contracts and public API surface
package/src/native/specs/overlays.ts, package/src/types/overlays.ts, package/src/types/index.ts, package/src/index.ts
Introduces MarkerImage, MarkerImageSource, MarkerAnchor, MarkerPoint; extends MarkerDescriptor and MarkerProps with image and visual fields; re-exports all new types publicly.
JS image normalization and caching
package/src/overlays/lruCache.ts, package/src/overlays/assetSourceResolver.ts, package/src/overlays/markerImageFromResolvedAsset.ts, package/src/overlays/resolveMarkerImage.ts, package/src/overlays/normalizeMarkerDescriptors.ts, package/src/overlays/__tests__/*, package/tsconfig.json
Adds LruCache, asset resolver wrapper, markerImageFromResolvedAsset/isMarkerImage, resolveMarkerImage with LRU caching, and reference-stable normalizeMarkerDescriptors; fully tested.
JS marker wiring
package/src/components/MapView.tsx, package/src/hooks/useCollectedOverlays.ts
Switches MapView to bulk normalizeMarkerDescriptors; propagates image, anchor, centerOffset, rotation, flat, opacity from Marker props into collected descriptors.
iOS marker pipeline
package/ios/MarkerDescriptor+Fingerprint.swift, package/ios/MarkerClusterEngine.swift, package/ios/OverlayEnteringAnimation.swift, package/ios/MapMarkerAnnotation.swift, package/ios/MarkerImageLoader.swift, package/ios/NitroImageAnnotationView.swift, package/ios/NitroPinAnnotationView.swift, package/ios/MapOverlayController.swift, package/ios/HybridMapViewDelegate.swift, package/ios/AppleMapProviderAdapter.swift, package/ios/MarkerViewportFilter.swift
Moves fingerprinting to dedicated extensions; extends MapMarkerAnnotation with visual state and change detection; adds MarkerImageLoader for cached async loading; adds NitroImageAnnotationView; applies opacity/rotation/offset to NitroPinAnnotationView; routes image-backed markers through dedicated views in delegate and overlay controller.
Android marker pipeline
package/android/.../MarkerDescriptor+Fingerprint.kt, package/android/.../MarkerDescriptor+MarkerOptions.kt, package/android/.../MarkerIconFactory.kt, package/android/.../MapOverlayController.kt, package/android/.../GoogleMapProviderAdapter.kt, package/android/.../MarkerViewportFilter.kt
Adds fingerprinting extension; removes hardcoded red icon; adds effectiveGoogleMapsAnchor; introduces MarkerIconFactory with LRU bitmap caching, async HTTP fetching, and stale-callback prevention; integrates visual props into all marker reconciliation paths; fixes viewport size sync.
Example app and README
example/examples/customMarkerImages.ts, example/examples/index.ts, example/App.tsx, example/index.js, example/package.json, README.md
Adds a custom marker images scenario covering all image source types, anchor/rotation/flat/opacity variants; adds safe-area-aware ScenarioDock with live rotation and flat-mode controls; updates README with usage examples, capability matrix row, type list, and react-native-maps migration table.

Sequence Diagram(s)

sequenceDiagram
  participant App as App (JS)
  participant resolveMarkerImage
  participant LruCache
  participant MapOverlayController as MapOverlayController (native)
  participant MarkerIconFactory as MarkerIconFactory (Android)
  participant NitroImageAnnotationView as NitroImageAnnotationView (iOS)
  participant MarkerImageLoader as MarkerImageLoader (iOS)

  App->>resolveMarkerImage: MarkerImageSource (require/URI/MarkerImage)
  resolveMarkerImage->>LruCache: lookup by cache key
  LruCache-->>resolveMarkerImage: hit or miss
  resolveMarkerImage-->>App: MarkerImage | undefined

  App->>MapOverlayController: setMarkers(descriptors with image)
  MapOverlayController->>MapOverlayController: markersFingerprint() — skip if unchanged

  rect rgba(100, 149, 237, 0.5)
    note over MapOverlayController, MarkerIconFactory: Android path
    MapOverlayController->>MarkerIconFactory: applyVisualProps(descriptor, marker, key)
    MarkerIconFactory->>MarkerIconFactory: decode or fetch bitmap (async HTTP)
    MarkerIconFactory-->>MapOverlayController: BitmapDescriptor applied to Marker
  end

  rect rgba(60, 179, 113, 0.5)
    note over MapOverlayController, MarkerImageLoader: iOS path
    MapOverlayController->>NitroImageAnnotationView: configure(for: marker)
    NitroImageAnnotationView->>MarkerImageLoader: load(image, completion)
    MarkerImageLoader-->>NitroImageAnnotationView: UIImage (cached or fetched)
    NitroImageAnnotationView->>NitroImageAnnotationView: applyLayout (centerOffset + rotation)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • piotr-graczyk-dev
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the feature and includes the required feat prefix.
Description check ✅ Passed The description is directly about custom marker image support and its implementation.
Linked Issues check ✅ Passed The PR covers the requested marker image support on JS, iOS, Android, docs, tests, and the example app.
Out of Scope Changes check ✅ Passed No clearly unrelated changes stand out; the edits all support the marker-image feature or its validation.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt`:
- Around line 297-312: The marker sync path is exposing internal `s:`-prefixed
keys to consumer callbacks. Update the marker tagging and callback flow in
`MapOverlayController` so the sync key stays internal while click/drag listeners
receive the raw descriptor ID, or strip the prefix consistently before
forwarding `marker.tag` in every non-cluster callback path. Use the `update` and
marker callback code around `marker.tag`, `applyVisualProps`, and the
non-cluster listeners to keep the external API free of internal sync prefixes.

In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor`+MarkerOptions.kt:
- Around line 13-25: `effectiveGoogleMapsAnchor` is mixing dp and px when
applying `centerOffset`, which can shift markers incorrectly on high-density
screens. Update this helper (or its caller) so `centerOffset` is converted to px
using density before subtracting it from `anchorX`/`anchorY`, and keep the unit
space consistent in `MarkerDescriptor.effectiveGoogleMapsAnchor` and
`MarkerOptions`.

In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt`:
- Around line 77-96: In MarkerIconFactory, the async icon-loading path leaves
pendingIconKeys stuck when loadIconAsync returns null, and repeated same-key
updates can keep the first onIconApplied callback with stale descriptor data.
Update the icon application flow around loadIconAsync, setPending, setApplied,
and isIconKeyCurrent so pending state is always cleared or replaced on
completion/failure, and ensure newer descriptor/anchor data is used when the
bitmap resolves instead of reusing the original closure.
- Around line 173-180: `loadRemoteIcon()`, the asset/file/resource decode paths,
and `resizeBitmap()` currently decode full-size bitmaps before downscaling,
which can spike memory on large images. Update `MarkerIconFactory` to use
bounds-first decoding (`inJustDecodeBounds`) and sampled decoding
(`inSampleSize`) for every source before creating the final bitmap, and add a
max pixel budget guard so oversized images are rejected or decoded at a smaller
sample. Keep the changes localized around `loadRemoteIcon`, the other decoder
helpers, and `resizeBitmap` so all image-loading paths share the same safe
decode strategy.

In `@package/ios/MapMarkerAnnotation.swift`:
- Around line 37-72: `update(from:)` in `MapMarkerAnnotation` is missing
interaction-state updates, so retained marker views can keep stale callout/drag
behavior. Update the method to assign `draggable` from `descriptor.draggable`
and include it in the change detection. Also make title/subtitle nil ↔ non-nil
transitions count as changes by comparing the existing and new values before
reassigning `title` and `subtitle`, so `MapOverlayController` will refresh the
view when these props change.

In `@package/ios/NitroImageAnnotationView.swift`:
- Around line 35-55: The load path in NitroImageAnnotationView.configure(for:)
leaves loadToken set even when MarkerImageLoader.load returns nil, so a failed
fetch is treated like a cache hit on the next call. Update the completion
handling to clear or invalidate loadToken when uiImage is nil, and only keep the
token on successful image loads so the same imageDescriptor will retry instead
of short-circuiting. Keep the fix localized around the loadToken fast path and
the MarkerImageLoader.load callback.

In `@package/src/overlays/markerImageFromResolvedAsset.ts`:
- Around line 13-14: The marker image validation is inconsistent:
markerImageFromResolvedAsset() treats an empty uri as missing, but
isMarkerImage() still accepts { uri: '' }, allowing invalid images to bypass
normalization. Update isMarkerImage() to reject empty uri values using the same
rule as markerImageFromResolvedAsset(), and keep the check aligned with the
marker image handling path so empty strings fall back to the default marker
behavior.

In `@package/src/types/overlays.ts`:
- Around line 33-34: MarkerProps.image is advertising a broader React Native
image union than the marker pipeline can actually preserve. Update the type in
overlays.ts so the MarkerProps.image/MarkerImage contract only accepts the
serializable subset that the resolver forwards (uri, width, height, scale), or
explicitly extend the native serialization path to support the full
ImageSourcePropType shape. Keep the fix aligned with the existing MarkerImage
and resolver code so the type matches runtime behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 441c34bf-f4e5-45ff-8b7f-f0f42176940b

📥 Commits

Reviewing files that changed from the base of the PR and between 06ac335 and 3f3ddfe.

⛔ Files ignored due to path filters (19)
  • bun.lock is excluded by !**/*.lock
  • example/assets/markerblue.png is excluded by !**/*.png
  • example/assets/markerblue@2x.png is excluded by !**/*.png
  • example/assets/markergold.png is excluded by !**/*.png
  • example/assets/markergold@2x.png is excluded by !**/*.png
  • example/assets/markergreen.png is excluded by !**/*.png
  • example/assets/markergreen@2x.png is excluded by !**/*.png
  • example/assets/markerorange.png is excluded by !**/*.png
  • example/assets/markerorange@2x.png is excluded by !**/*.png
  • example/assets/markerred.png is excluded by !**/*.png
  • example/assets/markerred@2x.png is excluded by !**/*.png
  • example/assets/markerviolet.png is excluded by !**/*.png
  • example/assets/markerviolet@2x.png is excluded by !**/*.png
  • example/assets/poiNavigation.png is excluded by !**/*.png
  • example/assets/poiNavigation@2x.png is excluded by !**/*.png
  • example/assets/poiRestaurant.png is excluded by !**/*.png
  • example/assets/poiRestaurant@2x.png is excluded by !**/*.png
  • example/assets/poiStar.png is excluded by !**/*.png
  • example/assets/poiStar@2x.png is excluded by !**/*.png
📒 Files selected for processing (38)
  • README.md
  • example/App.tsx
  • example/examples/customMarkerImages.ts
  • example/examples/index.ts
  • example/index.js
  • example/package.json
  • package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+Fingerprint.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+MarkerOptions.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt
  • package/ios/AppleMapProviderAdapter.swift
  • package/ios/HybridMapViewDelegate.swift
  • package/ios/MapMarkerAnnotation.swift
  • package/ios/MapOverlayController.swift
  • package/ios/MarkerDescriptor+Fingerprint.swift
  • package/ios/MarkerImageLoader.swift
  • package/ios/MarkerViewportFilter.swift
  • package/ios/NitroImageAnnotationView.swift
  • package/ios/NitroPinAnnotationView.swift
  • package/src/components/MapView.tsx
  • package/src/hooks/useCollectedOverlays.ts
  • package/src/index.ts
  • package/src/native/specs/overlays.ts
  • package/src/overlays/__tests__/lruCache.test.ts
  • package/src/overlays/__tests__/markerImageCacheKey.test.ts
  • package/src/overlays/__tests__/normalizeMarkerDescriptors.test.ts
  • package/src/overlays/__tests__/resolveMarkerImage.test.ts
  • package/src/overlays/assetSourceResolver.ts
  • package/src/overlays/lruCache.ts
  • package/src/overlays/markerImageCacheKey.ts
  • package/src/overlays/markerImageFromResolvedAsset.ts
  • package/src/overlays/normalizeMarkerDescriptors.ts
  • package/src/overlays/resolveMarkerImage.ts
  • package/src/types/index.ts
  • package/src/types/overlays.ts
  • package/tsconfig.json
💤 Files with no reviewable changes (2)
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt
  • package/ios/MarkerViewportFilter.swift

Comment thread package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt Outdated
Comment thread package/ios/MapMarkerAnnotation.swift
Comment thread package/ios/NitroImageAnnotationView.swift
Comment thread package/src/overlays/markerImageFromResolvedAsset.ts
Comment thread package/src/types/overlays.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
package/src/overlays/resolveMarkerImage.ts (1)

23-30: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop caching the caller’s object.

Line 29/Line 30 store and return source verbatim. That makes the cache mutable from the outside, so one later source.uri = ... leaves a stale entry under the old key and can feed the wrong image back on later lookups. It also preserves any extra runtime fields instead of normalizing to the Nitro-safe shape this PR is trying to guarantee. Clone the MarkerImage before caching/returning it.

Suggested fix
   if (isMarkerImage(source)) {
     const key = markerImageCacheKey(source);
     const cached = resolvedImageCache.get(key);
     if (cached != null) {
       return cached;
     }
-    resolvedImageCache.set(key, source);
-    return source;
+    const normalized = markerImageFromResolvedAsset(source)!;
+    resolvedImageCache.set(key, normalized);
+    return normalized;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package/src/overlays/resolveMarkerImage.ts` around lines 23 - 30, The cached
MarkerImage path in resolveMarkerImage is storing and returning the caller’s
source object directly, which leaves the cache mutable and preserves
non-normalized runtime fields. Update the isMarkerImage branch to clone the
MarkerImage before putting it into resolvedImageCache and before returning it,
so the cached value is isolated from later caller mutations and matches the
Nitro-safe shape; keep the existing markerImageCacheKey and resolvedImageCache
logic, just replace the verbatim source reuse.
package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt (2)

123-153: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Don’t decode local marker images on the UI thread.

applyVisualProps() calls applyAnchor(), which can hit displaySizePx() and decode local files/resources/assets synchronously. Then uncached local icons are decoded again through icon(image) on the main thread at Line 174. In bulk marker mode this can visibly block map interactions.

Move local decode work onto the executor and avoid decoding only to compute size; let cacheBitmap() populate sizeCache, then re-apply the anchor through the existing callback.

Also applies to: 174-174

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt`
around lines 123 - 153, Local marker images are being decoded synchronously on
the UI thread in MarkerIconFactory through displaySizePx(), applyAnchor(), and
icon(), which can block interactions in bulk marker mode. Move the local bitmap
loading/decoding work onto the existing executor path used by loadLocalBitmap()
and cacheBitmap(), and avoid decoding just to determine size; rely on
cacheBitmap() to fill sizeCache instead. Then re-apply the anchor via the
existing callback after the bitmap is cached so applyVisualProps() no longer
needs a synchronous decode.

73-86: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cancel pending loads before returning for an already-applied icon.

Right now a stale in-flight load can still win. Example: default icon is applied, image A starts loading, then props switch back to default. Line 74 returns because default is already applied, but the pending A load is not invalidated, so it can later set the wrong custom icon.

Proposed fix
     if (image == null) {
       if (isIconApplied(marker, DEFAULT_ICON_KEY)) {
+        invalidatePendingLoad(marker)
         return
       }
       marker.setIcon(BitmapDescriptorFactory.defaultMarker())
       setApplied(marker, DEFAULT_ICON_KEY)
@@
     val iconKey = cacheKey(image)
     if (isIconApplied(marker, iconKey)) {
+      invalidatePendingLoad(marker)
       return
     }

Also applies to: 185-188

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt`
around lines 73 - 86, In MarkerIconFactory’s icon-application flow, an
already-applied icon can return early while leaving a stale in-flight load
active, which may later overwrite the marker with the wrong image. Before any
early return in the image-null/default path and the cached-icon path, cancel or
invalidate any pending load associated with that marker/icon state. Update the
logic around setApplied, isIconApplied, and the load callback so the currently
requested icon remains the only one allowed to apply.
♻️ Duplicate comments (1)
package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt (1)

214-216: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound remote image bytes before decoding.

Bounds-first bitmap decode helps, but stream.readBytes() still allocates the full response first. A huge remote payload can exhaust memory before decodeByteArray() enforces MAX_DECODE_PIXELS.

Suggested direction
-        val bitmap = decodeByteArray(stream.readBytes(), image) ?: return null
+        val bytes = stream.readBoundedBytes(MAX_REMOTE_IMAGE_BYTES) ?: return null
+        val bitmap = decodeByteArray(bytes, image) ?: return null
         cacheBitmap(key, resizeBitmap(bitmap, image))

Add a small bounded read helper and a max byte constant sized for expected marker assets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt`
around lines 214 - 216, The remote image loading path in
MarkerIconFactory.decodeBitmap still reads the entire response into memory
before decoding, which can bypass the intended bounds check. Update the
connection.getInputStream() flow to use a bounded read helper with a max byte
limit for marker assets, then pass the bounded bytes into decodeByteArray so
oversized payloads are rejected before full allocation. Keep the fix localized
to the bitmap decode/cache path and reuse the existing MAX_DECODE_PIXELS logic
for decode safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package/ios/MapMarkerAnnotation.swift`:
- Line 52: The `MapMarkerAnnotation` update path is changing `isClusterable`
without reflecting that change in the refresh result, so the existing marker
view can miss clusterability flips. Update the refresh signal logic in
`MapMarkerAnnotation` (including the `isClusterable = descriptor.clusterable ??
true` assignment and the related return path in the same update method) so a
change to `isClusterable` also causes the returned `Bool` to report a refresh is
needed. Apply the same adjustment anywhere else in the `MapMarkerAnnotation`
refresh/update flow that mirrors this tracking.

---

Outside diff comments:
In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt`:
- Around line 123-153: Local marker images are being decoded synchronously on
the UI thread in MarkerIconFactory through displaySizePx(), applyAnchor(), and
icon(), which can block interactions in bulk marker mode. Move the local bitmap
loading/decoding work onto the existing executor path used by loadLocalBitmap()
and cacheBitmap(), and avoid decoding just to determine size; rely on
cacheBitmap() to fill sizeCache instead. Then re-apply the anchor via the
existing callback after the bitmap is cached so applyVisualProps() no longer
needs a synchronous decode.
- Around line 73-86: In MarkerIconFactory’s icon-application flow, an
already-applied icon can return early while leaving a stale in-flight load
active, which may later overwrite the marker with the wrong image. Before any
early return in the image-null/default path and the cached-icon path, cancel or
invalidate any pending load associated with that marker/icon state. Update the
logic around setApplied, isIconApplied, and the load callback so the currently
requested icon remains the only one allowed to apply.

In `@package/src/overlays/resolveMarkerImage.ts`:
- Around line 23-30: The cached MarkerImage path in resolveMarkerImage is
storing and returning the caller’s source object directly, which leaves the
cache mutable and preserves non-normalized runtime fields. Update the
isMarkerImage branch to clone the MarkerImage before putting it into
resolvedImageCache and before returning it, so the cached value is isolated from
later caller mutations and matches the Nitro-safe shape; keep the existing
markerImageCacheKey and resolvedImageCache logic, just replace the verbatim
source reuse.

---

Duplicate comments:
In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt`:
- Around line 214-216: The remote image loading path in
MarkerIconFactory.decodeBitmap still reads the entire response into memory
before decoding, which can bypass the intended bounds check. Update the
connection.getInputStream() flow to use a bounded read helper with a max byte
limit for marker assets, then pass the bounded bytes into decodeByteArray so
oversized payloads are rejected before full allocation. Keep the fix localized
to the bitmap decode/cache path and reuse the existing MAX_DECODE_PIXELS logic
for decode safety.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c00fcded-b0ab-4fd9-a151-f8f69cd43891

📥 Commits

Reviewing files that changed from the base of the PR and between 3f3ddfe and f77c5fd.

📒 Files selected for processing (15)
  • example/examples/customMarkerImages.ts
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+MarkerOptions.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt
  • package/ios/MapMarkerAnnotation.swift
  • package/ios/MarkerImageLoader.swift
  • package/ios/NitroImageAnnotationView.swift
  • package/src/index.ts
  • package/src/native/specs/overlays.ts
  • package/src/overlays/__tests__/resolveMarkerImage.test.ts
  • package/src/overlays/assetSourceResolver.ts
  • package/src/overlays/markerImageFromResolvedAsset.ts
  • package/src/overlays/resolveMarkerImage.ts
  • package/src/types/index.ts
  • package/src/types/overlays.ts
💤 Files with no reviewable changes (1)
  • package/ios/MarkerImageLoader.swift
🚧 Files skipped from review as they are similar to previous changes (7)
  • package/src/overlays/assetSourceResolver.ts
  • package/src/types/index.ts
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+MarkerOptions.kt
  • package/src/native/specs/overlays.ts
  • package/src/index.ts
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt
  • package/ios/NitroImageAnnotationView.swift

Comment thread package/ios/MapMarkerAnnotation.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (1)

318-318: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove custom bitmap images from Planned features.

This is already documented as supported above, so leaving it in the roadmap just makes the README contradict itself. Move it to a future wishlist or delete it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 318, The Planned features list in the README still
includes custom bitmap images even though that capability is already documented
as supported earlier, so update the roadmap entry to remove it or move it out of
Planned features into a future wishlist; adjust the affected README bullet that
mentions markers with callouts, drag support, and custom bitmap images so the
supported feature list is no longer contradictory.
package/ios/MapOverlayController.swift (1)

182-185: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't blow away the marker state just to swap view classes.

Removing and re-adding the annotation here kills the current selection/callout and replays the enter animation through HybridMapViewDelegate.mapView(_:didAdd:). That breaks the promised title/subtitle compatibility whenever a retained marker switches between pin and image mode. Preserve selection/callout state across this path, or you’ll get a lovely little UI regression every time the image presence flips.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package/ios/MapOverlayController.swift` around lines 182 - 185, The swap in
MapOverlayController’s annotation update path currently removes and re-adds the
marker, which resets selection/callout state and retriggers
HybridMapViewDelegate.mapView(_:didAdd:). Update the needsImageView/hasImageView
branch to preserve the existing annotation instance and its selection/callout
while switching between pin and image presentation, so retained markers keep
their title/subtitle behavior intact.
🧹 Nitpick comments (1)
package/src/overlays/normalizeMarkerDescriptors.ts (1)

6-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stop duplicating marker normalization.

This reimplements normalizeMarkerDescriptor from package/src/utils/enteringAnimation.ts almost field-for-field. Keeping two JS→native mappers for the same marker contract is how one path inevitably misses the next prop addition and bulk markers start behaving differently from collected markers. Reuse the shared helper here instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package/src/overlays/normalizeMarkerDescriptors.ts` around lines 6 - 23, The
marker normalization logic in normalizeDescriptor is duplicating the shared
JS-to-native mapping already implemented by normalizeMarkerDescriptor, which
risks drift when marker props change. Update normalizeMarkerDescriptors.ts to
reuse normalizeMarkerDescriptor from package/src/utils/enteringAnimation.ts
instead of manually rebuilding the object, while preserving the existing image
resolution behavior if needed by applying resolveMarkerImage through the shared
path. Verify the overlay path and any bulk marker handling still produce the
same MarkerDescriptor shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@package/ios/MapOverlayController.swift`:
- Around line 182-185: The swap in MapOverlayController’s annotation update path
currently removes and re-adds the marker, which resets selection/callout state
and retriggers HybridMapViewDelegate.mapView(_:didAdd:). Update the
needsImageView/hasImageView branch to preserve the existing annotation instance
and its selection/callout while switching between pin and image presentation, so
retained markers keep their title/subtitle behavior intact.

In `@README.md`:
- Line 318: The Planned features list in the README still includes custom bitmap
images even though that capability is already documented as supported earlier,
so update the roadmap entry to remove it or move it out of Planned features into
a future wishlist; adjust the affected README bullet that mentions markers with
callouts, drag support, and custom bitmap images so the supported feature list
is no longer contradictory.

---

Nitpick comments:
In `@package/src/overlays/normalizeMarkerDescriptors.ts`:
- Around line 6-23: The marker normalization logic in normalizeDescriptor is
duplicating the shared JS-to-native mapping already implemented by
normalizeMarkerDescriptor, which risks drift when marker props change. Update
normalizeMarkerDescriptors.ts to reuse normalizeMarkerDescriptor from
package/src/utils/enteringAnimation.ts instead of manually rebuilding the
object, while preserving the existing image resolution behavior if needed by
applying resolveMarkerImage through the shared path. Verify the overlay path and
any bulk marker handling still produce the same MarkerDescriptor shape.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ee2ccf2-35f5-41dc-9930-59a454935b85

📥 Commits

Reviewing files that changed from the base of the PR and between 74b03b8 and 28a4a34.

📒 Files selected for processing (18)
  • README.md
  • example/App.tsx
  • package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt
  • package/ios/AppleMapProviderAdapter.swift
  • package/ios/HybridMapViewDelegate.swift
  • package/ios/MapMarkerAnnotation.swift
  • package/ios/MapOverlayController.swift
  • package/ios/MarkerDescriptor+Fingerprint.swift
  • package/ios/NitroPinAnnotationView.swift
  • package/src/components/MapView.tsx
  • package/src/hooks/useCollectedOverlays.ts
  • package/src/index.ts
  • package/src/native/specs/overlays.ts
  • package/src/overlays/normalizeMarkerDescriptors.ts
  • package/src/types/index.ts
  • package/src/types/overlays.ts
  • package/src/utils/enteringAnimation.ts
✅ Files skipped from review due to trivial changes (3)
  • package/ios/AppleMapProviderAdapter.swift
  • package/src/types/index.ts
  • package/ios/NitroPinAnnotationView.swift
🚧 Files skipped from review as they are similar to previous changes (9)
  • package/src/index.ts
  • package/src/hooks/useCollectedOverlays.ts
  • package/ios/MarkerDescriptor+Fingerprint.swift
  • package/src/types/overlays.ts
  • package/ios/HybridMapViewDelegate.swift
  • package/ios/MapMarkerAnnotation.swift
  • package/src/native/specs/overlays.ts
  • example/App.tsx
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt

jkasprzyk17 and others added 4 commits June 28, 2026 19:53
Resolves image loading on JS and native layers for bundled assets,
with LRU caching, descriptor normalization, and example app demo.
Closes #14.

Co-authored-by: Cursor <cursoragent@cursor.com>
…pick

Co-authored-by: Cursor <cursoragent@cursor.com>
@jkasprzyk17
jkasprzyk17 force-pushed the feat/custom-marker-images branch from 28a4a34 to 061a99e Compare June 28, 2026 18:21
@coderabbitai
coderabbitai Bot requested a review from piotr-graczyk-dev June 28, 2026 18:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt`:
- Around line 156-175: The local image path in MarkerIconFactory.loadIconAsync
is still decoding bitmaps on the main thread because non-http(s) URIs call
icon(image) inside deliverOnMainThread. Move the local decode work through
loadExecutor just like the remote branch, then post the BitmapDescriptor? back
via deliverOnMainThread. Also check applyAnchor/displaySizePx so it does not
re-run loadLocalBitmap on the UI thread when width/height are missing; reuse the
already loaded bitmap or compute size off-thread where possible.
- Around line 209-222: The remote marker image fetch in loadRemoteIcon currently
accepts any public MarkerImage.uri and opens it directly, so tighten the input
validation before URL(image.uri).openConnection(). Add scheme checks to allow
only the intended protocols and enforce a host allowlist before any network
access, then keep the existing cacheBitmap/decodeByteArray flow unchanged once
the URL is verified. If the URI is not permitted, fail fast from
loadIconAsync/loadRemoteIcon and log the rejection with the TAG context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a4d49ee-95c8-46ef-bb31-a57a6daeda0c

📥 Commits

Reviewing files that changed from the base of the PR and between 28a4a34 and 061a99e.

⛔ Files ignored due to path filters (19)
  • bun.lock is excluded by !**/*.lock
  • example/assets/markerblue.png is excluded by !**/*.png
  • example/assets/markerblue@2x.png is excluded by !**/*.png
  • example/assets/markergold.png is excluded by !**/*.png
  • example/assets/markergold@2x.png is excluded by !**/*.png
  • example/assets/markergreen.png is excluded by !**/*.png
  • example/assets/markergreen@2x.png is excluded by !**/*.png
  • example/assets/markerorange.png is excluded by !**/*.png
  • example/assets/markerorange@2x.png is excluded by !**/*.png
  • example/assets/markerred.png is excluded by !**/*.png
  • example/assets/markerred@2x.png is excluded by !**/*.png
  • example/assets/markerviolet.png is excluded by !**/*.png
  • example/assets/markerviolet@2x.png is excluded by !**/*.png
  • example/assets/poiNavigation.png is excluded by !**/*.png
  • example/assets/poiNavigation@2x.png is excluded by !**/*.png
  • example/assets/poiRestaurant.png is excluded by !**/*.png
  • example/assets/poiRestaurant@2x.png is excluded by !**/*.png
  • example/assets/poiStar.png is excluded by !**/*.png
  • example/assets/poiStar@2x.png is excluded by !**/*.png
📒 Files selected for processing (38)
  • README.md
  • example/App.tsx
  • example/examples/customMarkerImages.ts
  • example/examples/index.ts
  • example/index.js
  • example/package.json
  • package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+Fingerprint.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+MarkerOptions.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt
  • package/ios/AppleMapProviderAdapter.swift
  • package/ios/HybridMapViewDelegate.swift
  • package/ios/MapMarkerAnnotation.swift
  • package/ios/MapOverlayController.swift
  • package/ios/MarkerClusterEngine.swift
  • package/ios/MarkerDescriptor+Fingerprint.swift
  • package/ios/MarkerImageLoader.swift
  • package/ios/MarkerViewportFilter.swift
  • package/ios/NitroImageAnnotationView.swift
  • package/ios/NitroPinAnnotationView.swift
  • package/ios/OverlayEnteringAnimation.swift
  • package/src/components/MapView.tsx
  • package/src/hooks/useCollectedOverlays.ts
  • package/src/index.ts
  • package/src/native/specs/overlays.ts
  • package/src/overlays/__tests__/lruCache.test.ts
  • package/src/overlays/__tests__/normalizeMarkerDescriptors.test.ts
  • package/src/overlays/__tests__/resolveMarkerImage.test.ts
  • package/src/overlays/assetSourceResolver.ts
  • package/src/overlays/lruCache.ts
  • package/src/overlays/markerImageFromResolvedAsset.ts
  • package/src/overlays/normalizeMarkerDescriptors.ts
  • package/src/overlays/resolveMarkerImage.ts
  • package/src/types/index.ts
  • package/src/types/overlays.ts
  • package/tsconfig.json
💤 Files with no reviewable changes (2)
  • package/ios/MarkerViewportFilter.swift
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt
✅ Files skipped from review due to trivial changes (4)
  • package/src/overlays/assetSourceResolver.ts
  • package/ios/OverlayEnteringAnimation.swift
  • package/src/types/index.ts
  • README.md
🚧 Files skipped from review as they are similar to previous changes (29)
  • example/index.js
  • package/src/overlays/tests/normalizeMarkerDescriptors.test.ts
  • package/src/overlays/lruCache.ts
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+Fingerprint.kt
  • package/ios/NitroPinAnnotationView.swift
  • package/ios/MapOverlayController.swift
  • package/ios/AppleMapProviderAdapter.swift
  • package/src/overlays/resolveMarkerImage.ts
  • example/package.json
  • package/tsconfig.json
  • package/ios/HybridMapViewDelegate.swift
  • package/ios/MarkerDescriptor+Fingerprint.swift
  • package/src/overlays/normalizeMarkerDescriptors.ts
  • package/src/index.ts
  • package/src/components/MapView.tsx
  • package/src/hooks/useCollectedOverlays.ts
  • example/examples/customMarkerImages.ts
  • package/src/native/specs/overlays.ts
  • example/examples/index.ts
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerDescriptor+MarkerOptions.kt
  • package/ios/NitroImageAnnotationView.swift
  • package/src/overlays/tests/resolveMarkerImage.test.ts
  • package/src/overlays/tests/lruCache.test.ts
  • package/src/types/overlays.ts
  • package/ios/MarkerImageLoader.swift
  • package/ios/MapMarkerAnnotation.swift
  • package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt
  • example/App.tsx
  • package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt

@piotr-graczyk-dev
piotr-graczyk-dev merged commit 9a7cab6 into main Jun 29, 2026
3 checks passed
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.

Add custom marker images with anchor, rotation, and flat mode

2 participants