Skip to content

feat: Add image and icon props to Marker component#65

Open
dacoto wants to merge 2 commits intolugg:mainfrom
dacoto:copilot/add-marker-image-icon-functionality
Open

feat: Add image and icon props to Marker component#65
dacoto wants to merge 2 commits intolugg:mainfrom
dacoto:copilot/add-marker-image-icon-functionality

Conversation

@dacoto
Copy link
Copy Markdown
Contributor

@dacoto dacoto commented Mar 28, 2026

Summary

Adds image and icon props to Marker, matching the react-native-maps API. Both accept local ImageSourcePropType resources and display them directly as the marker icon without requiring custom children views.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Test Plan

  • Rendered a <Marker image={require('./assets/pin.png')}> on iOS (Apple Maps & Google Maps), Android, and Web — icon appeared correctly anchored.
  • Verified icon takes priority over image on Google Maps (Android & iOS).
  • Confirmed bitmap/UIImage cache clears on URI change and on view recycle.
  • Tested Metro dev server (http://) and production bundle (asset://) URL schemes on Android.
  • Passed anchor, rotation, and scale together with image — transforms applied as expected on all targets.

Screenshots / Videos

Checklist

  • I tested on iOS
  • I tested on Android
  • I tested on Web
  • I updated the documentation (if needed)

Copilot AI and others added 2 commits March 27, 2026 23:43
Copilot AI review requested due to automatic review settings March 28, 2026 00:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds image and icon props to Marker to support rendering a marker icon from an ImageSourcePropType (matching react-native-maps), across native (Apple/Google), Android Google Maps, and Web Google Maps.

Changes:

  • Extend Marker props and Fabric native props with image/icon.
  • Implement image/icon rendering paths on Web, iOS (Apple Maps + Google Maps), and Android (Google Maps), including basic caching and scaling.
  • Document image/icon usage in docs/MARKER.md.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/fabric/LuggMarkerViewNativeComponent.ts Adds image/icon string props to the Fabric native component interface.
src/components/Marker.types.ts Exposes image/icon in the public MarkerProps type.
src/components/Marker.tsx Resolves asset sources to URIs and forwards image/icon to native.
src/components/Marker.web.tsx Resolves an asset URI and renders an <img> inside the Web marker when image/icon is provided.
ios/core/GoogleMapProvider.mm Adds Google Maps marker icon loading/scaling from imageUri/iconUri.
ios/core/AppleMapProvider.mm Adds Apple Maps annotation view path for imageUri markers with scaling/rotation/anchor support.
ios/LuggMarkerView.mm Tracks imageUri/iconUri props and maintains per-view cached images.
ios/LuggMarkerView.h Exposes image/icon URI + cache properties to map providers.
android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt Adds Android Google Maps icon loading/scaling from imageUri/iconUri.
android/src/main/java/com/luggmaps/LuggMarkerViewManager.kt Wires image/icon props from React to the native view.
android/src/main/java/com/luggmaps/LuggMarkerView.kt Stores imageUri/iconUri and bitmap caches, clears caches on URI changes/recycle.
docs/MARKER.md Documents image/icon props and example usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +70 to +76
image,
icon,
} = this.props;

const imageUri = image ? Image.resolveAssetSource(image)?.uri ?? '' : '';
const iconUri = icon ? Image.resolveAssetSource(icon)?.uri ?? '' : '';

Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

image/icon are intended to replace custom marker children, but this component still renders {children} unconditionally. Because native providers check hasCustomView first, passing children alongside image/icon will cause the custom view to win and the image/icon to be ignored. Consider rendering children only when neither image nor icon is provided (or change native precedence to prefer icon/image).

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +121
/**
* A custom image to be used as the marker's icon. Only local image resources are allowed.
* When set, takes the place of custom children as the marker icon.
*/
image?: ImageSourcePropType;
/**
* Marker icon to render (equivalent to icon property of GMSMarker).
* Only local image resources are allowed. Takes priority over `image` on Google Maps.
* @platform google
*/
icon?: ImageSourcePropType;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The JSDoc says “Only local image resources are allowed”, but the prop types are ImageSourcePropType (which includes { uri: ... }) and the implementation resolves/loads the resulting URI. This effectively allows remote URLs, which contradicts the docs and can trigger network fetches. If local-only is required, consider narrowing the types (e.g. to ImageRequireSource) and/or adding a runtime guard that rejects non-require() sources.

