diff --git a/MapboxAnnotation.js b/MapboxAnnotation.js
new file mode 100644
index 0000000..2b124d4
--- /dev/null
+++ b/MapboxAnnotation.js
@@ -0,0 +1,283 @@
+import React, { PropTypes } from 'react';
+import {
+ View,
+ requireNativeComponent,
+ StyleSheet,
+ Platform,
+ NativeModules,
+ Animated,
+ findNodeHandle,
+} from 'react-native';
+
+import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
+
+const viewConfig = {
+ uiViewClassName: 'RCTMapboxAnnotation',
+ validAttributes: {
+ coordinate: true,
+ },
+};
+
+const 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 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.
+ */
+ subtitle: 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,
+};
+
+const defaultProps = {
+ onPress() {},
+};
+
+class MapboxAnnotation extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.showCallout = this.showCallout.bind(this);
+ this.hideCallout = this.hideCallout.bind(this);
+ }
+
+ showCallout() {
+ this._runCommand('showCallout', []);
+ }
+
+ hideCallout() {
+ this._runCommand('hideCallout', []);
+ }
+
+ _getHandle() {
+ return findNodeHandle(this.marker);
+ }
+
+ _runCommand(name, args) {
+ switch (Platform.OS) {
+ case 'android':
+ NativeModules.UIManager.dispatchViewManagerCommand(
+ this._getHandle(),
+ NativeModules.UIManager.RCTMapboxAnnotation.Commands[name],
+ args
+ );
+ break;
+
+ case 'ios':
+ NativeModules.RCTMapboxAnnotationManager[name].apply(
+ NativeModules.RCTMapboxAnnotationManager[name],
+ [this._getHandle(), ...args]
+ );
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ render() {
+ let image;
+ if (this.props.image) {
+ image = resolveAssetSource(this.props.image) || {};
+ image = image.uri;
+ }
+
+ return (
+ { this.marker = ref; }}
+ {...this.props}
+ image={image}
+ style={[styles.marker, this.props.style]}
+ />
+ );
+ }
+}
+
+MapboxAnnotation.propTypes = propTypes;
+MapboxAnnotation.defaultProps = defaultProps;
+MapboxAnnotation.viewConfig = viewConfig;
+
+const styles = StyleSheet.create({
+ marker: {
+ position: 'absolute',
+ backgroundColor: 'transparent',
+ },
+});
+
+const RCTMapboxAnnotation = requireNativeComponent('RCTMapboxAnnotation', MapboxAnnotation);
+module.exports = MapboxAnnotation;
diff --git a/index.js b/index.js
index 0c9a445..a0811a9 100644
--- a/index.js
+++ b/index.js
@@ -13,6 +13,7 @@ import {
import cloneDeep from 'lodash/cloneDeep';
import clone from 'lodash/clone';
import isEqual from 'lodash/isEqual';
+import Annotation from './MapboxAnnotation';
const { MapboxGLManager } = NativeModules;
const { mapStyles, userTrackingMode, userLocationVerticalAlignment, unknownResourceCount } = MapboxGLManager;
@@ -438,6 +439,7 @@ const MapboxGLView = requireNativeComponent('RCTMapboxGL', MapView, {
const Mapbox = {
MapView,
+ Annotation,
mapStyles, userTrackingMode, userLocationVerticalAlignment, unknownResourceCount,
getMetricsEnabled, setMetricsEnabled,
setAccessToken,
diff --git a/ios/RCTMapboxGL.xcodeproj/project.pbxproj b/ios/RCTMapboxGL.xcodeproj/project.pbxproj
index e8b3e7d..bfac5c8 100644
--- a/ios/RCTMapboxGL.xcodeproj/project.pbxproj
+++ b/ios/RCTMapboxGL.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 874C0C441D9436D80034AF3F /* RCTMapboxAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 874C0C401D9436D80034AF3F /* RCTMapboxAnnotation.m */; };
+ 874C0C451D9436D80034AF3F /* RCTMapboxAnnotationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 874C0C421D9436D80034AF3F /* RCTMapboxAnnotationManager.m */; };
C167F89C1D18112B007C7A42 /* RCTMapboxGLConversions.m in Sources */ = {isa = PBXBuildFile; fileRef = C167F89B1D18112B007C7A42 /* RCTMapboxGLConversions.m */; };
C5DBB3441AF2EF2B00E611A9 /* RCTMapboxGL.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = C5DBB3431AF2EF2B00E611A9 /* RCTMapboxGL.h */; };
C5DBB3461AF2EF2B00E611A9 /* RCTMapboxGL.m in Sources */ = {isa = PBXBuildFile; fileRef = C5DBB3451AF2EF2B00E611A9 /* RCTMapboxGL.m */; };
@@ -38,6 +40,10 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 874C0C401D9436D80034AF3F /* RCTMapboxAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapboxAnnotation.m; sourceTree = ""; };
+ 874C0C411D9436D80034AF3F /* RCTMapboxAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapboxAnnotation.h; sourceTree = ""; };
+ 874C0C421D9436D80034AF3F /* RCTMapboxAnnotationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapboxAnnotationManager.m; sourceTree = ""; };
+ 874C0C431D9436D80034AF3F /* RCTMapboxAnnotationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapboxAnnotationManager.h; sourceTree = ""; };
C167F89A1D18111F007C7A42 /* RCTMapboxGLConversions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTMapboxGLConversions.h; sourceTree = ""; };
C167F89B1D18112B007C7A42 /* RCTMapboxGLConversions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapboxGLConversions.m; sourceTree = ""; };
C5DBB3401AF2EF2B00E611A9 /* libRCTMapboxGL.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTMapboxGL.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -89,6 +95,10 @@
C5DBB3421AF2EF2B00E611A9 /* RCTMapboxGL */ = {
isa = PBXGroup;
children = (
+ 874C0C401D9436D80034AF3F /* RCTMapboxAnnotation.m */,
+ 874C0C411D9436D80034AF3F /* RCTMapboxAnnotation.h */,
+ 874C0C421D9436D80034AF3F /* RCTMapboxAnnotationManager.m */,
+ 874C0C431D9436D80034AF3F /* RCTMapboxAnnotationManager.h */,
C5DBB3431AF2EF2B00E611A9 /* RCTMapboxGL.h */,
C5DBB3451AF2EF2B00E611A9 /* RCTMapboxGL.m */,
C5DBB3641AF2EFB500E611A9 /* RCTMapboxGLManager.h */,
@@ -206,6 +216,8 @@
C5DBB3461AF2EF2B00E611A9 /* RCTMapboxGL.m in Sources */,
C5DBB3661AF2EFB500E611A9 /* RCTMapboxGLManager.m in Sources */,
C167F89C1D18112B007C7A42 /* RCTMapboxGLConversions.m in Sources */,
+ 874C0C451D9436D80034AF3F /* RCTMapboxAnnotationManager.m in Sources */,
+ 874C0C441D9436D80034AF3F /* RCTMapboxAnnotation.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/ios/RCTMapboxGL/RCTMapboxAnnotation.h b/ios/RCTMapboxGL/RCTMapboxAnnotation.h
new file mode 100644
index 0000000..742cc76
--- /dev/null
+++ b/ios/RCTMapboxGL/RCTMapboxAnnotation.h
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "RCTMapboxAnnotation.h"
+
+#import
+#import
+
+#import "RCTConvert+MapKit.h"
+#import "RCTComponent.h"
+#import "RCTMapboxGL.h"
+
+@class RCTBridge;
+
+@interface RCTMapboxAnnotation : MGLAnnotationView
+
+@property (nonatomic, weak, nullable) RCTMapboxGL *map;
+@property (nonatomic, weak, nullable) RCTBridge *bridge;
+/**
+ The center point (specified as a map coordinate) of the annotation. (required)
+ (read-only)
+ */
+@property (nonatomic) CLLocationCoordinate2D coordinate;
+
+/**
+ The string containing the annotation’s title.
+
+ Although this property is optional, if you support the selection of annotations
+ in your map view, you are expected to provide this property. This string is
+ displayed in the callout for the associated annotation.
+ */
+@property (nonatomic, copy, nullable) NSString *title;
+
+/**
+ The string containing the annotation’s subtitle.
+
+ This string is displayed in the callout for the associated annotation.
+ */
+@property (nonatomic, copy, nullable) NSString *subtitle;
+
+
+@end
+
+
diff --git a/ios/RCTMapboxGL/RCTMapboxAnnotation.m b/ios/RCTMapboxGL/RCTMapboxAnnotation.m
new file mode 100644
index 0000000..12eae39
--- /dev/null
+++ b/ios/RCTMapboxGL/RCTMapboxAnnotation.m
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "RCTMapboxAnnotation.h"
+
+#import "RCTEventDispatcher.h"
+#import "UIView+React.h"
+#import "RCTBridge.h"
+#import "RCTUtils.h"
+
+@implementation RCTMapboxAnnotation {
+}
+-(void)layoutSubviews {
+ [super layoutSubviews];
+ [self.map layoutIfNeeded];
+}
+
+@end
diff --git a/ios/RCTMapboxGL/RCTMapboxAnnotationManager.h b/ios/RCTMapboxGL/RCTMapboxAnnotationManager.h
new file mode 100644
index 0000000..ec86dd2
--- /dev/null
+++ b/ios/RCTMapboxGL/RCTMapboxAnnotationManager.h
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "RCTViewManager.h"
+
+@interface RCTMapboxAnnotationManager : RCTViewManager
+
+@end
diff --git a/ios/RCTMapboxGL/RCTMapboxAnnotationManager.m b/ios/RCTMapboxGL/RCTMapboxAnnotationManager.m
new file mode 100644
index 0000000..345addd
--- /dev/null
+++ b/ios/RCTMapboxGL/RCTMapboxAnnotationManager.m
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "RCTMapboxAnnotationManager.h"
+
+#import "RCTUIManager.h"
+#import "RCTConvert+CoreLocation.h"
+#import "UIView+React.h"
+#import "RCTMapboxAnnotation.h"
+
+@interface RCTMapboxAnnotationManager ()
+
+@end
+
+@implementation RCTMapboxAnnotationManager
+
+RCT_EXPORT_MODULE()
+
+- (UIView *)view
+{
+ RCTMapboxAnnotation *marker = [RCTMapboxAnnotation new];
+// UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTap:)];
+// // setting this to NO allows the parent MapView to continue receiving marker selection events
+// tapGestureRecognizer.cancelsTouchesInView = NO;
+// [marker addGestureRecognizer:tapGestureRecognizer];
+ marker.bridge = self.bridge;
+ return marker;
+}
+
+
+RCT_EXPORT_VIEW_PROPERTY(title, NSString)
+RCT_EXPORT_VIEW_PROPERTY(subtitle, NSString)
+RCT_EXPORT_VIEW_PROPERTY(coordinate, CLLocationCoordinate2D)
+
+@end
diff --git a/ios/RCTMapboxGL/RCTMapboxGL.m b/ios/RCTMapboxGL/RCTMapboxGL.m
index 569097a..a6c2a38 100644
--- a/ios/RCTMapboxGL/RCTMapboxGL.m
+++ b/ios/RCTMapboxGL/RCTMapboxGL.m
@@ -12,6 +12,7 @@
#import "UIView+React.h"
#import "RCTLog.h"
#import "RCTMapboxGLConversions.h"
+#import "RCTMapboxAnnotation.h"
@implementation RCTMapboxGL {
/* Required to publish events */
@@ -42,6 +43,16 @@
MGLAnnotationVerticalAlignment _userLocationVerticalAlignment;
/* So we don't fire onChangeUserTracking mode when triggered by props */
BOOL _isChangingUserTracking;
+ // Array to manually track RN subviews
+ //
+ // AIRMap implicitly creates subviews that aren't regular RN children
+ // (SMCalloutView injects an overlay subview), which otherwise confuses RN
+ // during component re-renders:
+ // https://github.com/facebook/react-native/blob/v0.16.0/React/Modules/RCTUIManager.m#L657
+ //
+ // Implementation based on RCTTextField, another component with indirect children
+ // https://github.com/facebook/react-native/blob/v0.16.0/Libraries/Text/RCTTextField.m#L20
+ NSMutableArray *_reactSubviews;
}
// View creation
@@ -122,8 +133,35 @@
[self createMapIfNeeded];
}
_map.frame = self.bounds;
+ [_map layoutIfNeeded];
}
+// React subviews for custom annotation management
+- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex {
+ // Our desired API is to pass up markers/overlays as children to the mapview component.
+ // This is where we intercept them and do the appropriate underlying mapview action.
+ if ([subview isKindOfClass:[RCTMapboxAnnotation class]]) {
+ ((RCTMapboxAnnotation *) subview).map = self;
+ [_map addAnnotation:(id ) subview];
+ }
+ [_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex];
+}
+
+- (void)removeReactSubview:(id)subview {
+ // similarly, when the children are being removed we have to do the appropriate
+ // underlying mapview action here.
+ if ([subview isKindOfClass:[RCTMapboxAnnotation class]]) {
+ [self removeAnnotation:(id)subview];
+ }
+ [_reactSubviews removeObject:(UIView *)subview];
+}
+
+- (NSArray> *)reactSubviews {
+ return _reactSubviews;
+}
+
+
+
// Annotation management
- (void)upsertAnnotation:(RCTMGLAnnotation *) annotation {
@@ -203,6 +241,13 @@
return ([title length] != 0 || [subtitle length] != 0);
}
+- (nullable MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id )annotation {
+ if ([annotation isKindOfClass:[RCTMapboxAnnotation class]]){
+ return annotation;
+ }
+ return nil;
+}
+
- (UIButton *)mapView:(MGLMapView *)mapView rightCalloutAccessoryViewForAnnotation:(id )annotation;
{
if ([annotation isKindOfClass:[RCTMGLAnnotation class]]) {