Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Base64;
import android.util.Log;
import androidx.core.util.Supplier;
import com.facebook.react.bridge.UiThreadUtil;
Expand Down Expand Up @@ -250,6 +253,7 @@ public Marker addMarker(Map<String, Object> optionsMap) {
}

private Marker createMarker(Map<String, Object> optionsMap, String customId) {
String imageBase64 = CollectionUtil.getString("imageBase64", optionsMap);
String imagePath = CollectionUtil.getString("imgPath", optionsMap);
String title = CollectionUtil.getString("title", optionsMap);
String snippet = CollectionUtil.getString("snippet", optionsMap);
Expand All @@ -261,15 +265,28 @@ private Marker createMarker(Map<String, Object> optionsMap, String customId) {
boolean visible = CollectionUtil.getBool("visible", optionsMap, true);

MarkerOptions options = new MarkerOptions();
if (imagePath != null && !imagePath.isEmpty()) {
if (imageBase64 != null && !imageBase64.isEmpty()) {
byte[] decodedBytes = Base64.decode(imageBase64, Base64.DEFAULT);
Bitmap bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);
if (bitmap == null) {
throw new IllegalArgumentException(JsErrors.INVALID_IMAGE_ERROR_MESSAGE);
}
options.icon(BitmapDescriptorFactory.fromBitmap(bitmap));
} else if (imagePath != null && !imagePath.isEmpty()) {
try {
BitmapDescriptor icon = BitmapDescriptorFactory.fromAsset(imagePath);
options.icon(icon);
options.icon(BitmapDescriptorFactory.fromAsset(imagePath));
} catch (Exception e) {
throw new IllegalArgumentException(JsErrors.INVALID_IMAGE_ERROR_MESSAGE);
}
}

if (optionsMap.containsKey("anchor")) {
Map<String, Object> anchor = (Map<String, Object>) optionsMap.get("anchor");
float u = Double.valueOf(anchor.get("u").toString()).floatValue();
float v = Double.valueOf(anchor.get("v").toString()).floatValue();
options.anchor(u, v);
}

options.position(
ObjectTranslationUtil.getLatLngFromMap((Map<String, Object>) optionsMap.get("position")));

Expand Down Expand Up @@ -298,6 +315,7 @@ private Marker createMarker(Map<String, Object> optionsMap, String customId) {
}

private void updateMarker(Marker marker, Map<String, Object> optionsMap) {
String imageBase64 = CollectionUtil.getString("imageBase64", optionsMap);
String imagePath = CollectionUtil.getString("imgPath", optionsMap);
String title = CollectionUtil.getString("title", optionsMap);
String snippet = CollectionUtil.getString("snippet", optionsMap);
Expand All @@ -308,15 +326,28 @@ private void updateMarker(Marker marker, Map<String, Object> optionsMap) {
boolean flat = CollectionUtil.getBool("flat", optionsMap, false);
boolean visible = CollectionUtil.getBool("visible", optionsMap, true);

if (imagePath != null && !imagePath.isEmpty()) {
if (imageBase64 != null && !imageBase64.isEmpty()) {
byte[] decodedBytes = Base64.decode(imageBase64, Base64.DEFAULT);
Bitmap bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);
if (bitmap == null) {
throw new IllegalArgumentException(JsErrors.INVALID_IMAGE_ERROR_MESSAGE);
}
marker.setIcon(BitmapDescriptorFactory.fromBitmap(bitmap));
} else if (imagePath != null && !imagePath.isEmpty()) {
try {
BitmapDescriptor icon = BitmapDescriptorFactory.fromAsset(imagePath);
marker.setIcon(icon);
marker.setIcon(BitmapDescriptorFactory.fromAsset(imagePath));
} catch (Exception e) {
throw new IllegalArgumentException(JsErrors.INVALID_IMAGE_ERROR_MESSAGE);
}
}

if (optionsMap.containsKey("anchor")) {
Map<String, Object> anchor = (Map<String, Object>) optionsMap.get("anchor");
float u = Double.valueOf(anchor.get("u").toString()).floatValue();
float v = Double.valueOf(anchor.get("v").toString()).floatValue();
marker.setAnchor(u, v);
}

marker.setPosition(
ObjectTranslationUtil.getLatLngFromMap((Map<String, Object>) optionsMap.get("position")));

Expand Down
7 changes: 7 additions & 0 deletions example/e2e/map.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,11 @@ describe('Map view tests', () => {
await expectNoErrors();
await expectSuccess();
});

it('MT11 - test adding marker with imageBase64 and anchor', async () => {
await selectTestByName('testMarkerImageBase64');
await waitForTestToFinish();
await expectNoErrors();
await expectSuccess();
});
});
11 changes: 11 additions & 0 deletions example/src/screens/IntegrationTestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
testMinMaxZoomLevels,
testSetFollowingPerspective,
testNavInfoEventsAfterCleanup,
testMarkerImageBase64,
NO_ERRORS_DETECTED_LABEL,
} from './integration_tests/integration_test';

Expand Down Expand Up @@ -327,6 +328,9 @@ const IntegrationTestsScreen = () => {
case 'testNavInfoEventsAfterCleanup':
await testNavInfoEventsAfterCleanup(getTestTools());
break;
case 'testMarkerImageBase64':
await testMarkerImageBase64(getTestTools());
break;
default:
resetTestState();
break;
Expand Down Expand Up @@ -563,6 +567,13 @@ const IntegrationTestsScreen = () => {
}}
testID="testNavInfoEventsAfterCleanup"
/>
<ExampleAppButton
title="testMarkerImageBase64"
onPress={() => {
runTest('testMarkerImageBase64');
}}
testID="testMarkerImageBase64"
/>
</OverlayModal>
</View>
);
Expand Down
64 changes: 64 additions & 0 deletions example/src/screens/integration_tests/integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1971,3 +1971,67 @@ export const testNavInfoEventsAfterCleanup = async (testTools: TestTools) => {

await initializeNavigation(navigationController, failTest);
};