Copilot uses AI. Check for mistakes.
Comment on lines +721 to +739
BOOL isIconUri = markerView.hasIconUri;
__weak LuggMarkerView *weakMarkerView = markerView;
__weak GMSAdvancedMarker *weakMarker = marker;
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = data ? [UIImage imageWithData:data] : nil;
dispatch_async(dispatch_get_main_queue(), ^{
LuggMarkerView *strongMarkerView = weakMarkerView;
GMSAdvancedMarker *strongMarker = weakMarker;
if (!strongMarkerView || !strongMarker || !image) return;
if (isIconUri) {
strongMarkerView.cachedIcon = image;
} else {
strongMarkerView.cachedImage = image;
}
strongMarker.icon =
[self scaleUIImage:image scale:strongMarkerView.scale];
});
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

loadMarkerImageUri can race with prop updates/recycling: it doesn’t verify that the loaded image still corresponds to the current imageUri/iconUri (or that markerView.marker is still this marker) before caching/applying. This can display stale icons after rapid icon/image changes or view recycling. Capture the expected URI and, on the main thread, bail out unless the markerView’s current URI (and marker instance) still matches before setting cached* and marker.icon.

Copilot uses AI. Check for mistakes.
Comment on lines +1266 to +1285
- (void)loadMarkerImageUri:(NSString *)uri
forMarkerView:(LuggMarkerView *)markerView
annotationView:(MKAnnotationView *)annotationView {
NSURL *url = [NSURL URLWithString:uri];
if (!url) return;

__weak LuggMarkerView *weakMarkerView = markerView;
__weak MKAnnotationView *weakAnnotationView = annotationView;
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = data ? [UIImage imageWithData:data] : nil;
dispatch_async(dispatch_get_main_queue(), ^{
LuggMarkerView *strongMarkerView = weakMarkerView;
MKAnnotationView *strongAnnotationView = weakAnnotationView;
if (!strongMarkerView || !strongAnnotationView || !image) return;
strongMarkerView.cachedImage = image;
[self applyImageMarkerStyle:strongMarkerView
annotationView:strongAnnotationView];
});
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

loadMarkerImageUri can apply a stale image after the marker is reused or imageUri changes because it doesn’t check that markerView.imageUri is still equal to the requested uri before setting cachedImage and updating the annotation view. Add a guard on the main thread to ensure the URI still matches (and the annotationView is still the one associated with this marker) before applying.

Copilot uses AI. Check for mistakes.
Comment on lines +723 to +748
private fun applyImageIconToMarker(markerView: LuggMarkerView, marker: AdvancedMarker) {
val isIcon = markerView.hasIconUri
val cached = if (isIcon) markerView.cachedIconBitmap else markerView.cachedImageBitmap
if (cached != null) {
val scaled = scaleBitmap(cached, markerView.scale)
marker.setIcon(BitmapDescriptorFactory.fromBitmap(scaled))
return
}
val uri = if (isIcon) markerView.iconUri else markerView.imageUri
Thread {
try {
val bitmap = loadBitmapFromUri(uri)
if (bitmap != null) {
mapView?.post {
if (isIcon) {
markerView.cachedIconBitmap = bitmap
} else {
markerView.cachedImageBitmap = bitmap
}
val scaled = scaleBitmap(bitmap, markerView.scale)
marker.setIcon(BitmapDescriptorFactory.fromBitmap(scaled))
}
}
} catch (_: Exception) {}
}.start()
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

applyImageIconToMarker spawns a raw Thread and posts a result without verifying the marker is still current or that the URI hasn’t changed. If the marker view is recycled or imageUri/iconUri updates quickly, a stale load can overwrite the newer icon. Capture the requested uri and, before caching/setting the icon on the UI thread, confirm markerView.marker === marker and that the current URI still matches.

Copilot uses AI. Check for mistakes.
Comment on lines +759 to +761
connection.connect()
return try {
android.graphics.BitmapFactory.decodeStream(connection.inputStream)
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

loadBitmapFromUri uses HttpURLConnection without connect/read timeouts and doesn’t close the inputStream explicitly. This can hang background threads under poor networks and potentially leak resources. Set reasonable connectTimeout/readTimeout and wrap connection.inputStream in .use { ... } before disconnecting.

Suggested change
connection.connect()
return try {
android.graphics.BitmapFactory.decodeStream(connection.inputStream)
connection.connectTimeout = 10_000
connection.readTimeout = 10_000
connection.connect()
return try {
connection.inputStream.use { stream ->
android.graphics.BitmapFactory.decodeStream(stream)
}

Copilot uses AI. Check for mistakes.
Comment on lines +726 to +729
if (cached != null) {
val scaled = scaleBitmap(cached, markerView.scale)
marker.setIcon(BitmapDescriptorFactory.fromBitmap(scaled))
return
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

When cached is present, scaleBitmap(cached, markerView.scale) allocates a new scaled bitmap every sync. Since syncMarkerView can run frequently even when scale hasn’t changed, this can create avoidable allocations and GC pressure. Consider caching the scaled bitmap/descriptor per (uri, scale) or only rescaling when scale changes.

Copilot uses AI. Check for mistakes.
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.

3 participants