From 65d25b447c8e1a5bab921a3a571b299b702deeee Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 19 Oct 2017 10:27:42 -0700 Subject: [PATCH] Adds PointAnnotation and Callout JS components --- __tests__/interface.test.js | 2 + __tests__/utils/MapboxStyleSheet.test.js | 10 -- javascript/components/Callout.js | 124 +++++++++++++++++++++++ javascript/components/MapView.js | 21 ++++ javascript/components/PointAnnotation.js | 117 +++++++++++++++++++++ javascript/index.js | 4 + javascript/utils/MapboxStyleSheet.js | 2 +- 7 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 javascript/components/Callout.js create mode 100644 javascript/components/PointAnnotation.js 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);