// Minimal 1x1 transparent PNG encoded as base64 — used as a self-contained
// test fixture to verify the imageBase64 decoding path without network access.
const MINIMAL_PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';

export const testMarkerImageBase64 = async (testTools: TestTools) => {
const { mapViewController, passTest, failTest, expectFalseError } = testTools;
if (!mapViewController) {
return failTest('mapViewController was expected to exist');
}

// Test adding a marker with imageBase64 and a custom anchor
const marker = await mapViewController.addMarker({
position: { lat: 37.7749, lng: -122.4194 },
imageBase64: MINIMAL_PNG_BASE64,
anchor: { u: 0.5, v: 0.5 },
title: 'Base64 Marker',
});

if (!marker.id) {
return expectFalseError('marker.id should exist after adding imageBase64 marker');
}
if (marker.position.lat !== 37.7749 || marker.position.lng !== -122.4194) {
return expectFalseError('marker.position should match input');
}

// Verify getMarkers returns the marker
let markers = await mapViewController.getMarkers();
if (markers.length !== 1) {
return expectFalseError('getMarkers should return 1 marker');
}
if (markers[0]!.id !== marker.id) {
return expectFalseError('getMarkers should return marker with correct id');
}

// Test updating the marker with a new imageBase64 (same id → update path)
const updatedMarker = await mapViewController.addMarker({
id: marker.id,
position: { lat: 37.7749, lng: -122.4194 },
imageBase64: MINIMAL_PNG_BASE64,
anchor: { u: 0.5, v: 0.5 },
title: 'Updated Base64 Marker',
});

if (updatedMarker.id !== marker.id) {
return expectFalseError('updatedMarker.id should match original marker.id');
}

// Verify still one marker (update, not duplicate)
markers = await mapViewController.getMarkers();
if (markers.length !== 1) {
return expectFalseError('getMarkers should still return 1 marker after update');
}

// Remove and verify clean up
await mapViewController.removeMarker(marker.id);
markers = await mapViewController.getMarkers();
if (markers.length !== 0) {
return expectFalseError('getMarkers should return 0 markers after removal');
}

passTest();
};
22 changes: 21 additions & 1 deletion ios/react-native-navigation-sdk/NavAutoModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,34 @@ - (void)addMarker:(MarkerOptionsSpec &)options
dispatch_async(dispatch_get_main_queue(), ^{
CLLocationCoordinate2D position =
CLLocationCoordinate2DMake(optionsCopy.position().lat(), optionsCopy.position().lng());

NSString *imageBase64 = optionsCopy.imageBase64();
NSString *imgPath = optionsCopy.imgPath();
UIImage *icon = nil;
if (imgPath && [imgPath isKindOfClass:[NSString class]] && imgPath.length > 0) {

if (imageBase64 && imageBase64.length > 0) {
NSData *imageData =
[[NSData alloc] initWithBase64EncodedString:imageBase64
options:NSDataBase64DecodingIgnoreUnknownCharacters];
icon = [UIImage imageWithData:imageData];
if (!icon) {
reject(@"INVALID_IMAGE", @"Failed to decode base64 image data", nil);
return;
}
} else if (imgPath && imgPath.length > 0) {
icon = [UIImage imageNamed:imgPath];
if (!icon) {
reject(@"INVALID_IMAGE", @"Failed to load image from the provided path", nil);
return;
}
}

CGPoint anchorPoint = CGPointMake(0.5, 1.0);
if (optionsCopy.anchor().has_value()) {
auto anchor = optionsCopy.anchor().value();
anchorPoint = CGPointMake(anchor.u(), anchor.v());
}

GMSMarker *marker = [ObjectTranslationUtil
createMarker:position
title:optionsCopy.title()
Expand All @@ -164,6 +182,8 @@ - (void)addMarker:(MarkerOptionsSpec &)options
zIndex:optionsCopy.zIndex().has_value() ? @(optionsCopy.zIndex().value()) : nil
identifier:optionsCopy.id_()];

marker.groundAnchor = anchorPoint;

[self->_viewController addMarker:marker
visible:optionsCopy.visible().value_or(YES)
result:^(NSDictionary *result) {
Expand Down
22 changes: 21 additions & 1 deletion ios/react-native-navigation-sdk/NavViewModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,34 @@ - (void)addMarker:(NSString *)nativeID
dispatch_async(dispatch_get_main_queue(), ^{
CLLocationCoordinate2D position =
CLLocationCoordinate2DMake(optionsCopy.position().lat(), optionsCopy.position().lng());

NSString *imageBase64 = optionsCopy.imageBase64();
NSString *imgPath = optionsCopy.imgPath();
UIImage *icon = nil;
if (imgPath && [imgPath isKindOfClass:[NSString class]] && imgPath.length > 0) {

if (imageBase64 && imageBase64.length > 0) {
NSData *imageData =
[[NSData alloc] initWithBase64EncodedString:imageBase64
options:NSDataBase64DecodingIgnoreUnknownCharacters];
icon = [UIImage imageWithData:imageData];
if (!icon) {
reject(@"INVALID_IMAGE", @"Failed to decode base64 image data", nil);
return;
}
} else if (imgPath && imgPath.length > 0) {
icon = [UIImage imageNamed:imgPath];
if (!icon) {
reject(@"INVALID_IMAGE", @"Failed to load image from the provided path", nil);
return;
}
}

CGPoint anchorPoint = CGPointMake(0.5, 1.0);
if (optionsCopy.anchor().has_value()) {
auto anchor = optionsCopy.anchor().value();
anchorPoint = CGPointMake(anchor.u(), anchor.v());
}

GMSMarker *marker = [ObjectTranslationUtil
createMarker:position
title:optionsCopy.title()
Expand All @@ -167,6 +185,8 @@ - (void)addMarker:(NSString *)nativeID
zIndex:optionsCopy.zIndex().has_value() ? @(optionsCopy.zIndex().value()) : nil
identifier:optionsCopy.id_()];

marker.groundAnchor = anchorPoint;

[viewController addMarker:marker
visible:optionsCopy.visible().value_or(YES)
result:^(NSDictionary *result) {
Expand Down
6 changes: 5 additions & 1 deletion src/maps/mapView/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ export interface MarkerOptions {
id?: string;
/** The LatLng value for the marker's position on the map. You can change this value at any time if you want to move the marker. */
position: LatLng;
/** Path to a local image asset that should be displayed in the marker instead of using the default marker pin. */
/** Path to an image asset bundled with the application to use as the marker icon. Ignored when imageBase64 is provided. */
imgPath?: string;
/** A raw base64-encoded PNG or JPEG image to use as the marker icon. Takes priority over imgPath. */
imageBase64?: string;
/** The anchor point of the marker icon in normalized icon coordinates (u, v). Defaults to (0.5, 1.0). */
anchor?: { u: number; v: number };
/** A text string that's displayed in an info window when the user taps the marker. You can change this value at any time. */
title?: string;
/** Additional text that's displayed below the title. You can change this value at any time. */
Expand Down
2 changes: 2 additions & 0 deletions src/native/NativeNavViewModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ type MarkerOptionsSpec = Readonly<{
draggable?: WithDefault<boolean, false>;
flat?: WithDefault<boolean, false>;
id?: WithDefault<string, null>;
imageBase64?: WithDefault<string, null>;
imgPath?: WithDefault<string, null>;
anchor?: Readonly<{ u: Float; v: Float }>;
position: Readonly<{ lat: Float; lng: Float }>;
rotation?: WithDefault<Float, 0>;
snippet?: WithDefault<string, null>;
Expand Down