+ * The marker is wrapped in weakReference, so no need to remove it explicitly.
+ *
+ * @param marker
+ */
+ public synchronized void addMarker(MapMarker marker) {
+ this.markers.put(marker, true);
+ if (this.iconBitmapDescriptor != null) {
+ marker.setIconBitmapDescriptor(this.iconBitmapDescriptor, this.bitmap);
+ }
+ }
+
+ /**
+ * Remove marker from this shared icon.
+ *
+ * Marker will only need to call it when the marker receives a different marker image uri.
+ *
+ * @param marker
+ */
+ public synchronized void removeMarker(MapMarker marker) {
+ this.markers.remove(marker);
+ }
+
+ /**
+ * check if there is markers still listening on this icon.
+ * when there are not markers listen on it, we can remove it.
+ *
+ * @return true if there is, false otherwise
+ */
+ public synchronized boolean hasMarker() {
+ return this.markers.isEmpty();
+ }
+
+ /**
+ * Update the bitmap descriptor and bitmap for the image uri.
+ * And notify all subscribers about the update.
+ *
+ * @param bitmapDescriptor
+ * @param bitmap
+ */
+ public synchronized void updateIcon(BitmapDescriptor bitmapDescriptor, Bitmap bitmap) {
+
+ this.iconBitmapDescriptor = bitmapDescriptor;
+ this.bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
+
+ if (this.markers.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry markerEntry : markers.entrySet()) {
+ if (markerEntry.getKey() != null) {
+ markerEntry.getKey().setIconBitmapDescriptor(bitmapDescriptor, bitmap);
+ }
+ }
+ }
+ }
+
+ private final Map sharedIcons = new ConcurrentHashMap<>();
+
+ /**
+ * get the shared icon object, if not existed, create a new one and store it.
+ *
+ * @param uri
+ * @return the icon object for the given uri.
+ */
+ public AirMapMarkerSharedIcon getSharedIcon(String uri) {
+ AirMapMarkerSharedIcon icon = this.sharedIcons.get(uri);
+ if (icon == null) {
+ synchronized (this) {
+ if ((icon = this.sharedIcons.get(uri)) == null) {
+ icon = new AirMapMarkerSharedIcon();
+ this.sharedIcons.put(uri, icon);
+ }
+ }
+ }
+ return icon;
+ }
+
+ /**
+ * Remove the share icon object from our sharedIcons map when no markers are listening for it.
+ *
+ * @param uri
+ */
+ public void removeSharedIconIfEmpty(String uri) {
+ AirMapMarkerSharedIcon icon = this.sharedIcons.get(uri);
+ if (icon == null) {
+ return;
+ }
+ if (!icon.hasMarker()) {
+ synchronized (this) {
+ if ((icon = this.sharedIcons.get(uri)) != null && !icon.hasMarker()) {
+ this.sharedIcons.remove(uri);
+ }
+ }
+ }
+ }
+
+ public MapMarkerManager() {
+ }
+
+ @Override
+ public String getName() {
+ return "AIRMapMarker";
+ }
+
+ @Override
+ public MapMarker createViewInstance(ThemedReactContext context) {
+ return new MapMarker(context, this);
+ }
+
+ @ReactProp(name = "coordinate")
+ public void setCoordinate(MapMarker view, ReadableMap map) {
+ view.setCoordinate(map);
+ }
+
+ @ReactProp(name = "title")
+ public void setTitle(MapMarker view, String title) {
+ view.setTitle(title);
+ }
+
+ @ReactProp(name = "identifier")
+ public void setIdentifier(MapMarker view, String identifier) {
+ view.setIdentifier(identifier);
+ }
+
+ @ReactProp(name = "description")
+ public void setDescription(MapMarker view, String description) {
+ view.setSnippet(description);
+ }
+
+ // NOTE(lmr):
+ // android uses normalized coordinate systems for this, and is provided through the
+ // `anchor` property and `calloutAnchor` instead. Perhaps some work could be done
+ // to normalize iOS and android to use just one of the systems.
+// @ReactProp(name = "centerOffset")
+// public void setCenterOffset(AirMapMarker view, ReadableMap map) {
+//
+// }
+//
+// @ReactProp(name = "calloutOffset")
+// public void setCalloutOffset(AirMapMarker view, ReadableMap map) {
+//
+// }
+
+ @ReactProp(name = "anchor")
+ public void setAnchor(MapMarker view, ReadableMap map) {
+ // should default to (0.5, 1) (bottom middle)
+ double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5;
+ double y = map != null && map.hasKey("y") ? map.getDouble("y") : 1.0;
+ view.setAnchor(x, y);
+ }
+
+ @ReactProp(name = "calloutAnchor")
+ public void setCalloutAnchor(MapMarker view, ReadableMap map) {
+ // should default to (0.5, 0) (top middle)
+ double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5;
+ double y = map != null && map.hasKey("y") ? map.getDouble("y") : 0.0;
+ view.setCalloutAnchor(x, y);
+ }
+
+ @ReactProp(name = "image")
+ public void setImage(MapMarker view, @Nullable String source) {
+ view.setImage(source);
+ }
+// public void setImage(AirMapMarker view, ReadableMap image) {
+// view.setImage(image);
+// }
+
+ @ReactProp(name = "icon")
+ public void setIcon(MapMarker view, @Nullable String source) {
+ view.setImage(source);
+ }
+
+ @ReactProp(name = "pinColor", defaultInt = Color.RED, customType = "Color")
+ public void setPinColor(MapMarker view, int pinColor) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(pinColor, hsv);
+ // NOTE: android only supports a hue
+ view.setMarkerHue(hsv[0]);
+ }
+
+ @ReactProp(name = "rotation", defaultFloat = 0.0f)
+ public void setMarkerRotation(MapMarker view, float rotation) {
+ view.setRotation(rotation);
+ }
+
+ @ReactProp(name = "flat", defaultBoolean = false)
+ public void setFlat(MapMarker view, boolean flat) {
+ view.setFlat(flat);
+ }
+
+ @ReactProp(name = "draggable", defaultBoolean = false)
+ public void setDraggable(MapMarker view, boolean draggable) {
+ view.setDraggable(draggable);
+ }
+
+ @Override
+ @ReactProp(name = "zIndex", defaultFloat = 0.0f)
+ public void setZIndex(MapMarker view, float zIndex) {
+ super.setZIndex(view, zIndex);
+ int integerZIndex = Math.round(zIndex);
+ view.setZIndex(integerZIndex);
+ }
+
+ @Override
+ @ReactProp(name = "opacity", defaultFloat = 1.0f)
+ public void setOpacity(MapMarker view, float opacity) {
+ super.setOpacity(view, opacity);
+ view.setOpacity(opacity);
+ }
+
+ @ReactProp(name = "tracksViewChanges", defaultBoolean = true)
+ public void setTracksViewChanges(MapMarker view, boolean tracksViewChanges) {
+ view.setTracksViewChanges(tracksViewChanges);
+ }
+
+ @ReactProp(name = "accessibilityLabel")
+ public void setAccessibilityLabel(MapMarker view, @Nullable String accessibilityLabel) {
+ view.setTag(R.id.accessibility_label, accessibilityLabel);
+ }
+
+ @Override
+ public void addView(MapMarker parent, View child, int index) {
+ // if an component is a child, then it is a callout view, NOT part of the
+ // marker.
+ if (child instanceof MapCallout) {
+ parent.setCalloutView((MapCallout) child);
+ } else {
+ super.addView(parent, child, index);
+ parent.update(true);
+ }
+ }
+
+ @Override
+ public void removeViewAt(MapMarker parent, int index) {
+ super.removeViewAt(parent, index);
+ parent.update(true);
+ }
+
+ @Override
+ public void receiveCommand(@NonNull MapMarker view, String commandId, @Nullable ReadableArray args) {
+ int duration;
+ double lat;
+ double lng;
+ ReadableMap region;
+
+ switch (commandId) {
+ case "showCallout":
+ ((Marker) view.getFeature()).showInfoWindow();
+ break;
+
+ case "hideCallout":
+ ((Marker) view.getFeature()).hideInfoWindow();
+ break;
+
+ case "animateMarkerToCoordinate":
+ if (args == null) {
+ break;
+ }
+ region = args.getMap(0);
+ duration = args.getInt(1);
+
+ lng = region.getDouble("longitude");
+ lat = region.getDouble("latitude");
+ view.animateToCoodinate(new LatLng(lat, lng), duration);
+ break;
+
+ case "redraw":
+ view.updateMarkerIcon();
+ break;
+ }
+ }
+
+ @Override
+ @Nullable
+ public Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.>builder()
+ .put("onPress", MapBuilder.of("registrationName", "onPress"))
+ .put("onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress"))
+ .put("onDragStart", MapBuilder.of("registrationName", "onDragStart"))
+ .put("onDrag", MapBuilder.of("registrationName", "onDrag"))
+ .put("onDragEnd", MapBuilder.of("registrationName", "onDragEnd"))
+ .build();
+ }
+
+ @Override
+ @Nullable
+ public Map getExportedCustomBubblingEventTypeConstants() {
+ return MapBuilder.>builder()
+ .put("onSelect", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onSelect")))
+ .put("onDeselect", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onDeselect")))
+ .build();
+ }
+
+
+ @Override
+ public LayoutShadowNode createShadowNodeInstance() {
+ // we use a custom shadow node that emits the width/height of the view
+ // after layout with the updateExtraData method. Without this, we can't generate
+ // a bitmap of the appropriate width/height of the rendered view.
+ return new SizeReportingShadowNode();
+ }
+
+ @Override
+ public void updateExtraData(MapMarker view, Object extraData) {
+ // This method is called from the shadow node with the width/height of the rendered
+ // marker view.
+ HashMap data = (HashMap) extraData;
+ float width = data.get("width");
+ float height = data.get("height");
+ view.update((int) width, (int) height);
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapModule.java b/android/src/main/java/com/rnmaps/maps/MapModule.java
new file mode 100644
index 000000000..aef5bc5ff
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapModule.java
@@ -0,0 +1,283 @@
+package com.rnmaps.maps;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.location.Address;
+import android.location.Geocoder;
+import android.net.Uri;
+import android.util.Base64;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+import com.facebook.react.module.annotations.ReactModule;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLng;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@ReactModule(name = MapModule.NAME)
+public class MapModule extends ReactContextBaseJavaModule {
+
+ public static final String NAME = "AirMapModule";
+ private static final String SNAPSHOT_RESULT_FILE = "file";
+ private static final String SNAPSHOT_RESULT_BASE64 = "base64";
+ private static final String SNAPSHOT_FORMAT_PNG = "png";
+ private static final String SNAPSHOT_FORMAT_JPG = "jpg";
+
+ public MapModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public Map getConstants() {
+ final Map constants = new HashMap<>();
+ constants.put("legalNotice", "This license information is displayed in Settings > Google > Open Source on any device running Google Play services.");
+ return constants;
+ }
+
+ public Activity getActivity() {
+ return getCurrentActivity();
+ }
+
+ public static void closeQuietly(Closeable closeable) {
+ if (closeable == null) return;
+ try {
+ closeable.close();
+ } catch (IOException ignored) {
+ }
+ }
+
+ @ReactMethod
+ public void takeSnapshot(final int tag, final ReadableMap options, final Promise promise) {
+
+ // Parse and verity options
+ final ReactApplicationContext context = getReactApplicationContext();
+ final String format = options.hasKey("format") ? options.getString("format") : "png";
+ final Bitmap.CompressFormat compressFormat =
+ format.equals(SNAPSHOT_FORMAT_PNG) ? Bitmap.CompressFormat.PNG :
+ format.equals(SNAPSHOT_FORMAT_JPG) ? Bitmap.CompressFormat.JPEG : null;
+ final double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0;
+ final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ final Integer width =
+ options.hasKey("width") ? (int) (displayMetrics.density * options.getDouble("width")) : 0;
+ final Integer height =
+ options.hasKey("height") ? (int) (displayMetrics.density * options.getDouble("height")) : 0;
+ final String result = options.hasKey("result") ? options.getString("result") : "file";
+
+ MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
+ view.map.snapshot(new GoogleMap.SnapshotReadyCallback() {
+ public void onSnapshotReady(@Nullable Bitmap snapshot) {
+
+ // Convert image to requested width/height if necessary
+ if (snapshot == null) {
+ promise.reject("Failed to generate bitmap, snapshot = null");
+ return;
+ }
+ if ((width != 0) && (height != 0) &&
+ (width != snapshot.getWidth() || height != snapshot.getHeight())) {
+ snapshot = Bitmap.createScaledBitmap(snapshot, width, height, true);
+ }
+
+ // Save the snapshot to disk
+ if (result.equals(SNAPSHOT_RESULT_FILE)) {
+ File tempFile;
+ FileOutputStream outputStream;
+ try {
+ tempFile =
+ File.createTempFile("AirMapSnapshot", "." + format, context.getCacheDir());
+ outputStream = new FileOutputStream(tempFile);
+ } catch (Exception e) {
+ promise.reject(e);
+ return;
+ }
+ snapshot.compress(compressFormat, (int) (100.0 * quality), outputStream);
+ closeQuietly(outputStream);
+ String uri = Uri.fromFile(tempFile).toString();
+ promise.resolve(uri);
+ } else if (result.equals(SNAPSHOT_RESULT_BASE64)) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ snapshot.compress(compressFormat, (int) (100.0 * quality), outputStream);
+ closeQuietly(outputStream);
+ byte[] bytes = outputStream.toByteArray();
+ String data = Base64.encodeToString(bytes, Base64.NO_WRAP);
+ promise.resolve(data);
+ }
+ }
+ });
+
+ return null;
+ });
+
+ // Add UI-block so we can get a valid reference to the map-view
+
+ uiBlock.addToUIManager();
+ }
+
+ @ReactMethod
+ public void getCamera(final int tag, final Promise promise) {
+ final ReactApplicationContext context = getReactApplicationContext();
+
+ MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
+ CameraPosition position = view.map.getCameraPosition();
+
+ WritableMap centerJson = new WritableNativeMap();
+ centerJson.putDouble("latitude", position.target.latitude);
+ centerJson.putDouble("longitude", position.target.longitude);
+
+ WritableMap cameraJson = new WritableNativeMap();
+ cameraJson.putMap("center", centerJson);
+ cameraJson.putDouble("heading", (double)position.bearing);
+ cameraJson.putDouble("zoom", (double)position.zoom);
+ cameraJson.putDouble("pitch", (double)position.tilt);
+
+ promise.resolve(cameraJson);
+
+ return null;
+ });
+
+ uiBlock.addToUIManager();
+ }
+
+ @ReactMethod
+ public void getAddressFromCoordinates(final int tag, final ReadableMap coordinate, final Promise promise) {
+ final ReactApplicationContext context = getReactApplicationContext();
+
+ MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, mapView -> {
+ if (coordinate == null ||
+ !coordinate.hasKey("latitude") ||
+ !coordinate.hasKey("longitude")) {
+ promise.reject("Invalid coordinate format");
+ return null;
+ }
+ Geocoder geocoder = new Geocoder(context);
+ try {
+ List list =
+ geocoder.getFromLocation(coordinate.getDouble("latitude"),coordinate.getDouble("longitude"),1);
+ if (list.isEmpty()) {
+ promise.reject("Can not get address location");
+ return null;
+ }
+ Address address = list.get(0);
+
+ WritableMap addressJson = new WritableNativeMap();
+ addressJson.putString("name", address.getFeatureName());
+ addressJson.putString("locality", address.getLocality());
+ addressJson.putString("thoroughfare", address.getThoroughfare());
+ addressJson.putString("subThoroughfare", address.getSubThoroughfare());
+ addressJson.putString("subLocality", address.getSubLocality());
+ addressJson.putString("administrativeArea", address.getAdminArea());
+ addressJson.putString("subAdministrativeArea", address.getSubAdminArea());
+ addressJson.putString("postalCode", address.getPostalCode());
+ addressJson.putString("countryCode", address.getCountryCode());
+ addressJson.putString("country", address.getCountryName());
+
+ promise.resolve(addressJson);
+ } catch (IOException e) {
+ promise.reject("Can not get address location");
+ }
+
+ return null;
+ });
+
+ uiBlock.addToUIManager();
+ }
+
+ @ReactMethod
+ public void pointForCoordinate(final int tag, ReadableMap coordinate, final Promise promise) {
+ final ReactApplicationContext context = getReactApplicationContext();
+ final double density = (double) context.getResources().getDisplayMetrics().density;
+
+ final LatLng coord = new LatLng(
+ coordinate.hasKey("latitude") ? coordinate.getDouble("latitude") : 0.0,
+ coordinate.hasKey("longitude") ? coordinate.getDouble("longitude") : 0.0
+ );
+
+ MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
+ Point pt = view.map.getProjection().toScreenLocation(coord);
+
+ WritableMap ptJson = new WritableNativeMap();
+ ptJson.putDouble("x", (double)pt.x / density);
+ ptJson.putDouble("y", (double)pt.y / density);
+
+ promise.resolve(ptJson);
+
+ return null;
+ });
+
+ uiBlock.addToUIManager();
+ }
+
+ @ReactMethod
+ public void coordinateForPoint(final int tag, ReadableMap point, final Promise promise) {
+ final ReactApplicationContext context = getReactApplicationContext();
+ final double density = (double) context.getResources().getDisplayMetrics().density;
+
+ final Point pt = new Point(
+ point.hasKey("x") ? (int)(point.getDouble("x") * density) : 0,
+ point.hasKey("y") ? (int)(point.getDouble("y") * density) : 0
+ );
+
+ MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
+ LatLng coord = view.map.getProjection().fromScreenLocation(pt);
+
+ WritableMap coordJson = new WritableNativeMap();
+ coordJson.putDouble("latitude", coord.latitude);
+ coordJson.putDouble("longitude", coord.longitude);
+
+ promise.resolve(coordJson);
+
+ return null;
+ });
+
+ uiBlock.addToUIManager();
+ }
+
+ @ReactMethod
+ public void getMapBoundaries(final int tag, final Promise promise) {
+ final ReactApplicationContext context = getReactApplicationContext();
+
+ MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
+ double[][] boundaries = view.getMapBoundaries();
+
+ WritableMap coordinates = new WritableNativeMap();
+ WritableMap northEastHash = new WritableNativeMap();
+ WritableMap southWestHash = new WritableNativeMap();
+
+ northEastHash.putDouble("longitude", boundaries[0][0]);
+ northEastHash.putDouble("latitude", boundaries[0][1]);
+ southWestHash.putDouble("longitude", boundaries[1][0]);
+ southWestHash.putDouble("latitude", boundaries[1][1]);
+
+ coordinates.putMap("northEast", northEastHash);
+ coordinates.putMap("southWest", southWestHash);
+
+ promise.resolve(coordinates);
+
+ return null;
+ });
+
+ uiBlock.addToUIManager();
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapOverlay.java b/android/src/main/java/com/rnmaps/maps/MapOverlay.java
new file mode 100644
index 000000000..fc74c9bdd
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapOverlay.java
@@ -0,0 +1,166 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.google.android.gms.maps.model.BitmapDescriptor;
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
+import com.google.android.gms.maps.model.GroundOverlay;
+import com.google.android.gms.maps.model.GroundOverlayOptions;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+import com.google.maps.android.collections.GroundOverlayManager;
+
+public class MapOverlay extends MapFeature implements ImageReadable {
+
+ private GroundOverlayOptions groundOverlayOptions;
+ private GroundOverlay groundOverlay;
+ private LatLngBounds bounds;
+ private float bearing;
+ private BitmapDescriptor iconBitmapDescriptor;
+ private boolean tappable;
+ private float zIndex;
+ private float transparency;
+
+ private final ImageReader mImageReader;
+ private GroundOverlayManager.Collection groundOverlayCollection;
+
+ public MapOverlay(Context context) {
+ super(context);
+ this.mImageReader = new ImageReader(context, getResources(), this);
+ }
+
+ public void setBounds(ReadableArray bounds) {
+ LatLng sw = new LatLng(bounds.getArray(0).getDouble(0), bounds.getArray(0).getDouble(1));
+ LatLng ne = new LatLng(bounds.getArray(1).getDouble(0), bounds.getArray(1).getDouble(1));
+ this.bounds = new LatLngBounds(sw, ne);
+ if (this.groundOverlay != null) {
+ this.groundOverlay.setPositionFromBounds(this.bounds);
+ }
+ }
+
+ public void setBearing(float bearing){
+ this.bearing = bearing;
+ if (this.groundOverlay != null) {
+ this.groundOverlay.setBearing(bearing);
+ }
+ }
+
+ public void setZIndex(float zIndex) {
+ this.zIndex = zIndex;
+ if (this.groundOverlay != null) {
+ this.groundOverlay.setZIndex(zIndex);
+ }
+ }
+
+ public void setTransparency(float transparency) {
+ this.transparency = transparency;
+ if (groundOverlay != null) {
+ groundOverlay.setTransparency(transparency);
+ }
+ }
+
+ public void setImage(String uri) {
+ this.mImageReader.setImage(uri);
+ }
+
+ public void setTappable(boolean tapabble) {
+ this.tappable = tapabble;
+ if (groundOverlay != null) {
+ groundOverlay.setClickable(tappable);
+ }
+ }
+
+
+ public GroundOverlayOptions getGroundOverlayOptions() {
+ if (this.groundOverlayOptions == null) {
+ this.groundOverlayOptions = createGroundOverlayOptions();
+ }
+ return this.groundOverlayOptions;
+ }
+
+ private GroundOverlayOptions createGroundOverlayOptions() {
+ if (this.groundOverlayOptions != null) {
+ return this.groundOverlayOptions;
+ }
+ GroundOverlayOptions options = new GroundOverlayOptions();
+ if (this.iconBitmapDescriptor != null) {
+ options.image(iconBitmapDescriptor);
+ } else {
+ // add stub image to be able to instantiate the overlay
+ // and store a reference to it in MapView
+ options.image(BitmapDescriptorFactory.defaultMarker());
+ // hide overlay until real image gets added
+ options.visible(false);
+ }
+ options.positionFromBounds(bounds);
+ options.zIndex(zIndex);
+ options.bearing(bearing);
+ options.transparency(transparency);
+ return options;
+ }
+
+ @Override
+ public Object getFeature() {
+ return groundOverlay;
+ }
+
+ @Override
+ public void addToMap(Object collection) {
+ GroundOverlayManager.Collection groundOverlayCollection = (GroundOverlayManager.Collection) collection;
+ GroundOverlayOptions groundOverlayOptions = getGroundOverlayOptions();
+ if (groundOverlayOptions != null) {
+ groundOverlay = groundOverlayCollection.addGroundOverlay(groundOverlayOptions);
+ groundOverlay.setClickable(this.tappable);
+ } else {
+ this.groundOverlayCollection = groundOverlayCollection;
+ }
+ }
+
+ @Override
+ public void removeFromMap(Object collection) {
+ if (groundOverlay != null) {
+ GroundOverlayManager.Collection groundOverlayCollection = (GroundOverlayManager.Collection) collection;
+ groundOverlayCollection.remove(groundOverlay);
+ groundOverlay = null;
+ groundOverlayOptions = null;
+ }
+ groundOverlayCollection = null;
+ }
+
+ @Override
+ public void setIconBitmap(Bitmap bitmap) {
+ }
+
+ @Override
+ public void setIconBitmapDescriptor(
+ BitmapDescriptor iconBitmapDescriptor) {
+ this.iconBitmapDescriptor = iconBitmapDescriptor;
+ }
+
+ @Override
+ public void update() {
+ this.groundOverlay = getGroundOverlay();
+ if (this.groundOverlay != null) {
+ this.groundOverlay.setVisible(true);
+ this.groundOverlay.setImage(this.iconBitmapDescriptor);
+ this.groundOverlay.setTransparency(this.transparency);
+ this.groundOverlay.setClickable(this.tappable);
+ }
+ }
+
+ private GroundOverlay getGroundOverlay() {
+ if (this.groundOverlay != null) {
+ return this.groundOverlay;
+ }
+ if (this.groundOverlayCollection == null) {
+ return null;
+ }
+ GroundOverlayOptions groundOverlayOptions = getGroundOverlayOptions();
+ if (groundOverlayOptions != null) {
+ return this.groundOverlayCollection.addGroundOverlay(groundOverlayOptions);
+ }
+ return null;
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapOverlayManager.java b/android/src/main/java/com/rnmaps/maps/MapOverlayManager.java
new file mode 100644
index 000000000..c8f2498a1
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapOverlayManager.java
@@ -0,0 +1,75 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.annotations.ReactProp;
+
+import java.util.Map;
+
+public class MapOverlayManager extends ViewGroupManager {
+
+ public MapOverlayManager(ReactApplicationContext reactContext) {
+ super();
+ DisplayMetrics metrics = new DisplayMetrics();
+ ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay()
+ .getRealMetrics(metrics);
+ }
+
+ @Override
+ public String getName() {
+ return "AIRMapOverlay";
+ }
+
+ @Override
+ public MapOverlay createViewInstance(ThemedReactContext context) {
+ return new MapOverlay(context);
+ }
+
+ @ReactProp(name = "bounds")
+ public void setBounds(MapOverlay view, ReadableArray bounds) {
+ view.setBounds(bounds);
+ }
+
+ @ReactProp(name = "bearing")
+ public void setBearing(MapOverlay view, float bearing){
+ view.setBearing(bearing);
+ }
+
+ @ReactProp(name = "zIndex", defaultFloat = 1.0f)
+ public void setZIndex(MapOverlay view, float zIndex) {
+ view.setZIndex(zIndex);
+ }
+
+ @ReactProp(name = "opacity", defaultFloat = 1.0f)
+ public void setOpacity(MapOverlay view, float opacity) {
+ view.setTransparency(1 - opacity);
+ }
+
+ @ReactProp(name = "image")
+ public void setImage(MapOverlay view, @Nullable String source) {
+ view.setImage(source);
+ }
+
+ @ReactProp(name = "tappable", defaultBoolean = false)
+ public void setTappable(MapOverlay view, boolean tapabble) {
+ view.setTappable(tapabble);
+ }
+
+ @Override
+ @Nullable
+ public Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.of(
+ "onPress", MapBuilder.of("registrationName", "onPress")
+ );
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapPolygon.java b/android/src/main/java/com/rnmaps/maps/MapPolygon.java
new file mode 100644
index 000000000..4dabeef0e
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapPolygon.java
@@ -0,0 +1,194 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.google.android.gms.maps.model.Dash;
+import com.google.android.gms.maps.model.Gap;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.PatternItem;
+import com.google.android.gms.maps.model.Polygon;
+import com.google.android.gms.maps.model.PolygonOptions;
+import com.google.maps.android.collections.PolygonManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MapPolygon extends MapFeature {
+
+ private PolygonOptions polygonOptions;
+ private Polygon polygon;
+
+ private List coordinates;
+ private List> holes;
+ private int strokeColor;
+ private int fillColor;
+ private float strokeWidth;
+ private boolean geodesic;
+ private boolean tappable;
+ private float zIndex;
+ private ReadableArray patternValues;
+ private List pattern;
+
+ public MapPolygon(Context context) {
+ super(context);
+ }
+
+ public void setCoordinates(ReadableArray coordinates) {
+ // it's kind of a bummer that we can't run map() or anything on the ReadableArray
+ this.coordinates = new ArrayList<>(coordinates.size());
+ for (int i = 0; i < coordinates.size(); i++) {
+ ReadableMap coordinate = coordinates.getMap(i);
+ this.coordinates.add(i,
+ new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude")));
+ }
+ if (polygon != null) {
+ polygon.setPoints(this.coordinates);
+ }
+ }
+
+ public void setHoles(ReadableArray holes) {
+ if (holes == null) { return; }
+
+ this.holes = new ArrayList<>(holes.size());
+
+ for (int i = 0; i < holes.size(); i++) {
+ ReadableArray hole = holes.getArray(i);
+
+ if (hole.size() < 3) { continue; }
+
+ List coordinates = new ArrayList<>();
+ for (int j = 0; j < hole.size(); j++) {
+ ReadableMap coordinate = hole.getMap(j);
+ coordinates.add(new LatLng(
+ coordinate.getDouble("latitude"),
+ coordinate.getDouble("longitude")));
+ }
+
+ // If hole is triangle
+ if (coordinates.size() == 3) {
+ coordinates.add(coordinates.get(0));
+ }
+
+ this.holes.add(coordinates);
+ }
+
+ if (polygon != null) {
+ polygon.setHoles(this.holes);
+ }
+ }
+
+
+ public void setFillColor(int color) {
+ this.fillColor = color;
+ if (polygon != null) {
+ polygon.setFillColor(color);
+ }
+ }
+
+ public void setStrokeColor(int color) {
+ this.strokeColor = color;
+ if (polygon != null) {
+ polygon.setStrokeColor(color);
+ }
+ }
+
+ public void setStrokeWidth(float width) {
+ this.strokeWidth = width;
+ if (polygon != null) {
+ polygon.setStrokeWidth(width);
+ }
+ }
+
+ public void setTappable(boolean tapabble) {
+ this.tappable = tapabble;
+ if (polygon != null) {
+ polygon.setClickable(tappable);
+ }
+ }
+
+ public void setGeodesic(boolean geodesic) {
+ this.geodesic = geodesic;
+ if (polygon != null) {
+ polygon.setGeodesic(geodesic);
+ }
+ }
+
+ public void setZIndex(float zIndex) {
+ this.zIndex = zIndex;
+ if (polygon != null) {
+ polygon.setZIndex(zIndex);
+ }
+ }
+
+ public void setLineDashPattern(ReadableArray patternValues) {
+ this.patternValues = patternValues;
+ this.applyPattern();
+ }
+
+ private void applyPattern() {
+ if(patternValues == null) {
+ return;
+ }
+ this.pattern = new ArrayList<>(patternValues.size());
+ for (int i = 0; i < patternValues.size(); i++) {
+ float patternValue = (float) patternValues.getDouble(i);
+ boolean isGap = i % 2 != 0;
+ if(isGap) {
+ this.pattern.add(new Gap(patternValue));
+ }else {
+ PatternItem patternItem;
+ patternItem = new Dash(patternValue);
+ this.pattern.add(patternItem);
+ }
+ }
+ if(polygon != null) {
+ polygon.setStrokePattern(this.pattern);
+ }
+ }
+
+ public PolygonOptions getPolygonOptions() {
+ if (polygonOptions == null) {
+ polygonOptions = createPolygonOptions();
+ }
+ return polygonOptions;
+ }
+
+ private PolygonOptions createPolygonOptions() {
+ PolygonOptions options = new PolygonOptions();
+ options.addAll(coordinates);
+ options.fillColor(fillColor);
+ options.strokeColor(strokeColor);
+ options.strokeWidth(strokeWidth);
+ options.geodesic(geodesic);
+ options.zIndex(zIndex);
+ options.strokePattern(this.pattern);
+
+ if (this.holes != null) {
+ for (int i = 0; i < holes.size(); i++) {
+ options.addHole(holes.get(i));
+ }
+ }
+
+ return options;
+ }
+
+ @Override
+ public Object getFeature() {
+ return polygon;
+ }
+
+ @Override
+ public void addToMap(Object collection) {
+ PolygonManager.Collection polygonCollection = (PolygonManager.Collection) collection;
+ polygon = polygonCollection.addPolygon(getPolygonOptions());
+ polygon.setClickable(this.tappable);
+ }
+
+ @Override
+ public void removeFromMap(Object collection) {
+ PolygonManager.Collection polygonCollection = (PolygonManager.Collection) collection;
+ polygonCollection.remove(polygon);
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapPolygonManager.java b/android/src/main/java/com/rnmaps/maps/MapPolygonManager.java
new file mode 100644
index 000000000..26c3f78c0
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapPolygonManager.java
@@ -0,0 +1,93 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.annotations.ReactProp;
+
+import java.util.Map;
+
+public class MapPolygonManager extends ViewGroupManager {
+ private final DisplayMetrics metrics;
+
+ public MapPolygonManager(ReactApplicationContext reactContext) {
+ super();
+ metrics = new DisplayMetrics();
+ ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay()
+ .getRealMetrics(metrics);
+ }
+
+ @Override
+ public String getName() {
+ return "AIRMapPolygon";
+ }
+
+ @Override
+ public MapPolygon createViewInstance(ThemedReactContext context) {
+ return new MapPolygon(context);
+ }
+
+ @ReactProp(name = "coordinates")
+ public void setCoordinate(MapPolygon view, ReadableArray coordinates) {
+ view.setCoordinates(coordinates);
+ }
+
+ @ReactProp(name = "holes")
+ public void setHoles(MapPolygon view, ReadableArray holes) {
+ view.setHoles(holes);
+ }
+
+ @ReactProp(name = "strokeWidth", defaultFloat = 1f)
+ public void setStrokeWidth(MapPolygon view, float widthInPoints) {
+ float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS
+ view.setStrokeWidth(widthInScreenPx);
+ }
+
+ @ReactProp(name = "fillColor", defaultInt = Color.RED, customType = "Color")
+ public void setFillColor(MapPolygon view, int color) {
+ view.setFillColor(color);
+ }
+
+ @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color")
+ public void setStrokeColor(MapPolygon view, int color) {
+ view.setStrokeColor(color);
+ }
+
+ @ReactProp(name = "tappable", defaultBoolean = false)
+ public void setTappable(MapPolygon view, boolean tapabble) {
+ view.setTappable(tapabble);
+ }
+
+ @ReactProp(name = "geodesic", defaultBoolean = false)
+ public void setGeodesic(MapPolygon view, boolean geodesic) {
+ view.setGeodesic(geodesic);
+ }
+
+ @ReactProp(name = "zIndex", defaultFloat = 1.0f)
+ public void setZIndex(MapPolygon view, float zIndex) {
+ view.setZIndex(zIndex);
+ }
+
+ @ReactProp(name = "lineDashPattern")
+ public void setLineDashPattern(MapPolygon view, ReadableArray patternValues) {
+ view.setLineDashPattern(patternValues);
+ }
+
+ @Override
+ @Nullable
+ public Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.of(
+ "onPress", MapBuilder.of("registrationName", "onPress")
+ );
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapPolyline.java b/android/src/main/java/com/rnmaps/maps/MapPolyline.java
new file mode 100644
index 000000000..f008b8869
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapPolyline.java
@@ -0,0 +1,164 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.google.android.gms.maps.model.Cap;
+import com.google.android.gms.maps.model.Dash;
+import com.google.android.gms.maps.model.Dot;
+import com.google.android.gms.maps.model.Gap;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.PatternItem;
+import com.google.android.gms.maps.model.Polyline;
+import com.google.android.gms.maps.model.PolylineOptions;
+import com.google.android.gms.maps.model.RoundCap;
+import com.google.maps.android.collections.PolylineManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MapPolyline extends MapFeature {
+
+ private PolylineOptions polylineOptions;
+ private Polyline polyline;
+
+ private List coordinates;
+ private int color;
+ private float width;
+ private boolean tappable;
+ private boolean geodesic;
+ private float zIndex;
+ private Cap lineCap = new RoundCap();
+ private ReadableArray patternValues;
+ private List pattern;
+
+ public MapPolyline(Context context) {
+ super(context);
+ }
+
+ public void setCoordinates(ReadableArray coordinates) {
+ this.coordinates = new ArrayList<>(coordinates.size());
+ for (int i = 0; i < coordinates.size(); i++) {
+ ReadableMap coordinate = coordinates.getMap(i);
+ this.coordinates.add(i,
+ new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude")));
+ }
+ if (polyline != null) {
+ polyline.setPoints(this.coordinates);
+ }
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ if (polyline != null) {
+ polyline.setColor(color);
+ }
+ }
+
+ public void setWidth(float width) {
+ this.width = width;
+ if (polyline != null) {
+ polyline.setWidth(width);
+ }
+ }
+
+ public void setZIndex(float zIndex) {
+ this.zIndex = zIndex;
+ if (polyline != null) {
+ polyline.setZIndex(zIndex);
+ }
+ }
+
+ public void setTappable(boolean tapabble) {
+ this.tappable = tapabble;
+ if (polyline != null) {
+ polyline.setClickable(tappable);
+ }
+ }
+
+ public void setGeodesic(boolean geodesic) {
+ this.geodesic = geodesic;
+ if (polyline != null) {
+ polyline.setGeodesic(geodesic);
+ }
+ }
+
+ public void setLineCap(Cap cap) {
+ this.lineCap = cap;
+ if (polyline != null) {
+ polyline.setStartCap(cap);
+ polyline.setEndCap(cap);
+ }
+ this.applyPattern();
+ }
+
+ public void setLineDashPattern(ReadableArray patternValues) {
+ this.patternValues = patternValues;
+ this.applyPattern();
+ }
+
+ private void applyPattern() {
+ if(patternValues == null) {
+ return;
+ }
+ this.pattern = new ArrayList<>(patternValues.size());
+ for (int i = 0; i < patternValues.size(); i++) {
+ float patternValue = (float) patternValues.getDouble(i);
+ boolean isGap = i % 2 != 0;
+ if(isGap) {
+ this.pattern.add(new Gap(patternValue));
+ }else {
+ PatternItem patternItem;
+ boolean isLineCapRound = this.lineCap instanceof RoundCap;
+ if(isLineCapRound) {
+ patternItem = new Dot();
+ }else {
+ patternItem = new Dash(patternValue);
+ }
+ this.pattern.add(patternItem);
+ }
+ }
+ if(polyline != null) {
+ polyline.setPattern(this.pattern);
+ }
+ }
+
+ public PolylineOptions getPolylineOptions() {
+ if (polylineOptions == null) {
+ polylineOptions = createPolylineOptions();
+ }
+ return polylineOptions;
+ }
+
+ private PolylineOptions createPolylineOptions() {
+ PolylineOptions options = new PolylineOptions();
+ options.addAll(coordinates);
+ options.color(color);
+ options.width(width);
+ options.geodesic(geodesic);
+ options.zIndex(zIndex);
+ options.startCap(lineCap);
+ options.endCap(lineCap);
+ options.pattern(this.pattern);
+ return options;
+ }
+
+ @Override
+ public Object getFeature() {
+ return polyline;
+ }
+
+ @Override
+ public void addToMap(Object collection) {
+ PolylineManager.Collection polylineCollection = (PolylineManager.Collection) collection;
+ polyline = polylineCollection.addPolyline(getPolylineOptions());
+ polyline.setClickable(this.tappable);
+ }
+
+ @Override
+ public void removeFromMap(Object collection) {
+ PolylineManager.Collection polylineCollection = (PolylineManager.Collection) collection;
+ polylineCollection.remove(polyline);
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapPolylineManager.java b/android/src/main/java/com/rnmaps/maps/MapPolylineManager.java
new file mode 100644
index 000000000..178641704
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapPolylineManager.java
@@ -0,0 +1,107 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.annotations.ReactProp;
+import com.google.android.gms.maps.model.ButtCap;
+import com.google.android.gms.maps.model.Cap;
+import com.google.android.gms.maps.model.RoundCap;
+import com.google.android.gms.maps.model.SquareCap;
+
+import java.util.Map;
+
+public class MapPolylineManager extends ViewGroupManager {
+ private final DisplayMetrics metrics;
+
+ public MapPolylineManager(ReactApplicationContext reactContext) {
+ super();
+ metrics = new DisplayMetrics();
+ ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay()
+ .getRealMetrics(metrics);
+ }
+
+ @Override
+ public String getName() {
+ return "AIRMapPolyline";
+ }
+
+ @Override
+ public MapPolyline createViewInstance(ThemedReactContext context) {
+ return new MapPolyline(context);
+ }
+
+ @ReactProp(name = "coordinates")
+ public void setCoordinate(MapPolyline view, ReadableArray coordinates) {
+ view.setCoordinates(coordinates);
+ }
+
+ @ReactProp(name = "strokeWidth", defaultFloat = 1f)
+ public void setStrokeWidth(MapPolyline view, float widthInPoints) {
+ float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS
+ view.setWidth(widthInScreenPx);
+ }
+
+ @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color")
+ public void setStrokeColor(MapPolyline view, int color) {
+ view.setColor(color);
+ }
+
+ @ReactProp(name = "tappable", defaultBoolean = false)
+ public void setTappable(MapPolyline view, boolean tapabble) {
+ view.setTappable(tapabble);
+ }
+
+ @ReactProp(name = "geodesic", defaultBoolean = false)
+ public void setGeodesic(MapPolyline view, boolean geodesic) {
+ view.setGeodesic(geodesic);
+ }
+
+ @ReactProp(name = "zIndex", defaultFloat = 1.0f)
+ public void setZIndex(MapPolyline view, float zIndex) {
+ view.setZIndex(zIndex);
+ }
+
+ @ReactProp(name = "lineCap")
+ public void setlineCap(MapPolyline view, String lineCap) {
+ Cap cap = null;
+ switch (lineCap) {
+ case "butt":
+ cap = new ButtCap();
+ break;
+ case "round":
+ cap = new RoundCap();
+ break;
+ case "square":
+ cap = new SquareCap();
+ break;
+ default:
+ cap = new RoundCap();
+ break;
+ }
+ view.setLineCap(cap);
+ }
+
+ @ReactProp(name = "lineDashPattern")
+ public void setLineDashPattern(MapPolyline view, ReadableArray patternValues) {
+ view.setLineDashPattern(patternValues);
+ }
+
+ @Override
+ @Nullable
+ public Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.of(
+ "onPress", MapBuilder.of("registrationName", "onPress")
+ );
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapTileProvider.java b/android/src/main/java/com/rnmaps/maps/MapTileProvider.java
new file mode 100644
index 000000000..a974311c2
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapTileProvider.java
@@ -0,0 +1,494 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+
+import android.util.Log;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Future;
+import java.util.List;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Data;
+import androidx.work.Constraints;
+import androidx.work.NetworkType;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.Operation;
+import androidx.work.WorkInfo;
+
+import com.google.android.gms.maps.model.Tile;
+import com.google.android.gms.maps.model.TileProvider;
+import com.google.android.gms.maps.model.UrlTileProvider;
+
+import java.lang.System;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+public class MapTileProvider implements TileProvider {
+
+ class AIRMapUrlTileProvider extends UrlTileProvider {
+ private String urlTemplate;
+
+ public AIRMapUrlTileProvider(int width, int height, String urlTemplate) {
+ super(width, height);
+ this.urlTemplate = urlTemplate;
+ }
+
+ @Override
+ public URL getTileUrl(int x, int y, int zoom) {
+
+ if (MapTileProvider.this.flipY) {
+ y = (1 << zoom) - y - 1;
+ }
+
+ String s = this.urlTemplate
+ .replace("{x}", Integer.toString(x))
+ .replace("{y}", Integer.toString(y))
+ .replace("{z}", Integer.toString(zoom));
+ URL url;
+
+ if(MapTileProvider.this.maximumZ > 0 && zoom > MapTileProvider.this.maximumZ) {
+ return null;
+ }
+
+ if(MapTileProvider.this.minimumZ > 0 && zoom < MapTileProvider.this.minimumZ) {
+ return null;
+ }
+
+ try {
+ url = new URL(s);
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+ return url;
+ }
+
+ public void setUrlTemplate(String urlTemplate) {
+ this.urlTemplate = urlTemplate;
+ }
+ }
+
+ protected static final int BUFFER_SIZE = 16 * 1024;
+ protected static final int TARGET_TILE_SIZE = 512;
+ protected UrlTileProvider tileProvider;
+ protected String urlTemplate;
+ protected int tileSize;
+ protected boolean doubleTileSize;
+ protected int maximumZ;
+ protected int maximumNativeZ;
+ protected int minimumZ;
+ protected boolean flipY;
+ protected String tileCachePath;
+ protected int tileCacheMaxAge;
+ protected boolean offlineMode;
+ protected Context context;
+ protected boolean customMode;
+
+ public MapTileProvider(int tileSizet, boolean doubleTileSize, String urlTemplate,
+ int maximumZ, int maximumNativeZ, int minimumZ, boolean flipY, String tileCachePath,
+ int tileCacheMaxAge, boolean offlineMode, Context context, boolean customMode) {
+ this.tileProvider = new AIRMapUrlTileProvider(tileSizet, tileSizet, urlTemplate);
+
+ this.tileSize = tileSizet;
+ this.doubleTileSize = doubleTileSize;
+ this.urlTemplate = urlTemplate;
+ this.maximumZ = maximumZ;
+ this.maximumNativeZ = maximumNativeZ;
+ this.minimumZ = minimumZ;
+ this.flipY = flipY;
+ this.tileCachePath = tileCachePath;
+ this.tileCacheMaxAge = tileCacheMaxAge;
+ this.offlineMode = offlineMode;
+ this.context = context;
+ this.customMode = customMode;
+ }
+
+ @Override
+ public Tile getTile(int x, int y, int zoom) {
+ if (!this.customMode) return this.tileProvider.getTile(x, y, zoom);
+
+ byte[] image = null;
+ int maximumZ = this.maximumZ > 0 ? this.maximumZ : Integer.MAX_VALUE;
+
+ if (this.tileSize == 256 && this.doubleTileSize && zoom + 1 <= this.maximumNativeZ && zoom + 1 <= maximumZ) {
+ Log.d("urlTile", "pullTilesFromHigherZoom");
+ image = pullTilesFromHigherZoom(x, y, zoom);
+ }
+
+ if (zoom > this.maximumNativeZ) {
+ Log.d("urlTile", "scaleLowerZoomTile");
+ image = scaleLowerZoomTile(x, y, zoom, this.maximumNativeZ);
+ }
+
+ if (image == null && zoom <= maximumZ) {
+ Log.d("urlTile", "getTileImage");
+ image = getTileImage(x, y, zoom);
+ }
+
+ if (image == null && this.tileCachePath != null && this.offlineMode) {
+ Log.d("urlTile", "findLowerZoomTileForScaling");
+ int zoomLevelToStart = (zoom > this.maximumNativeZ) ? this.maximumNativeZ - 1 : zoom - 1;
+ int minimumZoomToSearch = Math.max(this.minimumZ, zoom - 3);
+ for (int tryZoom = zoomLevelToStart; tryZoom >= minimumZoomToSearch; tryZoom--) {
+ image = scaleLowerZoomTile(x, y, zoom, tryZoom);
+ if (image != null) {
+ break;
+ }
+ }
+ }
+
+ return image == null ? null : new Tile(this.tileSize, this.tileSize, image);
+ }
+
+ byte[] getTileImage(int x, int y, int zoom) {
+ byte[] image = null;
+
+ if (this.tileCachePath != null) {
+ image = readTileImage(x, y, zoom);
+ if (image != null) {
+ Log.d("urlTile", "tile cache HIT for " + zoom +
+ "/" + x + "/" + y);
+ } else {
+ Log.d("urlTile", "tile cache MISS for " + zoom +
+ "/" + x + "/" + y);
+ }
+ if (image != null && !this.offlineMode) {
+ checkForRefresh(x, y, zoom);
+ }
+ }
+
+ if (image == null && !this.offlineMode && this.tileCachePath != null) {
+ String fileName = getTileFilename(x, y, zoom);
+ Constraints constraints = new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build();
+ OneTimeWorkRequest tileRefreshWorkRequest = new OneTimeWorkRequest.Builder(MapTileWorker.class)
+ .setConstraints(constraints)
+ .addTag(fileName)
+ .setInputData(
+ new Data.Builder()
+ .putString("url", getTileUrl(x, y, zoom).toString())
+ .putString("filename", fileName)
+ .putInt("maxAge", -1)
+ .build()
+ )
+ .build();
+ WorkManager workManager = WorkManager.getInstance(this.context.getApplicationContext());
+ Operation fetchOperation = workManager
+ .enqueueUniqueWork(fileName, ExistingWorkPolicy.KEEP, tileRefreshWorkRequest);
+ Future operationFuture = fetchOperation.getResult();
+ try {
+ operationFuture.get(1L, TimeUnit.SECONDS);
+ Thread.sleep(500);
+ Future> fetchFuture = workManager.getWorkInfosByTag(fileName);
+ List workInfo = fetchFuture.get(1L, TimeUnit.SECONDS);
+ Log.d("urlTile: ", workInfo.get(0).toString());
+ if (this.tileCachePath != null) {
+ image = readTileImage(x, y, zoom);
+ if (image != null) {
+ Log.d("urlTile","tile cache fetch HIT for " + zoom +
+ "/" + x + "/" + y);
+ } else {
+ Log.d("urlTile","tile cache fetch MISS for " + zoom +
+ "/" + x + "/" + y);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else if (image == null && !this.offlineMode) {
+ Log.d("urlTile", "Normal fetch");
+ image = fetchTile(x, y, zoom);
+ if (image == null) {
+ Log.d("urlTile", "tile fetch TIMEOUT / FAIL for " + zoom +
+ "/" + x + "/" + y);
+ }
+ }
+
+ return image;
+ }
+
+ byte[] pullTilesFromHigherZoom(int x, int y, int zoom) {
+ byte[] data;
+ Bitmap image = getNewBitmap();
+ Canvas canvas = new Canvas(image);
+ Paint paint = new Paint();
+
+ x = x * 2;
+ y = y * 2;
+ byte[] leftTop = getTileImage(x, y, zoom + 1);
+ byte[] leftBottom = getTileImage(x, y + 1, zoom + 1);
+ byte[] rightTop = getTileImage(x + 1, y, zoom + 1);
+ byte[] rightBottom = getTileImage(x + 1, y + 1, zoom + 1);
+
+ if (leftTop == null || leftBottom == null || rightTop == null || rightBottom == null) {
+ return null;
+ }
+
+ Bitmap bitmap;
+
+ bitmap = BitmapFactory.decodeByteArray(leftTop, 0, leftTop.length);
+ canvas.drawBitmap(bitmap, 0, 0, paint);
+ bitmap.recycle();
+
+ bitmap = BitmapFactory.decodeByteArray(leftBottom, 0, leftBottom.length);
+ canvas.drawBitmap(bitmap, 0, 256, paint);
+ bitmap.recycle();
+
+ bitmap = BitmapFactory.decodeByteArray(rightTop, 0, rightTop.length);
+ canvas.drawBitmap(bitmap, 256, 0, paint);
+ bitmap.recycle();
+
+ bitmap = BitmapFactory.decodeByteArray(rightBottom, 0, rightBottom.length);
+ canvas.drawBitmap(bitmap, 256, 256, paint);
+ bitmap.recycle();
+
+ data = bitmapToByteArray(image);
+ image.recycle();
+ return data;
+ }
+
+ Bitmap getNewBitmap() {
+ Bitmap image = Bitmap.createBitmap(TARGET_TILE_SIZE, TARGET_TILE_SIZE, Bitmap.Config.ARGB_8888);
+ image.eraseColor(Color.TRANSPARENT);
+ return image;
+ }
+
+ byte[] bitmapToByteArray(Bitmap bm) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ bm.compress(Bitmap.CompressFormat.PNG, 100, bos);
+
+ byte[] data = bos.toByteArray();
+ try {
+ bos.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return data;
+ }
+
+ byte[] scaleLowerZoomTile(int x, int y, int zoom, int maximumZoom) {
+ int overZoomLevel = zoom - maximumZoom;
+ int zoomFactor = 1 << overZoomLevel;
+
+ int xParent = x >> overZoomLevel;
+ int yParent = y >> overZoomLevel;
+ int zoomParent = zoom - overZoomLevel;
+
+ int xOffset = x % zoomFactor;
+ int yOffset = y % zoomFactor;
+
+ byte[] data;
+ Bitmap image = getNewBitmap();
+ Canvas canvas = new Canvas(image);
+ Paint paint = new Paint();
+
+ data = getTileImage(xParent, yParent, zoomParent);
+ if (data == null) return null;
+
+ Bitmap sourceImage;
+ sourceImage = BitmapFactory.decodeByteArray(data, 0, data.length);
+
+ int subTileSize = this.tileSize / zoomFactor;
+ Rect sourceRect = new Rect(xOffset * subTileSize, yOffset * subTileSize, xOffset * subTileSize + subTileSize , yOffset * subTileSize + subTileSize);
+ Rect targetRect = new Rect(0,0,TARGET_TILE_SIZE, TARGET_TILE_SIZE);
+ canvas.drawBitmap(sourceImage, sourceRect, targetRect, paint);
+ sourceImage.recycle();
+
+ data = bitmapToByteArray(image);
+ image.recycle();
+ return data;
+ }
+
+ void checkForRefresh(int x, int y, int zoom) {
+ String fileName = getTileFilename(x, y, zoom);
+ File file = new File(fileName);
+ long lastModified = file.lastModified();
+ long now = System.currentTimeMillis();
+
+ if ((now - lastModified) / 1000 > this.tileCacheMaxAge) {
+ Log.d("urlTile", "Refreshing");
+ Constraints constraints = new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build();
+ OneTimeWorkRequest tileRefreshWorkRequest = new OneTimeWorkRequest.Builder(MapTileWorker.class)
+ .setConstraints(constraints)
+ .addTag(fileName)
+ .setInputData(
+ new Data.Builder()
+ .putString("url", getTileUrl(x, y, zoom).toString())
+ .putString("filename", fileName)
+ .putInt("maxAge", this.tileCacheMaxAge)
+ .build()
+ )
+ .build();
+ WorkManager.getInstance(this.context.getApplicationContext())
+ .enqueueUniqueWork(fileName, ExistingWorkPolicy.KEEP, tileRefreshWorkRequest);
+ }
+ }
+
+ byte[] fetchTile(int x, int y, int zoom) {
+ URL url = getTileUrl(x, y, zoom);
+ ByteArrayOutputStream buffer = null;
+ InputStream in = null;
+
+ try {
+ URLConnection conn = url.openConnection();
+ in = conn.getInputStream();
+ buffer = new ByteArrayOutputStream();
+
+ int nRead;
+ byte[] data = new byte[BUFFER_SIZE];
+
+ while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ buffer.flush();
+
+ return buffer.toByteArray();
+ } catch (IOException | OutOfMemoryError e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (in != null) try { in.close(); } catch (Exception ignored) {}
+ if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
+ }
+ }
+
+ byte[] readTileImage(int x, int y, int zoom) {
+ InputStream in = null;
+ ByteArrayOutputStream buffer = null;
+ String fileName = getTileFilename(x, y, zoom);
+ if (fileName == null) {
+ return null;
+ }
+
+ File file = new File(fileName);
+
+ try {
+ in = new FileInputStream(file);
+ buffer = new ByteArrayOutputStream();
+
+ int nRead;
+ byte[] data = new byte[BUFFER_SIZE];
+
+ while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ buffer.flush();
+
+ if (this.tileCacheMaxAge == 0) {
+ file.setLastModified(System.currentTimeMillis());
+ }
+
+ return buffer.toByteArray();
+ } catch (IOException | OutOfMemoryError e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (in != null) try { in.close(); } catch (Exception ignored) {}
+ if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
+ }
+ }
+
+ boolean writeTileImage(byte[] image, int x, int y, int zoom) {
+ OutputStream out = null;
+ String fileName = getTileFilename(x, y, zoom);
+ if (fileName == null) {
+ return false;
+ }
+
+ try {
+ File file = new File(fileName);
+ file.getParentFile().mkdirs();
+ out = new FileOutputStream(file);
+ out.write(image);
+
+ return true;
+ } catch (IOException | OutOfMemoryError e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ if (out != null) try { out.close(); } catch (Exception ignored) {}
+ }
+ }
+
+ String getTileFilename(int x, int y, int zoom) {
+ if (this.tileCachePath == null) {
+ return null;
+ }
+ return this.tileCachePath + '/' + zoom +
+ "/" + x + "/" + y;
+ }
+
+ protected URL getTileUrl(int x, int y, int zoom) {
+ return this.tileProvider.getTileUrl(x, y, zoom);
+ }
+
+ public void setUrlTemplate(String urlTemplate) {
+ if (this.urlTemplate != urlTemplate) {
+ this.tileProvider = new AIRMapUrlTileProvider(tileSize, tileSize, urlTemplate);
+ }
+
+ this.urlTemplate = urlTemplate;
+ }
+
+ public void setTileSize(int tileSize) {
+ if (this.tileSize != tileSize) {
+ this.tileProvider = new AIRMapUrlTileProvider(tileSize, tileSize, urlTemplate);
+ }
+ this.tileSize = tileSize;
+ }
+
+ public void setDoubleTileSize(boolean doubleTileSize) {
+ this.doubleTileSize = doubleTileSize;
+ }
+
+ public void setMaximumZ(int maximumZ) {
+ this.maximumZ = maximumZ;
+ }
+
+ public void setMaximumNativeZ(int maximumNativeZ) {
+ this.maximumNativeZ = maximumNativeZ;
+ }
+
+ public void setMinimumZ(int minimumZ) {
+ this.minimumZ = minimumZ;
+ }
+
+ public void setFlipY(boolean flipY) {
+ this.flipY = flipY;
+ }
+
+ public void setTileCachePath(String tileCachePath) {
+ this.tileCachePath = tileCachePath;
+ }
+
+ public void setTileCacheMaxAge(int tileCacheMaxAge) {
+ this.tileCacheMaxAge = tileCacheMaxAge;
+ }
+
+ public void setOfflineMode(boolean offlineMode) {
+ this.offlineMode = offlineMode;
+ }
+
+ public void setCustomMode() {
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/rnmaps/maps/MapTileWorker.java b/android/src/main/java/com/rnmaps/maps/MapTileWorker.java
new file mode 100644
index 000000000..ed94cb59d
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapTileWorker.java
@@ -0,0 +1,115 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class MapTileWorker extends Worker {
+ private static final int BUFFER_SIZE = 16 * 1024;
+
+ public MapTileWorker(
+ @NonNull Context context,
+ @NonNull WorkerParameters params) {
+ super(context, params);
+ }
+
+ @Override
+ public Result doWork() {
+ byte[] image;
+ URL url;
+ String fileName = getInputData().getString("filename");
+
+ try {
+ int tileCacheMaxAge = getInputData().getInt("maxAge", 0);
+ if (tileCacheMaxAge >= 0) {
+ File file = new File(fileName);
+ long lastModified = file.lastModified();
+ long now = System.currentTimeMillis();
+ if ((now - lastModified) / 1000 < tileCacheMaxAge) return Result.failure();
+ }
+ } catch (Error e) {
+ return Result.failure();
+ }
+
+ try {
+ url = new URL(getInputData().getString("url"));
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+
+ image = fetchTile(url);
+ if (image != null) {
+ boolean success = writeTileImage(image, fileName);
+ if (!success) {
+ return Result.failure();
+ }
+ } else {
+ return Result.retry();
+ }
+
+ // Indicate whether the work finished successfully with the Result
+ Log.d("urlTile", "Worker fetched " + fileName);
+ return Result.success();
+ }
+
+ private byte[] fetchTile(URL url) {
+ ByteArrayOutputStream buffer = null;
+ InputStream in = null;
+
+ try {
+ in = url.openStream();
+ buffer = new ByteArrayOutputStream();
+
+ int nRead;
+ byte[] data = new byte[BUFFER_SIZE];
+
+ while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ buffer.flush();
+
+ return buffer.toByteArray();
+ } catch (IOException | OutOfMemoryError e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (in != null) try { in.close(); } catch (Exception ignored) {}
+ if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
+ }
+ }
+
+ private boolean writeTileImage(byte[] image, String fileName) {
+ OutputStream out = null;
+ if (fileName == null) {
+ return false;
+ }
+
+ try {
+ File file = new File(fileName);
+ file.getParentFile().mkdirs();
+ out = new FileOutputStream(file);
+ out.write(image);
+
+ return true;
+ } catch (IOException | OutOfMemoryError e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ if (out != null) try { out.close(); } catch (Exception ignored) {}
+ }
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapUIBlock.java b/android/src/main/java/com/rnmaps/maps/MapUIBlock.java
new file mode 100644
index 000000000..cceab1a31
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapUIBlock.java
@@ -0,0 +1,64 @@
+package com.rnmaps.maps;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.UIManager;
+import com.facebook.react.fabric.FabricUIManager;
+import com.facebook.react.fabric.interop.UIBlockViewResolver;
+import com.facebook.react.uimanager.common.UIManagerType;
+import com.facebook.react.uimanager.NativeViewHierarchyManager;
+import com.facebook.react.uimanager.UIBlock;
+import com.facebook.react.uimanager.UIManagerHelper;
+import com.facebook.react.uimanager.UIManagerModule;
+
+import java.util.function.Function;
+
+public class MapUIBlock implements UIBlockInterface {
+ private int tag;
+ private Promise promise;
+ private ReactApplicationContext context;
+ private Function mapOperation;
+
+ public MapUIBlock(int tag, Promise promise, ReactApplicationContext context, Function mapOperation) {
+ this.tag = tag;
+ this.promise = promise;
+ this.context = context;
+ this.mapOperation = mapOperation;
+ }
+
+ @Override
+ public void execute(NativeViewHierarchyManager nvhm) {
+ executeImpl(nvhm, null);
+ }
+
+ @Override
+ public void execute(UIBlockViewResolver uiBlockViewResolver) {
+ executeImpl(null, uiBlockViewResolver);
+ }
+
+ private void executeImpl(NativeViewHierarchyManager nvhm, UIBlockViewResolver uiBlockViewResolver) {
+ MapView view = uiBlockViewResolver != null ? (MapView) uiBlockViewResolver.resolveView(tag) : (MapView) nvhm.resolveView(tag);
+ if (view == null) {
+ promise.reject("AirMapView not found");
+ return;
+ }
+ if (view.map == null) {
+ promise.reject("AirMapView.map is not valid");
+ return;
+ }
+
+ mapOperation.apply(view);
+ }
+
+ public void addToUIManager() {
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
+ UIManager uiManager = UIManagerHelper.getUIManager(context, UIManagerType.FABRIC);
+ ((FabricUIManager) uiManager).addUIBlock(this);
+ } else {
+ UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class);
+ uiManager.addUIBlock(this);
+ }
+ }
+}
+
+interface UIBlockInterface extends UIBlock, com.facebook.react.fabric.interop.UIBlock {}
diff --git a/android/src/main/java/com/rnmaps/maps/MapUrlTile.java b/android/src/main/java/com/rnmaps/maps/MapUrlTile.java
new file mode 100644
index 000000000..968d58ab2
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapUrlTile.java
@@ -0,0 +1,207 @@
+package com.rnmaps.maps;
+
+import android.util.Log;
+
+import android.content.Context;
+
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.TileOverlay;
+import com.google.android.gms.maps.model.TileOverlayOptions;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class MapUrlTile extends MapFeature {
+ protected TileOverlayOptions tileOverlayOptions;
+ protected TileOverlay tileOverlay;
+ protected MapTileProvider tileProvider;
+
+ protected String urlTemplate;
+ protected float zIndex;
+ protected float maximumZ;
+ protected float maximumNativeZ = 100;
+ protected float minimumZ;
+ protected boolean flipY = false;
+ protected float tileSize = 256;
+ protected boolean doubleTileSize = false;
+ protected String tileCachePath;
+ protected float tileCacheMaxAge;
+ protected boolean offlineMode = false;
+ protected float opacity = 1;
+ protected Context context;
+ protected boolean customTileProviderNeeded = false;
+
+ public MapUrlTile(Context context) {
+ super(context);
+ this.context = context;
+ }
+
+ public void setUrlTemplate(String urlTemplate) {
+ this.urlTemplate = urlTemplate;
+ if (tileProvider != null) {
+ tileProvider.setUrlTemplate(urlTemplate);
+ }
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setZIndex(float zIndex) {
+ this.zIndex = zIndex;
+ if (tileOverlay != null) {
+ tileOverlay.setZIndex(zIndex);
+ }
+ }
+
+ public void setMaximumZ(float maximumZ) {
+ this.maximumZ = maximumZ;
+ if (tileProvider != null) {
+ tileProvider.setMaximumZ((int)maximumZ);
+ }
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setMaximumNativeZ(float maximumNativeZ) {
+ this.maximumNativeZ = maximumNativeZ;
+ if (tileProvider != null) {
+ tileProvider.setMaximumNativeZ((int)maximumNativeZ);
+ }
+ setCustomTileProviderMode();
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setMinimumZ(float minimumZ) {
+ this.minimumZ = minimumZ;
+ if (tileProvider != null) {
+ tileProvider.setMinimumZ((int)minimumZ);
+ }
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setFlipY(boolean flipY) {
+ this.flipY = flipY;
+ if (tileProvider != null) {
+ tileProvider.setFlipY(flipY);
+ }
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setDoubleTileSize(boolean doubleTileSize) {
+ this.doubleTileSize = doubleTileSize;
+ if (tileProvider != null) {
+ tileProvider.setDoubleTileSize(doubleTileSize);
+ }
+ setCustomTileProviderMode();
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setTileSize(float tileSize) {
+ this.tileSize = tileSize;
+ if (tileProvider != null) {
+ tileProvider.setTileSize((int)tileSize);
+ }
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setTileCachePath(String tileCachePath) {
+ if (tileCachePath == null || tileCachePath.isEmpty()) return;
+
+ try {
+ URL url = new URL(tileCachePath);
+ this.tileCachePath = url.getPath();
+ } catch (MalformedURLException e) {
+ this.tileCachePath = tileCachePath;
+ } catch (Exception e) {
+ return;
+ }
+
+ if (tileProvider != null) {
+ tileProvider.setTileCachePath(tileCachePath);
+ }
+ setCustomTileProviderMode();
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setTileCacheMaxAge(float tileCacheMaxAge) {
+ this.tileCacheMaxAge = tileCacheMaxAge;
+ if (tileProvider != null) {
+ tileProvider.setTileCacheMaxAge((int)tileCacheMaxAge);
+ }
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setOfflineMode(boolean offlineMode) {
+ this.offlineMode = offlineMode;
+ if (tileProvider != null) {
+ tileProvider.setOfflineMode(offlineMode);
+ }
+ if (tileOverlay != null) {
+ tileOverlay.clearTileCache();
+ }
+ }
+
+ public void setOpacity(float opacity) {
+ this.opacity = opacity;
+ if (tileOverlay != null) {
+ tileOverlay.setTransparency(1 - opacity);
+ }
+ }
+
+ public TileOverlayOptions getTileOverlayOptions() {
+ if (tileOverlayOptions == null) {
+ tileOverlayOptions = createTileOverlayOptions();
+ }
+ return tileOverlayOptions;
+ }
+
+ protected void setCustomTileProviderMode() {
+ Log.d("urlTile ", "creating new mode TileProvider");
+ this.customTileProviderNeeded = true;
+ if (tileProvider != null) {
+ tileProvider.setCustomMode();
+ }
+ }
+
+ protected TileOverlayOptions createTileOverlayOptions() {
+ Log.d("urlTile ", "creating TileProvider");
+ TileOverlayOptions options = new TileOverlayOptions();
+ options.zIndex(zIndex);
+ options.transparency(1 - this.opacity);
+ this.tileProvider = new MapTileProvider((int)this.tileSize, this.doubleTileSize, this.urlTemplate,
+ (int)this.maximumZ, (int)this.maximumNativeZ, (int)this.minimumZ, this.flipY, this.tileCachePath,
+ (int)this.tileCacheMaxAge, this.offlineMode, this.context, this.customTileProviderNeeded);
+ options.tileProvider(this.tileProvider);
+ return options;
+ }
+
+ @Override
+ public Object getFeature() {
+ return tileOverlay;
+ }
+
+ @Override
+ public void addToMap(Object map) {
+ this.tileOverlay = ((GoogleMap) map).addTileOverlay(getTileOverlayOptions());
+ }
+
+ @Override
+ public void removeFromMap(Object map) {
+ tileOverlay.remove();
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapUrlTileManager.java b/android/src/main/java/com/rnmaps/maps/MapUrlTileManager.java
new file mode 100644
index 000000000..2f1c573be
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapUrlTileManager.java
@@ -0,0 +1,91 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.annotations.ReactProp;
+
+public class MapUrlTileManager extends ViewGroupManager {
+
+ public MapUrlTileManager(ReactApplicationContext reactContext) {
+ super();
+ DisplayMetrics metrics = new DisplayMetrics();
+ ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay()
+ .getRealMetrics(metrics);
+ }
+
+ @Override
+ public String getName() {
+ return "AIRMapUrlTile";
+ }
+
+ @Override
+ public MapUrlTile createViewInstance(ThemedReactContext context) {
+ return new MapUrlTile(context);
+ }
+
+ @ReactProp(name = "urlTemplate")
+ public void setUrlTemplate(MapUrlTile view, String urlTemplate) {
+ view.setUrlTemplate(urlTemplate);
+ }
+
+ @ReactProp(name = "zIndex", defaultFloat = -1.0f)
+ public void setZIndex(MapUrlTile view, float zIndex) {
+ view.setZIndex(zIndex);
+ }
+
+ @ReactProp(name = "minimumZ", defaultFloat = 0.0f)
+ public void setMinimumZ(MapUrlTile view, float minimumZ) {
+ view.setMinimumZ(minimumZ);
+ }
+
+ @ReactProp(name = "maximumZ", defaultFloat = 100.0f)
+ public void setMaximumZ(MapUrlTile view, float maximumZ) {
+ view.setMaximumZ(maximumZ);
+ }
+
+ @ReactProp(name = "maximumNativeZ", defaultFloat = 100.0f)
+ public void setMaximumNativeZ(MapUrlTile view, float maximumNativeZ) {
+ view.setMaximumNativeZ(maximumNativeZ);
+ }
+
+ @ReactProp(name = "flipY", defaultBoolean = false)
+ public void setFlipY(MapUrlTile view, boolean flipY) {
+ view.setFlipY(flipY);
+ }
+
+ @ReactProp(name = "tileSize", defaultFloat = 256.0f)
+ public void setTileSize(MapUrlTile view, float tileSize) {
+ view.setTileSize(tileSize);
+ }
+
+ @ReactProp(name = "doubleTileSize", defaultBoolean = false)
+ public void setDoubleTileSize(MapUrlTile view, boolean doubleTileSize) {
+ view.setDoubleTileSize(doubleTileSize);
+ }
+
+ @ReactProp(name = "tileCachePath")
+ public void setTileCachePath(MapUrlTile view, String tileCachePath) {
+ view.setTileCachePath(tileCachePath);
+ }
+
+ @ReactProp(name = "tileCacheMaxAge", defaultFloat = 0.0f)
+ public void setTileCacheMaxAge(MapUrlTile view, float tileCacheMaxAge) {
+ view.setTileCacheMaxAge(tileCacheMaxAge);
+ }
+
+ @ReactProp(name = "offlineMode", defaultBoolean = false)
+ public void setOfflineMode(MapUrlTile view, boolean offlineMode) {
+ view.setOfflineMode(offlineMode);
+ }
+
+ @ReactProp(name = "opacity", defaultFloat = 1.0f)
+ public void setOpacity(MapUrlTile view, float opacity) {
+ view.setOpacity(opacity);
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapView.java b/android/src/main/java/com/rnmaps/maps/MapView.java
new file mode 100644
index 000000000..a193a4753
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapView.java
@@ -0,0 +1,1519 @@
+package com.rnmaps.maps;
+
+import static androidx.core.content.PermissionChecker.checkSelfPermission;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.location.Location;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.PermissionChecker;
+import androidx.core.view.GestureDetectorCompat;
+import androidx.core.view.MotionEventCompat;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.LifecycleEventListener;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeArray;
+import com.facebook.react.bridge.WritableNativeMap;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIManagerHelper;
+import com.facebook.react.uimanager.common.UIManagerType;
+import com.facebook.react.uimanager.events.EventDispatcher;
+import com.google.android.gms.maps.CameraUpdate;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.GoogleMapOptions;
+import com.google.android.gms.maps.MapsInitializer;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.Projection;
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.GroundOverlay;
+import com.google.android.gms.maps.model.IndoorBuilding;
+import com.google.android.gms.maps.model.IndoorLevel;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+import com.google.android.gms.maps.model.MapStyleOptions;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.android.gms.maps.model.PointOfInterest;
+import com.google.android.gms.maps.model.Polygon;
+import com.google.android.gms.maps.model.Polyline;
+import com.google.android.gms.maps.model.TileOverlay;
+import com.google.maps.android.collections.CircleManager;
+import com.google.maps.android.collections.GroundOverlayManager;
+import com.google.maps.android.collections.MarkerManager;
+import com.google.maps.android.collections.PolygonManager;
+import com.google.maps.android.collections.PolylineManager;
+import com.google.maps.android.data.kml.KmlContainer;
+import com.google.maps.android.data.kml.KmlLayer;
+import com.google.maps.android.data.kml.KmlPlacemark;
+import com.google.maps.android.data.kml.KmlStyle;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+public class MapView extends com.google.android.gms.maps.MapView implements GoogleMap.InfoWindowAdapter,
+ GoogleMap.OnMarkerDragListener, OnMapReadyCallback, GoogleMap.OnPoiClickListener, GoogleMap.OnIndoorStateChangeListener {
+ public GoogleMap map;
+ private MarkerManager markerManager;
+ private MarkerManager.Collection markerCollection;
+ private PolylineManager polylineManager;
+ private PolylineManager.Collection polylineCollection;
+ private PolygonManager polygonManager;
+ private PolygonManager.Collection polygonCollection;
+ private CircleManager.Collection circleCollection;
+ private GroundOverlayManager groundOverlayManager;
+ private GroundOverlayManager.Collection groundOverlayCollection;
+ private ProgressBar mapLoadingProgressBar;
+ private RelativeLayout mapLoadingLayout;
+ private ImageView cacheImageView;
+ private Boolean isMapLoaded = false;
+ private Integer loadingBackgroundColor = null;
+ private Integer loadingIndicatorColor = null;
+
+ private LatLngBounds boundsToMove;
+ private CameraUpdate cameraToSet;
+ private boolean setPaddingDeferred = false;
+ private boolean showUserLocation = false;
+ private boolean handlePanDrag = false;
+ private boolean moveOnMarkerPress = true;
+ private boolean cacheEnabled = false;
+ private boolean poiClickEnabled = true;
+
+ private ReadableMap initialRegion;
+ private ReadableMap region;
+ private ReadableMap initialCamera;
+ private ReadableMap camera;
+ private String customMapStyleString;
+ private boolean initialRegionSet = false;
+ private boolean initialCameraSet = false;
+ private LatLngBounds cameraLastIdleBounds;
+ private int cameraMoveReason = 0;
+ private MapMarker selectedMarker;
+
+ private static final String[] PERMISSIONS = new String[]{
+ "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"};
+
+ private final List features = new ArrayList<>();
+ private final Map markerMap = new HashMap<>();
+ private final Map polylineMap = new HashMap<>();
+ private final Map polygonMap = new HashMap<>();
+ private final Map overlayMap = new HashMap<>();
+ private final Map heatmapMap = new HashMap<>();
+ private final Map gradientPolylineMap = new HashMap<>();
+ private final GestureDetectorCompat gestureDetector;
+ private final MapManager manager;
+ private LifecycleEventListener lifecycleListener;
+ private boolean paused = false;
+ private boolean destroyed = false;
+ private final ThemedReactContext context;
+ private final EventDispatcher eventDispatcher;
+ private final FusedLocationSource fusedLocationSource;
+
+ private final ViewAttacherGroup attacherGroup;
+ private LatLng tapLocation;
+
+ private static boolean contextHasBug(Context context) {
+ return context == null ||
+ context.getResources() == null ||
+ context.getResources().getConfiguration() == null;
+ }
+
+ // We do this to fix this bug:
+ // https://github.com/react-native-maps/react-native-maps/issues/271
+ //
+ // which conflicts with another bug regarding the passed in context:
+ // https://github.com/react-native-maps/react-native-maps/issues/1147
+ //
+ // Doing this allows us to avoid both bugs.
+ private static Context getNonBuggyContext(ThemedReactContext reactContext,
+ ReactApplicationContext appContext) {
+ Context superContext = reactContext;
+ if (!contextHasBug(appContext.getCurrentActivity())) {
+ superContext = appContext.getCurrentActivity();
+ } else if (contextHasBug(superContext)) {
+ // we have the bug! let's try to find a better context to use
+ if (!contextHasBug(reactContext.getCurrentActivity())) {
+ superContext = reactContext.getCurrentActivity();
+ } else if (!contextHasBug(reactContext.getApplicationContext())) {
+ superContext = reactContext.getApplicationContext();
+ }
+
+ }
+ return superContext;
+ }
+
+ public MapView(ThemedReactContext reactContext, ReactApplicationContext appContext,
+ MapManager manager,
+ GoogleMapOptions googleMapOptions) {
+ super(getNonBuggyContext(reactContext, appContext), googleMapOptions);
+
+ this.manager = manager;
+ this.context = reactContext;
+ MapsInitializer.initialize(context, this.manager.renderer, renderer -> Log.d("AirMapRenderer", renderer.toString()));
+ super.onCreate(null);
+ super.onResume();
+ super.getMapAsync(this);
+
+ final MapView view = this;
+
+ fusedLocationSource = new FusedLocationSource(context);
+
+ gestureDetector =
+ new GestureDetectorCompat(reactContext, new GestureDetector.SimpleOnGestureListener() {
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ if (handlePanDrag) {
+ onPanDrag(e2);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent ev) {
+ onDoublePress(ev);
+ return false;
+ }
+ });
+
+ this.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+ @Override public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if (!paused) {
+ MapView.this.cacheView();
+ }
+ }
+ });
+
+ int uiManagerType = UIManagerType.DEFAULT;
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
+ uiManagerType = UIManagerType.FABRIC;
+ }
+
+ eventDispatcher = UIManagerHelper
+ .getUIManager(reactContext, uiManagerType)
+ .getEventDispatcher();
+
+ // Set up a parent view for triggering visibility in subviews that depend on it.
+ // Mainly ReactImageView depends on Fresco which depends on onVisibilityChanged() event
+ attacherGroup = new ViewAttacherGroup(context);
+ LayoutParams attacherLayoutParams = new LayoutParams(0, 0);
+ attacherLayoutParams.width = 0;
+ attacherLayoutParams.height = 0;
+ attacherLayoutParams.leftMargin = 99999999;
+ attacherLayoutParams.topMargin = 99999999;
+ attacherGroup.setLayoutParams(attacherLayoutParams);
+ addView(attacherGroup);
+ }
+
+ @Override
+ public void onMapReady(@NonNull final GoogleMap map) {
+ if (destroyed) {
+ return;
+ }
+ this.map = map;
+
+ markerManager = new MarkerManager(map);
+ markerCollection = markerManager.newCollection();
+ polylineManager = new PolylineManager(map);
+ polylineCollection = polylineManager.newCollection();
+ polygonManager = new PolygonManager(map);
+ polygonCollection = polygonManager.newCollection();
+ CircleManager circleManager = new CircleManager(map);
+ circleCollection = circleManager.newCollection();
+ groundOverlayManager = new GroundOverlayManager(map);
+ groundOverlayCollection = groundOverlayManager.newCollection();
+
+ markerCollection.setInfoWindowAdapter(this);
+ markerCollection.setOnMarkerDragListener(this);
+ this.map.setOnIndoorStateChangeListener(this);
+
+ applyBridgedProps();
+
+ manager.pushEvent(context, this, "onMapReady", new WritableNativeMap());
+
+ final MapView view = this;
+
+ map.setOnMyLocationChangeListener(new GoogleMap.OnMyLocationChangeListener() {
+ @Override
+ public void onMyLocationChange(Location location){
+ WritableMap event = new WritableNativeMap();
+
+ WritableMap coordinate = new WritableNativeMap();
+ coordinate.putDouble("latitude", location.getLatitude());
+ coordinate.putDouble("longitude", location.getLongitude());
+ coordinate.putDouble("altitude", location.getAltitude());
+ coordinate.putDouble("timestamp", location.getTime());
+ coordinate.putDouble("accuracy", location.getAccuracy());
+ coordinate.putDouble("speed", location.getSpeed());
+ coordinate.putDouble("heading", location.getBearing());
+ coordinate.putBoolean("isFromMockProvider", location.isFromMockProvider());
+
+ event.putMap("coordinate", coordinate);
+
+ manager.pushEvent(context, view, "onUserLocationChange", event);
+ }
+ });
+
+ markerCollection.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
+ @Override
+ public boolean onMarkerClick(@NonNull Marker marker) {
+ MapMarker airMapMarker = getMarkerMap(marker);
+
+ WritableMap event = makeClickEventData(marker.getPosition());
+ event.putString("action", "marker-press");
+ event.putString("id", airMapMarker.getIdentifier());
+ manager.pushEvent(context, view, "onMarkerPress", event);
+
+ event = makeClickEventData(marker.getPosition());
+ event.putString("action", "marker-press");
+ event.putString("id", airMapMarker.getIdentifier());
+ manager.pushEvent(context, airMapMarker, "onPress", event);
+
+ handleMarkerSelection(airMapMarker);
+
+ // Return false to open the callout info window and center on the marker
+ // https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap
+ // .OnMarkerClickListener
+ if (view.moveOnMarkerPress) {
+ return false;
+ } else {
+ marker.showInfoWindow();
+ return true;
+ }
+ }
+ });
+
+ polygonCollection.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() {
+ @Override
+ public void onPolygonClick(@NonNull Polygon polygon) {
+ WritableMap event = makeClickEventData(tapLocation);
+ event.putString("action", "polygon-press");
+ manager.pushEvent(context, polygonMap.get(polygon), "onPress", event);
+ }
+ });
+
+ polylineCollection.setOnPolylineClickListener(new GoogleMap.OnPolylineClickListener() {
+ @Override
+ public void onPolylineClick(@NonNull Polyline polyline) {
+ WritableMap event = makeClickEventData(tapLocation);
+ event.putString("action", "polyline-press");
+ manager.pushEvent(context, polylineMap.get(polyline), "onPress", event);
+ }
+ });
+
+ markerCollection.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() {
+ @Override
+ public void onInfoWindowClick(@NonNull Marker marker) {
+ WritableMap event = makeClickEventData(marker.getPosition());
+ event.putString("action", "callout-press");
+ manager.pushEvent(context, view, "onCalloutPress", event);
+
+ event = makeClickEventData(marker.getPosition());
+ event.putString("action", "callout-press");
+ MapMarker markerView = getMarkerMap(marker);
+ manager.pushEvent(context, markerView, "onCalloutPress", event);
+
+ event = makeClickEventData(marker.getPosition());
+ event.putString("action", "callout-press");
+ MapCallout infoWindow = markerView.getCalloutView();
+ if (infoWindow != null) manager.pushEvent(context, infoWindow, "onPress", event);
+ }
+ });
+
+ map.setOnMapClickListener(new GoogleMap.OnMapClickListener() {
+ @Override
+ public void onMapClick(@NonNull LatLng point) {
+ WritableMap event = makeClickEventData(point);
+ event.putString("action", "press");
+ manager.pushEvent(context, view, "onPress", event);
+
+ handleMarkerSelection(null);
+ }
+ });
+
+ map.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() {
+ @Override
+ public void onMapLongClick(@NonNull LatLng point) {
+ WritableMap event = makeClickEventData(point);
+ event.putString("action", "long-press");
+ manager.pushEvent(context, view, "onLongPress", makeClickEventData(point));
+ }
+ });
+
+ groundOverlayCollection.setOnGroundOverlayClickListener(new GoogleMap.OnGroundOverlayClickListener() {
+ @Override
+ public void onGroundOverlayClick(@NonNull GroundOverlay groundOverlay) {
+ WritableMap event = makeClickEventData(groundOverlay.getPosition());
+ event.putString("action", "overlay-press");
+ manager.pushEvent(context, overlayMap.get(groundOverlay), "onPress", event);
+ }
+ });
+
+ map.setOnCameraMoveStartedListener(new GoogleMap.OnCameraMoveStartedListener() {
+ @Override
+ public void onCameraMoveStarted(int reason) {
+ cameraMoveReason = reason;
+ boolean isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == reason;
+ WritableMap event = new WritableNativeMap();
+ event.putBoolean("isGesture", isGesture);
+ manager.pushEvent(context, view, "onRegionChangeStart", event);
+ }
+ });
+
+ map.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
+ @Override
+ public void onCameraMove() {
+ LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds;
+
+ cameraLastIdleBounds = null;
+ boolean isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason;
+
+ RegionChangeEvent event = new RegionChangeEvent(getId(), bounds, true, isGesture);
+ eventDispatcher.dispatchEvent(event);
+ }
+ });
+
+ map.setOnCameraIdleListener(new GoogleMap.OnCameraIdleListener() {
+ @Override
+ public void onCameraIdle() {
+ LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds;
+ if ((cameraMoveReason != 0) &&
+ ((cameraLastIdleBounds == null) ||
+ LatLngBoundsUtils.BoundsAreDifferent(bounds, cameraLastIdleBounds))) {
+
+ cameraLastIdleBounds = bounds;
+ boolean isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason;
+
+ RegionChangeEvent event = new RegionChangeEvent(getId(), bounds, false, isGesture);
+ eventDispatcher.dispatchEvent(event);
+ }
+ }
+ });
+
+ map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
+ @Override public void onMapLoaded() {
+ isMapLoaded = true;
+ manager.pushEvent(context, view, "onMapLoaded", new WritableNativeMap());
+ MapView.this.cacheView();
+ }
+ });
+
+ // We need to be sure to disable location-tracking when app enters background, in-case some
+ // other module
+ // has acquired a wake-lock and is controlling location-updates, otherwise, location-manager
+ // will be left
+ // updating location constantly, killing the battery, even though some other location-mgmt
+ // module may
+ // desire to shut-down location-services.
+ lifecycleListener = new LifecycleEventListener() {
+ @Override
+ public void onHostResume() {
+ if (hasPermissions() && map != null) {
+ //noinspection MissingPermission
+ map.setMyLocationEnabled(showUserLocation);
+ map.setLocationSource(fusedLocationSource);
+ }
+ synchronized (MapView.this) {
+ if (!destroyed) {
+ MapView.this.onResume();
+ }
+ paused = false;
+ }
+ }
+
+ @Override
+ public void onHostPause() {
+ if (hasPermissions() && map != null) {
+ //noinspection MissingPermission
+ map.setMyLocationEnabled(false);
+ }
+ synchronized (MapView.this) {
+ if (!destroyed) {
+ MapView.this.onPause();
+ }
+ paused = true;
+ }
+ }
+
+ @Override
+ public void onHostDestroy() {
+ MapView.this.doDestroy();
+ }
+ };
+
+ context.addLifecycleEventListener(lifecycleListener);
+ }
+
+ private synchronized void handleMarkerSelection(MapMarker target) {
+ if (selectedMarker == target) {
+ return;
+ }
+
+ WritableMap event;
+
+ if (selectedMarker != null) {
+ event = makeClickEventData(selectedMarker.getPosition());
+ event.putString("action", "marker-deselect");
+ event.putString("id", selectedMarker.getIdentifier());
+ manager.pushEvent(context, selectedMarker, "onDeselect", event);
+
+ event = makeClickEventData(selectedMarker.getPosition());
+ event.putString("action", "marker-deselect");
+ event.putString("id", selectedMarker.getIdentifier());
+ manager.pushEvent(context, this, "onMarkerDeselect", event);
+ }
+
+ if (target != null) {
+ event = makeClickEventData(target.getPosition());
+ event.putString("action", "marker-select");
+ event.putString("id", target.getIdentifier());
+ manager.pushEvent(context, target, "onSelect", event);
+
+ event = makeClickEventData(target.getPosition());
+ event.putString("action", "marker-select");
+ event.putString("id", target.getIdentifier());
+ manager.pushEvent(context, this, "onMarkerSelect", event);
+ }
+
+ selectedMarker = target;
+ }
+
+ private boolean hasPermissions() {
+ return checkSelfPermission(getContext(), PERMISSIONS[0]) == PermissionChecker.PERMISSION_GRANTED ||
+ checkSelfPermission(getContext(), PERMISSIONS[1]) == PermissionChecker.PERMISSION_GRANTED;
+ }
+
+
+ /*
+ onDestroy is final method so I can't override it.
+ */
+ public synchronized void doDestroy() {
+ if (destroyed) {
+ return;
+ }
+ destroyed = true;
+
+ if (lifecycleListener != null && context != null) {
+ context.removeLifecycleEventListener(lifecycleListener);
+ lifecycleListener = null;
+ }
+ if (!paused) {
+ onPause();
+ paused = true;
+ }
+ onDestroy();
+ }
+
+ public void setInitialRegion(ReadableMap initialRegion) {
+ this.initialRegion = initialRegion;
+ // Theoretically onMapReady might be called before setInitialRegion
+ // In that case, trigger moveToRegion manually
+ if (!initialRegionSet && map != null) {
+ moveToRegion(initialRegion);
+ initialRegionSet = true;
+ }
+ }
+
+ public void setInitialCamera(ReadableMap initialCamera) {
+ this.initialCamera = initialCamera;
+ if (!initialCameraSet && map != null) {
+ moveToCamera(initialCamera);
+ initialCameraSet = true;
+ }
+ }
+
+ private void applyBridgedProps() {
+ if(initialRegion != null) {
+ moveToRegion(initialRegion);
+ initialRegionSet = true;
+ } else if(region != null) {
+ moveToRegion(region);
+ } else if (initialCamera != null) {
+ moveToCamera(initialCamera);
+ initialCameraSet = true;
+ } else if (camera != null) {
+ moveToCamera(camera);
+ }
+ if(customMapStyleString != null) {
+ map.setMapStyle(new MapStyleOptions(customMapStyleString));
+ }
+ this.setPoiClickEnabled(poiClickEnabled);
+ }
+
+ private void moveToRegion(ReadableMap region) {
+ if (region == null) return;
+
+ double lng = region.getDouble("longitude");
+ double lat = region.getDouble("latitude");
+ double lngDelta = region.getDouble("longitudeDelta");
+ double latDelta = region.getDouble("latitudeDelta");
+ LatLngBounds bounds = new LatLngBounds(
+ new LatLng(lat - latDelta / 2, lng - lngDelta / 2), // southwest
+ new LatLng(lat + latDelta / 2, lng + lngDelta / 2) // northeast
+ );
+ if (super.getHeight() <= 0 || super.getWidth() <= 0) {
+ // in this case, our map has not been laid out yet, so we save the bounds in a local
+ // variable, and make a guess of zoomLevel 10. Not to worry, though: as soon as layout
+ // occurs, we will move the camera to the saved bounds. Note that if we tried to move
+ // to the bounds now, it would trigger an exception.
+ map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10));
+ boundsToMove = bounds;
+ } else {
+ map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0));
+ boundsToMove = null;
+ }
+ }
+
+ public void setRegion(ReadableMap region) {
+ this.region = region;
+ if(region != null && map != null) {
+ moveToRegion(region);
+ }
+ }
+
+ public void setCamera(ReadableMap camera) {
+ this.camera = camera;
+ if(camera != null && map != null) {
+ moveToCamera(camera);
+ }
+ }
+public static CameraPosition cameraPositionFromMap(ReadableMap camera){
+ if (camera == null) return null;
+
+ CameraPosition.Builder builder = new CameraPosition.Builder();
+
+ ReadableMap center = camera.getMap("center");
+ if (center != null) {
+ double lng = center.getDouble("longitude");
+ double lat = center.getDouble("latitude");
+ builder.target(new LatLng(lat, lng));
+ }
+
+ builder.tilt((float)camera.getDouble("pitch"));
+ builder.bearing((float)camera.getDouble("heading"));
+ builder.zoom((float)camera.getDouble("zoom"));
+
+ return builder.build();
+}
+ public void moveToCamera(ReadableMap cameraMap) {
+ CameraPosition camera = cameraPositionFromMap(cameraMap);
+ if (camera == null) return;
+ CameraUpdate update = CameraUpdateFactory.newCameraPosition(camera);
+
+ if (super.getHeight() <= 0 || super.getWidth() <= 0) {
+ // in this case, our map has not been laid out yet, so we save the camera update in a
+ // local variable. As soon as layout occurs, we will move the camera to the saved update.
+ // Note that if we tried to move to the camera now, it would trigger an exception.
+ cameraToSet = update;
+ } else {
+ map.moveCamera(update);
+ cameraToSet = null;
+ }
+ }
+
+ public void setMapStyle(@Nullable String customMapStyleString) {
+ this.customMapStyleString = customMapStyleString;
+ if(map != null && customMapStyleString != null) {
+ map.setMapStyle(new MapStyleOptions(customMapStyleString));
+ }
+ }
+
+ public void setShowsUserLocation(boolean showUserLocation) {
+ this.showUserLocation = showUserLocation; // hold onto this for lifecycle handling
+ if (hasPermissions()) {
+ map.setLocationSource(fusedLocationSource);
+ //noinspection MissingPermission
+ map.setMyLocationEnabled(showUserLocation);
+ }
+ }
+
+ public void setUserLocationPriority(int priority){
+ fusedLocationSource.setPriority(priority);
+ }
+
+ public void setUserLocationUpdateInterval(int interval){
+ fusedLocationSource.setInterval(interval);
+ }
+
+ public void setUserLocationFastestInterval(int interval){
+ fusedLocationSource.setFastestInterval(interval);
+ }
+
+ public void setShowsMyLocationButton(boolean showMyLocationButton) {
+ if (hasPermissions() || !showMyLocationButton) {
+ map.getUiSettings().setMyLocationButtonEnabled(showMyLocationButton);
+ }
+ }
+
+ public void setToolbarEnabled(boolean toolbarEnabled) {
+ if (hasPermissions() || !toolbarEnabled) {
+ map.getUiSettings().setMapToolbarEnabled(toolbarEnabled);
+ }
+ }
+
+ public void setCacheEnabled(boolean cacheEnabled) {
+ this.cacheEnabled = cacheEnabled;
+ this.cacheView();
+ }
+
+ public void setPoiClickEnabled(boolean poiClickEnabled) {
+ this.poiClickEnabled = poiClickEnabled;
+ map.setOnPoiClickListener(poiClickEnabled ? this : null);
+ }
+
+ public void enableMapLoading(boolean loadingEnabled) {
+ if (loadingEnabled && !this.isMapLoaded) {
+ this.getMapLoadingLayoutView().setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setMoveOnMarkerPress(boolean moveOnPress) {
+ this.moveOnMarkerPress = moveOnPress;
+ }
+
+ public void setLoadingBackgroundColor(Integer loadingBackgroundColor) {
+ this.loadingBackgroundColor = loadingBackgroundColor;
+
+ if (this.mapLoadingLayout != null) {
+ if (loadingBackgroundColor == null) {
+ this.mapLoadingLayout.setBackgroundColor(Color.WHITE);
+ } else {
+ this.mapLoadingLayout.setBackgroundColor(this.loadingBackgroundColor);
+ }
+ }
+ }
+
+ public void setLoadingIndicatorColor(Integer loadingIndicatorColor) {
+ this.loadingIndicatorColor = loadingIndicatorColor;
+ if (this.mapLoadingProgressBar != null) {
+ Integer color = loadingIndicatorColor;
+ if (color == null) {
+ color = Color.parseColor("#606060");
+ }
+
+ ColorStateList progressTintList = ColorStateList.valueOf(loadingIndicatorColor);
+ ColorStateList secondaryProgressTintList = ColorStateList.valueOf(loadingIndicatorColor);
+ ColorStateList indeterminateTintList = ColorStateList.valueOf(loadingIndicatorColor);
+
+ this.mapLoadingProgressBar.setProgressTintList(progressTintList);
+ this.mapLoadingProgressBar.setSecondaryProgressTintList(secondaryProgressTintList);
+ this.mapLoadingProgressBar.setIndeterminateTintList(indeterminateTintList);
+ }
+ }
+
+ public void setHandlePanDrag(boolean handlePanDrag) {
+ this.handlePanDrag = handlePanDrag;
+ }
+
+ public void addFeature(View child, int index) {
+ // Our desired API is to pass up annotations/overlays as children to the mapview component.
+ // This is where we intercept them and do the appropriate underlying mapview action.
+ if (child instanceof MapMarker) {
+ MapMarker annotation = (MapMarker) child;
+ annotation.addToMap(markerCollection);
+ features.add(index, annotation);
+
+ // Allow visibility event to be triggered later
+ int visibility = annotation.getVisibility();
+ annotation.setVisibility(INVISIBLE);
+
+ // Remove from a view group if already present, prevent "specified child
+ // already had a parent" error.
+ ViewGroup annotationParent = (ViewGroup)annotation.getParent();
+ if (annotationParent != null) {
+ annotationParent.removeView(annotation);
+ }
+
+ // Add to the parent group
+ attacherGroup.addView(annotation);
+
+ // Trigger visibility event if necessary.
+ // With some testing, seems like it is not always
+ // triggered just by being added to a parent view.
+ annotation.setVisibility(visibility);
+
+ Marker marker = (Marker) annotation.getFeature();
+ markerMap.put(marker, annotation);
+ } else if (child instanceof MapPolyline) {
+ MapPolyline polylineView = (MapPolyline) child;
+ polylineView.addToMap(polylineCollection);
+ features.add(index, polylineView);
+ Polyline polyline = (Polyline) polylineView.getFeature();
+ polylineMap.put(polyline, polylineView);
+ } else if (child instanceof MapGradientPolyline) {
+ MapGradientPolyline polylineView = (MapGradientPolyline) child;
+ polylineView.addToMap(map);
+ features.add(index, polylineView);
+ TileOverlay tileOverlay = (TileOverlay) polylineView.getFeature();
+ gradientPolylineMap.put(tileOverlay, polylineView);
+ } else if (child instanceof MapPolygon) {
+ MapPolygon polygonView = (MapPolygon) child;
+ polygonView.addToMap(polygonCollection);
+ features.add(index, polygonView);
+ Polygon polygon = (Polygon) polygonView.getFeature();
+ polygonMap.put(polygon, polygonView);
+ } else if (child instanceof MapCircle) {
+ MapCircle circleView = (MapCircle) child;
+ circleView.addToMap(circleCollection);
+ features.add(index, circleView);
+ } else if (child instanceof MapUrlTile) {
+ MapUrlTile urlTileView = (MapUrlTile) child;
+ urlTileView.addToMap(map);
+ features.add(index, urlTileView);
+ } else if (child instanceof MapWMSTile) {
+ MapWMSTile urlTileView = (MapWMSTile) child;
+ urlTileView.addToMap(map);
+ features.add(index, urlTileView);
+ } else if (child instanceof MapLocalTile) {
+ MapLocalTile localTileView = (MapLocalTile) child;
+ localTileView.addToMap(map);
+ features.add(index, localTileView);
+ } else if (child instanceof MapOverlay) {
+ MapOverlay overlayView = (MapOverlay) child;
+ overlayView.addToMap(groundOverlayCollection);
+ features.add(index, overlayView);
+ GroundOverlay overlay = (GroundOverlay) overlayView.getFeature();
+ overlayMap.put(overlay, overlayView);
+ } else if (child instanceof MapHeatmap) {
+ MapHeatmap heatmapView = (MapHeatmap) child;
+ heatmapView.addToMap(map);
+ features.add(index, heatmapView);
+ TileOverlay heatmap = (TileOverlay)heatmapView.getFeature();
+ heatmapMap.put(heatmap, heatmapView);
+ } else if (child instanceof ViewGroup) {
+ ViewGroup children = (ViewGroup) child;
+ for (int i = 0; i < children.getChildCount(); i++) {
+ addFeature(children.getChildAt(i), index);
+ }
+ } else {
+ addView(child, index);
+ }
+ }
+
+ public int getFeatureCount() {
+ return features.size();
+ }
+
+ public View getFeatureAt(int index) {
+ return features.get(index);
+ }
+
+ public void removeFeatureAt(int index) {
+ MapFeature feature = features.remove(index);
+ if (feature instanceof MapMarker) {
+ markerMap.remove(feature.getFeature());
+ feature.removeFromMap(markerCollection);
+ attacherGroup.removeView(feature);
+ } else if (feature instanceof MapHeatmap) {
+ heatmapMap.remove(feature.getFeature());
+ feature.removeFromMap(map);
+ } else if(feature instanceof MapCircle) {
+ feature.removeFromMap(circleCollection);
+ } else if(feature instanceof MapOverlay) {
+ feature.removeFromMap(groundOverlayCollection);
+ } else if(feature instanceof MapPolygon) {
+ feature.removeFromMap(polygonCollection);
+ } else if(feature instanceof MapPolyline) {
+ feature.removeFromMap(polylineCollection);
+ } else {
+ feature.removeFromMap(map);
+ }
+ }
+
+ public WritableMap makeClickEventData(LatLng point) {
+ WritableMap event = new WritableNativeMap();
+
+ WritableMap coordinate = new WritableNativeMap();
+ coordinate.putDouble("latitude", point.latitude);
+ coordinate.putDouble("longitude", point.longitude);
+ event.putMap("coordinate", coordinate);
+
+ Projection projection = map.getProjection();
+ Point screenPoint = projection.toScreenLocation(point);
+
+ WritableMap position = new WritableNativeMap();
+ position.putDouble("x", screenPoint.x);
+ position.putDouble("y", screenPoint.y);
+ event.putMap("position", position);
+
+ return event;
+ }
+
+ public void updateExtraData(Object extraData) {
+ if (setPaddingDeferred && super.getHeight() > 0 && super.getWidth() > 0) {
+ CameraUpdate cu = CameraUpdateFactory.newCameraPosition(map.getCameraPosition());
+
+ map.setPadding(edgeLeftPadding + baseLeftMapPadding,
+ edgeTopPadding + baseTopMapPadding,
+ edgeRightPadding + baseRightMapPadding,
+ edgeBottomPadding + baseBottomMapPadding);
+ map.moveCamera(cu);
+
+ // Move the google logo to the default base padding value.
+ map.setPadding(baseLeftMapPadding, baseTopMapPadding, baseRightMapPadding, baseBottomMapPadding);
+
+ setPaddingDeferred = false;
+ }
+
+ // if boundsToMove is not null, we now have the MapView's width/height, so we can apply
+ // a proper camera move
+ if (boundsToMove != null) {
+ HashMap data = (HashMap) extraData;
+ int width = data.get("width") == null ? 0 : data.get("width").intValue();
+ int height = data.get("height") == null ? 0 : data.get("height").intValue();
+
+ //fix for https://github.com/react-native-maps/react-native-maps/issues/245,
+ //it's not guaranteed the passed-in height and width would be greater than 0.
+ if (width <= 0 || height <= 0) {
+ map.moveCamera(CameraUpdateFactory.newLatLngBounds(boundsToMove, 0));
+ } else {
+ map.moveCamera(CameraUpdateFactory.newLatLngBounds(boundsToMove, width, height, 0));
+ }
+
+ boundsToMove = null;
+ cameraToSet = null;
+ }
+ else if (cameraToSet != null) {
+ map.moveCamera(cameraToSet);
+ cameraToSet = null;
+ }
+ }
+
+ public void animateToCamera(ReadableMap camera, int duration) {
+ if (map == null) return;
+ CameraPosition.Builder builder = new CameraPosition.Builder(map.getCameraPosition());
+ if (camera.hasKey("zoom")) {
+ builder.zoom((float)camera.getDouble("zoom"));
+ }
+ if (camera.hasKey("heading")) {
+ builder.bearing((float)camera.getDouble("heading"));
+ }
+ if (camera.hasKey("pitch")) {
+ builder.tilt((float)camera.getDouble("pitch"));
+ }
+ if (camera.hasKey("center")) {
+ ReadableMap center = camera.getMap("center");
+ builder.target(new LatLng(center.getDouble("latitude"), center.getDouble("longitude")));
+ }
+
+ CameraUpdate update = CameraUpdateFactory.newCameraPosition(builder.build());
+
+ if (duration <= 0) {
+ map.moveCamera(update);
+ }
+ else {
+ map.animateCamera(update, duration, null);
+ }
+ }
+
+ public void animateToRegion(LatLngBounds bounds, int duration) {
+ if (map == null) return;
+ if(duration <= 0) {
+ map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0));
+ } else {
+ map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0), duration, null);
+ }
+ }
+
+ public void fitToElements(ReadableMap edgePadding, boolean animated) {
+ if (map == null) return;
+
+ LatLngBounds.Builder builder = new LatLngBounds.Builder();
+
+ boolean addedPosition = false;
+
+ for (MapFeature feature : features) {
+ if (feature instanceof MapMarker) {
+ Marker marker = (Marker) feature.getFeature();
+ builder.include(marker.getPosition());
+ addedPosition = true;
+ }
+ // TODO(lmr): may want to include shapes / etc.
+ }
+ if (addedPosition) {
+ LatLngBounds bounds = builder.build();
+ CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, 0);
+
+ if (edgePadding != null) {
+ appendMapPadding(edgePadding.getInt("left"), edgePadding.getInt("top"),
+ edgePadding.getInt("right"), edgePadding.getInt("bottom"));
+ }
+
+ if (animated) {
+ map.animateCamera(cu);
+ } else {
+ map.moveCamera(cu);
+ }
+ // Move the google logo to the default base padding value.
+ map.setPadding(baseLeftMapPadding, baseTopMapPadding, baseRightMapPadding, baseBottomMapPadding);
+ }
+ }
+
+ public void fitToSuppliedMarkers(ReadableArray markerIDsArray, ReadableMap edgePadding, boolean animated) {
+ if (map == null) return;
+
+ LatLngBounds.Builder builder = new LatLngBounds.Builder();
+
+ String[] markerIDs = new String[markerIDsArray.size()];
+ for (int i = 0; i < markerIDsArray.size(); i++) {
+ markerIDs[i] = markerIDsArray.getString(i);
+ }
+
+ boolean addedPosition = false;
+
+ List markerIDList = Arrays.asList(markerIDs);
+
+ for (MapFeature feature : features) {
+ if (feature instanceof MapMarker) {
+ String identifier = ((MapMarker) feature).getIdentifier();
+ Marker marker = (Marker) feature.getFeature();
+ if (markerIDList.contains(identifier)) {
+ builder.include(marker.getPosition());
+ addedPosition = true;
+ }
+ }
+ }
+
+ if (addedPosition) {
+ LatLngBounds bounds = builder.build();
+ CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, 0);
+
+ if (edgePadding != null) {
+ appendMapPadding(edgePadding.getInt("left"), edgePadding.getInt("top"),
+ edgePadding.getInt("right"), edgePadding.getInt("bottom"));
+ }
+
+ if (animated) {
+ map.animateCamera(cu);
+ } else {
+ map.moveCamera(cu);
+ }
+ // Move the google logo to the default base padding value.
+ map.setPadding(baseLeftMapPadding, baseTopMapPadding, baseRightMapPadding, baseBottomMapPadding);
+ }
+ }
+
+ // padding configured by 'mapPadding' property
+ int baseLeftMapPadding;
+ int baseRightMapPadding;
+ int baseTopMapPadding;
+ int baseBottomMapPadding;
+ // extra padding specified by 'edgePadding' option of fitToElements/fitToSuppliedMarkers/fitToCoordinates
+ int edgeLeftPadding;
+ int edgeRightPadding;
+ int edgeTopPadding;
+ int edgeBottomPadding;
+
+ public void applyBaseMapPadding(int left, int top, int right, int bottom){
+ if (super.getHeight() <= 0 || super.getWidth() <= 0) {
+ // the map is not laid out yet and calling setPadding() now has no effect
+ baseLeftMapPadding = left;
+ baseRightMapPadding = right;
+ baseTopMapPadding = top;
+ baseBottomMapPadding = bottom;
+ setPaddingDeferred = true;
+ return;
+ }
+
+ // retrieve current camera with current edge paddings configured
+ map.setPadding(edgeLeftPadding + baseLeftMapPadding,
+ edgeTopPadding + baseTopMapPadding,
+ edgeRightPadding + baseRightMapPadding,
+ edgeBottomPadding + baseBottomMapPadding);
+ CameraUpdate cu = CameraUpdateFactory.newCameraPosition(map.getCameraPosition());
+
+ baseLeftMapPadding = left;
+ baseRightMapPadding = right;
+ baseTopMapPadding = top;
+ baseBottomMapPadding = bottom;
+
+ // apply base paddings and restore center position of the map
+ map.setPadding(edgeLeftPadding + left,
+ edgeTopPadding + top,
+ edgeRightPadding + right,
+ edgeBottomPadding + bottom);
+ map.moveCamera(cu);
+
+ // Move the google logo to the default base padding value.
+ map.setPadding(left, top, right, bottom);
+ }
+
+ public void fitToCoordinates(ReadableArray coordinatesArray, ReadableMap edgePadding,
+ boolean animated) {
+ if (map == null) return;
+
+ LatLngBounds.Builder builder = new LatLngBounds.Builder();
+
+ for (int i = 0; i < coordinatesArray.size(); i++) {
+ ReadableMap latLng = coordinatesArray.getMap(i);
+ double lat = latLng.getDouble("latitude");
+ double lng = latLng.getDouble("longitude");
+ builder.include(new LatLng(lat, lng));
+ }
+
+ LatLngBounds bounds = builder.build();
+ CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, 0);
+
+ if (edgePadding != null) {
+ appendMapPadding(edgePadding.getInt("left"), edgePadding.getInt("top"), edgePadding.getInt("right"), edgePadding.getInt("bottom"));
+ }
+
+ if (animated) {
+ map.animateCamera(cu);
+ } else {
+ map.moveCamera(cu);
+ }
+ // Move the google logo to the default base padding value.
+ map.setPadding(baseLeftMapPadding, baseTopMapPadding, baseRightMapPadding, baseBottomMapPadding);
+ }
+
+ private void appendMapPadding(int iLeft,int iTop, int iRight, int iBottom) {
+ double density = getResources().getDisplayMetrics().density;
+
+ edgeLeftPadding = (int) (iLeft * density);
+ edgeTopPadding = (int) (iTop * density);
+ edgeRightPadding = (int) (iRight * density);
+ edgeBottomPadding = (int) (iBottom * density);
+
+ map.setPadding(edgeLeftPadding + baseLeftMapPadding,
+ edgeTopPadding + baseTopMapPadding,
+ edgeRightPadding + baseRightMapPadding,
+ edgeBottomPadding + baseBottomMapPadding);
+ }
+
+ public double[][] getMapBoundaries() {
+ LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds;
+ LatLng northEast = bounds.northeast;
+ LatLng southWest = bounds.southwest;
+
+ return new double[][] {
+ {northEast.longitude, northEast.latitude},
+ {southWest.longitude, southWest.latitude}
+ };
+ }
+
+ public void setMapBoundaries(ReadableMap northEast, ReadableMap southWest) {
+ if (map == null) return;
+
+ LatLngBounds.Builder builder = new LatLngBounds.Builder();
+
+ double latNE = northEast.getDouble("latitude");
+ double lngNE = northEast.getDouble("longitude");
+ builder.include(new LatLng(latNE, lngNE));
+
+ double latSW = southWest.getDouble("latitude");
+ double lngSW = southWest.getDouble("longitude");
+ builder.include(new LatLng(latSW, lngSW));
+
+ LatLngBounds bounds = builder.build();
+
+ map.setLatLngBoundsForCameraTarget(bounds);
+ }
+
+ // InfoWindowAdapter interface
+
+ @Override
+ public View getInfoWindow(Marker marker) {
+ MapMarker markerView = getMarkerMap(marker);
+ return markerView.getCallout();
+ }
+
+ @Override
+ public View getInfoContents(Marker marker) {
+ MapMarker markerView = getMarkerMap(marker);
+ return markerView.getInfoContents();
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ gestureDetector.onTouchEvent(ev);
+
+ int X = (int)ev.getX();
+ int Y = (int)ev.getY();
+ if(map != null) {
+ tapLocation = map.getProjection().fromScreenLocation(new Point(X,Y));
+ }
+
+ int action = MotionEventCompat.getActionMasked(ev);
+
+ switch (action) {
+ case (MotionEvent.ACTION_DOWN):
+ this.getParent().requestDisallowInterceptTouchEvent(
+ map != null && map.getUiSettings().isScrollGesturesEnabled());
+ break;
+ case (MotionEvent.ACTION_UP):
+ // Clear this regardless, since isScrollGesturesEnabled() may have been updated
+ this.getParent().requestDisallowInterceptTouchEvent(false);
+ break;
+ }
+ super.dispatchTouchEvent(ev);
+ return true;
+ }
+
+ @Override
+ public void onMarkerDragStart(Marker marker) {
+ WritableMap event = makeClickEventData(marker.getPosition());
+ manager.pushEvent(context, this, "onMarkerDragStart", event);
+
+ MapMarker markerView = getMarkerMap(marker);
+ event = makeClickEventData(marker.getPosition());
+ manager.pushEvent(context, markerView, "onDragStart", event);
+ }
+
+ @Override
+ public void onMarkerDrag(Marker marker) {
+ WritableMap event = makeClickEventData(marker.getPosition());
+ manager.pushEvent(context, this, "onMarkerDrag", event);
+
+ MapMarker markerView = getMarkerMap(marker);
+ event = makeClickEventData(marker.getPosition());
+ manager.pushEvent(context, markerView, "onDrag", event);
+ }
+
+ @Override
+ public void onMarkerDragEnd(Marker marker) {
+ WritableMap event = makeClickEventData(marker.getPosition());
+ manager.pushEvent(context, this, "onMarkerDragEnd", event);
+
+ MapMarker markerView = getMarkerMap(marker);
+ event = makeClickEventData(marker.getPosition());
+ manager.pushEvent(context, markerView, "onDragEnd", event);
+ }
+
+ @Override
+ public void onPoiClick(PointOfInterest poi) {
+ WritableMap event = makeClickEventData(poi.latLng);
+
+ event.putString("placeId", poi.placeId);
+ event.putString("name", poi.name);
+
+ manager.pushEvent(context, this, "onPoiClick", event);
+ }
+
+ private ProgressBar getMapLoadingProgressBar() {
+ if (this.mapLoadingProgressBar == null) {
+ this.mapLoadingProgressBar = new ProgressBar(getContext());
+ this.mapLoadingProgressBar.setIndeterminate(true);
+ }
+ if (this.loadingIndicatorColor != null) {
+ this.setLoadingIndicatorColor(this.loadingIndicatorColor);
+ }
+ return this.mapLoadingProgressBar;
+ }
+
+ private RelativeLayout getMapLoadingLayoutView() {
+ if (this.mapLoadingLayout == null) {
+ this.mapLoadingLayout = new RelativeLayout(getContext());
+ this.mapLoadingLayout.setBackgroundColor(Color.LTGRAY);
+ this.addView(this.mapLoadingLayout,
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
+ params.addRule(RelativeLayout.CENTER_IN_PARENT);
+ this.mapLoadingLayout.addView(this.getMapLoadingProgressBar(), params);
+
+ this.mapLoadingLayout.setVisibility(View.INVISIBLE);
+ }
+ this.setLoadingBackgroundColor(this.loadingBackgroundColor);
+ return this.mapLoadingLayout;
+ }
+
+ private ImageView getCacheImageView() {
+ if (this.cacheImageView == null) {
+ this.cacheImageView = new ImageView(getContext());
+ this.addView(this.cacheImageView,
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ this.cacheImageView.setVisibility(View.INVISIBLE);
+ }
+ return this.cacheImageView;
+ }
+
+ private void removeCacheImageView() {
+ if (this.cacheImageView != null) {
+ ((ViewGroup) this.cacheImageView.getParent()).removeView(this.cacheImageView);
+ this.cacheImageView = null;
+ }
+ }
+
+ private void removeMapLoadingProgressBar() {
+ if (this.mapLoadingProgressBar != null) {
+ ((ViewGroup) this.mapLoadingProgressBar.getParent()).removeView(this.mapLoadingProgressBar);
+ this.mapLoadingProgressBar = null;
+ }
+ }
+
+ private void removeMapLoadingLayoutView() {
+ this.removeMapLoadingProgressBar();
+ if (this.mapLoadingLayout != null) {
+ ((ViewGroup) this.mapLoadingLayout.getParent()).removeView(this.mapLoadingLayout);
+ this.mapLoadingLayout = null;
+ }
+ }
+
+ private void cacheView() {
+ if (this.cacheEnabled) {
+ final ImageView cacheImageView = this.getCacheImageView();
+ final RelativeLayout mapLoadingLayout = this.getMapLoadingLayoutView();
+ cacheImageView.setVisibility(View.INVISIBLE);
+ mapLoadingLayout.setVisibility(View.VISIBLE);
+ if (this.isMapLoaded) {
+ this.map.snapshot(new GoogleMap.SnapshotReadyCallback() {
+ @Override public void onSnapshotReady(Bitmap bitmap) {
+ cacheImageView.setImageBitmap(bitmap);
+ cacheImageView.setVisibility(View.VISIBLE);
+ mapLoadingLayout.setVisibility(View.INVISIBLE);
+ }
+ });
+ }
+ } else {
+ this.removeCacheImageView();
+ if (this.isMapLoaded) {
+ this.removeMapLoadingLayoutView();
+ }
+ }
+ }
+
+ public void onPanDrag(MotionEvent ev) {
+ Point point = new Point((int) ev.getX(), (int) ev.getY());
+ LatLng coords = this.map.getProjection().fromScreenLocation(point);
+ WritableMap event = makeClickEventData(coords);
+ manager.pushEvent(context, this, "onPanDrag", event);
+ }
+
+ public void onDoublePress(MotionEvent ev) {
+ if (this.map == null) return;
+ Point point = new Point((int) ev.getX(), (int) ev.getY());
+ LatLng coords = this.map.getProjection().fromScreenLocation(point);
+ WritableMap event = makeClickEventData(coords);
+ manager.pushEvent(context, this, "onDoublePress", event);
+ }
+
+ public void setKmlSrc(String kmlSrc) {
+ try {
+ InputStream kmlStream = new FileUtil(context).execute(kmlSrc).get();
+
+ if (kmlStream == null) {
+ return;
+ }
+
+ KmlLayer kmlLayer = new KmlLayer(map, kmlStream, context, markerManager, polygonManager, polylineManager, groundOverlayManager, null);
+ kmlLayer.addLayerToMap();
+
+ WritableMap pointers = new WritableNativeMap();
+ WritableArray markers = new WritableNativeArray();
+
+ if (kmlLayer.getContainers() == null) {
+ manager.pushEvent(context, this, "onKmlReady", pointers);
+ return;
+ }
+
+ //Retrieve a nested container within the first container
+ KmlContainer container = kmlLayer.getContainers().iterator().next();
+ if (container == null || container.getContainers() == null) {
+ manager.pushEvent(context, this, "onKmlReady", pointers);
+ return;
+ }
+
+
+ if (container.getContainers().iterator().hasNext()) {
+ container = container.getContainers().iterator().next();
+ }
+
+ int index = 0;
+ for (KmlPlacemark placemark : container.getPlacemarks()) {
+ MarkerOptions options = new MarkerOptions();
+
+ if (placemark.getInlineStyle() != null) {
+ options = placemark.getMarkerOptions();
+ } else {
+ options.icon(BitmapDescriptorFactory.defaultMarker());
+ }
+
+ LatLng latLng = ((LatLng) placemark.getGeometry().getGeometryObject());
+ String title = "";
+ String snippet = "";
+
+ if (placemark.hasProperty("name")) {
+ title = placemark.getProperty("name");
+ }
+
+ if (placemark.hasProperty("description")) {
+ snippet = placemark.getProperty("description");
+ }
+
+ options.position(latLng);
+ options.title(title);
+ options.snippet(snippet);
+
+ MapMarker marker = new MapMarker(context, options, this.manager.getMarkerManager());
+
+ if (placemark.getInlineStyle() != null
+ && placemark.getInlineStyle().getIconUrl() != null) {
+ marker.setImage(placemark.getInlineStyle().getIconUrl());
+ } else if (container.getStyle(placemark.getStyleId()) != null) {
+ KmlStyle style = container.getStyle(placemark.getStyleId());
+ marker.setImage(style.getIconUrl());
+ }
+
+ String identifier = title + " - " + index;
+
+ marker.setIdentifier(identifier);
+
+ addFeature(marker, index++);
+
+ WritableMap loadedMarker = makeClickEventData(latLng);
+ loadedMarker.putString("id", identifier);
+ loadedMarker.putString("title", title);
+ loadedMarker.putString("description", snippet);
+
+ markers.pushMap(loadedMarker);
+ }
+
+ pointers.putArray("markers", markers);
+
+ manager.pushEvent(context, this, "onKmlReady", pointers);
+
+ } catch (XmlPullParserException | IOException | InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onIndoorBuildingFocused() {
+ IndoorBuilding building = this.map.getFocusedBuilding();
+ if (building != null) {
+ List levels = building.getLevels();
+ int index = 0;
+ WritableArray levelsArray = Arguments.createArray();
+ for (IndoorLevel level : levels) {
+ WritableMap levelMap = Arguments.createMap();
+ levelMap.putInt("index", index);
+ levelMap.putString("name", level.getName());
+ levelMap.putString("shortName", level.getShortName());
+ levelsArray.pushMap(levelMap);
+ index++;
+ }
+ WritableMap event = Arguments.createMap();
+ WritableMap indoorBuilding = Arguments.createMap();
+ indoorBuilding.putArray("levels", levelsArray);
+ indoorBuilding.putInt("activeLevelIndex", building.getActiveLevelIndex());
+ indoorBuilding.putBoolean("underground", building.isUnderground());
+
+ event.putMap("IndoorBuilding", indoorBuilding);
+
+ manager.pushEvent(context, this, "onIndoorBuildingFocused", event);
+ } else {
+ WritableMap event = Arguments.createMap();
+ WritableArray levelsArray = Arguments.createArray();
+ WritableMap indoorBuilding = Arguments.createMap();
+ indoorBuilding.putArray("levels", levelsArray);
+ indoorBuilding.putInt("activeLevelIndex", 0);
+ indoorBuilding.putBoolean("underground", false);
+
+ event.putMap("IndoorBuilding", indoorBuilding);
+
+ manager.pushEvent(context, this, "onIndoorBuildingFocused", event);
+ }
+ }
+
+ @Override
+ public void onIndoorLevelActivated(IndoorBuilding building) {
+ if (building == null) {
+ return;
+ }
+ int activeLevelIndex = building.getActiveLevelIndex();
+ if (activeLevelIndex < 0 || activeLevelIndex >= building.getLevels().size()) {
+ return;
+ }
+ IndoorLevel level = building.getLevels().get(activeLevelIndex);
+
+ WritableMap event = Arguments.createMap();
+ WritableMap indoorlevel = Arguments.createMap();
+
+ indoorlevel.putInt("activeLevelIndex", activeLevelIndex);
+ indoorlevel.putString("name", level.getName());
+ indoorlevel.putString("shortName", level.getShortName());
+
+ event.putMap("IndoorLevel", indoorlevel);
+
+ manager.pushEvent(context, this, "onIndoorLevelActivated", event);
+ }
+
+ public void setIndoorActiveLevelIndex(int activeLevelIndex) {
+ IndoorBuilding building = this.map.getFocusedBuilding();
+ if (building != null) {
+ if (activeLevelIndex >= 0 && activeLevelIndex < building.getLevels().size()) {
+ IndoorLevel level = building.getLevels().get(activeLevelIndex);
+ if (level != null) {
+ level.activate();
+ }
+ }
+ }
+ }
+
+ private MapMarker getMarkerMap(Marker marker) {
+ MapMarker airMarker = markerMap.get(marker);
+
+ if (airMarker != null) {
+ return airMarker;
+ }
+
+ for (Map.Entry entryMarker : markerMap.entrySet()) {
+ if (entryMarker.getKey().getPosition().equals(marker.getPosition())
+ && entryMarker.getKey().getTitle().equals(marker.getTitle())) {
+ airMarker = entryMarker.getValue();
+ break;
+ }
+ }
+
+ return airMarker;
+ }
+
+ @Override
+ public void requestLayout() {
+ super.requestLayout();
+ post(measureAndLayout);
+ }
+
+ private final Runnable measureAndLayout = new Runnable() {
+ @Override
+ public void run() {
+ measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
+ layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+ };
+
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapWMSTile.java b/android/src/main/java/com/rnmaps/maps/MapWMSTile.java
new file mode 100644
index 000000000..b455b523f
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapWMSTile.java
@@ -0,0 +1,94 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+
+import com.google.android.gms.maps.model.TileOverlayOptions;
+import com.google.android.gms.maps.model.UrlTileProvider;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class MapWMSTile extends MapUrlTile {
+ private static final double[] mapBound = {-20037508.34789244, 20037508.34789244};
+ private static final double FULL = 20037508.34789244 * 2;
+
+ class AIRMapGSUrlTileProvider extends MapTileProvider {
+
+ class AIRMapWMSTileProvider extends UrlTileProvider {
+ private String urlTemplate;
+ private final int tileSize;
+
+ public AIRMapWMSTileProvider(int width, int height, String urlTemplate) {
+ super(width, height);
+ this.urlTemplate = urlTemplate;
+ this.tileSize = width;
+ }
+
+ @Override
+ public URL getTileUrl(int x, int y, int zoom) {
+ if(MapWMSTile.this.maximumZ > 0 && zoom > maximumZ) {
+ return null;
+ }
+
+ if(MapWMSTile.this.minimumZ > 0 && zoom < minimumZ) {
+ return null;
+ }
+
+ double[] bb = getBoundingBox(x, y, zoom);
+ String s = this.urlTemplate
+ .replace("{minX}", Double.toString(bb[0]))
+ .replace("{minY}", Double.toString(bb[1]))
+ .replace("{maxX}", Double.toString(bb[2]))
+ .replace("{maxY}", Double.toString(bb[3]))
+ .replace("{width}", Integer.toString(this.tileSize))
+ .replace("{height}", Integer.toString(this.tileSize));
+ URL url = null;
+
+ try {
+ url = new URL(s);
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+ return url;
+ }
+
+ private double[] getBoundingBox(int x, int y, int zoom) {
+ double tile = FULL / Math.pow(2, zoom);
+ return new double[]{
+ mapBound[0] + x * tile,
+ mapBound[1] - (y + 1) * tile,
+ mapBound[0] + (x + 1) * tile,
+ mapBound[1] - y * tile
+ };
+ }
+
+ public void setUrlTemplate(String urlTemplate) {
+ this.urlTemplate = urlTemplate;
+ }
+ }
+
+ public AIRMapGSUrlTileProvider(int tileSizet, String urlTemplate,
+ int maximumZ, int maximumNativeZ, int minimumZ, String tileCachePath,
+ int tileCacheMaxAge, boolean offlineMode, Context context, boolean customMode) {
+ super(tileSizet, false, urlTemplate, maximumZ, maximumNativeZ, minimumZ, false,
+ tileCachePath, tileCacheMaxAge, offlineMode, context, customMode);
+ this.tileProvider = new AIRMapWMSTileProvider(tileSizet, tileSizet, urlTemplate);
+ }
+ }
+
+ public MapWMSTile(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected TileOverlayOptions createTileOverlayOptions() {
+ TileOverlayOptions options = new TileOverlayOptions();
+ options.zIndex(zIndex);
+ options.transparency(1 - this.opacity);
+ AIRMapGSUrlTileProvider tileProvider = new AIRMapGSUrlTileProvider((int) this.tileSize, this.urlTemplate,
+ (int) this.maximumZ, (int) this.maximumNativeZ, (int) this.minimumZ, this.tileCachePath,
+ (int) this.tileCacheMaxAge, this.offlineMode, this.context, this.customTileProviderNeeded);
+ options.tileProvider(tileProvider);
+ return options;
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapWMSTileManager.java b/android/src/main/java/com/rnmaps/maps/MapWMSTileManager.java
new file mode 100644
index 000000000..38071ca58
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapWMSTileManager.java
@@ -0,0 +1,81 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.annotations.ReactProp;
+
+public class MapWMSTileManager extends ViewGroupManager {
+
+ public MapWMSTileManager(ReactApplicationContext reactContext) {
+ super();
+ DisplayMetrics metrics = new DisplayMetrics();
+ ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay()
+ .getRealMetrics(metrics);
+ }
+
+ @Override
+ public String getName() {
+ return "AIRMapWMSTile";
+ }
+
+ @Override
+ public MapWMSTile createViewInstance(ThemedReactContext context) {
+ return new MapWMSTile(context);
+ }
+
+ @ReactProp(name = "urlTemplate")
+ public void setUrlTemplate(MapWMSTile view, String urlTemplate) {
+ view.setUrlTemplate(urlTemplate);
+ }
+
+ @ReactProp(name = "zIndex", defaultFloat = -1.0f)
+ public void setZIndex(MapWMSTile view, float zIndex) {
+ view.setZIndex(zIndex);
+ }
+
+ @ReactProp(name = "minimumZ", defaultFloat = 0.0f)
+ public void setMinimumZ(MapWMSTile view, float minimumZ) {
+ view.setMinimumZ(minimumZ);
+ }
+
+ @ReactProp(name = "maximumZ", defaultFloat = 100.0f)
+ public void setMaximumZ(MapWMSTile view, float maximumZ) {
+ view.setMaximumZ(maximumZ);
+ }
+
+ @ReactProp(name = "maximumNativeZ", defaultFloat = 100.0f)
+ public void setMaximumNativeZ(MapWMSTile view, float maximumNativeZ) {
+ view.setMaximumNativeZ(maximumNativeZ);
+ }
+
+ @ReactProp(name = "tileSize", defaultFloat = 256.0f)
+ public void setTileSize(MapWMSTile view, float tileSize) {
+ view.setTileSize(tileSize);
+ }
+
+ @ReactProp(name = "tileCachePath")
+ public void setTileCachePath(MapWMSTile view, String tileCachePath) {
+ view.setTileCachePath(tileCachePath);
+ }
+
+ @ReactProp(name = "tileCacheMaxAge", defaultFloat = 0.0f)
+ public void setTileCacheMaxAge(MapWMSTile view, float tileCacheMaxAge) {
+ view.setTileCacheMaxAge(tileCacheMaxAge);
+ }
+
+ @ReactProp(name = "offlineMode", defaultBoolean = false)
+ public void setOfflineMode(MapWMSTile view, boolean offlineMode) {
+ view.setOfflineMode(offlineMode);
+ }
+
+ @ReactProp(name = "opacity", defaultFloat = 1.0f)
+ public void setOpacity(MapWMSTile view, float opacity) {
+ view.setOpacity(opacity);
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/MapsPackage.java b/android/src/main/java/com/rnmaps/maps/MapsPackage.java
new file mode 100644
index 000000000..3bf751de9
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/MapsPackage.java
@@ -0,0 +1,45 @@
+package com.rnmaps.maps;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MapsPackage implements ReactPackage {
+
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ List modules = new ArrayList<>();
+
+ modules.add(new MapModule(reactContext));
+
+ return modules;
+ }
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ MapManager mapManager = new MapManager(reactContext);
+ MapMarkerManager annotationManager = new MapMarkerManager();
+ mapManager.setMarkerManager(annotationManager);
+
+ List viewManagers = new ArrayList<>();
+
+ viewManagers.add(mapManager);
+ viewManagers.add(annotationManager);
+ viewManagers.add(new MapCalloutManager());
+ viewManagers.add(new MapPolylineManager(reactContext));
+ viewManagers.add(new MapGradientPolylineManager(reactContext));
+ viewManagers.add(new MapPolygonManager(reactContext));
+ viewManagers.add(new MapCircleManager(reactContext));
+ viewManagers.add(new MapUrlTileManager(reactContext));
+ viewManagers.add(new MapWMSTileManager(reactContext));
+ viewManagers.add(new MapLocalTileManager(reactContext));
+ viewManagers.add(new MapOverlayManager(reactContext));
+ viewManagers.add(new MapHeatmapManager());
+
+ return viewManagers;
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/RegionChangeEvent.java b/android/src/main/java/com/rnmaps/maps/RegionChangeEvent.java
new file mode 100644
index 000000000..202998563
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/RegionChangeEvent.java
@@ -0,0 +1,48 @@
+package com.rnmaps.maps;
+
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+
+public class RegionChangeEvent extends Event {
+ private final LatLngBounds bounds;
+ private final boolean continuous;
+ private final boolean isGesture;
+
+ public RegionChangeEvent(int id, LatLngBounds bounds, boolean continuous, boolean isGesture) {
+ super(id);
+ this.bounds = bounds;
+ this.continuous = continuous;
+ this.isGesture = isGesture;
+ }
+
+ @Override
+ public String getEventName() {
+ return "topChange";
+ }
+
+ @Override
+ public boolean canCoalesce() {
+ return false;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ WritableMap event = new WritableNativeMap();
+ event.putBoolean("continuous", continuous);
+
+ WritableMap region = new WritableNativeMap();
+ LatLng center = bounds.getCenter();
+ region.putDouble("latitude", center.latitude);
+ region.putDouble("longitude", center.longitude);
+ region.putDouble("latitudeDelta", bounds.northeast.latitude - bounds.southwest.latitude);
+ region.putDouble("longitudeDelta", bounds.northeast.longitude - bounds.southwest.longitude);
+ event.putMap("region", region);
+ event.putBoolean("isGesture", isGesture);
+
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event);
+ }
+}
diff --git a/android/lib/src/main/java/com/airbnb/android/react/maps/SizeReportingShadowNode.java b/android/src/main/java/com/rnmaps/maps/SizeReportingShadowNode.java
similarity index 62%
rename from android/lib/src/main/java/com/airbnb/android/react/maps/SizeReportingShadowNode.java
rename to android/src/main/java/com/rnmaps/maps/SizeReportingShadowNode.java
index 40ace480f..a49478158 100644
--- a/android/lib/src/main/java/com/airbnb/android/react/maps/SizeReportingShadowNode.java
+++ b/android/src/main/java/com/rnmaps/maps/SizeReportingShadowNode.java
@@ -6,7 +6,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
-package com.airbnb.android.react.maps;
+package com.rnmaps.maps;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.UIViewOperationQueue;
@@ -18,14 +18,14 @@
// which sends the width/height of the view after layout occurs.
public class SizeReportingShadowNode extends LayoutShadowNode {
- @Override
- public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
- super.onCollectExtraUpdates(uiViewOperationQueue);
+ @Override
+ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
+ super.onCollectExtraUpdates(uiViewOperationQueue);
- Map data = new HashMap<>();
- data.put("width", getLayoutWidth());
- data.put("height", getLayoutHeight());
+ Map data = new HashMap<>();
+ data.put("width", getLayoutWidth());
+ data.put("height", getLayoutHeight());
- uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), data);
- }
+ uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), data);
+ }
}
diff --git a/android/src/main/java/com/rnmaps/maps/ViewAttacherGroup.java b/android/src/main/java/com/rnmaps/maps/ViewAttacherGroup.java
new file mode 100644
index 000000000..9104bb279
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/ViewAttacherGroup.java
@@ -0,0 +1,26 @@
+package com.rnmaps.maps;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.facebook.react.views.view.ReactViewGroup;
+
+public class ViewAttacherGroup extends ReactViewGroup {
+
+ public ViewAttacherGroup(Context context) {
+ super(context);
+
+ this.setWillNotDraw(true);
+ this.setVisibility(VISIBLE);
+ this.setAlpha(0.0f);
+ this.setRemoveClippedSubviews(false);
+ this.setClipBounds(new Rect(0, 0, 0, 0));
+ this.setOverflow("hidden"); // Change to ViewProps.HIDDEN until RN 0.57 is base
+ }
+
+ // This should make it more performant, avoid trying to hard to overlap layers with opacity.
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+}
diff --git a/android/src/main/java/com/rnmaps/maps/ViewChangesTracker.java b/android/src/main/java/com/rnmaps/maps/ViewChangesTracker.java
new file mode 100644
index 000000000..238fcb537
--- /dev/null
+++ b/android/src/main/java/com/rnmaps/maps/ViewChangesTracker.java
@@ -0,0 +1,76 @@
+package com.rnmaps.maps;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.LinkedList;
+
+public class ViewChangesTracker {
+
+ private static ViewChangesTracker instance;
+ private final Handler handler;
+ private final LinkedList markers = new LinkedList<>();
+ private boolean hasScheduledFrame = false;
+ private final Runnable updateRunnable;
+ private final long fps = 40;
+
+ private ViewChangesTracker() {
+ handler = new Handler(Looper.myLooper());
+ updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ update();
+
+ if (markers.size() > 0) {
+ handler.postDelayed(updateRunnable, fps);
+ } else {
+ hasScheduledFrame = false;
+ }
+ }
+ };
+ }
+
+ static ViewChangesTracker getInstance() {
+ if (instance == null) {
+ synchronized (ViewChangesTracker.class) {
+ instance = new ViewChangesTracker();
+ }
+ }
+
+ return instance;
+ }
+
+ public void addMarker(MapMarker marker) {
+ markers.add(marker);
+
+ if (!hasScheduledFrame) {
+ hasScheduledFrame = true;
+ handler.postDelayed(updateRunnable, fps);
+ }
+ }
+
+ public void removeMarker(MapMarker marker) {
+ markers.remove(marker);
+ }
+
+ public boolean containsMarker(MapMarker marker) {
+ return markers.contains(marker);
+ }
+
+ private final LinkedList markersToRemove = new LinkedList<>();
+
+ public void update() {
+ for (MapMarker marker : markers) {
+ if (!marker.updateCustomForTracking()) {
+ markersToRemove.add(marker);
+ }
+ }
+
+ // Remove markers that are not active anymore
+ if (markersToRemove.size() > 0) {
+ markers.removeAll(markersToRemove);
+ markersToRemove.clear();
+ }
+ }
+
+}
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 000000000..f7b3da3b3
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: ['module:@react-native/babel-preset'],
+};
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 000000000..3347cb961
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1 @@
+module.exports = {extends: ['@commitlint/config-conventional']};
diff --git a/components/AnimatedRegion.js b/components/AnimatedRegion.js
deleted file mode 100644
index d2f633a60..000000000
--- a/components/AnimatedRegion.js
+++ /dev/null
@@ -1,171 +0,0 @@
-class AnimatedRegion extends AnimatedWithChildren {
- //latitude: AnimatedValue;
- //longitude: AnimatedValue;
- //latitudeDelta: AnimatedValue;
- //longitudeDelta: AnimatedValue;
- //_listeners: {[key: string]: {
- // latitude: string,
- // longitude: string,
- // latitudeDelta: string;
- // longitudeDelta: string,
- //}};
-
- constructor(valueIn) {
- super();
- var value = valueIn || { // probably want to come up with better defaults
- latitude: 0,
- longitude: 0,
- latitudeDelta: 0,
- longitudeDelta: 0,
- };
- this.latitude = value.latitude instanceof Animated
- ? value.latitude
- : new AnimatedValue(value.latitude);
- this.longitude = value.longitude instanceof Animated
- ? value.longitude
- : new AnimatedValue(value.longitude);
- this.latitudeDelta = value.latitudeDelta instanceof Animated
- ? value.latitudeDelta
- : new AnimatedValue(value.latitudeDelta);
- this.longitudeDelta = value.longitudeDelta instanceof Animated
- ? value.longitudeDelta
- : new AnimatedValue(value.longitudeDelta);
- this._listeners = {};
- }
-
- setValue(value) {
- //this.latitude.setValue(value.latitude);
- //this.longitude.setValue(value.longitude);
- //this.latitudeDelta.setValue(value.latitudeDelta);
- //this.longitudeDelta.setValue(value.longitudeDelta);
- this.latitude._value = value.latitude;
- this.longitude._value = value.longitude;
- this.latitudeDelta._value = value.latitudeDelta;
- this.longitudeDelta._value = value.longitudeDelta;
- }
-
- setOffset(offset) {
- this.latitude.setOffset(offset.latitude);
- this.longitude.setOffset(offset.longitude);
- this.latitudeDelta.setOffset(offset.latitudeDelta);
- this.longitudeDelta.setOffset(offset.longitudeDelta);
- }
-
- flattenOffset() {
- this.latitude.flattenOffset();
- this.longitude.flattenOffset();
- this.latitudeDelta.flattenOffset();
- this.longitudeDelta.flattenOffset();
- }
-
- __getValue() {
- return {
- latitude: this.latitude.__getValue(),
- longitude: this.longitude.__getValue(),
- latitudeDelta: this.latitudeDelta.__getValue(),
- longitudeDelta: this.longitudeDelta.__getValue(),
- };
- }
-
- __attach() {
- this.latitude.__addChild(this);
- this.longitude.__addChild(this);
- this.latitudeDelta.__addChild(this);
- this.longitudeDelta.__addChild(this);
- }
-
- __detach() {
- this.latitude.__removeChild(this);
- this.longitude.__removeChild(this);
- this.latitudeDelta.__removeChild(this);
- this.longitudeDelta.__removeChild(this);
- }
-
- stopAnimation(callback) {
- this.latitude.stopAnimation();
- this.longitude.stopAnimation();
- this.latitudeDelta.stopAnimation();
- this.longitudeDelta.stopAnimation();
- callback && callback(this.__getValue());
- }
-
- addListener(callback) {
- var id = String(_uniqueId++);
- var jointCallback = ({value: number}) => {
- callback(this.__getValue());
- };
- this._listeners[id] = {
- latitude: this.latitude.addListener(jointCallback),
- longitude: this.longitude.addListener(jointCallback),
- latitudeDelta: this.latitudeDelta.addListener(jointCallback),
- longitudeDelta: this.longitudeDelta.addListener(jointCallback),
- };
- return id;
- }
-
- removeListener(id) {
- this.latitude.removeListener(this._listeners[id].latitude);
- this.longitude.removeListener(this._listeners[id].longitude);
- this.latitudeDelta.removeListener(this._listeners[id].latitudeDelta);
- this.longitudeDelta.removeListener(this._listeners[id].longitudeDelta);
- delete this._listeners[id];
- }
-
- spring(config) {
- var animations = [];
- config.hasOwnProperty('latitude') &&
- animations.push(timing(this.latitude, {
- ...config,
- toValue: config.latitude,
- }));
-
- config.hasOwnProperty('longitude') &&
- animations.push(timing(this.longitude, {
- ...config,
- toValue: config.longitude,
- }));
-
- config.hasOwnProperty('latitudeDelta') &&
- animations.push(timing(this.latitudeDelta, {
- ...config,
- toValue: config.latitudeDelta,
- }));
-
- config.hasOwnProperty('longitudeDelta') &&
- animations.push(timing(this.longitudeDelta, {
- ...config,
- toValue: config.longitudeDelta,
- }));
-
- return parallel(animations);
- }
-
- timing(config) {
- var animations = [];
- config.hasOwnProperty('latitude') &&
- animations.push(timing(this.latitude, {
- ...config,
- toValue: config.latitude,
- }));
-
- config.hasOwnProperty('longitude') &&
- animations.push(timing(this.longitude, {
- ...config,
- toValue: config.longitude,
- }));
-
- config.hasOwnProperty('latitudeDelta') &&
- animations.push(timing(this.latitudeDelta, {
- ...config,
- toValue: config.latitudeDelta,
- }));
-
- config.hasOwnProperty('longitudeDelta') &&
- animations.push(timing(this.longitudeDelta, {
- ...config,
- toValue: config.longitudeDelta,
- }));
-
- return parallel(animations);
- }
-}
diff --git a/components/MapCallout.js b/components/MapCallout.js
deleted file mode 100644
index a4b4f809b..000000000
--- a/components/MapCallout.js
+++ /dev/null
@@ -1,45 +0,0 @@
-
-var React = require('react');
-var {
- PropTypes,
-} = React;
-
-var ReactNative = require('react-native');
-var {
- View,
- NativeMethodsMixin,
- requireNativeComponent,
- StyleSheet,
-} = ReactNative;
-
-var MapCallout = React.createClass({
- mixins: [NativeMethodsMixin],
-
- propTypes: {
- ...View.propTypes,
- tooltip: PropTypes.bool,
- onPress: PropTypes.func,
- },
-
- getDefaultProps: function() {
- return {
- tooltip: false,
- };
- },
-
- render: function() {
- return ;
- },
-});
-
-var styles = StyleSheet.create({
- callout: {
- position: 'absolute',
- //flex: 0,
- //backgroundColor: 'transparent',
- },
-});
-
-var AIRMapCallout = requireNativeComponent('AIRMapCallout', MapCallout);
-
-module.exports = MapCallout;
diff --git a/components/MapCircle.js b/components/MapCircle.js
deleted file mode 100644
index cd6fe2425..000000000
--- a/components/MapCircle.js
+++ /dev/null
@@ -1,153 +0,0 @@
-
-var React = require('react');
-var {
- PropTypes,
-} = React;
-
-var ReactNative = require('react-native');
-var {
- View,
- NativeMethodsMixin,
- requireNativeComponent,
- StyleSheet,
-} = ReactNative;
-
-var MapCircle = React.createClass({
- mixins: [NativeMethodsMixin],
-
- propTypes: {
- ...View.propTypes,
-
- /**
- * The coordinate of the center of the circle
- */
- center: PropTypes.shape({
- /**
- * Coordinates for the center of the circle.
- */
- latitude: PropTypes.number.isRequired,
- longitude: PropTypes.number.isRequired,
- }).isRequired,
-
- /**
- * The radius of the circle to be drawn (in meters)
- */
- radius: PropTypes.number.isRequired,
-
- /**
- * Callback that is called when the user presses on the circle
- */
- onPress: PropTypes.func,
-
- /**
- * The stroke width to use for the path.
- */
- strokeWidth: PropTypes.number,
-
- /**
- * The stroke color to use for the path.
- */
- strokeColor: PropTypes.string,
-
- /**
- * The fill color to use for the path.
- */
- fillColor: PropTypes.string,
-
- /**
- * The order in which this tile overlay is drawn with respect to other overlays. An overlay
- * with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays
- * with the same z-index is arbitrary. The default zIndex is 0.
- *
- * @platform android
- */
- zIndex: PropTypes.number,
-
- /**
- * The line cap style to apply to the open ends of the path.
- * The default style is `round`.
- *
- * @platform ios
- */
- lineCap: PropTypes.oneOf([
- 'butt',
- 'round',
- 'square',
- ]),
-
- /**
- * The line join style to apply to corners of the path.
- * The default style is `round`.
- *
- * @platform ios
- */
- lineJoin: PropTypes.oneOf([
- 'miter',
- 'round',
- 'bevel',
- ]),
-
- /**
- * The limiting value that helps avoid spikes at junctions between connected line segments.
- * The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If
- * the ratio of the miter length—that is, the diagonal length of the miter join—to the line
- * thickness exceeds the miter limit, the joint is converted to a bevel join. The default
- * miter limit is 10, which results in the conversion of miters whose angle at the joint
- * is less than 11 degrees.
- *
- * @platform ios
- */
- miterLimit: PropTypes.number,
-
- /**
- * The offset (in points) at which to start drawing the dash pattern.
- *
- * Use this property to start drawing a dashed line partway through a segment or gap. For
- * example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the
- * middle of the first gap.
- *
- * The default value of this property is 0.
- *
- * @platform ios
- */
- lineDashPhase: PropTypes.number,
-
- /**
- * An array of numbers specifying the dash pattern to use for the path.
- *
- * The array contains one or more numbers that indicate the lengths (measured in points) of the
- * line segments and gaps in the pattern. The values in the array alternate, starting with the
- * first line segment length, followed by the first gap length, followed by the second line
- * segment length, and so on.
- *
- * This property is set to `null` by default, which indicates no line dash pattern.
- *
- * @platform ios
- */
- lineDashPattern: PropTypes.arrayOf(PropTypes.number),
- },
-
- getDefaultProps: function() {
- return {
- strokeColor: '#000',
- strokeWidth: 1,
- };
- },
-
- _onPress: function(e) {
- this.props.onPress && this.props.onPress(e);
- },
-
- render: function() {
- return (
-
- );
- },
-});
-
-var AIRMapCircle = requireNativeComponent('AIRMapCircle', MapCircle);
-
-module.exports = MapCircle;
diff --git a/components/MapMarker.js b/components/MapMarker.js
deleted file mode 100644
index e35302c81..000000000
--- a/components/MapMarker.js
+++ /dev/null
@@ -1,276 +0,0 @@
-'use strict';
-
-var React = require('react');
-var {
- PropTypes,
-} = React;
-
-var ReactNative = require('react-native');
-var {
- View,
- NativeMethodsMixin,
- requireNativeComponent,
- StyleSheet,
- Platform,
- NativeModules,
- Animated,
-} = ReactNative;
-
-var resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource');
-
-var MapMarker = React.createClass({
- mixins: [NativeMethodsMixin],
-
- viewConfig: {
- uiViewClassName: 'AIRMapMarker',
- validAttributes: {
- coordinate: true,
- },
- },
-
- propTypes: {
- ...View.propTypes,
-
- // TODO(lmr): get rid of these?
- identifier: PropTypes.string,
- reuseIdentifier: PropTypes.string,
-
- /**
- * The title of the marker. This is only used if the component has no children that
- * are an ``, in which case the default callout behavior will be used, which
- * will show both the `title` and the `description`, if provided.
- */
- title: PropTypes.string,
-
- /**
- * The description of the marker. This is only used if the component has no children
- * that are an ``, in which case the default callout behavior will be used,
- * which will show both the `title` and the `description`, if provided.
- */
- description: PropTypes.string,
-
- /**
- * A custom image to be used as the marker's icon. Only local image resources are allowed to be
- * used.
- */
- image: PropTypes.any,
-
- /**
- * If no custom marker view or custom image is provided, the platform default pin will be used,
- * which can be customized by this color. Ignored if a custom marker is being used.
- */
- pinColor: PropTypes.string,
-
- /**
- * The coordinate for the marker.
- */
- coordinate: PropTypes.shape({
- /**
- * Coordinates for the anchor point of the marker.
- */
- latitude: PropTypes.number.isRequired,
- longitude: PropTypes.number.isRequired,
- }).isRequired,
-
- /**
- * The offset (in points) at which to display the view.
- *
- * By default, the center point of an annotation view is placed at the coordinate point of the
- * associated annotation. You can use this property to reposition the annotation view as
- * needed. This x and y offset values are measured in points. Positive offset values move the
- * annotation view down and to the right, while negative values move it up and to the left.
- *
- * For android, see the `anchor` prop.
- *
- * @platform ios
- */
- centerOffset: PropTypes.shape({
- /**
- * Offset from the anchor point
- */
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- }),
-
- /**
- * The offset (in points) at which to place the callout bubble.
- *
- * This property determines the additional distance by which to move the callout bubble. When
- * this property is set to (0, 0), the anchor point of the callout bubble is placed on the
- * top-center point of the marker view’s frame. Specifying positive offset values moves the
- * callout bubble down and to the right, while specifying negative values moves it up and to
- * the left.
- *
- * For android, see the `calloutAnchor` prop.
- *
- * @platform ios
- */
- calloutOffset: PropTypes.shape({
- /**
- * Offset to the callout
- */
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- }),
-
- /**
- * Sets the anchor point for the marker.
- *
- * The anchor specifies the point in the icon image that is anchored to the marker's position
- * on the Earth's surface.
- *
- * The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0], where (0, 0)
- * is the top-left corner of the image, and (1, 1) is the bottom-right corner. The anchoring
- * point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid,
- * obtained by scaling the then rounding. For example, in a 4 x 2 image, the anchor point
- * (0.7, 0.6) resolves to the grid point at (3, 1).
- *
- * For ios, see the `centerOffset` prop.
- *
- * @platform android
- */
- anchor: PropTypes.shape({
- /**
- * Offset to the callout
- */
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- }),
-
- /**
- * Specifies the point in the marker image at which to anchor the callout when it is displayed.
- * This is specified in the same coordinate system as the anchor. See the `andor` prop for more
- * details.
- *
- * The default is the top middle of the image.
- *
- * For ios, see the `calloutOffset` prop.
- *
- * @platform android
- */
- calloutAnchor: PropTypes.shape({
- /**
- * Offset to the callout
- */
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- }),
-
- /**
- * Sets whether this marker should be flat against the map true or a billboard facing the
- * camera false.
- *
- * @platform android
- */
- flat: PropTypes.bool,
-
- draggable: PropTypes.bool,
-
- /**
- * Callback that is called when the user presses on the marker
- */
- onPress: PropTypes.func,
-
- /**
- * Callback that is called when the user selects the marker, before the callout is shown.
- *
- * @platform ios
- */
- onSelect: PropTypes.func,
-
- /**
- * Callback that is called when the marker is deselected, before the callout is hidden.
- *
- * @platform ios
- */
- onDeselect: PropTypes.func,
-
- /**
- * Callback that is called when the user taps the callout view.
- */
- onCalloutPress: PropTypes.func,
-
- /**
- * Callback that is called when the user initiates a drag on this marker (if it is draggable)
- */
- onDragStart: PropTypes.func,
-
- /**
- * Callback called continuously as the marker is dragged
- */
- onDrag: PropTypes.func,
-
- /**
- * Callback that is called when a drag on this marker finishes. This is usually the point you
- * will want to setState on the marker's coordinate again
- */
- onDragEnd: PropTypes.func,
-
- },
-
- showCallout: function() {
- this._runCommand('showCallout', []);
- },
-
- hideCallout: function() {
- this._runCommand('hideCallout', []);
- },
-
- _getHandle: function() {
- return ReactNative.findNodeHandle(this.refs.marker);
- },
-
- _runCommand: function (name, args) {
- switch (Platform.OS) {
- case 'android':
- NativeModules.UIManager.dispatchViewManagerCommand(
- this._getHandle(),
- NativeModules.UIManager.AIRMapMarker.Commands[name],
- args
- );
- break;
-
- case 'ios':
- NativeModules.AIRMapMarkerManager[name].apply(
- NativeModules.AIRMapMarkerManager[name],
- [this._getHandle(), ...args]
- );
- break;
- }
- },
-
- _onPress: function(e) {
- this.props.onPress && this.props.onPress(e);
- },
-
- render: function() {
- var image = undefined;
- if (this.props.image) {
- image = resolveAssetSource(this.props.image) || {};
- image = image.uri;
- }
-
- return (
-
- );
- },
-});
-
-var styles = StyleSheet.create({
- marker: {
- position: 'absolute',
- backgroundColor: 'transparent',
- },
-});
-
-var AIRMapMarker = requireNativeComponent('AIRMapMarker', MapMarker);
-
-MapMarker.Animated = Animated.createAnimatedComponent(MapMarker);
-
-module.exports = MapMarker;
diff --git a/components/MapPolygon.js b/components/MapPolygon.js
deleted file mode 100644
index a87ccb22d..000000000
--- a/components/MapPolygon.js
+++ /dev/null
@@ -1,165 +0,0 @@
-var React = require('react');
-var {
- PropTypes,
-} = React;
-
-var ReactNative = require('react-native');
-var {
- View,
- NativeMethodsMixin,
- requireNativeComponent,
- StyleSheet,
-} = ReactNative;
-
-var MapPolygon = React.createClass({
- mixins: [NativeMethodsMixin],
-
- propTypes: {
- ...View.propTypes,
-
- /**
- * An array of coordinates to describe the polygon
- */
- coordinates: PropTypes.arrayOf(PropTypes.shape({
- /**
- * Latitude/Longitude coordinates
- */
- latitude: PropTypes.number.isRequired,
- longitude: PropTypes.number.isRequired,
- })),
-
- /**
- * Callback that is called when the user presses on the polygon
- */
- onPress: PropTypes.func,
-
- /**
- * The stroke width to use for the path.
- */
- strokeWidth: PropTypes.number,
-
- /**
- * The stroke color to use for the path.
- */
- strokeColor: PropTypes.string,
-
- /**
- * The fill color to use for the path.
- */
- fillColor: PropTypes.string,
-
- /**
- * The order in which this tile overlay is drawn with respect to other overlays. An overlay
- * with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays
- * with the same z-index is arbitrary. The default zIndex is 0.
- *
- * @platform android
- */
- zIndex: PropTypes.number,
-
- /**
- * The line cap style to apply to the open ends of the path.
- * The default style is `round`.
- *
- * @platform ios
- */
- lineCap: PropTypes.oneOf([
- 'butt',
- 'round',
- 'square',
- ]),
-
- /**
- * The line join style to apply to corners of the path.
- * The default style is `round`.
- *
- * @platform ios
- */
- lineJoin: PropTypes.oneOf([
- 'miter',
- 'round',
- 'bevel',
- ]),
-
- /**
- * The limiting value that helps avoid spikes at junctions between connected line segments.
- * The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If
- * the ratio of the miter length—that is, the diagonal length of the miter join—to the line
- * thickness exceeds the miter limit, the joint is converted to a bevel join. The default
- * miter limit is 10, which results in the conversion of miters whose angle at the joint
- * is less than 11 degrees.
- *
- * @platform ios
- */
- miterLimit: PropTypes.number,
-
- /**
- * Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to
- * straight lines on the Mercator projection. A geodesic is the shortest path between two
- * points on the Earth's surface. The geodesic curve is constructed assuming the Earth is
- * a sphere.
- *
- * @platform android
- */
- geodesic: PropTypes.bool,
-
- /**
- * The offset (in points) at which to start drawing the dash pattern.
- *
- * Use this property to start drawing a dashed line partway through a segment or gap. For
- * example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the
- * middle of the first gap.
- *
- * The default value of this property is 0.
- *
- * @platform ios
- */
- lineDashPhase: PropTypes.number,
-
- /**
- * An array of numbers specifying the dash pattern to use for the path.
- *
- * The array contains one or more numbers that indicate the lengths (measured in points) of the
- * line segments and gaps in the pattern. The values in the array alternate, starting with the
- * first line segment length, followed by the first gap length, followed by the second line
- * segment length, and so on.
- *
- * This property is set to `null` by default, which indicates no line dash pattern.
- *
- * @platform ios
- */
- lineDashPattern: PropTypes.arrayOf(PropTypes.number),
- },
-
- getDefaultProps: function() {
- return {
- strokeColor: '#000',
- strokeWidth: 1,
- };
- },
-
- _onPress: function(e) {
- this.props.onPress && this.props.onPress(e);
- },
-
- render: function() {
- return (
-
- );
- },
-});
-
-var styles = StyleSheet.create({
- polyline: {
- position: 'absolute',
- width: 0,
- height: 0,
- },
-});
-
-var AIRMapPolygon = requireNativeComponent('AIRMapPolygon', MapPolygon);
-
-module.exports = MapPolygon;
diff --git a/components/MapPolyline.js b/components/MapPolyline.js
deleted file mode 100644
index c32a7db89..000000000
--- a/components/MapPolyline.js
+++ /dev/null
@@ -1,153 +0,0 @@
-var React = require('react');
-var {
- PropTypes,
-} = React;
-
-var ReactNative = require('react-native');
-var {
- View,
- NativeMethodsMixin,
- requireNativeComponent,
- StyleSheet,
-} = ReactNative;
-
-var MapPolyline = React.createClass({
- mixins: [NativeMethodsMixin],
-
- propTypes: {
- ...View.propTypes,
-
- /**
- * An array of coordinates to describe the polygon
- */
- coordinates: PropTypes.arrayOf(PropTypes.shape({
- /**
- * Latitude/Longitude coordinates
- */
- latitude: PropTypes.number.isRequired,
- longitude: PropTypes.number.isRequired,
- })),
-
- /**
- * Callback that is called when the user presses on the polyline
- */
- onPress: PropTypes.func,
-
- /**
- * The stroke width to use for the path.
- */
- strokeWidth: PropTypes.number,
-
- /**
- * The stroke color to use for the path.
- */
- strokeColor: PropTypes.string,
-
- /**
- * The order in which this tile overlay is drawn with respect to other overlays. An overlay
- * with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays
- * with the same z-index is arbitrary. The default zIndex is 0.
- *
- * @platform android
- */
- zIndex: PropTypes.number,
-
- /**
- * The line cap style to apply to the open ends of the path.
- * The default style is `round`.
- *
- * @platform ios
- */
- lineCap: PropTypes.oneOf([
- 'butt',
- 'round',
- 'square',
- ]),
-
- /**
- * The line join style to apply to corners of the path.
- * The default style is `round`.
- *
- * @platform ios
- */
- lineJoin: PropTypes.oneOf([
- 'miter',
- 'round',
- 'bevel',
- ]),
-
- /**
- * The limiting value that helps avoid spikes at junctions between connected line segments.
- * The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If
- * the ratio of the miter length—that is, the diagonal length of the miter join—to the line
- * thickness exceeds the miter limit, the joint is converted to a bevel join. The default
- * miter limit is 10, which results in the conversion of miters whose angle at the joint
- * is less than 11 degrees.
- *
- * @platform ios
- */
- miterLimit: PropTypes.number,
-
- /**
- * Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to
- * straight lines on the Mercator projection. A geodesic is the shortest path between two
- * points on the Earth's surface. The geodesic curve is constructed assuming the Earth is
- * a sphere.
- *
- * @platform android
- */
- geodesic: PropTypes.bool,
-
- /**
- * The offset (in points) at which to start drawing the dash pattern.
- *
- * Use this property to start drawing a dashed line partway through a segment or gap. For
- * example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the
- * middle of the first gap.
- *
- * The default value of this property is 0.
- *
- * @platform ios
- */
- lineDashPhase: PropTypes.number,
-
- /**
- * An array of numbers specifying the dash pattern to use for the path.
- *
- * The array contains one or more numbers that indicate the lengths (measured in points) of the
- * line segments and gaps in the pattern. The values in the array alternate, starting with the
- * first line segment length, followed by the first gap length, followed by the second line
- * segment length, and so on.
- *
- * This property is set to `null` by default, which indicates no line dash pattern.
- *
- * @platform ios
- */
- lineDashPattern: PropTypes.arrayOf(PropTypes.number),
- },
-
- getDefaultProps: function() {
- return {
- strokeColor: '#000',
- strokeWidth: 1,
- };
- },
-
- render: function() {
- return (
-
- );
- },
-});
-
-var styles = StyleSheet.create({
- polyline: {
- position: 'absolute',
- width: 0,
- height: 0,
- },
-});
-
-var AIRMapPolyline = requireNativeComponent('AIRMapPolyline', MapPolyline);
-
-module.exports = MapPolyline;
diff --git a/components/MapView.js b/components/MapView.js
deleted file mode 100644
index 774d7c2b0..000000000
--- a/components/MapView.js
+++ /dev/null
@@ -1,483 +0,0 @@
-'use strict';
-
-var React = require('react');
-var {
- PropTypes,
-} = React;
-var ReactNative = require('react-native');
-var {
- EdgeInsetsPropType,
- NativeMethodsMixin,
- Platform,
- ReactNativeViewAttributes,
- View,
- Animated,
- requireNativeComponent,
- NativeModules,
- ColorPropType,
-} = ReactNative;
-
-var MapMarker = require('./MapMarker');
-var MapPolyline = require('./MapPolyline');
-var MapPolygon = require('./MapPolygon');
-var MapCircle = require('./MapCircle');
-var MapCallout = require('./MapCallout');
-
-var MapView = React.createClass({
- mixins: [NativeMethodsMixin],
-
- viewConfig: {
- uiViewClassName: 'AIRMap',
- validAttributes: {
- region: true,
- },
- },
-
- propTypes: {
- ...View.propTypes,
- /**
- * Used to style and layout the `MapView`. See `StyleSheet.js` and
- * `ViewStylePropTypes.js` for more info.
- */
- style: View.propTypes.style,
-
- /**
- * If `true` the app will ask for the user's location.
- * Default value is `false`.
- *
- * **NOTE**: You need to add NSLocationWhenInUseUsageDescription key in
- * Info.plist to enable geolocation, otherwise it is going
- * to *fail silently*!
- */
- showsUserLocation: PropTypes.bool,
-
- /**
- * If `false` hide the button to move map to the current user's location.
- * Default value is `true`.
- *
- * @platform android
- */
- showsMyLocationButton: PropTypes.bool,
-
- /**
- * If `true` the map will focus on the user's location. This only works if
- * `showsUserLocation` is true and the user has shared their location.
- * Default value is `false`.
- *
- * @platform ios
- */
- followsUserLocation: PropTypes.bool,
-
- /**
- * If `false` points of interest won't be displayed on the map.
- * Default value is `true`.
- *
- */
- showsPointsOfInterest: PropTypes.bool,
-
- /**
- * If `false` compass won't be displayed on the map.
- * Default value is `true`.
- *
- * @platform ios
- */
- showsCompass: PropTypes.bool,
-
- /**
- * If `false` the user won't be able to pinch/zoom the map.
- * Default value is `true`.
- *
- */
- zoomEnabled: PropTypes.bool,
-
- /**
- * If `false` the user won't be able to pinch/rotate the map.
- * Default value is `true`.
- *
- */
- rotateEnabled: PropTypes.bool,
-
- /**
- * If `true` the map will be cached to an Image for performance
- * Default value is `false`.
- *
- */
- cacheEnabled: PropTypes.bool,
-
- /**
- * If `true` the map will be showing a loading indicator
- * Default value is `false`.
- *
- */
- loadingEnabled: PropTypes.bool,
-
- /**
- * Loading background color while generating map cache image or loading the map
- * Default color is light gray.
- *
- */
- loadingBackgroundColor: ColorPropType,
-
- /**
- * Loading indicator color while generating map cache image or loading the map
- * Default color is gray color for iOS, theme color for Android.
- *
- */
- loadingIndicatorColor: ColorPropType,
-
- /**
- * If `false` the user won't be able to change the map region being displayed.
- * Default value is `true`.
- *
- */
- scrollEnabled: PropTypes.bool,
-
- /**
- * If `false` the user won't be able to adjust the camera’s pitch angle.
- * Default value is `true`.
- *
- */
- pitchEnabled: PropTypes.bool,
-
- /**
- * If `false` will hide 'Navigate' and 'Open in Maps' buttons on marker press
- * Default value is `true`.
- *
- * @platform android
- */
- toolbarEnabled: PropTypes.bool,
-
- /**
- * A Boolean indicating whether the map shows scale information.
- * Default value is `false`
- *
- */
- showsScale: PropTypes.bool,
-
- /**
- * A Boolean indicating whether the map displays extruded building information.
- * Default value is `true`.
- */
- showsBuildings: PropTypes.bool,
-
- /**
- * A Boolean value indicating whether the map displays traffic information.
- * Default value is `false`.
- */
- showsTraffic: PropTypes.bool,
-
- /**
- * A Boolean indicating whether indoor maps should be enabled.
- * Default value is `false`
- *
- * @platform android
- */
- showsIndoors: PropTypes.bool,
-
- /**
- * The map type to be displayed.
- *
- * - standard: standard road map (default)
- * - satellite: satellite view
- * - hybrid: satellite view with roads and points of interest overlayed
- * - terrain: (Android only) topographic view
- */
- mapType: PropTypes.oneOf([
- 'standard',
- 'satellite',
- 'hybrid',
- 'terrain',
- ]),
-
- /**
- * The region to be displayed by the map.
- *
- * The region is defined by the center coordinates and the span of
- * coordinates to display.
- */
- region: PropTypes.shape({
- /**
- * Coordinates for the center of the map.
- */
- latitude: PropTypes.number.isRequired,
- longitude: PropTypes.number.isRequired,
-
- /**
- * Difference between the minimun and the maximum latitude/longitude
- * to be displayed.
- */
- latitudeDelta: PropTypes.number.isRequired,
- longitudeDelta: PropTypes.number.isRequired,
- }),
-
- /**
- * The initial region to be displayed by the map. Use this prop instead of `region`
- * only if you don't want to control the viewport of the map besides the initial region.
- *
- * Changing this prop after the component has mounted will not result in a region change.
- *
- * This is similar to the `initialValue` prop of a text input.
- */
- initialRegion: PropTypes.shape({
- /**
- * Coordinates for the center of the map.
- */
- latitude: PropTypes.number.isRequired,
- longitude: PropTypes.number.isRequired,
-
- /**
- * Difference between the minimun and the maximum latitude/longitude
- * to be displayed.
- */
- latitudeDelta: PropTypes.number.isRequired,
- longitudeDelta: PropTypes.number.isRequired,
- }),
-
- /**
- * Maximum size of area that can be displayed.
- *
- * @platform ios
- */
- maxDelta: PropTypes.number,
-
- /**
- * Minimum size of area that can be displayed.
- *
- * @platform ios
- */
- minDelta: PropTypes.number,
-
- /**
- * Insets for the map's legal label, originally at bottom left of the map.
- * See `EdgeInsetsPropType.js` for more information.
- */
- legalLabelInsets: EdgeInsetsPropType,
-
- /**
- * Callback that is called continuously when the user is dragging the map.
- */
- onRegionChange: PropTypes.func,
-
- /**
- * Callback that is called once, when the user is done moving the map.
- */
- onRegionChangeComplete: PropTypes.func,
-
- /**
- * Callback that is called when user taps on the map.
- */
- onPress: PropTypes.func,
-
- /**
- * Callback that is called when user makes a "long press" somewhere on the map.
- */
- onLongPress: PropTypes.func,
-
- /**
- * Callback that is called when user makes a "drag" somewhere on the map
- */
- onPanDrag: PropTypes.func,
-
- /**
- * Callback that is called when a marker on the map is tapped by the user.
- */
- onMarkerPress: PropTypes.func,
-
- /**
- * Callback that is called when a marker on the map becomes selected. This will be called when
- * the callout for that marker is about to be shown.
- *
- * @platform ios
- */
- onMarkerSelect: PropTypes.func,
-
- /**
- * Callback that is called when a marker on the map becomes deselected. This will be called when
- * the callout for that marker is about to be hidden.
- *
- * @platform ios
- */
- onMarkerDeselect: PropTypes.func,
-
- /**
- * Callback that is called when a callout is tapped by the user.
- */
- onCalloutPress: PropTypes.func,
-
- /**
- * Callback that is called when the user initiates a drag on a marker (if it is draggable)
- */
- onMarkerDragStart: PropTypes.func,
-
- /**
- * Callback called continuously as a marker is dragged
- */
- onMarkerDrag: PropTypes.func,
-
- /**
- * Callback that is called when a drag on a marker finishes. This is usually the point you
- * will want to setState on the marker's coordinate again
- */
- onMarkerDragEnd: PropTypes.func,
-
- },
-
- getInitialState: function() {
- return {
- isReady: Platform.OS === 'ios',
- };
- },
-
- componentDidMount: function() {
- const { region, initialRegion } = this.props;
- if (region && this.state.isReady) {
- this.refs.map.setNativeProps({ region });
- } else if (initialRegion && this.state.isReady) {
- this.refs.map.setNativeProps({ region: initialRegion });
- }
- },
-
- componentWillUpdate: function(nextProps) {
- var a = this.__lastRegion;
- var b = nextProps.region;
- if (!a || !b) return;
- if (
- a.latitude !== b.latitude ||
- a.longitude !== b.longitude ||
- a.latitudeDelta !== b.latitudeDelta ||
- a.longitudeDelta !== b.longitudeDelta
- ) {
- this.refs.map.setNativeProps({ region: b });
- }
- },
-
- _onMapReady: function() {
- const { region, initialRegion } = this.props;
- if (region) {
- this.refs.map.setNativeProps({ region });
- } else if (initialRegion) {
- this.refs.map.setNativeProps({ region: initialRegion });
- }
- this.setState({ isReady: true });
- },
-
- _onLayout: function(e) {
- const { region, initialRegion, onLayout } = this.props;
- const { isReady } = this.state;
- if (region && isReady && !this.__layoutCalled) {
- this.__layoutCalled = true;
- this.refs.map.setNativeProps({ region });
- } else if (initialRegion && isReady && !this.__layoutCalled) {
- this.__layoutCalled = true;
- this.refs.map.setNativeProps({ region: initialRegion });
- }
- onLayout && onLayout(e);
- },
-
- _onChange: function(event: Event) {
- this.__lastRegion = event.nativeEvent.region;
- if (event.nativeEvent.continuous) {
- this.props.onRegionChange &&
- this.props.onRegionChange(event.nativeEvent.region);
- } else {
- this.props.onRegionChangeComplete &&
- this.props.onRegionChangeComplete(event.nativeEvent.region);
- }
- },
-
- animateToRegion: function (region, duration) {
- this._runCommand('animateToRegion', [region, duration || 500]);
- },
-
- animateToCoordinate: function (latLng, duration) {
- this._runCommand('animateToCoordinate', [latLng, duration || 500]);
- },
-
- fitToElements: function(animated) {
- this._runCommand('fitToElements', [animated]);
- },
-
- fitToSuppliedMarkers: function(markers, animated) {
- this._runCommand('fitToSuppliedMarkers', [markers, animated]);
- },
-
- takeSnapshot: function (width, height, region, callback) {
- if (!region) {
- region = this.props.region || this.props.initialRegion;
- }
- this._runCommand('takeSnapshot', [width, height, region, callback]);
- },
-
- _getHandle: function() {
- return ReactNative.findNodeHandle(this.refs.map);
- },
-
- _runCommand: function (name, args) {
- switch (Platform.OS) {
- case 'android':
- NativeModules.UIManager.dispatchViewManagerCommand(
- this._getHandle(),
- NativeModules.UIManager.AIRMap.Commands[name],
- args
- );
- break;
-
- case 'ios':
- NativeModules.AIRMapManager[name].apply(
- NativeModules.AIRMapManager[name],
- [this._getHandle(), ...args]
- );
- break;
- }
- },
-
- render: function() {
- let props;
-
- if (this.state.isReady) {
- props = {
- ...this.props,
- region: null,
- initialRegion: null,
- onChange: this._onChange,
- onMapReady: this._onMapReady,
- onLayout: this._onLayout,
- };
- if (Platform.OS === 'ios' && props.mapType === 'terrain') {
- props.mapType = 'standard';
- }
- props.handlePanDrag = !!props.onPanDrag;
- } else {
- props = {
- style: this.props.style,
- region: null,
- initialRegion: null,
- onChange: this._onChange,
- onMapReady: this._onMapReady,
- onLayout: this._onLayout,
- };
- }
-
- return (
-
- );
- },
-});
-
-var AIRMap = requireNativeComponent('AIRMap', MapView, {
- nativeOnly: {
- onChange: true,
- onMapReady: true,
- handlePanDrag: true,
- },
-});
-
-MapView.Marker = MapMarker;
-MapView.Polyline = MapPolyline;
-MapView.Polygon = MapPolygon;
-MapView.Circle = MapCircle;
-MapView.Callout = MapCallout;
-
-MapView.Animated = Animated.createAnimatedComponent(MapView);
-
-module.exports = MapView;
diff --git a/docs/callout.md b/docs/callout.md
index 58b59a6bd..e927562b6 100644
--- a/docs/callout.md
+++ b/docs/callout.md
@@ -1,14 +1,35 @@
-# `` Component API
+# `` Component API
## Props
-| Prop | Type | Default | Note |
-|---|---|---|---|
-| `tooltip` | `Boolean` | `false` | If `false`, a default "tooltip" bubble window will be drawn around this callouts children. If `true`, the child views can fully customize their appearance, including any "bubble" like styles.
+| Prop | Type | Default | Note |
+| -------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `tooltip` | `Boolean` | `false` | If `false`, a default "tooltip" bubble window will be drawn around this callouts children. If `true`, the child views can fully customize their appearance, including any "bubble" like styles. |
+| `alphaHitTest` | `Boolean` | `false` | If `true`, clicks on transparent areas in callout will be passed to map. **Note**: iOS only. |
+## Events
+
+| Event Name | Returns | Notes |
+| ---------- | ------- | ------------------------------------------------------------ |
+| `onPress` | | Callback that is called when the user presses on the callout |
+
+---
+
+# `` Component API
+
+**Note**: Supported on iOS only.
+Use to handle press on specific subview of callout.
+Put this component inside ``.
## Events
-| Event Name | Returns | Notes
-|---|---|---|
-| `onPress` | | Callback that is called when the user presses on the callout
+| Event Name | Returns | Notes |
+| ---------- | ------- | ---------------------------------------------------------------------------- |
+| `onPress` | | Callback that is called when the user presses on this subview inside callout |
+
+## Notes
+
+Native press event has property `action`, which is:
+
+- `callout-press` (or `marker-overlay-press` for GoogleMaps on iOS) for press on ``
+- `callout-inside-press` (or `marker-inside-overlay-press` for GoogleMaps on iOS) for press on ``
diff --git a/docs/circle.md b/docs/circle.md
index 78a850283..5a42a2cd1 100644
--- a/docs/circle.md
+++ b/docs/circle.md
@@ -1,21 +1,20 @@
-# `` Component API
+# `` Component API
## Props
-| Prop | Type | Default | Note |
-|---|---|---|---|
-| `center` | `LatLng` | (Required) | The coordinate of the center of the circle
-| `radius` | `Number` | (Required) | The radius of the circle to be drawn (in meters)
-| `strokeWidth` | `Number` | `1` | The stroke width to use for the path.
-| `strokeColor` | `String` | `#000` | The stroke color to use for the path.
-| `fillColor` | `String` | | The fill color to use for the path.
-| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path.
-| `lineJoin` | `Array` | | The line join style to apply to corners of the path.
-| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees.
-| `geodesic` | `Boolean` | | Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to straight lines on the Mercator projection. A geodesic is the shortest path between two points on the Earth's surface. The geodesic curve is constructed assuming the Earth is a sphere.
-| `lineDashPhase` | `Number` | `0` | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap.
-| `lineDashPattern` | `Array` | `null` | (iOS only) An array of numbers specifying the dash pattern to use for the path. The array contains one or more numbers that indicate the lengths (measured in points) of the line segments and gaps in the pattern. The values in the array alternate, starting with the first line segment length, followed by the first gap length, followed by the second line segment length, and so on.
-
+| Prop | Type | Default | Note |
+| ----------------- | --------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `center` | `LatLng` | (Required) | The coordinate of the center of the circle |
+| `radius` | `Number` | (Required) | The radius of the circle to be drawn (in meters) |
+| `strokeWidth` | `Number` | `1` | The stroke width to use for the path. |
+| `strokeColor` | `String` | `#000`, `rgba(r,g,b,0.5)` | The stroke color to use for the path. |
+| `fillColor` | `String` | `#000`, `rgba(r,g,b,0.5)` | The fill color to use for the path. |
+| `zIndex` | `Number` | 0 | The order in which this tile overlay is drawn with respect to other overlays. An overlay with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays with the same z-index is arbitrary. The default zIndex is 0. (Android Only) |
+| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path. Other values : `butt`, `square` |
+| `lineJoin` | `String` | | The line join style to apply to corners of the path. possible value: `miter`, `round`, `bevel` |
+| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees. |
+| `lineDashPhase` | `Number` | `0` | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap. |
+| `lineDashPattern` | `Array` | `null` | (iOS only) An array of numbers specifying the dash pattern to use for the path. The array contains one or more numbers that indicate the lengths (measured in points) of the line segments and gaps in the pattern. The values in the array alternate, starting with the first line segment length, followed by the first gap length, followed by the second line segment length, and so on. |
## Types
diff --git a/docs/examples-setup.md b/docs/examples-setup.md
new file mode 100644
index 000000000..686f96124
--- /dev/null
+++ b/docs/examples-setup.md
@@ -0,0 +1,12 @@
+# Examples Setup
+
+- Clone or download the repository.
+- From the root of the project run `yarn bootstrap`
+- Add your API key(s)
+ - Android
+ - Open `example/android/local.properties` (or create the file if it doesn't already exist)
+ - Add the following line: `MAPS_API_KEY=your_api_key_here`
+ - iOS
+ - Open `example/ios/Config.xcconfig` (or create the file if it doesn't already exist)
+ - Add the following line: `MAPS_API_KEY=your_api_key_here`
+- Run `yarn android` or `yarn ios` within the example folder
diff --git a/docs/geojson.md b/docs/geojson.md
new file mode 100644
index 000000000..f4d9c9c29
--- /dev/null
+++ b/docs/geojson.md
@@ -0,0 +1,55 @@
+# `` Component API
+
+## Props
+
+| Prop | Type | Default | Note |
+| ------------------- | ------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `geojson` | `GeoJSON` | | [Geojson](https://geojson.org/) description of object. |
+| `strokeColor` | `String` | `stroke` property in GeoJson if present else `#000` | The stroke color to use for polygons and polylines. |
+| `fillColor` | `String` | `fill` property in GeoJson | The fill color to use for polygons. |
+| `strokeWidth` | `Number` | `stroke-width` property in Geojson if present else `1` | The stroke width to use for polygons and polylines. |
+| `color` | `String` | `marker-color` property in GeoJson | The color to use for points. |
+| `lineDashPhase` | `Number` | | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap. |
+| `lineDashPattern` | `Array` | | An array of numbers specifying the dash pattern to use for the path. The array contains one or more numbers that indicate the lengths (measured in points) of the line segments and gaps in the pattern. The values in the array alternate, starting with the first line segment length, followed by the first gap length, followed by the second line segment length, and so on. |
+| `lineCap` | `'butt' \| 'round' \| 'square'` | | The line cap style to apply to the open ends of the path. Possible values are `butt`, `round` or `square`. Note: lineCap is not yet supported for GoogleMaps provider on iOS. |
+| `lineJoin` | `'miter' \| 'round' \| 'bevel'` | | The line join style to apply to corners of the path. Possible values are `miter`, `round` or `bevel`. |
+| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees. |
+| `zIndex` | `Number` | | Layer level of the z-index value |
+| `onPress` | `Function` | | returns the selected overlay value with the onPress functionality |
+| `markerComponent` | `React Node` | | Component to render in place of the default marker when the overlay type is a `point` |
+| `title` | `string` | | The title of the marker. This is only used if the component has no children that are a `` |
+| `tracksViewChanges` | `Boolean` | true | Sets whether this marker should track view changes. It's recommended to turn it off whenever it's possible to improve custom marker performance. This is the default value for all point markers in your geojson data. It can be overriden on a per point basis by adding a `trackViewChanges` property to the `properties` object on the point. |
+| `anchor` | `Point` | (0.5, 1) | Sets the anchor point for the marker.
The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.
The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0], where (0, 0) is the top-left corner of the image, and (1, 1) is the bottom-right corner. The anchoring point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid, obtained by scaling the then rounding. For example, in a 4 x 2 image, the anchor point (0.7, 0.6) resolves to the grid point at (3, 1).
For MapKit on iOS, see the `centerOffset` prop. |
+| `centerOffset` | `Point` | (0, 0) | The offset (in points) at which to display the view.
By default, the center point of an annotation view is placed at the coordinate point of the associated annotation. You can use this property to reposition the annotation view as needed. This x and y offset values are measured in points. Positive offset values move the annotation view down and to the right, while negative values move it up and to the left.
For Google Maps, see the `anchor` prop. |
+
+## Example
+
+```
+import React from 'react';
+import MapView, {Geojson} from 'react-native-maps';
+
+const myPlace = {
+ type: 'FeatureCollection',
+ features: [
+ {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'Point',
+ coordinates: [64.165329, 48.844287],
+ }
+ }
+ ]
+};
+
+const Map = props => (
+
+
+
+);
+```
diff --git a/docs/heatmap.md b/docs/heatmap.md
new file mode 100644
index 000000000..12ab3fe3a
--- /dev/null
+++ b/docs/heatmap.md
@@ -0,0 +1,32 @@
+# `` Component API
+
+**Note**: Supported on Google Maps only.
+
+## Props
+
+| Prop | Type | Default | Note |
+| ---------- | ----------------------- | ------- | ----------------------------------------------------------------- |
+| `points` | `Array` | | Array of heatmap entries to apply towards density. |
+| `radius` | `Number` | `20` | The radius of the heatmap points in pixels, between 10 and 50. |
+| `opacity` | `Number` | `0.7` | The opacity of the heatmap. |
+| `gradient` | `Object` | | Heatmap gradient configuration (See below for _Gradient Config_). |
+
+## Gradient Config
+
+[Android Doc](https://developers.google.com/maps/documentation/android-sdk/utility/heatmap#custom) | [iOS Doc](https://developers.google.com/maps/documentation/ios-sdk/utility/heatmap#customize)
+
+| Prop | Type | Default | Note |
+| -------------- | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| `colors` | `Array` | | Colors (one or more) to use for gradient. |
+| `startPoints` | `Array` | | Array of floating point values from 0 to 1 representing where each color starts. Array length must be equal to `colors` array length. |
+| `colorMapSize` | `Number` | `256` | Resolution of color map -- number corresponding to the number of steps colors are interpolated into. |
+
+## Types
+
+```
+type WeightedLatLng = {
+ latitude: Number;
+ longitude: Number;
+ weight?: Number;
+}
+```
diff --git a/docs/installation.md b/docs/installation.md
index 3c3f18255..c39f8a72f 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -1,194 +1,252 @@
# Installation
-First, download the library from npm:
+Install the library from npm:
-```
-npm install react-native-maps --save
+```sh
+$ npm install react-native-maps
+# --- or ---
+$ yarn add react-native-maps
```
-Then you must install the native dependencies. You can use [`rnpm`](https://github.com/rnpm/rnpm) to
-add native dependencies automatically:
+The actual map implementation depends on the platform. On Android, one has to use [Google Maps](https://developers.google.com/maps/documentation/), which in turn requires you to obtain an [API key for the Android SDK](https://developers.google.com/maps/documentation/android-sdk/signup).
-`$ rnpm link`
+On iOS, one can choose between Google Maps or the native [Apple Maps](https://developer.apple.com/documentation/mapkit/) implementation.
-Go to step 4 to configure Google Maps API KEY in Android.
+When using Google Maps on iOS, you need also to obtain an [API key for the iOS SDK](https://developers.google.com/maps/documentation/ios-sdk/get-api-key) and include the Google Maps library in your build. The native Apple Maps based implementation works out-of-the-box and is therefore simpler to use at the price of missing some of the features supported by the Google Maps backend.
->This installation should work in physical devices. For Genymotion, please check installation step 5
+> **WARNING**: Before you can start using the Google Maps Platform APIs and SDKs, you must sign up and create a [billing account](https://developers.google.com/maps/gmp-get-started#create-billing-account)!
-or do it manually as described below:
+---
## iOS
-### Cocoapods
-To install using Cocoapods, simply insert the following line into your `Podfile` and run `pod install`
-
-`pod 'react-native-maps', :path => '../node_modules/react-native-maps'`
-
-### Manually
-1. Open your project in XCode, right click on `Libraries` and click `Add
- Files to "Your Project Name"` Look under `node_modules/react-native-maps/ios` and add `AIRMaps.xcodeproj`.
-2. Add `libAIRMaps.a` to `Build Phases -> Link Binary With Libraries.
-3. Click on `AIRMaps.xcodeproj` in `Libraries` and go the `Build
- Settings` tab. Double click the text to the right of `Header Search
- Paths` and verify that it has `$(SRCROOT)/../../react-native/React` as well as `$(SRCROOT)/../../react-native/Libraries/Image` - if they
- aren't, then add them. This is so XCode is able to find the headers that
- the `AIRMaps` source files are referring to by pointing to the
- header files installed within the `react-native` `node_modules`
- directory.
-4. Whenever you want to use it within React code now you can: `var MapView =
- require('react-native-maps');`
+After installing the npm package, we need to install the pod.
-## Android
+```sh
+$ (cd ios && pod install)
+# --- or ---
+$ npx pod-install
+```
-1. in your `build.gradle` add:
-```groovy
+### Enabling Google Maps
+
+If you want to enable Google Maps on iOS, obtain the Google API key and edit your `AppDelegate.m(m)` as follows:
+
+```diff
++ #import
+
+@implementation AppDelegate
+...
+
+(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
++ [GMSServices provideAPIKey:@"_YOUR_API_KEY_"]; // add this line using the api key obtained from Google Console
...
-dependencies {
- ...
- compile 'com.airbnb.android:react-native-maps:0.7.1'
-}
```
-For React Native v0.29.0 or above:
+The `[GMSServices provideAPIKey]` should be the **first call** of the method.
-2. in your application object, add:
+Google Maps SDK for iOS requires iOS 14, so make sure that your deployment target is >= 4 in your iOS project settings.
-```java
-public class MyApplication extends Application implements ReactApplication {
- private final ReactNativeHost reactNativeHost = new ReactNativeHost(this) {
- @Override protected List getPackages() {
- return Arrays.asList(
- new MainReactPackage(),
- new MapsPackage());
- }
- };
+Also make sure that your Podfile deployment target is set to >= 14 at the top of your Podfile, eg:
- @Override public ReactNativeHost getReactNativeHost() {
- return reactNativeHost;
- }
-}
+```ruby
+platform :ios, '14'
```
-For older versions of React Native:
+Add the following to your Podfile above the `use_native_modules!` function and run `pod install` in the ios folder:
-```java
-@Override protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mReactRootView = new ReactRootView(this);
+```ruby
+# React Native Maps dependencies
- mReactInstanceManager = ReactInstanceManager.builder()
- .setApplication(getApplication())
- .setBundleAssetName("index.android.bundle")
- .setJSMainModuleName("index.android")
- .addPackage(new MainReactPackage())
- .addPackage(new MapsPackage()) // <---- and This!
- .setUseDeveloperSupport(BuildConfig.DEBUG)
- .setInitialLifecycleState(LifecycleState.RESUMED)
- .build();
+rn_maps_path = '../node_modules/react-native-maps'
+pod 'react-native-google-maps', :path => rn_maps_path
+```
- mReactRootView.startReactApplication(mReactInstanceManager, "MyApp", null);
+The app's Info.plist file must contain a NSLocationWhenInUseUsageDescription with a user-facing purpose string explaining clearly and completely why your app needs the location, otherwise Apple will reject your app submission. This is required whether or not you are accessing the users location, as Google Maps iOS SDK contains the code required to access the users location.
- setContentView(mReactRootView);
-}
-```
+That's it, you made it! 👍
+
+---
-4. Specify your Google Maps API Key:
- > To develop is recommended a ***Browser Key*** without refeer restriction. Go to https://console.developers.google.com/apis/credentials to check your credentials.
+## Android
+
+### Specify your Google Maps API key
-Add your API key to your manifest file:
+Add your API key to your manifest file (`android/app/src/main/AndroidManifest.xml`):
```xml
-
-
+
+
```
-5. ensure that you have Google Play Services installed:
- * For Genymotion you can follow [these instructions](http://stackoverflow.com/a/20137324/1424349).
- * For a physical device you need to search on Google 'Google Play Services'. There will be a link that takes you to the play store and from there you will see a button to update it (do not search within the Play Store).
+### Upgrading to >= v0.31.0
+
+The installation documentation previously specified adding `supportLibVersion`, `playServicesVersion` and `androidMapsUtilsVersion` to `build.gradle`.
+
+None of these keys are required anymore and can be removed, if not used by other modules in your project.
+
+> **ATTENTION**: If you leave `playServicesVersion` in `build.gradle`, the version must be at least `18.0.0`
+
+### Ensure that you have Google Play Services installed
+
+- For the Genymotion emulator, you can follow [these instructions](https://www.genymotion.com/help/desktop/faq/#google-play-services).
+- For a physical device you need to search on Google for 'Google Play
+ Services'. There will be a link that takes you to the Play Store and
+ from there you will see a button to update it (do not search within the
+ Play Store).
-**Troubleshooting**
+## Troubleshooting
-If you have a blank map issue, ([#118](https://github.com/lelandrichardson/react-native-maps/issues/118), [#176](https://github.com/lelandrichardson/react-native-maps/issues/176)) try the following lines :
+### The map background is blank (Google Maps)
-**On iOS :**
+If google logo/markers/polylines etc are displayed but the map
+background is otherwise blank, this is likely an API key issue. Verify
+your API keys and their restrictions. Ensure the native `provideAPIKey`
+call is the first line of `didFinishLaunchingWithOptions`.
- You have to link dependencies with rnpm and re-run the build :
-1. `rnpm link`
-2. `react-native run-ios`
+Ensure also that the relevant Google APIs have been enabled for your
+project from the URLs below:
-**On Android :**
+- [Google Maps SDK Android](https://console.developers.google.com/apis/library/maps-android-backend.googleapis.com/)
+- [Google Maps SDK iOS (if required)](https://console.developers.google.com/apis/library/maps-ios-backend.googleapis.com)
-1. Set this Stylesheet in your map component
+For reference, you may read the relevant issue reports: ([#118](https://github.com/react-native-maps/react-native-maps/issues/118), [#176](https://github.com/react-native-maps/react-native-maps/issues/176), [#684](https://github.com/react-native-maps/react-native-maps/issues/684)).
+
+### The map background is gray (Google Maps)
+
+If you get grey screen on android device create google_maps_api.xml in android/app/src/main/res/values.
+
+```xml
+
+ (api key here)
+
```
+
+### No map whatsoever
+
+Ensure the map component and its container have viewport dimensions. An
+example is below:
+
+```jsx
+import MapView, { PROVIDER_GOOGLE } from 'react-native-maps'; // remove PROVIDER_GOOGLE import if not using Google Maps
...
const styles = StyleSheet.create({
- container: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- height: 400,
- width: 400,
- justifyContent: 'flex-end',
- alignItems: 'center',
- },
- map: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- },
+ container: {
+ ...StyleSheet.absoluteFillObject,
+ height: 400,
+ width: 400,
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ },
+ map: {
+ ...StyleSheet.absoluteFillObject,
+ },
});
-module.exports = React.createClass({
-
- render: function () {
- const { region } = this.props;
- console.log(region);
-
- return (
-
-
-
-
- )
- }
-})
+export default () => (
+
+
+
+
+);
+```
+
+### Build issues with Google Maps iOS Utils (iOS)
+
+If your XCode project uses dynamic frameworks (e.g. you also have Swift
+code in your project), you cannot install `Google-Maps-iOS-Utils` with
+CocoaPods. The issue and a workaround for it has been documented
+[here](https://github.com/googlemaps/google-maps-ios-utils/blob/b721e95a500d0c9a4fd93738e83fc86c2a57ac89/Swift.md).
+
+### Runtime errors on iOS (Apple Maps)
+
+If you are trying to mount the map with the `GOOGLE_PROVIDER` during
+runtime, but your build has been configured for the Apple Maps backend,
+a runtime exception will be raised.
+
+In addition, when using Apple Maps, some Google-only functionalities
+have been disabled via runtime errors.
+
+An exception will be raised if you try to use advanced features that
+depend on the [Google Maps SDK for
+iOS](https://github.com/googlemaps/google-maps-ios-utils). These include
+
+- Making markers from KML files
+- Heatmap rendering
+- Marker clustering
+- etc.
+
+### Clearing caches
+
+Run these commands to clean caches
+
+```sh
+watchman watch-del-all
+npm cache clean
+
+# Android, if you encounter `com.android.dex.DexException: Multiple dex files define Landroid/support/v7/appcompat/R$anim`, then clear build folder.
+cd android
+./gradlew clean
+cd ..
```
-2. Run "android" and make sure every packages is updated.
-3. If not installed yet, you have to install the following packages :
- - Extras / Google Play services
- - Extras / Google Repository
- - Android 6.0 (API 23) / Google APIs Intel x86 Atom System Image Rev. 13
-4. Check manual installation steps
-5. Generate your SHA1 key :
- `keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android`
-6. Go to [Google API Console](https://console.developers.google.com/flows/enableapi?apiid=maps_android_backend) and select your project, or create one.
-In `Overview -> Google Maps API -> Google Maps Android API ` -> Check if it's enabled
-Create a new key by clicking on `Create credentials -> API Key -> Android Key`, enter the name of the API key and your SHA1 key, generated before, and create it.
-Check installation step 4.
+### When using Android studio
+
+Make sure your Android studio is up to date and set up following the [React Native docs](https://reactnative.dev/docs/environment-setup).
+
+In particular, the following packages have to be installed:
+
+- Extras / Google Play services
+- Extras / Google Repository
+- Android 6.0 (API 23) / Google APIs Intel x86 Atom System Image Rev. 19
+- Android SDK Build-tools 23.0.3
+
+### Android emulator issues
+
+- When starting Android emulator, make sure you have enabled `Wipe user data`.
+- If you are using Android Virtual Devices (AVD), ensure that `Use Host GPU` is checked in the settings for your virtual device.
+- If using an emulator and the only thing that shows up on the screen is
+ the message: `[APPNAME] won't run without Google Play services which are not supported by your device.`, you need to change the emulator
+ CPU/ABI setting to a system image that includes Google APIs. These may
+ need to be downloaded from the Android SDK Manager first.
+
+### Google Play Services conflicting issues with other modules
+
+In case you have multiple modules using the same Google Play Services dependencies (such as `react-native-onesignal`), you can exclude the conflicting dependencies from the modules and import the Google Play Services dependencies in the project-wide `build.gradle` file like the following example:
+
+```groovy
+ implementation(project(':react-native-onesignal')){
+ exclude group: 'com.google.android.gms'
+ }
+
+ implementation(project(':react-native-maps')){
+ exclude group: 'com.google.android.gms'
+ }
+ implementation 'com.google.android.gms:play-services-base:18.0.1'
+ implementation 'com.google.android.gms:play-services-location:19.0.1'
+ implementation 'com.google.android.gms:play-services-maps:18.0.2'
+```
-7. Clean the cache :
- `watchman watch-del-all`
- `npm cache clean`
+A list of the current dependencies can be found [here](https://developers.google.com/android/guides/setup#list-dependencies).
-8. When starting emulator, make sure you have enabled `Wipe user data`.
+> **ATTENTION**: `react-native-maps` requires `play-services-maps >= 18.0.0`
-9. Run `react-native run-android`
+### Trouble with Google Play services
-10. At this step it should work, but if not, go to your [Google API Console](https://console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&pli=1) and create a `Browser key` instead of a `Android key` and go to step 6.
+- Make sure that your emulator has Google Play (Go to Android studio -> Virtual Devices -> Check that you have icon in "Play Store" column)
+- Click to bottom dots icon in the emulator
+- Go to Google Play Tab and click Update
diff --git a/docs/mapview.md b/docs/mapview.md
index 1b8abc8d7..2cdea8809 100644
--- a/docs/mapview.md
+++ b/docs/mapview.md
@@ -2,60 +2,102 @@
## Props
-| Prop | Type | Default | Note |
-|---|---|---|---|
-| `region` | `Region` | | The region to be displayed by the map.
The region is defined by the center coordinates and the span of coordinates to display.
-| `initialRegion` | `Region` | | The initial region to be displayed by the map. Use this prop instead of `region` only if you don't want to control the viewport of the map besides the initial region.
Changing this prop after the component has mounted will not result in a region change.
This is similar to the `initialValue` prop of a text input.
-| `mapType` | `String` | `"standard"` | The map type to be displayed.
- standard: standard road map (default) - satellite: satellite view - hybrid: satellite view with roads and points of interest overlayed - terrain: (Android only) topographic view
-| `showsUserLocation` | `Boolean` | `false` | If `true` the app will ask for the user's location. **NOTE**: You need to add `NSLocationWhenInUseUsageDescription` key in Info.plist to enable geolocation, otherwise it is going to *fail silently*!
-| `followsUserLocation` | `Boolean` | `false` | If `true` the map will focus on the user's location. This only works if `showsUserLocation` is true and the user has shared their location.
-| `showsPointsOfInterest` | `Boolean` | `true` | If `false` points of interest won't be displayed on the map.
-| `showsCompass` | `Boolean` | `true` | If `false` compass won't be displayed on the map.
-| `showsScale` | `Boolean` | `true` | A Boolean indicating whether the map shows scale information.
-| `showsBuildings` | `Boolean` | `true` | A Boolean indicating whether the map displays extruded building information.
-| `showsTraffic` | `Boolean` | `true` | A Boolean value indicating whether the map displays traffic information.
-| `showsIndoors` | `Boolean` | `true` | A Boolean indicating whether indoor maps should be enabled.
-| `zoomEnabled` | `Boolean` | `true` | If `false` the user won't be able to pinch/zoom the map.
-| `rotateEnabled` | `Boolean` | `true` | If `false` the user won't be able to pinch/rotate the map.
-| `scrollEnabled` | `Boolean` | `true` | If `false` the user won't be able to change the map region being displayed.
-| `pitchEnabled` | `Boolean` | `true` | If `false` the user won't be able to adjust the camera’s pitch angle.
-| `toolbarEnabled` | `Boolean` | `true` | `Android only` If `false` will hide 'Navigate' and 'Open in Maps' buttons on marker press
-| `cacheEnabled` | `Boolean` | `false` | If `true` map will be cached and displayed as a image instead of being interactable, for performance usage.
-| `loadingEnabled` | `Boolean` | `false` | If `true` a loading indicator will show while the map is loading.
-| `loadingIndicatorColor` | `Color` | `#606060` | Sets loading indicator color, default to `#606060`.
-| `loadingBackgroundColor` | `Color` | `#FFFFFF` | Sets loading background color, default to `#FFFFFF`.
-
-
+| Prop | Type | Default | Note |
+| --------------------------------- | ------------------------------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `provider` | `string` | | The map framework to use.
Either `"google"` for GoogleMaps, otherwise `null` or `undefined` to use the native map framework (`MapKit` in iOS and `GoogleMaps` in android). |
+| `region` | `Region` | | The region to be displayed by the map.
The region is defined by the center coordinates and the span of coordinates to display. |
+| `initialRegion` | `Region` | | The initial region to be displayed by the map. Use this prop instead of `region` only if you don't want to control the viewport of the map besides the initial region.
Changing this prop after the component has mounted will not result in a region change.
This is similar to the `initialValue` prop of a text input. |
+| `camera` | `Camera` | | The camera view the map should display. If you use this, the `region` property is ignored. |
+| `initialCamera` | `Camera` | | Like `initialRegion`, use this prop instead of `camera` only if you don't want to control the viewport of the map besides the initial camera setting.
Changing this prop after the component has mounted will not result in a region change.
This is similar to the `initialValue` prop of a text input. |
+| `mapPadding` | `EdgePadding` | | Adds custom padding to each side of the map. Useful when map elements/markers are obscured. |
+| `paddingAdjustmentBehavior` | 'always' \| 'automatic' \| 'never' | 'never' | Indicates how/when to affect padding with safe area insets (`GoogleMaps` in iOS only) |
+| `liteMode` | `Boolean` | `false` | Enable [lite mode](https://developers.google.com/maps/documentation/android-sdk/lite#overview_of_lite_mode). **Note**: Android only. |
+| `googleMapId` | `String` | | Google Map ID (only for Provider "google") [google map id](https://developers.google.com/maps/documentation/get-map-id) |
+| `mapType` | `String` | `"standard"` | The map type to be displayed.
- standard: standard road map (default) - none: no map **Note** Not available on MapKit - satellite: satellite view - hybrid: satellite view with roads and points of interest overlayed - terrain: topographic view - mutedStandard: more subtle, makes markers/lines pop more (iOS 11.0+ only) - satelliteFlyover: 3D globe with sattelite view (iOS 13.0+ Apple Maps only) - hybridFlyover: 3D globe with hybrid view (iOS 13.0+ Apple Maps only) |
+| `customMapStyle` | `Array` | | Adds custom styling to the map component. See [README](https://github.com/react-native-maps/react-native-maps#customizing-the-map-style) for more information. |
+| `userInterfaceStyle` | 'light' \| 'dark' | | Sets the map to the style selected. Default is whatever the system settings is. **Note:** iOS Maps only (aka MapKit). |
+| `showsUserLocation` | `Boolean` | `false` | If `true` the users location will be shown on the map. **NOTE**: You need runtime location permissions prior to setting this to true, otherwise it is going to _fail silently_! Checkout the excellent [react-native-permissions](https://github.com/zoontek/react-native-permissions) for this. |
+| `userLocationPriority` | 'balanced'\|'high'\|'low'\|'passive' | 'high' | Set power priority of user location tracking. See [Google APIs documentation](https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest.html). **Note:** Android only. |
+| `userLocationUpdateInterval` | `Number` | 5000 | Interval of user location updates in milliseconds. See [Google APIs documentation](https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest.html). **Note:** Android only. |
+| `userLocationFastestInterval` | `Number` | 5000 | Fastest interval the application will actively acquire locations. See [Google APIs documentation](https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest.html). **Note:** Android only. |
+| `userLocationAnnotationTitle` | `String` | | The title of the annotation for current user location. This only works if `showsUserLocation` is true. There is a default value `My Location` set by MapView. **Note**: iOS only. |
+| `followsUserLocation` | `Boolean` | `false` | If `true` the map will focus on the user's location. This only works if `showsUserLocation` is true and the user has shared their location. **Note**: Apple Maps only. |
+| `userLocationCalloutEnabled` | `Boolean` | `false` | If `true` clicking user location will show the default callout for userLocation annotation. **Note**: Apple Maps only. |
+| `showsMyLocationButton` | `Boolean` | `true` | If `false` hide the button to move map to the current user's location. |
+| `showsPointsOfInterest` | `Boolean` | `true` | If `false` points of interest won't be displayed on the map. **Note**: Apple Maps only. |
+| `showsCompass` | `Boolean` | `true` | If `false` compass won't be displayed on the map. |
+| `showsScale` | `Boolean` | `true` | A Boolean indicating whether the map shows scale information. **Note**: Apple Maps only. |
+| `showsBuildings` | `Boolean` | `true` | A Boolean indicating whether the map displays extruded building information. |
+| `showsTraffic` | `Boolean` | `false` | A Boolean value indicating whether the map displays traffic information. |
+| `showsIndoors` | `Boolean` | `true` | A Boolean indicating whether indoor maps should be enabled. |
+| `showsIndoorLevelPicker` | `Boolean` | `false` | A Boolean indicating whether indoor level picker should be enabled. **Note:** Google Maps only (either Android or iOS with `PROVIDER_GOOGLE`). |
+| `zoomEnabled` | `Boolean` | `true` | If `false` the user won't be able to pinch/zoom the map. |
+| `zoomTapEnabled` | `Boolean` | `true` | If `false` the user won't be able to double tap to zoom the map. **Note:** But it will greatly decrease delay of tap gesture recognition. **Note:** Google Maps on iOS only |
+| `zoomControlEnabled` | `Boolean` | `true` | If `false` the zoom control at the bottom right of the map won't be visible **Note:** Android only. |
+| `minZoomLevel` | `Number` | `0` | Minimum zoom value for the map, must be between 0 and 20. **Note:** Deprecated on Apple Maps. Use cameraZoomRange instead. |
+| `maxZoomLevel` | `Number` | `20` | Maximum zoom value for the map, must be between 0 and 20. **Note:** Deprecated on Apple Maps. Use cameraZoomRange instead. |
+| `rotateEnabled` | `Boolean` | `true` | If `false` the user won't be able to pinch/rotate the map. |
+| `scrollEnabled` | `Boolean` | `true` | If `false` the user won't be able to change the map region being displayed. |
+| `scrollDuringRotateOrZoomEnabled` | `Boolean` | `true` | If `false` the map will stay centered while rotating or zooming. **Note:** Google Maps only |
+| `pitchEnabled` | `Boolean` | `true` | If `false` the user won't be able to adjust the camera’s pitch angle. |
+| `toolbarEnabled` | `Boolean` | `true` | `Android only` If `false` will hide 'Navigate' and 'Open in Maps' buttons on marker press. If you enable the toolbar, make sure to [edit your AndroidManifest.xml](https://github.com/react-native-maps/react-native-maps/issues/4403#issuecomment-1219856534) |
+| `cacheEnabled` | `Boolean` | `false` | If `true` map will be cached and displayed as an image instead of being interactable, for performance usage. **Note:** Apple Maps only |
+| `loadingEnabled` | `Boolean` | `false` | If `true` a loading indicator will show while the map is loading. |
+| `loadingIndicatorColor` | `Color` | `#606060` | Sets loading indicator color, default to `#606060`. |
+| `loadingBackgroundColor` | `Color` | `#FFFFFF` | Sets loading background color, default to `#FFFFFF`. |
+| `tintColor` | `color` | `null` | Sets the tint color of the map. (Changes the color of the position indicator) Defaults to system blue. **Note:** iOS (Apple maps) only. |
+| `moveOnMarkerPress` | `Boolean` | `true` | `Android only` If `false` the map won't move when a marker is pressed. |
+| `legalLabelInsets` | `EdgeInsets` | | If set, changes the position of the "Legal" label link from the OS default. **Note:** iOS only. |
+| `kmlSrc` | `string` | | The URL from KML file. **Note:** Google Maps and Markers only (either Android or iOS with `PROVIDER_GOOGLE`). |
+| `compassOffset` | `Point` | | If set, changes the position of the compass. **Note:** iOS Maps only. |
+| `isAccessibilityElement` | `Boolean` | `false` | Determines whether the MapView captures VoiceOver touches or forwards them to children. When `true`, map markers are not visible to VoiceOver. **Note:** iOS Maps only. |
+| `cameraZoomRange` | `CameraZoomRange` | | Map camera distance limits. `minCenterCoordinateDistance` for minimum distance, `maxCenterCoordinateDistance` for maximum, `animated` for animated zoom range changes. Takes precedence if conflicting with `minZoomLevel`, `maxZoomLevel`. **Note**: iOS 13.0+ only. |
## Events
-| Event Name | Returns | Notes
-|---|---|---|
-| `onRegionChange` | `Region` | Fired when the map ends panning or zooming.
-| `onRegionChangeComplete` | `Region` | Fired when the map ends panning or zooming.
-| `onPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when user taps on the map.
-| `onPanDrag` | `{ coordinate: LatLng, position: Point }` | Callback that is called when user presses and drags the map. **NOTE**: for iOS `scrollEnabled` should be set to false to trigger the event
-| `onLongPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when user makes a "long press" somewhere on the map.
-| `onMarkerPress` | | Callback that is called when a marker on the map is tapped by the user.
-| `onMarkerSelect` | | Callback that is called when a marker on the map becomes selected. This will be called when the callout for that marker is about to be shown. **Note**: iOS only.
-| `onMarkerDeselect` | | Callback that is called when a marker on the map becomes deselected. This will be called when the callout for that marker is about to be hidden. **Note**: iOS only.
-| `onCalloutPress` | | Callback that is called when a callout is tapped by the user.
-| `onMarkerDragStart` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user initiates a drag on a marker (if it is draggable)
-| `onMarkerDrag` | `{ coordinate: LatLng, position: Point }` | Callback called continuously as a marker is dragged
-| `onMarkerDragEnd` | `{ coordinate: LatLng, position: Point }` | Callback that is called when a drag on a marker finishes. This is usually the point you will want to setState on the marker's coordinate again
-
-
+To access event data, you will need to use `e.nativeEvent`. For example, `onPress={e => console.log(e.nativeEvent)}` will log the entire event object to your console.
+
+| Event Name | Returns | Notes |
+| ------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `onMapReady` | | Callback that is called once the map is fully loaded. |
+| `onKmlReady` | `KmlContainer` | Callback that is called once the kml is fully loaded. |
+| `onRegionChangeStart` | `{ isGesture: boolean }` | Callback that is called once before the region changes, such as when the user starts moving the map. The second parameter is an object containing more details about the move. `isGesture` property indicates if the move was from the user (true) or an animation (false). **Note**: `isGesture` is supported by Google Maps only. |
+| `onRegionChange` | (`Region`, `{isGesture: boolean}`) | Callback that is called continuously when the region changes, such as when a user is dragging the map. The second parameter is an object containing more details about the move. `isGesture` property indicates if the move was from the user (true) or an animation (false). **Note**: `isGesture` is supported by Google Maps only. |
+| `onRegionChangeComplete` | (`Region`, `{isGesture: boolean}`) | Callback that is called once when the region changes, such as when the user is done moving the map. The second parameter is an object containing more details about the move. `isGesture` property indicates if the move was from the user (true) or an animation (false). **Note**: `isGesture` is supported by Google Maps only. |
+| `onUserLocationChange` | `{ coordinate: Location }` | Callback that is called when the underlying map figures our users current location (coordinate also includes isFromMockProvider value for Android API 18 and above). Make sure **showsUserLocation** is set to _true_. |
+| `onPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when user taps on the map. |
+| `onDoublePress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when user double taps on the map. **NOTE**: Not supported on Google Maps iOS |
+| `onPanDrag` | `{ coordinate: LatLng, position: Point, numberOfTouches: number, }` | Callback that is called when user presses and drags the map. **NOTE** numberOfTouches is iOS only |
+| `onPoiClick` | `{ coordinate: LatLng, position: Point, placeId: string, name: string }` | Callback that is called when user click on a POI. |
+| `onLongPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when user makes a "long press" somewhere on the map. |
+| `onMarkerPress` | | Callback that is called when a marker on the map is tapped by the user. |
+| `onMarkerSelect` | | Callback that is called when a marker on the map becomes selected. This will be called when the callout for that marker is about to be shown. |
+| `onMarkerDeselect` | | Callback that is called when a marker on the map becomes deselected. This will be called when the callout for that marker is about to be hidden. |
+| `onCalloutPress` | | Callback that is called when a callout is tapped by the user. |
+| `onMarkerDragStart` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user initiates a drag on a marker (if it is draggable) |
+| `onMarkerDrag` | `{ coordinate: LatLng, position: Point }` | Callback called continuously as a marker is dragged |
+| `onMarkerDragEnd` | `{ coordinate: LatLng, position: Point }` | Callback that is called when a drag on a marker finishes. This is usually the point you will want to setState on the marker's coordinate again |
+| `onIndoorLevelActivated` | `IndoorLevel` | Callback that is called when a level on indoor building is activated |
+| `onIndoorBuildingFocused` | `IndoorBuilding` | Callback that is called when a indoor building is focused/unfocused |
## Methods
-| Method Name | Arguments | Notes
-|---|---|---|
-| `animateToRegion` | `region: Region`, `duration: Number` |
-| `animateToCoordinate` | `region: Coordinate`, `duration: Number` |
-| `fitToElements` | `animated: Boolean` |
-| `fitToSuppliedMarkers` | `markerIDs: String[]` |
-
-
+| Method Name | Arguments | Notes |
+| --------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `getCamera` | | Returns a `Promise` structure indicating the current camera configuration. |
+| `animateCamera` | `camera: Camera`, `{ duration: Number }` | Animate the camera to a new view. You can pass a partial camera object here; any property not given will remain unmodified. |
+| `setCamera` | `camera: Camera`, `{ duration: Number }` | Like `animateCamera`, but sets the new view instantly, without an animation. |
+| `animateToRegion` | `region: Region`, `duration: Number` |
+| `getMapBoundaries` | | `Promise<{northEast: LatLng, southWest: LatLng}>` |
+| `setMapBoundaries` | `northEast: LatLng`, `southWest: LatLng` | The boundary is defined by the map's center coordinates, not the device's viewport itself. **Note:** Google Maps only. |
+| `setIndoorActiveLevelIndex` | `levelIndex: Number` |
+| `fitToElements` | `options: { edgePadding: EdgePadding, animated: Boolean }` | **Note** edgePadding is Google Maps only |
+| `fitToSuppliedMarkers` | `markerIDs: String[], options: { edgePadding: EdgePadding, animated: Boolean }` | If you need to use this in `ComponentDidMount`, make sure you put it in a timeout or it will cause performance problems. **Note** edgePadding is Google Maps only |
+| `fitToCoordinates` | `coordinates: Array, options: { edgePadding: EdgePadding, animated: Boolean }` | If called in `ComponentDidMount` in android, it will cause an exception. It is recommended to call it from the MapView `onLayout` event. |
+| `addressForCoordinate` | `coordinate: LatLng` | Converts a map coordinate to a address (`Address`). Returns a `Promise` **Note** Not supported on Google Maps for iOS. |
+| `pointForCoordinate` | `coordinate: LatLng` | Converts a map coordinate to a view coordinate (`Point`). Returns a `Promise`. |
+| `coordinateForPoint` | `point: Point` | Converts a view coordinate (`Point`) to a map coordinate. Returns a `Promise`. |
+| `getMarkersFrames` | `onlyVisible: Boolean` | Get markers' centers and frames in view coordinates. Returns a `Promise<{ "markerID" : { point: Point, frame: Frame } }>`. **Note**: iOS only. |
## Types
@@ -68,6 +110,33 @@ type Region {
}
```
+```
+type Camera = {
+ center: {
+ latitude: number,
+ longitude: number,
+ },
+ pitch: number,
+ heading: number,
+
+ // Only on iOS MapKit, in meters. The property is ignored by Google Maps.
+ altitude: number,
+
+ // Only when using Google Maps.
+ zoom: number
+}
+```
+
+Latitude and longitude are self explanatory while latitudeDelta and longitudeDelta may not.
+On the [developer.apple.com](https://developer.apple.com/reference/mapkit/mkcoordinatespan/1452417-latitudedelta) website this is how the "latitudeDelta" property is explained:
+
+> The amount of north-to-south distance (measured in degrees) to display on the map. Unlike longitudinal distances, which vary based on the latitude, one degree of latitude is always approximately 111 kilometers (69 miles).
+
+If this is not enough, you can find a [visual explanation on stackoverflow](https://stackoverflow.com/questions/36685372/how-to-zoom-in-out-in-react-native-map/36688156#36688156).
+
+Note that when using the `Camera`, MapKit on iOS and Google Maps differ in how the height is specified. For a cross-platform app, it is necessary
+to specify both the zoom level and the altitude separately.
+
```
type LatLng {
latitude: Number,
@@ -75,6 +144,18 @@ type LatLng {
}
```
+```
+type Location {
+ latitude: Number,
+ longitude: Number,
+ altitude: Number,
+ timestamp: Number, //Milliseconds since Unix epoch
+ accuracy: Number,
+ altitudeAccuracy: Number,
+ speed: Number,
+}
+```
+
```
type Point {
x: Number,
@@ -82,6 +163,15 @@ type Point {
}
```
+```
+type Frame {
+ x: Number,
+ y: Number,
+ width: Number,
+ height: Number,
+}
+```
+
```
enum MapType : String {
"standard",
@@ -90,3 +180,75 @@ enum MapType : String {
"terrain" //Android only
}
```
+
+```
+type EdgePadding {
+ top: Number,
+ right: Number,
+ bottom: Number,
+ left: Number
+}
+```
+
+```
+type EdgeInsets {
+ top: Number,
+ left: Number,
+ bottom: Number,
+ right: Number
+}
+```
+
+```
+type Marker {
+ id: String,
+ coordinate: LatLng,
+ title: String,
+ description: String
+}
+```
+
+```
+type KmlContainer {
+ markers: [Marker]
+}
+```
+
+```
+type IndoorBuilding {
+ underground: boolean,
+ activeLevelIndex: Number,
+ levels: Array,
+}
+```
+
+```
+type IndoorLevel {
+ index: Number,
+ name: String,
+ shortName: String,
+}
+```
+
+```
+type Address {
+ name: String,
+ thoroughfare: String,
+ subThoroughfare: String,
+ locality: String,
+ subLocality: String,
+ administrativeArea: String,
+ subAdministrativeArea: String,
+ postalCode: String,
+ countryCode: String,
+ country: String,
+}
+```
+
+```
+type CameraZoomRange = {
+ minCenterCoordinateDistance?: number;
+ maxCenterCoordinateDistance?: number;
+ animated?: boolean;
+};
+```
diff --git a/docs/marker.md b/docs/marker.md
index 557490ab4..e36dfa8f2 100644
--- a/docs/marker.md
+++ b/docs/marker.md
@@ -1,42 +1,59 @@
-# `` Component API
+# `` Component API
## Props
-| Prop | Type | Default | Note |
-|---|---|---|---|
-| `title` | `String` | | The title of the marker. This is only used if the component has no children that are an ``, in which case the default callout behavior will be used, which will show both the `title` and the `description`, if provided.
-| `description` | `String` | | The description of the marker. This is only used if the component has no children that are an ``, in which case the default callout behavior will be used, which will show both the `title` and the `description`, if provided.
-| `image` | `ImageSource` | | A custom image to be used as the marker's icon. Only local image resources are allowed to be used.
-| `pinColor` | `Color` | | If no custom marker view or custom image is provided, the platform default pin will be used, which can be customized by this color. Ignored if a custom marker is being used.
-| `coordinate` | `LatLng` | | The coordinate for the marker.
-| `centerOffset` | `Point` | | The offset (in points) at which to display the view.
By default, the center point of an annotation view is placed at the coordinate point of the associated annotation. You can use this property to reposition the annotation view as needed. This x and y offset values are measured in points. Positive offset values move the annotation view down and to the right, while negative values move it up and to the left.
For android, see the `anchor` prop.
-| `calloutOffset` | `Point` | | The offset (in points) at which to place the callout bubble.
This property determines the additional distance by which to move the callout bubble. When this property is set to (0, 0), the anchor point of the callout bubble is placed on the top-center point of the marker view’s frame. Specifying positive offset values moves the callout bubble down and to the right, while specifying negative values moves it up and to the left.
For android, see the `calloutAnchor` prop.
-| `anchor` | `Point` | | Sets the anchor point for the marker.
The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.
The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0], where (0, 0) is the top-left corner of the image, and (1, 1) is the bottom-right corner. The anchoring point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid, obtained by scaling the then rounding. For example, in a 4 x 2 image, the anchor point (0.7, 0.6) resolves to the grid point at (3, 1).
For ios, see the `centerOffset` prop.
-| `calloutAnchor` | `Point` | | Specifies the point in the marker image at which to anchor the callout when it is displayed. This is specified in the same coordinate system as the anchor. See the `anchor` prop for more details.
The default is the top middle of the image.
For ios, see the `calloutOffset` prop.
-| `flat` | `Boolean` | | Sets whether this marker should be flat against the map true or a billboard facing the camera false.
-| `identifier` | `String` | | An identifier used to reference this marker at a later date.
+| Prop | Type | Default | Note |
+| ------------------------- | ------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `title` | `String` | | The title of the marker. This is only used if the component has no children that are a ``, in which case the default callout behavior will be used, which will show both the `title` and the `description`, if provided. |
+| `description` | `String` | | The description of the marker. This is only used if the component has no children that are a ``, in which case the default callout behavior will be used, which will show both the `title` and the `description`, if provided. |
+| `image` | `ImageSource`\* | | A custom image to be used as the marker's icon. Only local image resources are allowed to be used. |
+| `icon` | `ImageSource`\* | | Marker icon to render (equivalent to `icon` property of GMSMarker Class). Only local image resources are allowed to be used. **Note:** Google maps only! |
+| `pinColor` | `Color` | | If no custom marker view or custom image is provided, the platform default pin will be used, which can be customized by this color. Ignored if a custom marker is being used.
For Android, the set of available colors is limited. Unsupported colors will fall back to red. See [#887](https://github.com/react-community/react-native-maps/issues/887) for more information. |
+| `coordinate` | `LatLng` | | The coordinate for the marker. |
+| `centerOffset` | `Point` | (0, 0) | The offset (in points) at which to display the view.
By default, the center point of an annotation view is placed at the coordinate point of the associated annotation. You can use this property to reposition the annotation view as needed. This x and y offset values are measured in points. Positive offset values move the annotation view down and to the right, while negative values move it up and to the left.
For Google Maps, see the `anchor` prop. |
+| `calloutOffset` | `Point` | (0, 0) | The offset (in points) at which to place the callout bubble.
This property determines the additional distance by which to move the callout bubble. When this property is set to (0, 0), the anchor point of the callout bubble is placed on the top-center point of the marker view’s frame. Specifying positive offset values moves the callout bubble down and to the right, while specifying negative values moves it up and to the left.
For Google Maps, see the `calloutAnchor` prop. |
+| `anchor` | `Point` | (0.5, 1) | Sets the anchor point for the marker.
The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.
The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0], where (0, 0) is the top-left corner of the image, and (1, 1) is the bottom-right corner. The anchoring point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid, obtained by scaling the then rounding. For example, in a 4 x 2 image, the anchor point (0.7, 0.6) resolves to the grid point at (3, 1).
For MapKit on iOS, see the `centerOffset` prop. |
+| `calloutAnchor` | `Point` | (0.5, 0) | Specifies the point in the marker image at which to anchor the callout when it is displayed. This is specified in the same coordinate system as the anchor. See the `anchor` prop for more details.
The default is the top middle of the image.
For MapKit on iOS, see the `calloutOffset` prop. |
+| `flat` | `Boolean` | false | Sets whether this marker should be flat against the map true or a billboard facing the camera. |
+| `identifier` | `String` | | An identifier used to reference this marker at a later date. |
+| `rotation` | `Float` | 0 | A float number indicating marker's rotation angle, in degrees. |
+| `draggable` | `` | | This is a non-value based prop. Adding this allows the marker to be draggable (re-positioned). |
+| `tappable` | `Boolean` | true | Sets whether marker should be tappable. If set to false, the marker will not have onPress events. **Note**: iOS Google Maps only. |
+| `tracksViewChanges` | `Boolean` | true | Sets whether this marker should track changes to child views. When using a child view to create a custom marker this option determines if changes to the content will be tracked after the first render pass. This option has a performance impact so when possible it's recommended to disable it and explictly call the `redraw` method if the marker content changes. |
+| `tracksInfoWindowChanges` | `Boolean` | false | Sets whether this marker should track view changes in info window. Enabling it will let marker change content of info window after first render pass, but will lead to decreased performance, so it's recommended to disable it whenever you don't need it. **Note**: iOS Google Maps only. |
+| `stopPropagation` | `Boolean` | false | Sets whether this marker should propagate `onPress` events. Enabling it will stop the parent `MapView`'s `onPress` from being called. **Note**: iOS only. Android does not propagate `onPress` events. See [#1132](https://github.com/react-community/react-native-maps/issues/1132) for more information. |
+| `opacity` | `Float` | 1.0 | The marker's opacity between 0.0 and 1.0. |
+| `isPreselected` | `Boolean` | false | When true, the marker will be pre-selected. Setting this to true allows the user to drag the marker without needing to tap on it once to focus on it. **Note**: iOS Apple Maps only. |
+| `key` | `String` | | If no key or non-unique `key` is specified, the `` will be reused, therefore there is an animation when the position is changed. If you want to disable the animation, add a `key` prop with a unique value like `key_${item.longitude}_${item.latitude}`. **Note**: iOS only. |
+| `titleVisibility` | `visible \| hidden \| adaptive` | 'hidden' | Visibility of the title text rendered beneath Marker balloon. **Note**: iOS Apple Maps only. |
+| `subtitleVisibility` | `visible \| hidden \| adaptive` | 'hidden' | Visibility of the subtitle text rendered beneath Marker balloon. **Note**: iOS Apple Maps only. |
+| `useLegacyPinView` | `Boolean` | false | Decide whether legacy MKPinAnnotationView or new MKMarkerAnnotationView should be used. **Note**: iOS Apple Maps only. |
+
+\* `ImageSource` [docs](https://reactnative.dev/docs/image#imagesource)
## Events
-| Event Name | Returns | Notes
-|---|---|---|
-| `onPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user presses on the marker
-| `onSelect` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user selects the marker, before the callout is shown. **Note**: iOS only.
-| `onDeselect` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the marker is deselected, before the callout is hidden. **Note**: iOS only.
-| `onCalloutPress` | | Callback that is called when the user taps the callout view.
-| `onDragStart` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user initiates a drag on this marker (if it is draggable)
-| `onDrag` | `{ coordinate: LatLng, position: Point }` | Callback called continuously as the marker is dragged
-| `onDragEnd` | `{ coordinate: LatLng, position: Point }` | Callback that is called when a drag on this marker finishes. This is usually the point you will want to setState on the marker's coordinate again
+To access event data, you will need to use `e.nativeEvent`. For example, `onPress={e => console.log(e.nativeEvent)}` will log the entire event object to your console.
+| Event Name | Returns | Notes |
+| ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `onPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user presses on the marker |
+| `onSelect` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user selects the marker, before the callout is shown. |
+| `onDeselect` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the marker is deselected, before the callout is hidden. |
+| `onCalloutPress` | | Callback that is called when the user taps the callout view. |
+| `onDragStart` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user initiates a drag on this marker (if it is draggable) |
+| `onDrag` | `{ coordinate: LatLng, position: Point }` | Callback called continuously as the marker is dragged |
+| `onDragEnd` | `{ coordinate: LatLng, position: Point }` | Callback that is called when a drag on this marker finishes. This is usually the point you will want to setState on the marker's coordinate again |
## Methods
-| Method Name | Arguments | Notes
-|---|---|---|
-| `showCallout` | | Shows the callout for this marker
-| `hideCallout` | | Hides the callout for this marker
-
-
+| Method Name | Arguments | Notes |
+| --------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
+| `showCallout` | | Shows the callout for this marker |
+| `hideCallout` | | Hides the callout for this marker |
+| `redrawCallout` | | Causes a redraw of the marker's callout. Useful for Google Maps on iOS. **Note**: iOS only. |
+| `animateMarkerToCoordinate` | `coordinate: LatLng, duration: number` | Animates marker movement. **Note**: Android only |
+| `redraw` | | Causes a redraw of the marker. Useful when there are updates to the marker and `tracksViewChanges` comes with a cost that is too high. |
## Types
@@ -53,3 +70,19 @@ type Point {
y: Number,
}
```
+
+## Children Components
+
+Children components can be added within a Marker and rendered content will replace the marker symbol. This is a way of creating custom markers and allowing use of native SVGs.
+
+Example:
+
+```
+
+
+ SF
+
+
+```
+
+Displaying a large number of custom markers can have a negative performance impact when tracking changes to the marker content. If displaying a large number of markers consider disabling the `tracksViewChanges` option and manually calling the `redraw` method as required.
diff --git a/docs/overlay.md b/docs/overlay.md
new file mode 100644
index 000000000..8581e6c30
--- /dev/null
+++ b/docs/overlay.md
@@ -0,0 +1,26 @@
+# `` Component API
+
+## Props
+
+| Prop | Type | Default | Note |
+| ---------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
+| `image` | `ImageSource` | A custom image to be used as the overlay. Only required local image resources and uri (as for images located in the net) are allowed to be used. |
+| `bounds` | `Array` | | The coordinates for the image (bottom-left corner, top-right corner). ie.`[[lat, long], [lat, long]]` |
+| `bearing` | `Number ` | `0` | `Google Maps API only` The bearing in degrees clockwise from north. Values outside the range [0, 360) will be normalized. |
+| `tappable` | `Bool` | `false` | `Android only` Boolean to allow an overlay to be tappable and use the onPress function. |
+| `opacity` | `Number` | `1.0` | `Google maps only` The opacity of the overlay. |
+
+## Events
+
+| Event Name | Returns | Notes |
+| ---------- | ------- | --------------------------------------------------------------------------- |
+| `onPress` | | `Android only` Callback that is called when the user presses on the overlay |
+
+## Types
+
+```
+type LatLng = [
+ latitude: Number,
+ longitude: Number,
+]
+```
diff --git a/docs/polygon.md b/docs/polygon.md
index cc9220ba0..4b18b4b32 100644
--- a/docs/polygon.md
+++ b/docs/polygon.md
@@ -1,20 +1,28 @@
-# `` Component API
+# `` Component API
## Props
-| Prop | Type | Default | Note |
-|---|---|---|---|
-| `coordinates` | `Array` | (Required) | An array of coordinates to describe the polygon
-| `strokeWidth` | `Number` | `1` | The stroke width to use for the path.
-| `strokeColor` | `String` | `#000` | The stroke color to use for the path.
-| `fillColor` | `String` | | The fill color to use for the path.
-| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path.
-| `lineJoin` | `Array` | | The line join style to apply to corners of the path.
-| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees.
-| `geodesic` | `Boolean` | | Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to straight lines on the Mercator projection. A geodesic is the shortest path between two points on the Earth's surface. The geodesic curve is constructed assuming the Earth is a sphere.
-| `lineDashPhase` | `Number` | `0` | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap.
-| `lineDashPattern` | `Array` | `null` | (iOS only) An array of numbers specifying the dash pattern to use for the path. The array contains one or more numbers that indicate the lengths (measured in points) of the line segments and gaps in the pattern. The values in the array alternate, starting with the first line segment length, followed by the first gap length, followed by the second line segment length, and so on.
+| Prop | Type | Default | Note |
+| ----------------- | ---------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `coordinates` | `Array` | (Required) | An array of coordinates to describe the polygon |
+| `holes` | `Array>` | | A 2d array of coordinates to describe holes of the polygon where each hole has at least 3 points. |
+| `strokeWidth` | `Number` | `1` | The stroke width to use for the path. |
+| `strokeColor` | `String` | `#000`, `rgba(r,g,b,0.5)` | The stroke color to use for the path. |
+| `fillColor` | `String` | `#000`, `rgba(r,g,b,0.5)` | The fill color to use for the path. |
+| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path. |
+| `lineJoin` | `Array` | | The line join style to apply to corners of the path. |
+| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees. |
+| `geodesic` | `Boolean` | | Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to straight lines on the Mercator projection. A geodesic is the shortest path between two points on the Earth's surface. The geodesic curve is constructed assuming the Earth is a sphere. |
+| `lineDashPhase` | `Number` | `0` | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap. |
+| `lineDashPattern` | `Array` | `null` | (iOS only) An array of numbers specifying the dash pattern to use for the path. The array contains one or more numbers that indicate the lengths (measured in points) of the line segments and gaps in the pattern. The values in the array alternate, starting with the first line segment length, followed by the first gap length, followed by the second line segment length, and so on. |
+| `tappable` | `Bool` | `false` | Boolean to allow a polygon to be tappable and use the onPress function. |
+| `zIndex` | `Number` | `0` | (Android Only) The order in which this polygon overlay is drawn with respect to other overlays. An overlay with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays with the same z-index is arbitrary. The default zIndex is 0. |
+## Events
+
+| Event Name | Returns | Notes |
+| ---------- | ------- | ------------------------------------------------------------ |
+| `onPress` | | Callback that is called when the user presses on the polygon |
## Types
diff --git a/docs/polyline.md b/docs/polyline.md
index f9950e96f..50d63d11f 100644
--- a/docs/polyline.md
+++ b/docs/polyline.md
@@ -1,19 +1,26 @@
-# `` Component API
+# `` Component API
## Props
-| Prop | Type | Default | Note |
-|---|---|---|---|
-| `coordinates` | `Array` | (Required) | An array of coordinates to describe the polygon
-| `strokeWidth` | `Number` | `1` | The stroke width to use for the path.
-| `strokeColor` | `String` | `#000` | The stroke color to use for the path.
-| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path.
-| `lineJoin` | `Array` | | The line join style to apply to corners of the path.
-| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees.
-| `geodesic` | `Boolean` | | Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to straight lines on the Mercator projection. A geodesic is the shortest path between two points on the Earth's surface. The geodesic curve is constructed assuming the Earth is a sphere.
-| `lineDashPhase` | `Number` | `0` | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap.
-| `lineDashPattern` | `Array` | `null` | (iOS only) An array of numbers specifying the dash pattern to use for the path. The array contains one or more numbers that indicate the lengths (measured in points) of the line segments and gaps in the pattern. The values in the array alternate, starting with the first line segment length, followed by the first gap length, followed by the second line segment length, and so on.
+| Prop | Type | Default | Note |
+| ----------------- | --------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `coordinates` | `Array` | (Required) | An array of coordinates to describe the polyline |
+| `strokeWidth` | `Number` | `1` | The stroke width to use for the path. |
+| `strokeColor` | `String` | `#000, rgba(r,g,b,0.5)` | The stroke color to use for the path. |
+| `strokeColors` | `Array` | `null` | The stroke colors to use for the path (iOS only). Must be the same length as `coordinates`. |
+| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path. Possible values are `butt`, `round` or `square`. Note: lineCap is not yet supported for GoogleMaps provider on iOS. |
+| `lineJoin` | `String` | `round` | The line join style to apply to corners of the path. Possible values are `miter`, `round` or `bevel`. |
+| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees. |
+| `geodesic` | `Boolean` | | Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to straight lines on the Mercator projection. A geodesic is the shortest path between two points on the Earth's surface. The geodesic curve is constructed assuming the Earth is a sphere. |
+| `lineDashPhase` | `Number` | `0` | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap. |
+| `lineDashPattern` | `Array` | `null` | An array of numbers specifying the dash pattern to use for the path. The array contains one or more numbers that indicate the lengths (measured in points) of the line segments and gaps in the pattern. The values in the array alternate, starting with the first line segment length, followed by the first gap length, followed by the second line segment length, and so on. |
+| `tappable` | `Bool` | false | Boolean to allow a polyline to be tappable and use the onPress function. |
+## Events
+
+| Event Name | Returns | Notes |
+| ---------- | ------- | ------------------------------------------------------------- |
+| `onPress` | | Callback that is called when the user presses on the polyline |
## Types
@@ -23,3 +30,36 @@ type LatLng {
longitude: Number,
}
```
+
+## Gradient Polylines (iOS MapKit only)
+
+Gradient polylines can be created by using the `strokeColors` prop. `strokeColors` must be an array with the same number of elements as `coordinates`.
+
+Example:
+
+```js
+import MapView, {Polyline} from 'react-native-maps';
+
+
+
+;
+```
diff --git a/docs/tiles.md b/docs/tiles.md
new file mode 100644
index 000000000..0955855d8
--- /dev/null
+++ b/docs/tiles.md
@@ -0,0 +1,35 @@
+# `` and ``Component API
+
+## Props
+
+| Prop | Type | Default | Note |
+| ------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `urlTemplate` | `String` | | The url template of the map tileserver.
(URLTile) The patterns {x} {y} {z} will be replaced at runtime. For example, http://c.tile.openstreetmap.org/{z}/{x}/{y}.png. It is also possible to refer to tiles in local filesystem with file:///top-level-directory/sub-directory/{z}/{x}/{y}.png URL-format.
(WMSTile) The patterns {minX} {maxX} {minY} {maxY} {width} {height} will be replaced at runtime according to EPSG:900913 specification bounding box. For example, https://demo.geo-solutions.it/geoserver/tiger/wms?service=WMS&version=1.1.0&request=GetMap&layers=tiger:poi&styles=&bbox={minX},{minY},{maxX},{maxY}&width={width}&height={height}&srs=EPSG:900913&format=image/png&transparent=true&format_options=dpi:213. |
+| `minimumZ` | `Number` | | The minimum zoom level for this tile overlay. |
+| `maximumZ` | `Number` | | The maximum zoom level for this tile overlay. |
+| `maximumNativeZ` | `Number` | | (Optional) The maximum native zoom level for this tile overlay i.e. the highest zoom level that the tile server provides. Tiles are auto-scaled for higher zoom levels. |
+| `zIndex` | `Number` | `-1` | (Optional) The order in which this tile overlay is drawn with respect to other overlays. An overlay with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays with the same z-index is arbitrary. |
+| `tileSize` | `Number` | `256` | (Optional) Tile size, default size is 256 (for tiles of 256 _ 256 pixels). High-res (aka 'retina') tiles are 512 (tiles of 512 _ 512 pixels). |
+| `doubleTileSize` | `Boolean` | `false` | (Optional) Doubles tile size from 256 to 512 utilising higher zoom levels i.e loading 4 higher zoom level tiles and combining them for one high-resolution tile. iOS does this automatically, even if it is not desirable always. NB! using this makes text labels smaller than in the original map style. |
+| `shouldReplaceMapContent` | `Boolean` | `false` | (iOS) Corresponds to MKTileOverlay canReplaceMapContent i.e. if true then underlying iOS basemap is not shown. |
+| `flipY` | `Boolean` | `false` | (Optional)Allow tiles using the TMS coordinate system (origin bottom left) to be used, and displayed at their correct coordinates. |
+| `tileCachePath` | `String` | | (Optional) Enable caching of tiles in the specified directory. Directory can be specified either as a normal path or in URL format (`file://`). Tiles are stored in tileCachePath directory as `/{z}/{x}/{y}` i.e. in sub-directories 2-levels deep, filename is tile y-coordinate without any filetype-extension.