diff --git a/__tests__/interface.test.js b/__tests__/interface.test.js
index 7172cff..de30368 100644
--- a/__tests__/interface.test.js
+++ b/__tests__/interface.test.js
@@ -8,6 +8,8 @@ describe('Public Interface', () => {
'MapView',
'StyleSheet',
'Light',
+ 'PointAnnotation',
+ 'Callout',
// layers
'FillLayer',
diff --git a/__tests__/utils/MapboxStyleSheet.test.js b/__tests__/utils/MapboxStyleSheet.test.js
index 05814a6..1fa3df0 100644
--- a/__tests__/utils/MapboxStyleSheet.test.js
+++ b/__tests__/utils/MapboxStyleSheet.test.js
@@ -185,16 +185,6 @@ describe('MapboxStyleSheet', () => {
}).toThrow();
});
- it('should throw error for undefined or null style values', () => {
- expect(() => {
- MapboxGL.StyleSheet.create({ fillOpacity: undefined });
- }).toThrow();
-
- expect(() => {
- MapboxGL.StyleSheet.create({ fillOpacity: null });
- }).toThrow();
- });
-
it('should throw error for passing in undefined or null', () => {
expect(() => MapboxGL.StyleSheet.create()).toThrow();
expect(() => MapboxGL.StyleSheet.create(null)).toThrow();
diff --git a/javascript/components/Callout.js b/javascript/components/Callout.js
new file mode 100644
index 0000000..3972564
--- /dev/null
+++ b/javascript/components/Callout.js
@@ -0,0 +1,124 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { View, Text, Animated, requireNativeComponent, StyleSheet } from 'react-native';
+
+export const NATIVE_MODULE_NAME = 'RCTMGLCallout';
+const RCTMGLCallout = requireNativeComponent(NATIVE_MODULE_NAME, Callout);
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 180,
+ zIndex: 9999999,
+ },
+ tip: {
+ zIndex: 1000,
+ marginTop: -2,
+ elevation: 0,
+ backgroundColor: 'transparent',
+ borderTopWidth: 16,
+ borderRightWidth: 8,
+ borderBottomWidth: 0,
+ borderLeftWidth: 8,
+ borderTopColor: 'white',
+ borderRightColor: 'transparent',
+ borderBottomColor: 'transparent',
+ borderLeftColor: 'transparent',
+ },
+ content: {
+ position: 'relative',
+ padding: 8,
+ flex: 1,
+ borderRadius: 3,
+ borderWidth: 1,
+ borderColor: 'rgba(0, 0, 0, 0.2)',
+ backgroundColor: 'white',
+ },
+ title: {
+ color: 'black',
+ textAlign: 'center',
+ },
+});
+
+/**
+ * Callout that displays information about a selected annotation near the annotation.
+ */
+class Callout extends React.PureComponent {
+ static propTypes = {
+ /**
+ * String that get's displayed in the default callout.
+ */
+ title: PropTypes.string,
+
+ /**
+ * Style property for the Animated.View wrapper, apply animations to this
+ */
+ style: PropTypes.any,
+
+ /**
+ * Style property for the native RCTMGLCallout container, set at your own risk.
+ */
+ containerStyle: PropTypes.any,
+
+ /**
+ * Style property for the content bubble.
+ */
+ contentStyle: PropTypes.any,
+
+ /**
+ * Style property for the triangle tip under the content.
+ */
+ tipStyle: PropTypes.any,
+
+ /**
+ * Style property for the title in the content bubble.
+ */
+ textStyle: PropTypes.any,
+ };
+
+ get containerStyle () {
+ return [
+ {
+ position: 'absolute',
+ zIndex: 999,
+ backgroundColor: 'transparent',
+ },
+ this.props.containerStyle,
+ ];
+ }
+
+ get hasChildren () {
+ return React.Children.count(this.props.children) > 0;
+ }
+
+ renderDefaultCallout () {
+ return (
+
+
+ {this.props.title}
+
+
+
+ );
+ }
+
+ renderCustomCallout () {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+
+ render () {
+ const calloutContent = this.hasChildren ? this.renderCustomCallout() : this.renderDefaultCallout();
+ return (
+
+ {calloutContent}
+
+ );
+ }
+}
+
+export default Callout;
diff --git a/javascript/components/MapView.js b/javascript/components/MapView.js
index ba081b1..42a3cf9 100644
--- a/javascript/components/MapView.js
+++ b/javascript/components/MapView.js
@@ -354,6 +354,27 @@ class MapView extends React.Component {
});
}
+ /**
+ * Map camera will move to new coordinate at the same zoom level
+ *
+ * @example
+ * this.map.moveTo([lng, lat], 200) // eases camera to new location based on duration
+ * this.map.moveTo([lng, lat]) // snaps camera to new location without any easing
+ *
+ * @param {Array} coordinates - Coordinates that map camera will move too
+ * @param {Number=} duration - Duration of camera animation
+ * @return {void}
+ */
+ moveTo (coordinates, duration = 0) {
+ if (!this._nativeRef) {
+ return Promise.reject('No native reference found');
+ }
+ return this.setCamera({
+ centerCoordinate: coordinates,
+ duration: duration,
+ });
+ }
+
/**
* Map camera will zoom to specified level
*
diff --git a/javascript/components/PointAnnotation.js b/javascript/components/PointAnnotation.js
new file mode 100644
index 0000000..e2d81f1
--- /dev/null
+++ b/javascript/components/PointAnnotation.js
@@ -0,0 +1,117 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { requireNativeComponent, StyleSheet } from 'react-native';
+
+import { toJSONString, isFunction } from '../utils';
+import { makePoint } from '../utils/geoUtils';
+
+export const NATIVE_MODULE_NAME = 'RCTMGLPointAnnotation';
+
+const RCTMGLPointAnnotation = requireNativeComponent(NATIVE_MODULE_NAME, PointAnnotation);
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ position: 'absolute',
+ },
+});
+
+/**
+ * PointAnnotation represents a one-dimensional shape located at a single geographical coordinate.
+ */
+class PointAnnotation extends React.PureComponent {
+ static propTypes = {
+ /**
+ * A string that uniquely identifies the annotation
+ */
+ id: PropTypes.string.isRequired,
+
+ /**
+ * The string containing the annotation’s title. Note this is required to be set if you want to see a callout appear on iOS.
+ */
+ title: PropTypes.string,
+
+ /**
+ * The string containing the annotation’s snippet(subtitle). Not displayed in the default callout.
+ */
+ snippet: PropTypes.string,
+
+ /**
+ * Manually selects/deselects annotation
+ * @type {[type]}
+ */
+ selected: PropTypes.bool,
+
+ /**
+ * The center point (specified as a map coordinate) of the annotation.
+ */
+ coordinate: PropTypes.arrayOf(PropTypes.number).isRequired,
+
+ /**
+ * Specifies the anchor being set on a particular point of the annotation.
+ * 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.
+ * Note this is only for custom annotations not the default pin view.
+ * Defaults to the center of the view.
+ */
+ anchor: PropTypes.shape({
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
+ }),
+
+ /**
+ * This callback is fired once this annotation is selected. Returns a Feature as the first param.
+ */
+ onSelected: PropTypes.func,
+
+ /**
+ * This callback is fired once this annotation is deselected.
+ */
+ onDeselected: PropTypes.func,
+ };
+
+ static defaultProps = {
+ anchor: { x: 0.5, y: 0.5 },
+ }
+
+ constructor (props) {
+ super(props);
+ this._onSelected = this._onSelected.bind(this);
+ }
+
+ _onSelected (e) {
+ if (isFunction(this.props.onSelected)) {
+ this.props.onSelected(e.nativeEvent.payload);
+ }
+ }
+
+ _getCoordinate () {
+ if (!this.props.coordinate) {
+ return;
+ }
+ return toJSONString(makePoint(this.props.coordinate));
+ }
+
+ render () {
+ const props = {
+ id: this.props.id,
+ title: this.props.title,
+ snippet: this.props.snippet,
+ anchor: this.props.anchor,
+ selected: this.props.selected,
+ style: [this.props.style, styles.container],
+ hasOnPress: typeof this.props.onPress === 'function',
+ onMapboxPointAnnotationSelected: this._onSelected,
+ onMapboxPointAnnotationDeselected: this.props.onDeselected,
+ coordinate: this._getCoordinate(),
+ };
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+export default PointAnnotation;
diff --git a/javascript/index.js b/javascript/index.js
index 4debc57..bded458 100644
--- a/javascript/index.js
+++ b/javascript/index.js
@@ -6,6 +6,8 @@ import * as geoUtils from './utils/geoUtils';
import MapView from './components/MapView';
import MapboxStyleSheet from './utils/MapboxStyleSheet';
import Light from './components/Light';
+import PointAnnotation from './components/PointAnnotation';
+import Callout from './components/Callout';
// sources
import VectorSource from './components/VectorSource';
@@ -52,6 +54,8 @@ MapboxGL.requestAndroidLocationPermissions = async function () {
MapboxGL.MapView = MapView;
MapboxGL.StyleSheet = MapboxStyleSheet;
MapboxGL.Light = Light;
+MapboxGL.PointAnnotation = PointAnnotation;
+MapboxGL.Callout = Callout;
// sources
MapboxGL.VectorSource = VectorSource;
diff --git a/javascript/utils/MapboxStyleSheet.js b/javascript/utils/MapboxStyleSheet.js
index e507657..557f264 100644
--- a/javascript/utils/MapboxStyleSheet.js
+++ b/javascript/utils/MapboxStyleSheet.js
@@ -120,7 +120,7 @@ class MapboxStyleSheet {
} else if (!styleMap[styleProp] && (depth > 0 || isPrimitive(userStyle))) {
throw new Error(`Invalid Mapbox Style ${styleProp}`);
} else if (isUndefined(userStyle) || userStyle === null) {
- throw new Error(`Invalid Mapbox Style ${styleProp} cannot be undefined/null`);
+ continue;
}
style[styleProp] = makeStyleValue(styleProp, userStyle);