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]]) {