diff --git a/API.md b/API.md new file mode 100644 index 000000000..2143064fd --- /dev/null +++ b/API.md @@ -0,0 +1,438 @@ +# iOS API Docs + +## Access token + +The first thing you need to do before using the map is getting a Mapbox access +token by [signing up to a Mapbox account](https://www.mapbox.com/signup). + +Then, make sure you run this before mounting any `MapView`s: + +```javascript +import Mapbox from 'react-native-mapbox-gl'; +Mapbox.setAccessToken('your-mapbox.com-access-token'); +``` + +## Props + +Import the component to use it: + +```jsx +import { MapView } from 'react-native-mapbox-gl'; + +``` + +| Prop | Type | Required | Description | Default | +|---|---|---|---|---| +| `initialCenterCoordinate` | `object` | Optional | Initial `latitude`/`longitude` the map will load at. | `{ latitude:0, longitude: 0 }` | +| `initialZoomLevel` | `number` | Optional | Initial zoom level the map will load at. 0 is the entire world, 18 is rooftop level. | `0` | +| `initialDirection` | `number` | Optional | Initial heading of the map in degrees, where 0 is north and 180 is south | `0` | +| `rotateEnabled` | `boolean` | Optional | Whether the map can rotate. | `true` | +| `scrollEnabled` | `boolean` | Optional | Whether the map can be scrolled. | `true` | +| `zoomEnabled` | `boolean` | Optional | Whether the map zoom level can be changed. | `true` | +| `showsUserLocation` | `boolean` | Optional | Whether the user's location is shown on the map. Note: The map will not zoom to their location. | `false` | +| `userTrackingMode` | `enum` | Optional | Wether the map is zoomed to and follows the user's location. One of `Mapbox.userTrackingMode.none`, `Mapbox.userTrackingMode.follow`, `Mapbox.userTrackingMode.followWithCourse`, `Mapbox.userTrackingMode.followWithHeading` | `Mapbox.userTrackingMode.none` | +| `userLocationVerticalAlignment` | `enum` | Optional | Change the alignment of where the user location shows on the screen. One of `Mapbox.userLocationVerticalAlignment.top`, `Mapbox.userLocationVerticalAlignment.center`, `Mapbox.userLocationVerticalAlignment.bottom` | `Mapbox.userLocationVerticalAlignment.center` | +| `styleURL` | `string` | Optional | A Mapbox style. See [Styles](#styles) for valid values. | `Mapbox.mapStyles.streets` | +| `annotations` | `array` | Optional | An array of annotation objects. See [Annotations](#annotations) | `[]` | +| `annotationsAreImmutable` | `boolean` | Optional | Set this to `true` if you don't ever mutate the `annotations` array or the annotations themselves. This enables optimizations when props change. | `false` | +| `attributionButtonIsHidden` | `boolean` | Optional | Whether attribution button is visible in lower right corner. *[If true you must still attribute OpenStreetMap in your app.](https://www.mapbox.com/about/maps/)* | `false` | +| `logoIsHidden` | `boolean` | Optional | Whether logo is visible in lower left corner. | `false` | +| `compassIsHidden` | `boolean` | Optional | Whether compass is visible when map is rotated. | `false` | +| `contentInset` | `array` | Optional | Change the padding of the viewport of the map. Offset is in pixels. `[top, right, bottom, left]` `[0, 0, 0, 0]` | +| `style` | React styles | Optional | Styles the actual map view container | N/A | +| `debugActive` | `boolean` | Optional | Turns on debug mode. | `false` | + +## Callback props + +```javascript + { + //... +}}/> +``` + +| Prop | Payload shape | Description +|---|---|---| +| `onRegionWillChange` | `{latitude: 0, longitude: 0, zoomLevel: 0, direction: 0, pitch: 0, animated: false}` | Fired when the map begins panning or zooming. `animated` indicates whether the action is user-driven or animation-driven. +| `onRegionDidChange` | `{latitude: 0, longitude: 0, zoomLevel: 0, direction: 0, pitch: 0, animated: false}` | Fired when the map ends panning or zooming. +| `onOpenAnnotation` | `{id: 'marker_id', title: null, subtitle: null, latitude: 0, longitude: 0}` | Fired when tapping an annotation. +| `onRightAnnotationTapped` | `{id: 'marker_id', title: null, subtitle: null, latitude: 0, longitude: 0}` | Fired when user taps the `rightCalloutAccessory` of an annotation. +| `onChangeUserTrackingMode` | `Mapbox.userTrackingMode.none` | Fired when the user tracking mode gets changed by an user pan or rotate. +| `onUpdateUserLocation` | `{latitude: 0, longitude: 0, verticalAccuracy: 0, horizontalAccuracy: 0, headingAccuracy: 0, magneticHeading: 0, trueHeading: 0, isUpdating: false}` | Fired when the user's location updates. `headingAccuracy` and `isUpdating` are only supported on iOS. `verticalAccuracy` and `horizontalAccuracy` will be the same on Android, or might not exist in some circumstances. +| `onLocateUserFailed` | `{message: 'Error message'}` | Fired when there is an error getting the user's location. Do not rely on the string that is returned for determining what kind of error it is. +| `onTap` | `{latitude: 0, longitude: 0, screenCoordX: 0, screenCoordY: 0}` | Fired when the users taps the screen. +| `onLongPress` | `{latitude: 0, longitude: 0, screenCoordX: 0, screenCoordX: 0}` | Fired when the user taps and holds screen for 1 second. +| `onStartLoadingMap` | `undefined` | Fired once the map begins loading the style. | +| `onFinishLoadingMap` | `undefined` | Fired once the map has loaded the style. | + +## Methods + +You first need to get a ref to your `MapView` component: + +```jsx + { this._map = map; }} /> +``` + +Then call methods as `this._map.methodName()`. + +--- + +```javascript +this._map.setDirection(direction, animated = true, callback); +this._map.setZoomLevel(zoomLevel, animated = true, callback); +this._map.setCenterCoordinate(latitude, longitude, animated = true, callback); +this._map.setCenterCoordinateZoomLevel(latitude, longitude, zoomLevel, animated = true, callback); +this._map.setCenterCoordinateZoomLevelPitch(latitude, longitude, zoomLevel, pitch, animated = true, callback); +this._map.setPitch(pitch, animated = true, callback); +this._map.easeTo({ latitude, longitude, zoomLevel, altitude, direction, pitch }, animated = true, callback); +``` + +This set of methods sets the location the map is centered on, the zoom level, +the heading and the pitch of the map. + +The transition to the desired location is animated by default, but can be made +instantaneous by passing `animated` as `false`. + +For `easeTo`, all arguments inside the options object are optional. You can specify +any combination of center coords, zoomLevel, altitude, direction and pitch. What is not +specified stays at their current values. + +The `altitude` refers to the viewing altitude of the camera. It's a replacement for `zoomLevel`, +hence `zoomLevel` and `altitude` must not be specified at the same time. + +On iOS, `pitch` can't be specified at the same time as `zoomLevel`. `altitude` must +be used instead. + +`altitude` is not available on Android. + +The methods accept an optional `callback` that will get fired when the animation +has ended. Additionally, the return value is a promise that gets resolved when the +animation has ended. + +--- + +```javascript +this._map.setVisibleCoordinateBounds(latitudeSW, longitudeSW, latitudeNE, longitudeNE, paddingTop = 0, paddingRight = 0, paddingBottom = 0, paddingLeft = 0, animated = true); +``` + +This method adjusts the center location and the zoomLevel of the map so that +the rectangle determined by `latitudeSW`, `longitudeSW`, `latitudeNE`, +`longitudeNE` fits inside the viewport. + +You can optionally pass a minimum padding (in screen points) that will be +visible around the given coordinate bounds. + +The transition is animated unless you pass `animated` as `false`. + +--- + +```javascript +this._map.getCenterCoordinateZoomLevel(data => { + // ... +}); +``` + +Gets the current coordinates and zoom level of the map. + +`data` is an object of the form `{ latitude, longitude, zoomLevel }` + +--- + +```javascript +this._map.getDirection(direction => { + // ... +}); +``` + +Gets the current heading of the map. + +`direction` is the heading in degrees. + +--- + +```javascript +this._map.getPitch(pitch => { + // ... +}); +``` + +Gets the current tilt of the map. (Android only) + +`pitch` is the tilt in degrees measured from the normal to the map. + +--- + +```javascript +this._map.getBounds(bounds => { + // ... +}); +``` + +Gets the bounding rectangle in GPS coordinates that is currently visible on +within the map's viewport. + +`bounds` is an array representing `[ latitudeSW, longitudeSW, latitudeNE, longitudeNE ]` + +--- + +```javascript +this._map.selectAnnotation(id, animated = true); +``` + +Selects the annotation tagged with `id`, as if it would be tapped by the user. + +The transition is animated unless you pass `animated` as `false`. + +## Styles + +#### Default styles + +Mapbox GL ships with 6 included styles: + +* `Mapbox.mapStyles.streets` +* `Mapbox.mapStyles.dark` +* `Mapbox.mapStyles.light` +* `Mapbox.mapStyles.satellite` +* `Mapbox.mapStyles.hybrid` +* `Mapbox.mapStyles.emerald` (deprecated) + +To use one of these, just pass it as a prop to `MapView`: + +```jsx + +``` + +#### Custom styles + +You can also create a custom style in [Mapbox Studio](https://www.mapbox.com/studio/) and add it your map. Simply grab the style url. It should look something like: + +``` +mapbox://styles/bobbysud/cigtw1pzy0000aam2346f7ex0 +``` + +## Annotations + +#### Object shape + +```javascript +[{ + coordinates, // required. For type polyline and polygon must be an array of arrays. For type point, single array with 2 coordinates + type, // required. One of 'point', 'polyline' or 'polygon' + title, // optional. Title string. Appears when marker pressed + subtitle, // optional. Subtitle string. Appears when marker pressed + fillAlpha, // optional. number. Only for type=polygon. Controls the opacity of the polygon + fillColor, // optional. string. Only for type=polygon. CSS color (#rrggbb). Controls the fill color of the polygon + strokeAlpha, // optional. number. Only for type=polygon or type=polyline. Controls the opacity of the line + strokeColor, // optional. string. Only for type=polygon or type=polyline. CSS color (#rrggbb). Controls line color. + strokeWidth, // optional. number. Only for type=polygon or type=polyline. Controls line width. + id, // required. string. Unique identifier used for adding or selecting an annotation. + annotationImage, { // optional. Marker image for type=point + source: { + uri // required. string. Either remote image URL or the name (without extension) of a bundled image + }, + height, // required. number. Image height + width, // required. number. Image width + }, + rightCalloutAccessory, { // optional. iOS only. Clickable image that appears when type=point marker pressed + source: { + uri // required. string. Either remote image URL or the name (without extension) of a bundled image + }, + height, // required. number. Image height + width, // required. number. Image width + }, +}] +``` +**For using locally bundled images, on iOS see [adding static resources to your app using Images.xcassets docs](https://facebook.github.io/react-native/docs/image.html#adding-static-resources-to-your-app-using-images-xcassets) +and on Android, put images in `android/app/src/main/res/drawable/yourImage.png`**. + +#### Example + +```javascript +annotations: [{ + coordinates: [40.72052634, -73.97686958312988], + type: 'point', + title: 'This is marker 1', + subtitle: 'It has a rightCalloutAccessory too', + rightCalloutAccessory: { + source: { uri: 'https://cldup.com/9Lp0EaBw5s.png' }, + height: 25, + width: 25 + }, + annotationImage: { + source: { uri: 'https://cldup.com/CnRLZem9k9.png' }, + height: 25, + width: 25 + }, + id: 'marker1' +}, { + coordinates: [40.714541341726175,-74.00579452514648], + type: 'point', + title: 'Important', + subtitle: 'Neat, this is a custom annotation image', + annotationImage: { + source: { uri: 'https://cldup.com/7NLZklp8zS.png' }, + height: 25, + width: 25 + }, + id: 'marker2' +}, { + coordinates: [[40.76572150042782,-73.99429321289062],[40.743485405490695, -74.00218963623047],[40.728266950429735,-74.00218963623047],[40.728266950429735,-73.99154663085938],[40.73633186448861,-73.98983001708984],[40.74465591168391,-73.98914337158203],[40.749337730454826,-73.9870834350586]], + type: 'polyline', + strokeColor: '#00FB00', + strokeWidth: 3, + strokeAlpha: 0.5, + id: 'line' +}, { + coordinates: [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], + type: 'polygon', + fillAlpha:1, + fillColor: '#C32C2C', + strokeColor: '#DDDDD', + id: 'route' +}] +``` + +#### Immutability + +When adding new annotations or modifying existing ones, it's recommended not +to mutate the annotations array, but rather treat it as immutable and create +a new one with the same objects plus your modifications. + +If your `annotations` array is immutable and you enable `annotationsAreImmutable`, +this enables important performance optimizations when this component is +re-rendered. + +See [the example](./example.js#L116) for an illustration of this. + +## Mapbox Telemetry (metrics) + +If you hide the attribution button, you need to provide the user with a way to +opt-out of telemetry. For this, you need to add `MGLMapboxMetricsEnabledSettingShownInApp` +as `YES` in `Info.plist`, then create a switch that toggles metrics. + +To get the current state of metrics, use `Mapbox.getMetricsEnabled()`. + +To enable or disable metrics, use `Mapbox.setMetricsEnabled(enabled: boolean)`. + +## Offline + +There are 3 main methods for interacting with the offline API: +* `Mapbox.addOfflinePackForRegion`: Creates an offline pack +* `Mapbox.getOfflinePacks`: Returns an array of all offline packs on the device +* `Mapbox.removeOfflinePack`: Removes a single pack + +Before using them, don't forget to set an access token with `Mapbox.setAccessToken(accessToken)` + +These methods return a promise, but they also accept a callback as the last +argument with the signature `(err, value) => {}`. + +#### Creating a pack + +```javascript +Mapbox.addOfflinePack({ + name: 'test', // required + type: 'bbox', // required, only type currently supported` + metadata: { // optional. You can put any information in here that may be useful to you + date: new Date(), + foo: 'bar' + }, + bounds: [ // required. The corners of the bounded rectangle region being saved offline + latitudeSW, longitudeSW, latitudeNE, longitudeNE + ], + minZoomLevel: 10, // required + maxZoomLevel: 13, // required + styleURL: Mapbox.mapStyles.emerald // required. Valid styleURL +}).then(() => { + // Called after the pack has been added successfully +}).catch(err => { + console.error(err); // Handle error +}); +``` + +#### Deleting a pack + +To delete a pack, provide the `name` of the pack to delete. + +```javascript +Mapbox.removeOfflinePack('test') + .then(info => { + if (info.deleted) { + console.log(`Deleted pack named ${info.deleted}`); // The pack has been deleted successfully + } else { + console.log('No packs to delete'); // There are no packs named 'test' + } + }) + .catch(err => { + console.error(err); // Handle error + }); +``` + +#### Querying progress + +```javascript +Mapbox.getOfflinePacks() + .then(packs => { + // packs is an array of progress objects + }) + .catch(err => { + console.error(err); // Handle error + }) +``` + +A progress object has the following shape: + +```javascript +{ + name: 'test', // The name this pack was registered with + metadata, // The value that was previously passed as metadata + countOfBytesCompleted: 0, // The number of bytes downloaded for this pack + countOfResourcesCompleted: 0, // The number of tiles that have been downloaded for this pack + countOfResourcesExpected: 0, // The estimated minimum number of total tiles in this pack + maximumResourcesExpected: 0 // The estimated maximum number of total tiles in this pack +} +``` + +#### Subscribing to progress notifications + +```javascript +const subscription = Mapbox.addOfflinePackProgressListener(progressObject => { + // progressObject has the same format as above +}); + +// Remove the listener when it is not needed anymore +subscription.remove(); +``` + +Due to high volume, progress notifications are throttled so as not to starve the +run loop and make the JS thread unresponsive. + +By default, you'll get at most one progress notification per pack each 300 ms. + +You can configure this interval with: + +```javascript +Mapbox.setOfflinePackProgressThrottleInterval(milis); +``` + +#### Subscribing to error events + +```javascript +const subscription = Mapbox.addOfflineErrorListener(payload => { + console.log(`Offline pack named ${payload.name} experienced an error: ${payload.error}`); +}); + +// Remove the listener when it is not needed anymore +subscription.remove(); +``` + +```javascript +const subscription = Mapbox.addOfflineMaxAllowedTilesListener(payload => { + console.log(`Offline pack named ${payload.name} reached max tiles quota of ${payload.maxTiles} tiles`); +}); + +// Remove the listener when it is not needed anymore +subscription.remove(); +``` + +Check out our [help page](https://www.mapbox.com/help/mobile-offline/) for more information on offline. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce4a4768..2ce1e2725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +#v5.0.0 + +* Major breaking API changes. See the [API documentation](/API.md) for details. +* Unifies Android & iOS APIs. +* Adds support for telemetry opt-out. +* Adds offline maps support for Android. + #v4.1.1 * [Android] Fixes `Scrollable` error diff --git a/android/API.md b/android/API.md deleted file mode 100644 index 31897746e..000000000 --- a/android/API.md +++ /dev/null @@ -1,105 +0,0 @@ -# Android API Docs - -## Options - -| Option | Type | Opt/Required | Default | Note | -|---|---|---|---|---| -| `accessToken` | `string` | Required | NA |Mapbox access token. Sign up for a [Mapbox account here](https://www.mapbox.com/signup). -| `centerCoordinate` | `object` | Optional | `0,0`| Initial `latitude`/`longitude` the map will load at, defaults to `0,0`. -| `zoomLevel` | `double` | Optional | `0` | Initial zoom level the map will load at. 0 is the entire world, 18 is rooftop level. Defaults to 0. -| `rotateEnabled` | `bool` | Optional | `true` | Whether the map can rotate | -| `scrollEnabled` | `bool` | Optional | `true` | Whether the map can be scrolled | -| `zoomEnabled` | `bool` | Optional | `true` | Whether the map zoom level can be changed | -|`showsUserLocation` | `bool` | Optional | `false` | Whether the user's location is shown on the map. Note - the map will not zoom to their location.| -| `styleURL` | `string` | Optional | Mapbox Streets | A Mapbox GL style sheet. Defaults to `streets-v8`. -| `annotations` | `array` | Optional | NA | An array of annotation objects. See [annotation detail](https://github.com/bsudekum/react-native-mapbox-gl/blob/master/android/API.md#annotations) -| `direction` | `double` | Optional | `0` | Heading of the map in degrees where 0 is north and 180 is south | -| `debugActive` | `bool` | Optional | `false` | Turns on debug mode. | -| `style` | flexbox `view` | Optional | NA | Styles the actual map view container | -| `attributionButtonIsHidden` | `bool` | Optional | `false` | Whether attribution button is visible in lower left corner. *If true you must still attribute OpenStreetMap in your app. [Ref](https://www.mapbox.com/about/maps/)* | -| `logoIsHidden` | `bool` | Optional | `false` | Whether logo is visible in lower left corner. | -| `compassIsHidden` | `bool` | Optional | `false` | Whether compass is visible when map is rotated. | - -## Events - -| Event Name | Returns | Notes -|---|---|---| -| `onRegionChange` | `{latitude: 0, longitude: 0, zoom: 0}` | Fired when the map is panning or zooming. -| `getCenterCoordinateZoomLevel` | `mapViewRef`, `callback` | Gets the current center location and zoom level. Returns a single callback object. | -| `getDirection` | `mapViewRef`, `callback` | Gets the current direction. Returns a single callback object. | -| `onOpenAnnotation` | `{title: null, subtitle: null, latitude: 0, longitude: 0}` | Fired when focusing a an annotation. If the annotation is opened already, the event will not fire. -| `onLongPress` | `{latitude: 0, longitude: 0}` | Fired when the user taps and holds the map. -| `getBounds` | `mapViewRef`, `callback` | Returns current bounds for view (NE & SW). - -## Methods for Modifying the Map State - -Each method also requires you to pass in a string as the first argument which is equal to the `ref` on the map view you wish to modify. See the [example](https://github.com/mapbox/react-native-mapbox-gl/blob/master/android/example.js) on how this is implemented. - -| Method Name | Arguments | Notes -|---|---|---| -| `setDirectionAnimated` | `mapViewRef`, `heading` | Rotates the map to a new heading -| `setCenterCoordinateAnimated` | `mapViewRef`, `latitude`, `longitude` | Moves the map to a new coordinate. Note, the zoom level stay at the current zoom level -| `setCenterCoordinateZoomLevelAnimated` | `mapViewRef`, `latitude`, `longitude`, `zoomLevel` | Moves the map to a new coordinate and zoom level -| `addAnnotations` | `mapViewRef`, `` (array of annotation objects, see [#annotations](https://github.com/bsudekum/react-native-mapbox-gl/blob/master/android/API.md#annotations)) | Adds annotation(s) to the map without redrawing the map. Note, this will remove all previous annotations from the map. -| `removeAllAnnotations` | `mapViewRef` | Removes all annotations on map. -| `setVisibleCoordinateBoundsAnimated` | `mapViewRef`, `latitude1`, `longitude1`, `latitude2`, `longitude2`, `padding top`, `padding right`, `padding bottom`, `padding left` | Changes the viewport to fit the given coordinate bounds and some additional padding on each side. -| `setUserTrackingMode` | `mapViewRef`, `NONE` or `FOLLOW` | Modifies the tracking mode. - -## GL Styles - -You can change the `styleURL` to any valid GL stylesheet, here are a few: - -* `mapbox://styles/dark-v8.json` -* `mapbox://styles/light-v8.json` -* `mapbox://styles/emerald-v8.json` -* `mapbox://styles/streets-v8.json` -* `mapbox://styles/satellite-v8.json` - -## Annotations -```json -[{ - "coordinates": "required. For type polyline and polygon must be an array of arrays. For type point, single array", - "type": "required: point, polyline or polygon", - "title": "optional string", - "subtitle": "optional string", - "fillAlpha": "optional, only used for type=polygon. Controls the opacity of polygon", - "fillColor": "optional string hex color including #, only used for type=polygon*", - "strokeAlpha": "optional number from 0-1. Only used for type=poyline. Controls opacity of line", - "strokeColor": "optional string hex color including #, used for type=polygon and type=polyline*", - "strokeWidth": "optional number. Only used for type=poyline. Controls line width", - "id": "optional string, unique identifier.", - "annotationImage": { - "url": "Optional. Either remote image or specify via 'image!yourImage'", - "height": "required if url specified", - "width": "required if url specified" - }, -}] -``` -_*[Valid colors can be seen here](http://developer.android.com/reference/android/graphics/Color.html#parseColor%28java.lang.String%29)_ - -#### Example -```json -annotations: [{ - "coordinates": [40.72052634, -73.97686958312988], - "type": "point", - "title": "This is marker 1", - "subtitle": "It has a rightCalloutAccessory too", -}, { - "coordinates": [40.714541341726175,-74.00579452514648], - "type": "point", - "title": "Important", - "subtitle": "Neat, this is a custom annotation image", -}, { - "coordinates": [[40.76572150042782,-73.99429321289062],[40.743485405490695, -74.00218963623047],[40.728266950429735,-74.00218963623047],[40.728266950429735,-73.99154663085938],[40.73633186448861,-73.98983001708984],[40.74465591168391,-73.98914337158203],[40.749337730454826,-73.9870834350586]], - "type": "polyline", - "strokeColor": "#00FB00", - "strokeWidth": 3, - "strokeAlpha": 0.5 -}, { - "coordinates": [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], - "type": "polygon", - "fillAlpha":1, - "fillColor": "#C32C2C", - "strokeColor": "#DDDDD" -}] -``` diff --git a/android/build.gradle b/android/build.gradle index 8c055750d..eadbd51ed 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,7 +31,7 @@ repositories { dependencies { compile 'com.facebook.react:react-native:0.19.+' - compile('com.mapbox.mapboxsdk:mapbox-android-sdk:3.2.0@aar') { + compile('com.mapbox.mapboxsdk:mapbox-android-sdk:4.1.1@aar') { transitive = true } } diff --git a/android/example.js b/android/example.js deleted file mode 100644 index 6b4808336..000000000 --- a/android/example.js +++ /dev/null @@ -1,145 +0,0 @@ - -import React, { Component } from 'react'; -var Mapbox = require('react-native-mapbox-gl'); -var mapRef = 'mapRef'; -import { - AppRegistry, - StyleSheet, - Text, - StatusBar, - View -} from 'react-native'; - -var MapExample = React.createClass({ - mixins: [Mapbox.Mixin], - getInitialState() { - return { - center: { - latitude: 40.7223, - longitude: -73.9878 - }, - annotations: [{ - coordinates: [40.7223, -73.9878], - type: 'point', - title: 'Important!', - subtitle: 'Neat, this is a custom annotation image', - id: 'marker2', - annotationImage: { - url: 'https://cldup.com/7NLZklp8zS.png', - height: 25, - width: 25 - } - }, { - coordinates: [40.7923, -73.9178], - type: 'point', - title: 'Important!', - subtitle: 'Neat, this is a custom annotation image' - }, { - coordinates: [[40.76572150042782,-73.99429321289062],[40.743485405490695, -74.00218963623047],[40.728266950429735,-74.00218963623047],[40.728266950429735,-73.99154663085938],[40.73633186448861,-73.98983001708984],[40.74465591168391,-73.98914337158203],[40.749337730454826,-73.9870834350586]], - type: 'polyline', - strokeColor: '#00FB00', - strokeWidth: 3, - alpha: 0.5, - id: 'foobar' - }, { - coordinates: [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], - type: 'polygon', - alpha:1, - fillColor: '#FFFFFF', - strokeColor: '#FFFFFF', - strokeWidth: 1, - id: 'zap' - }] - } - }, - onUserLocationChange(location) { - console.log(location); - }, - onLongPress(location) { - console.log(location); - }, - onOpenAnnotation(annotation) { - console.log(annotation); - }, - render() { - return ( - - this.setDirectionAnimated(mapRef, 0)}> - Set direction to 0 - - this.setCenterCoordinateAnimated(mapRef, 40.68454331694491, -73.93592834472656)}> - Go to New York at current zoom level - - this.setCenterCoordinateZoomLevelAnimated(mapRef, 35.68829, 139.77492, 14)}> - Go to Tokyo at fixed zoom level 14 - - this.addAnnotations(mapRef, [{ - coordinates: [40.73312,-73.989], - type: 'point', - title: 'This is a new marker', - id: 'foo' - }, { - 'coordinates': [[40.75974059207392, -74.02484893798828], [40.68454331694491, -73.93592834472656]], - 'type': 'polyline' - }])}> - Add new marker - - this.setUserTrackingMode(mapRef, this.userTrackingMode.follow)}> - Set userTrackingMode to follow - - this.removeAllAnnotations(mapRef)}> - Remove all annotations - - this.setTilt(mapRef, 50)}> - Set tilt to 50 - - this.setVisibleCoordinateBoundsAnimated(mapRef, 40.712, -74.227, 40.774, -74.125, 100, 100, 100, 100)}> - Set visible bounds to 40.7, -74.2, 40.7, -74.1 - - { - this.getDirection(mapRef, (direction) => { - console.log(direction); - }); - }}> - Get direction - - { - this.getCenterCoordinateZoomLevel(mapRef, (location) => { - console.log(location); - }); - }}> - Get location - - - - ); - } -}); - -var styles = StyleSheet.create({ - container: { - flex: 1 - } -}); - -AppRegistry.registerComponent('your-app-name', () => MapExample); diff --git a/android/install.md b/android/install.md index e5e20e9f7..49846f098 100644 --- a/android/install.md +++ b/android/install.md @@ -5,7 +5,18 @@ Run with ```--ignore-scripts``` to disable ios startup script ```shell npm install --save react-native-mapbox-gl --ignore-scripts ``` -#### Step 2 - Update Gradle Settings + +#### Step 2 - Use with Gradle + +##### Option A - With [rnpm](https://github.com/rnpm/rnpm) + +```shell +rnpm link +``` + +##### Option B - Manually + +Edit the following files: ```gradle // file: android/settings.gradle @@ -15,8 +26,6 @@ include ':reactnativemapboxgl' project(':reactnativemapboxgl').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-mapbox-gl/android') ``` -#### Step 3 - Update app Gradle Build - ```gradle // file: android/app/build.gradle ... @@ -27,45 +36,11 @@ dependencies { } ``` -#### Step 4 - Register React Package - -##### react-native < v0.18.0 - ```java -... +// file: android/app/src/main/java/com/yourcompany/yourapp/MainActivity.java import com.mapbox.reactnativemapboxgl.ReactNativeMapboxGLPackage; // <-- import ... - -public class MainActivity extends FragmentActivity implements DefaultHardwareBackBtnHandler { - - private ReactInstanceManager mReactInstanceManager; - private ReactRootView mReactRootView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mReactRootView = new ReactRootView(this); - mReactInstanceManager = ReactInstanceManager.builder() - .setApplication(getApplication()) - .setBundleAssetName("index.android.bundle") - .setJSMainModuleName("index.android") - .addPackage(new MainReactPackage()) - .addPackage(new ReactNativeMapboxGLPackage()) // <-- Register package here - .setUseDeveloperSupport(BuildConfig.DEBUG) - .setInitialLifecycleState(LifecycleState.RESUMED) - .build(); - mReactRootView.startReactApplication(mReactInstanceManager, "AwesomeProject", null); - setContentView(mReactRootView); - } -... -``` - -##### react-native >= v0.18.0, <0.29.0 - -```java -import com.mapbox.reactnativemapboxgl.ReactNativeMapboxGLPackage; // <-- import -... - /** +/** * A list of packages used by the app. If the app uses additional views * or modules besides the default ones, add more packages here. */ @@ -77,25 +52,21 @@ import com.mapbox.reactnativemapboxgl.ReactNativeMapboxGLPackage; // <-- import } ``` -##### react-native >= v0.29.0 +#### Step 3 - Add Mapbox to AndroidManifest.xml -```java -// file: android/app/src/main/java/com//MainApplication.java -... -import com.mapbox.reactnativemapboxgl.ReactNativeMapboxGLPackage; // <-- import -... -public class MainApplication extends Application implements ReactApplication { -... - /** - * A list of packages used by the app. If the app uses additional views - * or modules besides the default ones, add more packages here. - */ - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new ReactNativeMapboxGLPackage()); // <-- Register package here - } +Add the following permissions to your `AndroidManifest.xml`: + +```xml + + + + +``` + +Also, add the Mapbox analytics service: + +```xml + ``` -#### Step 5 - Add to project, [see example](./example.js) +#### Step 4 - Add to project, [see example](../example.js) diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationOptions.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationOptions.java new file mode 100644 index 000000000..1d273cc26 --- /dev/null +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationOptions.java @@ -0,0 +1,8 @@ +package com.mapbox.reactnativemapboxgl; + +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +public interface RNMGLAnnotationOptions { + public abstract Annotation addToMap(MapboxMap map); +} \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationOptionsFactory.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationOptionsFactory.java new file mode 100644 index 000000000..f3cdc7ad2 --- /dev/null +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationOptionsFactory.java @@ -0,0 +1,227 @@ +package com.mapbox.reactnativemapboxgl; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; + +import com.facebook.react.bridge.ReadableMap; +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.IconFactory; +import com.mapbox.mapboxsdk.annotations.MarkerOptions; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; +import com.mapbox.mapboxsdk.annotations.PolylineOptions; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +class RNMGLMarkerOptions implements RNMGLAnnotationOptions { + protected MarkerOptions _options; + + public RNMGLMarkerOptions(MarkerOptions options) { + _options = options; + } + + @Override + public Annotation addToMap(MapboxMap map) { + return map.addMarker(_options); + } +} + +class RNMGLPolylineOptions implements RNMGLAnnotationOptions { + protected PolylineOptions _options; + + public RNMGLPolylineOptions(PolylineOptions options) { + _options = options; + } + + @Override + public Annotation addToMap(MapboxMap map) { + return map.addPolyline(_options); + } +} + +class RNMGLPolygonOptions implements RNMGLAnnotationOptions { + protected PolygonOptions _options; + + public RNMGLPolygonOptions(PolygonOptions options) { + _options = options; + } + + @Override + public Annotation addToMap(MapboxMap map) { + return map.addPolygon(_options); + } +} + +public class RNMGLAnnotationOptionsFactory { + + public static RNMGLAnnotationOptions annotationOptionsFromJS(ReadableMap annotation, Context context) { + String type = annotation.getString("type"); + + if (type.equals("point")) { + return markerOptionsFromJS(annotation, context); + } else if (type.equals("polyline")) { + return polylineOptionsFromJS(annotation); + } else if (type.equals("polygon")) { + return polygonOptionsFromJS(annotation); + } + + return null; + } + + static Drawable drawableFromDrawableName(Context context, String drawableName) { + int resID = context.getResources().getIdentifier(drawableName, "drawable", context.getApplicationContext().getPackageName()); + return ContextCompat.getDrawable(context, resID); + } + + static Drawable drawableFromUrl(Context context, String url) throws IOException { + // This doesn't currently work, as it throws NetworkOnMainThreadException + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.connect(); + InputStream input = connection.getInputStream(); + + Bitmap bitmap = BitmapFactory.decodeStream(input); + return new BitmapDrawable(context.getResources(), bitmap); + } + + static Map iconCache = new HashMap(); + + static Icon iconFromSourceAndSize(Context context, ReadableMap source, int width, int height) throws IOException { + String path = source.getString("uri"); + String cacheKey = path + "||" + width + "||" + height; + Icon icon = iconCache.get(cacheKey); + if (icon != null) { return icon; } + + Drawable drawable; + try { + drawable = drawableFromUrl(context, path); + } catch (MalformedURLException ex) { + drawable = drawableFromDrawableName(context, path); + } + + IconFactory iconFactory = IconFactory.getInstance(context); + + int intrinsicWidth = drawable.getIntrinsicWidth(); + int intrinsicHeight = drawable.getIntrinsicHeight(); + + if (width < 0) { width = intrinsicWidth; } + if (height < 0) { height = intrinsicHeight; } + + // Check if a rescale would be superfluous + if ((drawable instanceof BitmapDrawable) && width == intrinsicWidth && height == intrinsicHeight) { + icon = iconFactory.fromBitmap(((BitmapDrawable)drawable).getBitmap()); + } else { + icon = iconFactory.fromDrawable(drawable, width, height); + } + + iconCache.put(cacheKey, icon); + return icon; + } + + static RNMGLAnnotationOptions markerOptionsFromJS(ReadableMap annotation, Context context) { + MarkerOptions marker = new MarkerOptions(); + + double latitude = annotation.getArray("coordinates").getDouble(0); + double longitude = annotation.getArray("coordinates").getDouble(1); + LatLng markerCenter = new LatLng(latitude, longitude); + marker.position(markerCenter); + + if (annotation.hasKey("title")) { + String title = annotation.getString("title"); + marker.title(title); + } + + if (annotation.hasKey("subtitle")) { + String subtitle = annotation.getString("subtitle"); + marker.snippet(subtitle); + } + + if (annotation.hasKey("annotationImage")) { + ReadableMap annotationImage = annotation.getMap("annotationImage"); + ReadableMap annotationSource = annotationImage.getMap("source"); + try { + int width = -1; + int height = -1; + + if (annotationImage.hasKey("height") && annotationImage.hasKey("width")) { + float scale = context.getResources().getDisplayMetrics().density; + height = Math.round((float)annotationImage.getInt("height") * scale); + width = Math.round((float)annotationImage.getInt("width") * scale); + } + + marker.icon(iconFromSourceAndSize(context, annotationSource, width, height)); + } catch (Exception e) { + e.printStackTrace(); + } + } + return new RNMGLMarkerOptions(marker); + } + + static RNMGLAnnotationOptions polylineOptionsFromJS(ReadableMap annotation) { + PolylineOptions polyline = new PolylineOptions(); + + int coordSize = annotation.getArray("coordinates").size(); + for (int p = 0; p < coordSize; p++) { + double latitude = annotation.getArray("coordinates").getArray(p).getDouble(0); + double longitude = annotation.getArray("coordinates").getArray(p).getDouble(1); + polyline.add(new LatLng(latitude, longitude)); + } + + if (annotation.hasKey("alpha")) { + double strokeAlpha = annotation.getDouble("alpha"); + polyline.alpha((float) strokeAlpha); + } + + if (annotation.hasKey("strokeColor")) { + int strokeColor = Color.parseColor(annotation.getString("strokeColor")); + polyline.color(strokeColor); + } + + if (annotation.hasKey("strokeWidth")) { + float strokeWidth = annotation.getInt("strokeWidth"); + polyline.width(strokeWidth); + } + + return new RNMGLPolylineOptions(polyline); + } + + static RNMGLAnnotationOptions polygonOptionsFromJS(ReadableMap annotation) { + PolygonOptions polygon = new PolygonOptions(); + + int coordSize = annotation.getArray("coordinates").size(); + for (int p = 0; p < coordSize; p++) { + double latitude = annotation.getArray("coordinates").getArray(p).getDouble(0); + double longitude = annotation.getArray("coordinates").getArray(p).getDouble(1); + polygon.add(new LatLng(latitude, longitude)); + } + + if (annotation.hasKey("alpha")) { + double fillAlpha = annotation.getDouble("alpha"); + polygon.alpha((float) fillAlpha); + } + + if (annotation.hasKey("fillColor")) { + int fillColor = Color.parseColor(annotation.getString("fillColor")); + polygon.fillColor(fillColor); + } + + if (annotation.hasKey("strokeColor")) { + int strokeColor = Color.parseColor(annotation.getString("strokeColor")); + polygon.strokeColor(strokeColor); + } + + return new RNMGLPolygonOptions(polygon); + } +} diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java index 29bc6a02d..301c22e8a 100644 --- a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java @@ -1,440 +1,374 @@ package com.mapbox.reactnativemapboxgl; -import android.graphics.Color; -import android.util.Log; -import android.os.StrictMode; -import android.location.Location; - +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +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.common.MapBuilder; +import com.facebook.react.modules.core.RCTNativeAppEventEmitter; import com.facebook.react.uimanager.annotations.ReactProp; -import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.mapbox.mapboxsdk.annotations.Marker; -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; -import android.graphics.RectF; -import com.mapbox.mapboxsdk.geometry.CoordinateBounds; -import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; -import com.mapbox.mapboxsdk.annotations.MarkerOptions; -import com.mapbox.mapboxsdk.annotations.PolygonOptions; -import com.mapbox.mapboxsdk.annotations.PolylineOptions; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.views.MapView; +import com.facebook.react.uimanager.SimpleViewManager; import com.mapbox.mapboxsdk.camera.CameraPosition; -import com.mapbox.mapboxsdk.annotations.Icon; -import com.mapbox.mapboxsdk.annotations.IconFactory; -import android.graphics.drawable.Drawable; -import android.graphics.BitmapFactory; -import android.graphics.Bitmap; -import java.io.InputStream; -import java.io.IOException; -import java.net.URL; -import java.net.HttpURLConnection; -import android.graphics.drawable.BitmapDrawable; +import com.mapbox.mapboxsdk.camera.CameraUpdate; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; + +import java.util.Map; + +import javax.annotation.Nonnull; import javax.annotation.Nullable; -import android.graphics.PointF; - -public class ReactNativeMapboxGLManager extends SimpleViewManager { - - public static final String REACT_CLASS = "RCTMapbox"; - - public static final String PROP_ACCESS_TOKEN = "accessToken"; - public static final String PROP_ANNOTATIONS = "annotations"; - public static final String PROP_CENTER_COORDINATE = "centerCoordinate"; - public static final String PROP_DEBUG_ACTIVE = "debugActive"; - public static final String PROP_DIRECTION = "direction"; - public static final String PROP_ONOPENANNOTATION = "onOpenAnnotation"; - public static final String PROP_ONLONGPRESS = "onLongPress"; - public static final String PROP_ONREGIONCHANGE = "onRegionChange"; - public static final String PROP_ONUSER_LOCATION_CHANGE = "onUserLocationChange"; - public static final String PROP_ROTATION_ENABLED = "rotateEnabled"; - public static final String PROP_SCROLL_ENABLED = "scrollEnabled"; - public static final String PROP_USER_LOCATION = "showsUserLocation"; - public static final String PROP_STYLE_URL = "styleURL"; - public static final String PROP_USER_TRACKING_MODE = "userTrackingMode"; - public static final String PROP_ZOOM_ENABLED = "zoomEnabled"; - public static final String PROP_ZOOM_LEVEL = "zoomLevel"; - public static final String PROP_SET_TILT = "tilt"; - public static final String PROP_COMPASS_IS_HIDDEN = "compassIsHidden"; - public static final String PROP_LOGO_IS_HIDDEN = "logoIsHidden"; - public static final String PROP_ATTRIBUTION_BUTTON_IS_HIDDEN = "attributionButtonIsHidden"; - private static String APPLICATION_ID; - private MapView mapView; + +public class ReactNativeMapboxGLManager extends SimpleViewManager { + + private static final String REACT_CLASS = "RCTMapboxGL"; + + private ReactApplicationContext _context; + + public ReactNativeMapboxGLManager(ReactApplicationContext context) { + super(); + _context = context; + } @Override public String getName() { return REACT_CLASS; } + public ReactApplicationContext getContext() { + return _context; + } + + // Lifecycle methods + @Override - public MapView createViewInstance(ThemedReactContext context) { - mapView = new MapView(context, "pk.foo"); - mapView.onCreate(null); - APPLICATION_ID = context.getPackageName(); - StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); - StrictMode.setThreadPolicy(policy); - return mapView; - } - - @ReactProp(name = PROP_ACCESS_TOKEN) - public void setAccessToken(MapView view, @Nullable String value) { - if (value == null || value.isEmpty()) { - Log.e(REACT_CLASS, "Error: No access token provided"); - } else { - view.setAccessToken(value); - } + public ReactNativeMapboxGLView createViewInstance(ThemedReactContext context) { + return new ReactNativeMapboxGLView(context, this); } - @ReactProp(name = PROP_SET_TILT) - public void setTilt(MapView view, @Nullable double pitch) { - mapView.setTilt(pitch, 1L); + @Override + protected void onAfterUpdateTransaction(ReactNativeMapboxGLView view) { + super.onAfterUpdateTransaction(view); + view.onAfterUpdateTransaction(); } - public static Drawable drawableFromUrl(MapView view, String url) throws IOException { - Bitmap x; + @Override + public void onDropViewInstance(ReactNativeMapboxGLView view) { + view.onDrop(); + } - HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - connection.connect(); - InputStream input = connection.getInputStream(); + // Event types - x = BitmapFactory.decodeStream(input); - return new BitmapDrawable(view.getResources(), x); + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.builder() + .put("onRegionDidChange", MapBuilder.of("registrationName", "onRegionDidChange")) + .put("onRegionWillChange", MapBuilder.of("registrationName", "onRegionWillChange")) + .put("onOpenAnnotation", MapBuilder.of("registrationName", "onOpenAnnotation")) + .put("onRightAnnotationTapped", MapBuilder.of("registrationName", "onRightAnnotationTapped")) + .put("onChangeUserTrackingMode", MapBuilder.of("registrationName", "onChangeUserTrackingMode")) + .put("onUpdateUserLocation", MapBuilder.of("registrationName", "onUpdateUserLocation")) + .put("onLongPress", MapBuilder.of("registrationName", "onLongPress")) + .put("onTap", MapBuilder.of("registrationName", "onTap")) + .put("onFinishLoadingMap", MapBuilder.of("registrationName", "onFinishLoadingMap")) + .put("onStartLoadingMap", MapBuilder.of("registrationName", "onStartLoadingMap")) + .put("onLocateUserFailed", MapBuilder.of("registrationName", "onLocateUserFailed")) + .build(); } - public static Drawable drawableFromDrawableName(MapView view, String drawableName) { - Bitmap x; - int resID = view.getResources().getIdentifier(drawableName, "drawable", APPLICATION_ID); - x = BitmapFactory.decodeResource(view.getResources(), resID); - return new BitmapDrawable(view.getResources(), x); - } + // Props - @ReactProp(name = PROP_ANNOTATIONS) - public void setAnnotationClear(MapView view, @Nullable ReadableArray value) { - setAnnotations(view, value, true); + @ReactProp(name = "initialZoomLevel") + public void setInitialZoomLevel(ReactNativeMapboxGLView view, double value) { + view.setInitialZoomLevel(value); } - public void setAnnotations(MapView view, @Nullable ReadableArray value, boolean clearMap) { - if (value == null) { - Log.e(REACT_CLASS, "Error: No annotations"); - } else { - if (clearMap) { - view.removeAllAnnotations(); - } - int size = value.size(); - for (int i = 0; i < size; i++) { - ReadableMap annotation = value.getMap(i); - String type = annotation.getString("type"); - if (type.equals("point")) { - double latitude = annotation.getArray("coordinates").getDouble(0); - double longitude = annotation.getArray("coordinates").getDouble(1); - LatLng markerCenter = new LatLng(latitude, longitude); - MarkerOptions marker = new MarkerOptions(); - marker.position(markerCenter); - if (annotation.hasKey("title")) { - String title = annotation.getString("title"); - marker.title(title); - } - if (annotation.hasKey("subtitle")) { - String subtitle = annotation.getString("subtitle"); - marker.snippet(subtitle); - } - if (annotation.hasKey("annotationImage")) { - ReadableMap annotationImage = annotation.getMap("annotationImage"); - String annotationURL = annotationImage.getString("url"); - try { - Drawable image; - if (annotationURL.startsWith("image!")) { - image = drawableFromDrawableName(mapView, annotationURL.replace("image!", "")); - } else { - image = drawableFromUrl(mapView, annotationURL); - } - IconFactory iconFactory = view.getIconFactory(); - Icon icon; - if (annotationImage.hasKey("height") && annotationImage.hasKey("width")) { - float scale = view.getResources().getDisplayMetrics().density; - int height = Math.round((float)annotationImage.getInt("height") * scale); - int width = Math.round((float)annotationImage.getInt("width") * scale); - icon = iconFactory.fromDrawable(image, width, height); - } else { - icon = iconFactory.fromDrawable(image); - } - marker.icon(icon); - } catch (Exception e) { - e.printStackTrace(); - } - } - view.addMarker(marker); - } else if (type.equals("polyline")) { - int coordSize = annotation.getArray("coordinates").size(); - PolylineOptions polyline = new PolylineOptions(); - for (int p = 0; p < coordSize; p++) { - double latitude = annotation.getArray("coordinates").getArray(p).getDouble(0); - double longitude = annotation.getArray("coordinates").getArray(p).getDouble(1); - polyline.add(new LatLng(latitude, longitude)); - } - if (annotation.hasKey("alpha")) { - double strokeAlpha = annotation.getDouble("alpha"); - polyline.alpha((float) strokeAlpha); - } - if (annotation.hasKey("strokeColor")) { - int strokeColor = Color.parseColor(annotation.getString("strokeColor")); - polyline.color(strokeColor); - } - if (annotation.hasKey("strokeWidth")) { - float strokeWidth = annotation.getInt("strokeWidth"); - polyline.width(strokeWidth); - } - view.addPolyline(polyline); - } else if (type.equals("polygon")) { - int coordSize = annotation.getArray("coordinates").size(); - PolygonOptions polygon = new PolygonOptions(); - for (int p = 0; p < coordSize; p++) { - double latitude = annotation.getArray("coordinates").getArray(p).getDouble(0); - double longitude = annotation.getArray("coordinates").getArray(p).getDouble(1); - polygon.add(new LatLng(latitude, longitude)); - } - if (annotation.hasKey("alpha")) { - double fillAlpha = annotation.getDouble("alpha"); - polygon.alpha((float) fillAlpha); - } - if (annotation.hasKey("fillColor")) { - int fillColor = Color.parseColor(annotation.getString("fillColor")); - polygon.fillColor(fillColor); - } - if (annotation.hasKey("strokeColor")) { - int strokeColor = Color.parseColor(annotation.getString("strokeColor")); - polygon.strokeColor(strokeColor); - } - view.addPolygon(polygon); - } - } - } + @ReactProp(name = "initialDirection") + public void setInitialDirection(ReactNativeMapboxGLView view, double value) { + view.setInitialDirection(value); } - @ReactProp(name = PROP_DEBUG_ACTIVE, defaultBoolean = false) - public void setDebugActive(MapView view, Boolean value) { - view.setDebugActive(value); + @ReactProp(name = "initialCenterCoordinate") + public void setInitialCenterCoordinate(ReactNativeMapboxGLView view, ReadableMap coord) { + double lat = coord.getDouble("latitude"); + double lon = coord.getDouble("longitude"); + view.setInitialCenterCoordinate(lat, lon); } - @ReactProp(name = PROP_DIRECTION, defaultDouble = 0) - public void setDirection(MapView view, double value) { - view.setDirection(value, true); + @ReactProp(name = "enableOnRegionDidChange") + public void setEnableOnRegionDidChange(ReactNativeMapboxGLView view, boolean value) { + view.setEnableOnRegionDidChange(value); } - @ReactProp(name = PROP_ONREGIONCHANGE, defaultBoolean = true) - public void onMapChanged(final MapView view, Boolean value) { - view.addOnMapChangedListener(new MapView.OnMapChangedListener() { - @Override - public void onMapChanged(int change) { - if (change == MapView.REGION_DID_CHANGE || change == MapView.REGION_DID_CHANGE_ANIMATED) { - WritableMap event = Arguments.createMap(); - WritableMap location = Arguments.createMap(); - location.putDouble("latitude", view.getCenterCoordinate().getLatitude()); - location.putDouble("longitude", view.getCenterCoordinate().getLongitude()); - location.putDouble("zoom", view.getZoomLevel()); - event.putMap("src", location); - ReactContext reactContext = (ReactContext) view.getContext(); - reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("onRegionChange", event); - } - } - }); + @ReactProp(name = "enableOnRegionWillChange") + public void setEnableOnRegionWillChange(ReactNativeMapboxGLView view, boolean value) { + view.setEnableOnRegionWillChange(value); } - @ReactProp(name = PROP_ONUSER_LOCATION_CHANGE, defaultBoolean = true) - public void onMyLocationChange(final MapView view, Boolean value) { - view.setOnMyLocationChangeListener(new MapView.OnMyLocationChangeListener() { - @Override - public void onMyLocationChange(@Nullable Location location) { - WritableMap event = Arguments.createMap(); - WritableMap locationMap = Arguments.createMap(); - locationMap.putDouble("latitude", location.getLatitude()); - locationMap.putDouble("longitude", location.getLongitude()); - locationMap.putDouble("accuracy", location.getAccuracy()); - locationMap.putDouble("altitude", location.getAltitude()); - locationMap.putDouble("bearing", location.getBearing()); - locationMap.putDouble("speed", location.getSpeed()); - locationMap.putString("provider", location.getProvider()); - event.putMap("src", locationMap); - ReactContext reactContext = (ReactContext) view.getContext(); - reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("onUserLocationChange", event); - } - }); + @ReactProp(name = "debugActive") + public void setDebugActive(ReactNativeMapboxGLView view, boolean value) { + view.setDebugActive(value); } - @ReactProp(name = PROP_ONOPENANNOTATION, defaultBoolean = true) - public void onMarkerClick(final MapView view, Boolean value) { - view.setOnMarkerClickListener(new MapView.OnMarkerClickListener() { - @Override - public boolean onMarkerClick(@Nullable Marker marker) { - WritableMap event = Arguments.createMap(); - WritableMap markerObject = Arguments.createMap(); - markerObject.putString("title", marker.getTitle()); - markerObject.putString("subtitle", marker.getSnippet()); - markerObject.putDouble("latitude", marker.getPosition().getLatitude()); - markerObject.putDouble("longitude", marker.getPosition().getLongitude()); - event.putMap("src", markerObject); - ReactContext reactContext = (ReactContext) view.getContext(); - reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("onOpenAnnotation", event); - return false; - } - }); + @ReactProp(name = "rotateEnabled") + public void setRotateEnabled(ReactNativeMapboxGLView view, boolean value) { + view.setRotateEnabled(value); } - @ReactProp(name = PROP_ONLONGPRESS, defaultBoolean = true) - public void onMapLongClick(final MapView view, Boolean value) { - view.setOnMapLongClickListener(new MapView.OnMapLongClickListener() { - @Override - public void onMapLongClick(@Nullable LatLng location) { - WritableMap event = Arguments.createMap(); - WritableMap loc = Arguments.createMap(); - loc.putDouble("latitude", location.getLatitude()); - loc.putDouble("longitude", location.getLongitude()); - event.putMap("src", loc); - ReactContext reactContext = (ReactContext) view.getContext(); - reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("onLongPress", event); - } - }); - } - - @ReactProp(name = PROP_CENTER_COORDINATE) - public void setCenterCoordinate(MapView view, @Nullable ReadableMap center) { - if (center != null) { - double latitude = center.getDouble("latitude"); - double longitude = center.getDouble("longitude"); - CameraPosition cameraPosition = new CameraPosition.Builder() - .target(new LatLng(latitude, longitude)) - .build(); - view.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); - }else{ - Log.w(REACT_CLASS, "No CenterCoordinate provided"); - } + @ReactProp(name = "scrollEnabled") + public void setScrollEnabled(ReactNativeMapboxGLView view, boolean value) { + view.setScrollEnabled(value); } - @ReactProp(name = PROP_ROTATION_ENABLED, defaultBoolean = true) - public void setRotateEnabled(MapView view, Boolean value) { - view.setRotateEnabled(value); + @ReactProp(name = "zoomEnabled") + public void setZoomEnabled(ReactNativeMapboxGLView view, boolean value) { + view.setZoomEnabled(value); } - @ReactProp(name = PROP_USER_LOCATION, defaultBoolean = true) - public void setMyLocationEnabled(MapView view, Boolean value) { - view.setMyLocationEnabled(value); + @ReactProp(name = "showsUserLocation") + public void setShowsUserLocation(ReactNativeMapboxGLView view, boolean value) { + view.setShowsUserLocation(value); } - @ReactProp(name = PROP_STYLE_URL) - public void setStyleUrl(MapView view, @Nullable String value) { - if (value != null && !value.isEmpty()) { - view.setStyleUrl(value); - }else{ - Log.w(REACT_CLASS, "No StyleUrl provided"); - } + @ReactProp(name = "styleURL") + public void setStyleUrl(ReactNativeMapboxGLView view, @Nonnull String styleURL) { + view.setStyleURL(styleURL); } - @ReactProp(name = PROP_USER_TRACKING_MODE, defaultInt = 0) - public void setMyLocationTrackingMode(MapView view, @Nullable int mode) { - view.setMyLocationTrackingMode(mode); + @ReactProp(name = "userTrackingMode") + public void setUserTrackingMode(ReactNativeMapboxGLView view, int mode) { + view.setLocationTracking(ReactNativeMapboxGLModule.locationTrackingModes[mode]); + view.setBearingTracking(ReactNativeMapboxGLModule.bearingTrackingModes[mode]); } - @ReactProp(name = PROP_ZOOM_ENABLED, defaultBoolean = true) - public void setZoomEnabled(MapView view, Boolean value) { - view.setZoomEnabled(value); + @ReactProp(name = "attributionButtonIsHidden") + public void setAttributionButtonIsHidden(ReactNativeMapboxGLView view, boolean value) { + view.setAttributionButtonIsHidden(value); } - @ReactProp(name = PROP_ZOOM_LEVEL, defaultFloat = 0f) - public void setZoomLevel(MapView view, float value) { - view.setZoomLevel(value); + @ReactProp(name = "logoIsHidden") + public void setLogoIsHidden(ReactNativeMapboxGLView view, boolean value) { + view.setLogoIsHidden(value); } - @ReactProp(name = PROP_SCROLL_ENABLED, defaultBoolean = true) - public void setScrollEnabled(MapView view, Boolean value) { - view.setScrollEnabled(value); + @ReactProp(name = "compassIsHidden") + public void setCompassIsHidden(ReactNativeMapboxGLView view, boolean value) { + view.setCompassIsHidden(value); } - @ReactProp(name = PROP_COMPASS_IS_HIDDEN) - public void setCompassIsHidden(MapView view, Boolean value) { - view.setCompassEnabled(!value); + @ReactProp(name = "contentInset") + public void setContentInset(ReactNativeMapboxGLView view, ReadableArray inset) { + view.setContentInset(inset.getInt(0), inset.getInt(1), inset.getInt(2), inset.getInt(3)); } - @ReactProp(name = PROP_LOGO_IS_HIDDEN) - public void setLogoIsHidden(MapView view, Boolean value) { - int visibility = (value ? android.view.View.INVISIBLE : android.view.View.VISIBLE); - view.setLogoVisibility(visibility); - } + // Commands - @ReactProp(name = PROP_ATTRIBUTION_BUTTON_IS_HIDDEN) - public void setAttributionButtonIsHidden(MapView view, Boolean value) { - int visibility = (value ? android.view.View.INVISIBLE : android.view.View.VISIBLE); - view.setAttributionVisibility(visibility); + public static final int COMMAND_GET_DIRECTION = 0; + public static final int COMMAND_GET_PITCH = 1; + public static final int COMMAND_GET_CENTER_COORDINATE_ZOOM_LEVEL = 2; + public static final int COMMAND_GET_BOUNDS = 3; + public static final int COMMAND_EASE_TO = 4; + public static final int COMMAND_SET_VISIBLE_COORDINATE_BOUNDS = 6; + public static final int COMMAND_SELECT_ANNOTATION = 7; + public static final int COMMAND_SPLICE_ANNOTATIONS = 8; + + @Override + public + @Nullable + Map getCommandsMap() { + return MapBuilder.builder() + .put("getDirection", COMMAND_GET_DIRECTION) + .put("getPitch", COMMAND_GET_PITCH) + .put("getCenterCoordinateZoomLevel", COMMAND_GET_CENTER_COORDINATE_ZOOM_LEVEL) + .put("getBounds", COMMAND_GET_BOUNDS) + .put("easeTo", COMMAND_EASE_TO) + .put("setVisibleCoordinateBounds", COMMAND_SET_VISIBLE_COORDINATE_BOUNDS) + .put("selectAnnotation", COMMAND_SELECT_ANNOTATION) + .put("spliceAnnotations", COMMAND_SPLICE_ANNOTATIONS) + .build(); + } + + private void fireCallback(int callbackId, WritableArray args) { + WritableArray event = Arguments.createArray(); + event.pushInt(callbackId); + event.pushArray(args); + + _context.getJSModule(RCTNativeAppEventEmitter.class) + .emit("MapboxAndroidCallback", event); } - public void setCenterCoordinateZoomLevel(MapView view, @Nullable ReadableMap center) { - if (center != null) { - double latitude = center.getDouble("latitude"); - double longitude = center.getDouble("longitude"); - float zoom = (float)center.getDouble("zoom"); - CameraPosition cameraPosition = new CameraPosition.Builder() - .target(new LatLng(latitude, longitude)) - .zoom(zoom) - .build(); - view.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); - }else{ - Log.w(REACT_CLASS, "No CenterCoordinate provided"); + @Override + public void receiveCommand(ReactNativeMapboxGLView view, int commandId, @Nullable ReadableArray args) { + Assertions.assertNotNull(args); + switch (commandId) { + case COMMAND_GET_DIRECTION: + getDirection(view, args.getInt(0)); + break; + case COMMAND_GET_PITCH: + getPitch(view, args.getInt(0)); + break; + case COMMAND_GET_CENTER_COORDINATE_ZOOM_LEVEL: + getCenterCoordinateZoomLevel(view, args.getInt(0)); + break; + case COMMAND_GET_BOUNDS: + getBounds(view, args.getInt(0)); + break; + case COMMAND_EASE_TO: + easeTo(view, args.getMap(0), args.getBoolean(1), args.getInt(2)); + break; + case COMMAND_SET_VISIBLE_COORDINATE_BOUNDS: + setVisibleCoordinateBounds(view, + args.getDouble(0), args.getDouble(1), args.getDouble(2), args.getDouble(3), + args.getDouble(4), args.getDouble(5), args.getDouble(6), args.getDouble(7), + args.getBoolean(8) + ); + break; + case COMMAND_SELECT_ANNOTATION: + selectAnnotation(view, args.getString(0), args.getBoolean(1)); + break; + case COMMAND_SPLICE_ANNOTATIONS: + spliceAnnotations(view, args.getBoolean(0), args.getArray(1), args.getArray(2)); + break; + default: + throw new JSApplicationIllegalArgumentException("Invalid commandId " + commandId + " sent to " + getClass().getSimpleName()); } } - public void setVisibleCoordinateBounds(MapView view, @Nullable ReadableMap info) { - final LatLng sw = new LatLng(info.getDouble("latSW"), info.getDouble("lngSW")); - final LatLng ne = new LatLng(info.getDouble("latNE"), info.getDouble("lngNE")); - view.setVisibleCoordinateBounds(new CoordinateBounds(sw, ne), new RectF((float) info.getDouble("paddingLeft"), (float) info.getDouble("paddingTop"), (float) info.getDouble("paddingRight"), (float) info.getDouble("paddingBottom")), true); + // Getters + + private void getDirection(ReactNativeMapboxGLView view, int callbackId) { + WritableArray result = Arguments.createArray(); + result.pushDouble(view.getCameraPosition().bearing); + fireCallback(callbackId, result); } - public void removeAllAnnotations(MapView view, @Nullable Boolean placeHolder) { - view.removeAllAnnotations(); + private void getPitch(ReactNativeMapboxGLView view, int callbackId) { + WritableArray result = Arguments.createArray(); + result.pushDouble(view.getCameraPosition().tilt); + fireCallback(callbackId, result); } - public WritableMap getDirection(MapView view) { - WritableMap callbackDict = Arguments.createMap(); - callbackDict.putDouble("direction", view.getDirection()); - return callbackDict; + private void getCenterCoordinateZoomLevel(ReactNativeMapboxGLView view, int callbackId) { + CameraPosition camera = view.getCameraPosition(); + + WritableArray args = Arguments.createArray(); + WritableMap result = Arguments.createMap(); + result.putDouble("latitude", camera.target.getLatitude()); + result.putDouble("longitude", camera.target.getLongitude()); + result.putDouble("zoomLevel", camera.zoom); + args.pushMap(result); + + fireCallback(callbackId, args); } - public WritableMap getCenterCoordinateZoomLevel(MapView view) { - WritableMap callbackDict = Arguments.createMap(); - CameraPosition center = view.getCameraPosition(); - callbackDict.putDouble("latitude", center.target.getLatitude()); - callbackDict.putDouble("longitude", center.target.getLongitude()); - callbackDict.putDouble("zoomLevel", center.zoom); + private void getBounds(ReactNativeMapboxGLView view, int callbackId) { + LatLngBounds bounds = view.getBounds(); + + WritableArray args = Arguments.createArray(); + WritableArray result = Arguments.createArray(); + result.pushDouble(bounds.getLatSouth()); + result.pushDouble(bounds.getLonWest()); + result.pushDouble(bounds.getLatNorth()); + result.pushDouble(bounds.getLonEast()); + args.pushArray(result); - return callbackDict; + fireCallback(callbackId, args); } - public WritableMap getBounds(MapView view) { - WritableMap callbackDict = Arguments.createMap(); - int viewportWidth = view.getWidth(); - int viewportHeight = view.getHeight(); - if (viewportWidth > 0 && viewportHeight > 0) { - LatLng ne = view.fromScreenLocation(new PointF(viewportWidth, 0)); - LatLng sw = view.fromScreenLocation(new PointF(0, viewportHeight)); - callbackDict.putDouble("latNE", ne.getLatitude()); - callbackDict.putDouble("lngNE", ne.getLongitude()); - callbackDict.putDouble("latSW", sw.getLatitude()); - callbackDict.putDouble("lngSW", sw.getLongitude()); - } - return callbackDict; + // Setters + + private void easeTo(ReactNativeMapboxGLView view, ReadableMap updates, boolean animated, int callbackId) { + CameraPosition oldPosition = view.getCameraPosition(); + CameraPosition.Builder cameraBuilder = new CameraPosition.Builder(oldPosition); + + if (updates.hasKey("latitude") && updates.hasKey("longitude")) { + cameraBuilder.target(new LatLng(updates.getDouble("latitude"), updates.getDouble("longitude"))); + } + if (updates.hasKey("zoomLevel")) { + cameraBuilder.zoom(updates.getDouble("zoomLevel")); + } + if (updates.hasKey("direction")) { + cameraBuilder.bearing(updates.getDouble("direction")); + } + if (updates.hasKey("pitch")) { + cameraBuilder.tilt(updates.getDouble("pitch")); + } + + // I want lambdas :( + class CallbackRunnable implements Runnable { + int callbackId; + ReactNativeMapboxGLManager manager; + + CallbackRunnable(ReactNativeMapboxGLManager manager, int callbackId) { + this.callbackId = callbackId; + this.manager = manager; + } + + @Override + public void run() { + manager.fireCallback(callbackId, Arguments.createArray()); + } + } + + int duration = animated ? MapboxConstants.ANIMATION_DURATION : 0; + view.setCameraPosition(cameraBuilder.build(), duration, new CallbackRunnable(this, callbackId)); + } + + public void setCamera( + ReactNativeMapboxGLView view, + double latitude, double longitude, + double altitude, double pitch, double direction, + double duration) { + throw new JSApplicationIllegalArgumentException("MapView.setCamera() is not supported on Android. If you're trying to change pitch, use MapView.easeTo()"); + } + + public void setVisibleCoordinateBounds( + ReactNativeMapboxGLView view, + double latS, double lonW, double latN, double lonE, + double paddingTop, double paddingRight, double paddingBottom, double paddingLeft, + boolean animated) { + CameraUpdate update = CameraUpdateFactory.newLatLngBounds( + new LatLngBounds.Builder() + .include(new LatLng(latS, lonW)) + .include(new LatLng(latN, lonE)) + .build(), + (int) paddingLeft, + (int) paddingTop, + (int) paddingRight, + (int) paddingBottom + ); + view.setCameraUpdate(update, animated ? MapboxConstants.ANIMATION_DURATION : 0, null); + } + + // Annotations + + public void spliceAnnotations(ReactNativeMapboxGLView view, boolean removeAll, ReadableArray itemsToRemove, ReadableArray itemsToAdd) { + if (removeAll) { + view.removeAllAnnotations(); + } else { + int removeCount = itemsToRemove.size(); + for (int i = 0; i < removeCount; i++) { + view.removeAnnotation(itemsToRemove.getString(i)); + } + } + + int addCount = itemsToAdd.size(); + for (int i = 0; i < addCount; i++) { + ReadableMap annotation = itemsToAdd.getMap(i); + RNMGLAnnotationOptions annotationOptions = RNMGLAnnotationOptionsFactory.annotationOptionsFromJS(annotation, view.getContext()); + + String name = annotation.getString("id"); + view.setAnnotation(name, annotationOptions); + } } - public MapView getMapView() { - return mapView; + public void selectAnnotation(ReactNativeMapboxGLView view, String annotationId, boolean animated) { + view.selectAnnotation(annotationId, animated); } } diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLModule.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLModule.java index ef8a109ab..54f8d63ab 100644 --- a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLModule.java +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLModule.java @@ -2,22 +2,52 @@ package com.mapbox.reactnativemapboxgl; import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.support.annotation.MainThread; +import android.support.annotation.UiThread; import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import com.mapbox.mapboxsdk.constants.Style; +import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +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.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.modules.core.RCTNativeAppEventEmitter; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.mapbox.mapboxsdk.MapboxAccountManager; import com.mapbox.mapboxsdk.constants.MyLocationTracking; +import com.mapbox.mapboxsdk.constants.MyBearingTracking; +import com.mapbox.mapboxsdk.constants.Style; import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.mapboxsdk.views.MapView; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.offline.OfflineManager; +import com.mapbox.mapboxsdk.offline.OfflineRegion; +import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition; +import com.mapbox.mapboxsdk.offline.OfflineRegionError; +import com.mapbox.mapboxsdk.offline.OfflineRegionStatus; +import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition; +import com.mapbox.mapboxsdk.telemetry.MapboxEventManager; import javax.annotation.Nullable; @@ -25,12 +55,18 @@ public class ReactNativeMapboxGLModule extends ReactContextBaseJavaModule { private static final String TAG = ReactNativeMapboxGLModule.class.getSimpleName(); - private Context context; + private ReactApplicationContext context; private ReactNativeMapboxGLPackage aPackage; + Handler mainHandler; + private int throttleInterval = 300; - public ReactNativeMapboxGLModule(ReactApplicationContext reactContext) { + private static boolean initialized = false; + + public ReactNativeMapboxGLModule(ReactApplicationContext reactContext, ReactNativeMapboxGLPackage thePackage) { super(reactContext); + this.mainHandler = new Handler(reactContext.getApplicationContext().getMainLooper()); this.context = reactContext; + this.aPackage = thePackage; Log.d(TAG, "Context " + context); Log.d(TAG, "reactContext " + reactContext); } @@ -40,16 +76,40 @@ public String getName() { return "MapboxGLManager"; } + static private ArrayList serializeTracking(int locationTracking, int bearingTracking) { + ArrayList result = new ArrayList(); + result.add(locationTracking); + result.add(bearingTracking); + return result; + } + + public static final int[] locationTrackingModes = new int[] { + MyLocationTracking.TRACKING_NONE, + MyLocationTracking.TRACKING_FOLLOW, + MyLocationTracking.TRACKING_FOLLOW, + MyLocationTracking.TRACKING_FOLLOW + }; + + public static final int[] bearingTrackingModes = new int[] { + MyBearingTracking.NONE, + MyBearingTracking.NONE, + MyBearingTracking.GPS, + MyBearingTracking.COMPASS + }; + @Override public @Nullable Map getConstants() { HashMap constants = new HashMap(); HashMap userTrackingMode = new HashMap(); HashMap mapStyles = new HashMap(); + HashMap userLocationVerticalAlignment = new HashMap(); // User tracking constants - userTrackingMode.put("none", MyLocationTracking.TRACKING_NONE); - userTrackingMode.put("follow", MyLocationTracking.TRACKING_FOLLOW); + userTrackingMode.put("none", 0); + userTrackingMode.put("follow", 1); + userTrackingMode.put("followWithCourse", 2); + userTrackingMode.put("followWithHeading", 3); // Style constants mapStyles.put("light", Style.LIGHT); @@ -59,87 +119,395 @@ public String getName() { mapStyles.put("satellite", Style.SATELLITE); mapStyles.put("hybrid", Style.SATELLITE_STREETS); + // These need to be here for compatibility, even if they're not supported on Android + userLocationVerticalAlignment.put("center", 0); + userLocationVerticalAlignment.put("top", 1); + userLocationVerticalAlignment.put("bottom", 2); + + // Other constants + constants.put("unknownResourceCount", Long.MAX_VALUE); + constants.put("metricsEnabled", MapboxEventManager.getMapboxEventManager().isTelemetryEnabled()); + constants.put("userTrackingMode", userTrackingMode); constants.put("mapStyles", mapStyles); + constants.put("userLocationVerticalAlignment", userLocationVerticalAlignment); return constants; } + // Access Token + @ReactMethod - public void setDirectionAnimated(int mapRef, int direction) { - aPackage.getManager().setDirection(aPackage.getManager().getMapView(), direction); + public void setAccessToken(String accessToken) { + if (accessToken == null || accessToken.length() == 0 || accessToken.equals("your-mapbox.com-access-token")) { + throw new JSApplicationIllegalArgumentException("Invalid access token. Register to mapbox.com and request an access token, then pass it to setAccessToken()"); + } + if (initialized) { + String oldToken = MapboxAccountManager.getInstance().getAccessToken(); + if (!oldToken.equals(accessToken)) { + throw new JSApplicationIllegalArgumentException("Mapbox access token cannot be initialized twice with different values"); + } + return; + } + initialized = true; + MapboxAccountManager.start(context.getApplicationContext(), accessToken); + initializeOfflinePacks(); } + // Metrics + @ReactMethod - public void setCenterCoordinateAnimated(int mapRef, double latitude, double longitude) { - WritableMap location = Arguments.createMap(); - location.putDouble("latitude", latitude); - location.putDouble("longitude", longitude); - aPackage.getManager().setCenterCoordinate(aPackage.getManager().getMapView(), location); + public void setMetricsEnabled(boolean value) { + MapboxEventManager.getMapboxEventManager().setTelemetryEnabled(value); } - @ReactMethod - public void setCenterCoordinateZoomLevelAnimated(int mapRef, double latitude, double longitude, double zoom) { - WritableMap location = Arguments.createMap(); - location.putDouble("latitude", latitude); - location.putDouble("longitude", longitude); - location.putDouble("zoom", zoom); - aPackage.getManager().setCenterCoordinateZoomLevel(aPackage.getManager().getMapView(), location); + // Offline packs + + // Offline pack events and initialization + + class OfflineRegionProgressObserver implements OfflineRegion.OfflineRegionObserver { + ReactNativeMapboxGLModule module; + OfflineRegion region; + OfflineRegionStatus status; + String name; + boolean recentlyUpdated = false; + boolean throttled = true; + boolean invalid = false; + + OfflineRegionProgressObserver(ReactNativeMapboxGLModule module, OfflineRegion region, String name) { + this.module = module; + this.region = region; + if (name == null) { + this.name = getOfflineRegionName(region); + } else { + this.name = name; + } + } + + void fireUpdateEvent() { + if (invalid) { return; } + + recentlyUpdated = true; + WritableMap event = serializeOfflineRegionStatus(region, this.status); + module.getReactApplicationContext().getJSModule(RCTNativeAppEventEmitter.class) + .emit("MapboxOfflineProgressDidChange", event); + + module.mainHandler.postDelayed(new Runnable() { + @Override + public void run() { + recentlyUpdated = false; + if (throttled) { + throttled = false; + fireUpdateEvent(); + } + } + }, throttleInterval); + } + + @Override + public void onStatusChanged(OfflineRegionStatus status) { + if (invalid) { return; } + + this.status = status; + + if (!recentlyUpdated) { + fireUpdateEvent(); + } else { + throttled = true; + } + } + + @Override + public void onError(OfflineRegionError error) { + if (invalid) { return; } + + WritableMap event = Arguments.createMap(); + event.putString("name", getOfflineRegionName(region)); + event.putString("error", error.toString()); + + module.getReactApplicationContext().getJSModule(RCTNativeAppEventEmitter.class) + .emit("MapboxOfflineError", event); + } + + @Override + public void mapboxTileCountLimitExceeded(long limit) { + if (invalid) { return; } + + WritableMap event = Arguments.createMap(); + event.putString("name", getOfflineRegionName(region)); + event.putDouble("maxTiles", limit); + + module.getReactApplicationContext().getJSModule(RCTNativeAppEventEmitter.class) + .emit("MapboxOfflineMaxAllowedTiles", event); + } + + public void invalidate() { + invalid = true; + } } - @ReactMethod - public void addAnnotations(int mapRef, ReadableArray value) { - aPackage.getManager().setAnnotations(aPackage.getManager().getMapView(), value, false); + private int uninitializedObserverCount = -1; + private ArrayList offlinePackObservers = new ArrayList<>(); + private ArrayList offlinePackListingRequests = new ArrayList<>(); + + void flushListingRequests() { + WritableArray result = _getOfflinePacks(); + for (Promise promise : offlinePackListingRequests) { + promise.resolve(result); + } + offlinePackListingRequests.clear(); } - @ReactMethod - public void setUserTrackingMode(int mapRef, int mode) { - aPackage.getManager().setMyLocationTrackingMode(aPackage.getManager().getMapView(), mode); + class OfflineRegionsInitialRequest implements OfflineManager.ListOfflineRegionsCallback { + ReactNativeMapboxGLModule module; + + OfflineRegionsInitialRequest(ReactNativeMapboxGLModule module) { + this.module = module; + } + + @Override + public void onList(OfflineRegion[] offlineRegions) { + uninitializedObserverCount = offlineRegions.length; + for (OfflineRegion region : offlineRegions) { + final OfflineRegionProgressObserver observer = new OfflineRegionProgressObserver(module, region, null); + offlinePackObservers.add(observer); + region.setObserver(observer); + region.setDownloadState(OfflineRegion.STATE_ACTIVE); + region.getStatus(new OfflineRegion.OfflineRegionStatusCallback() { + @Override + public void onStatus(OfflineRegionStatus status) { + observer.onStatusChanged(status); + uninitializedObserverCount--; + if (uninitializedObserverCount == 0) { + flushListingRequests(); + } + } + @Override + public void onError(String error) { + Log.e(context.getApplicationContext().getPackageName(), error); + } + }); + } + } + + @Override + public void onError(String error) { + Log.e(module.getReactApplicationContext().getPackageName(), error); + } } - @ReactMethod - public void removeAllAnnotations(int mapRef) { - aPackage.getManager().removeAllAnnotations(aPackage.getManager().getMapView(), true); + void initializeOfflinePacks() { + final ReactNativeMapboxGLModule _this = this; + mainHandler.post(new Runnable() { + @Override + public void run() { + OfflineManager.getInstance(context.getApplicationContext()).listOfflineRegions( + new OfflineRegionsInitialRequest(_this) + ); + } + }); + } - @ReactMethod - public void setTilt(int mapRef, double pitch) { - aPackage.getManager().setTilt(aPackage.getManager().getMapView(), pitch); + // Offline pack utils + + static WritableMap serializeOfflineRegionStatus(OfflineRegion region, OfflineRegionStatus status) { + WritableMap result = Arguments.createMap(); + + try { + ByteArrayInputStream bis = new ByteArrayInputStream(region.getMetadata()); + ObjectInputStream ois = new ObjectInputStream(bis); + + result.putString("name", (String)ois.readObject()); + result.putString("metadata", (String)ois.readObject()); + + ois.close(); + } catch (Throwable e) { + e.printStackTrace(); + } + + result.putInt("countOfBytesCompleted", (int)status.getCompletedResourceSize()); + result.putInt("countOfResourcesCompleted", (int)status.getCompletedResourceCount()); + result.putInt("countOfResourcesExpected", (int)status.getRequiredResourceCount()); + result.putInt("maximumResourcesExpected", (int)status.getRequiredResourceCount()); + + return result; } - @ReactMethod - public void setVisibleCoordinateBoundsAnimated(int mapRef, double latSW, double lngSW,double latNE, double lngNE, float paddingTop, float paddingRight, float paddingBottom, float paddingLeft) { - WritableMap info = Arguments.createMap(); - info.putDouble("latSW", latSW); - info.putDouble("lngSW", lngSW); - info.putDouble("latNE", latNE); - info.putDouble("lngNE", lngNE); - info.putDouble("paddingTop", paddingTop); - info.putDouble("paddingRight", paddingRight); - info.putDouble("paddingBottom", paddingBottom); - info.putDouble("paddingLeft", paddingLeft); - aPackage.getManager().setVisibleCoordinateBounds(aPackage.getManager().getMapView(), info); + static String getOfflineRegionName(OfflineRegion region) { + try { + ByteArrayInputStream bis = new ByteArrayInputStream(region.getMetadata()); + ObjectInputStream ois = new ObjectInputStream(bis); + String name = (String)ois.readObject(); + ois.close(); + return name; + } catch (Throwable e) { + e.printStackTrace(); + return null; + } + } + + // Offline pack listing + + WritableArray _getOfflinePacks() { + WritableArray result = Arguments.createArray(); + for (OfflineRegionProgressObserver observer : offlinePackObservers) { + result.pushMap(serializeOfflineRegionStatus(observer.region, observer.status)); + } + return result; } @ReactMethod - public void getDirection(int mapRef, Callback successCallback) { - WritableMap direction = aPackage.getManager().getDirection(aPackage.getManager().getMapView()); - successCallback.invoke(direction); + public void getOfflinePacks(final Promise promise) { + mainHandler.post(new Runnable() { + @Override + public void run() { + promise.resolve(_getOfflinePacks()); + } + }); } + // Offline pack insertion + @ReactMethod - public void getCenterCoordinateZoomLevel(int mapRef, Callback successCallback) { - WritableMap location = aPackage.getManager().getCenterCoordinateZoomLevel(aPackage.getManager().getMapView()); - successCallback.invoke(location); + public void addOfflinePack(ReadableMap options, final Promise promise) { + if (!options.hasKey("name")) { + promise.reject(new JSApplicationIllegalArgumentException("addOfflinePack(): name is required.")); + return; + } + if (!options.hasKey("minZoomLevel")) { + promise.reject(new JSApplicationIllegalArgumentException("addOfflinePack(): minZoomLevel is required.")); + return; + } + if (!options.hasKey("maxZoomLevel")) { + promise.reject(new JSApplicationIllegalArgumentException("addOfflinePack(): maxZoomLevel is required.")); + return; + } + if (!options.hasKey("bounds")) { + promise.reject(new JSApplicationIllegalArgumentException("addOfflinePack(): bounds is required.")); + return; + } + if (!options.hasKey("styleURL")) { + promise.reject(new JSApplicationIllegalArgumentException("addOfflinePack(): styleURL is required.")); + return; + } + if (!options.hasKey("type")) { + promise.reject(new JSApplicationIllegalArgumentException("addOfflinePack(): type is required.")); + return; + } + if (!options.getString("type").equals("bbox")) { + promise.reject(new JSApplicationIllegalArgumentException("addOfflinePack(): Offline pack type " + + options.getString("type") + + " not supported. Only \"bbox\" is currently supported.")); + return; + } + + float pixelRatio = context.getResources().getDisplayMetrics().density; + pixelRatio = pixelRatio < 1.5f ? 1.0f : 2.0f; + + ReadableArray boundsArray = options.getArray("bounds"); + LatLngBounds bounds = new LatLngBounds.Builder() + .include(new LatLng(boundsArray.getDouble(0), boundsArray.getDouble(1))) + .include(new LatLng(boundsArray.getDouble(2), boundsArray.getDouble(3))) + .build(); + + final OfflineTilePyramidRegionDefinition regionDef = new OfflineTilePyramidRegionDefinition( + options.getString("styleURL"), + bounds, + options.getDouble("minZoomLevel"), + options.getDouble("maxZoomLevel"), + pixelRatio + ); + + byte [] metadata; + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(options.getString("name")); + oos.writeObject(options.getString("metadata")); + oos.close(); + metadata = bos.toByteArray(); + } catch (IOException e) { + promise.reject(e); + return; + } + + final ReactNativeMapboxGLModule _this = this; + final byte [] _metadata = metadata; + mainHandler.post(new Runnable() { + @Override + public void run() { + OfflineManager.getInstance(context.getApplicationContext()).createOfflineRegion( + regionDef, + _metadata, + new OfflineManager.CreateOfflineRegionCallback() { + @Override + public void onCreate(OfflineRegion offlineRegion) { + OfflineRegionProgressObserver observer = new OfflineRegionProgressObserver(_this, offlineRegion, null); + offlinePackObservers.add(observer); + offlineRegion.setObserver(observer); + offlineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE); + promise.resolve(null); + } + + @Override + public void onError(String error) { + promise.reject(new JSApplicationIllegalArgumentException(error)); + } + } + ); + } + }); } + // Offline pack removal + @ReactMethod - public void getBounds(int mapRef, Callback successCallback) { - WritableMap bounds = aPackage.getManager().getBounds(aPackage.getManager().getMapView()); - successCallback.invoke(bounds); + public void removeOfflinePack(final String packName, final Promise promise) { + mainHandler.post(new Runnable() { + @Override + public void run() { + OfflineRegionProgressObserver foundObserver = null; + + for (OfflineRegionProgressObserver observer : offlinePackObservers) { + if (packName.equals(observer.name)) { + foundObserver = observer; + break; + } + } + + if (foundObserver == null) { + promise.resolve(Arguments.createMap()); + return; + } + + offlinePackObservers.remove(foundObserver); + foundObserver.invalidate(); + foundObserver.region.setDownloadState(OfflineRegion.STATE_INACTIVE); + + final OfflineRegionProgressObserver _foundObserver = foundObserver; + foundObserver.region.delete(new OfflineRegion.OfflineRegionDeleteCallback() { + @Override + public void onDelete() { + WritableMap result = Arguments.createMap(); + result.putString("deleted", _foundObserver.name); + promise.resolve(result); + } + + @Override + public void onError(String error) { + promise.reject(new JSApplicationIllegalArgumentException(error)); + } + }); + } + }); } - public void setPackage(ReactNativeMapboxGLPackage aPackage) { - this.aPackage = aPackage; + // Offline throttle control + + @ReactMethod + public void setOfflinePackProgressThrottleInterval(int milis) { + throttleInterval = milis; } } \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLPackage.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLPackage.java index 4295eec27..d0618a999 100644 --- a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLPackage.java +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLPackage.java @@ -13,13 +13,10 @@ public class ReactNativeMapboxGLPackage implements ReactPackage { - private ReactNativeMapboxGLManager glManager; - @Override public List createNativeModules(ReactApplicationContext reactContext) { List modules = new ArrayList<>(); - ReactNativeMapboxGLModule module = new ReactNativeMapboxGLModule(reactContext); - module.setPackage(this); + ReactNativeMapboxGLModule module = new ReactNativeMapboxGLModule(reactContext, this); modules.add(module); return modules; } @@ -31,13 +28,8 @@ public List> createJSModules() { @Override public List createViewManagers(ReactApplicationContext reactContext) { - glManager = new ReactNativeMapboxGLManager(); return Arrays.asList( - glManager + new ReactNativeMapboxGLManager(reactContext) ); } - - public ReactNativeMapboxGLManager getManager() { - return glManager; - } } \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLView.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLView.java new file mode 100644 index 000000000..788a93355 --- /dev/null +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLView.java @@ -0,0 +1,669 @@ +package com.mapbox.reactnativemapboxgl; + +import android.content.Context; +import android.graphics.PointF; +import android.hardware.GeomagneticField; +import android.location.Location; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; +import android.view.View; +import android.widget.RelativeLayout; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.MarkerOptions; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; +import com.mapbox.mapboxsdk.annotations.PolylineOptions; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdate; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.MapboxMapOptions; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; +import com.mapbox.mapboxsdk.maps.UiSettings; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; + +public class ReactNativeMapboxGLView extends RelativeLayout implements + OnMapReadyCallback, LifecycleEventListener, + MapboxMap.OnMapClickListener, MapboxMap.OnMapLongClickListener, + MapboxMap.OnMyBearingTrackingModeChangeListener, MapboxMap.OnMyLocationTrackingModeChangeListener, + MapboxMap.OnMyLocationChangeListener, + MapboxMap.OnMarkerClickListener, MapboxMap.OnInfoWindowClickListener, + MapView.OnMapChangedListener +{ + + private MapboxMap _map = null; + private MapView _mapView = null; + private ReactNativeMapboxGLManager _manager; + private boolean _paused = false; + + private CameraPosition.Builder _initialCamera = new CameraPosition.Builder(); + private MapboxMapOptions _mapOptions; + private int _locationTrackingMode; + private int _bearingTrackingMode; + private boolean _trackingModeUpdateScheduled = false; + private boolean _showsUserLocation; + private boolean _zoomEnabled = true; + private boolean _scrollEnabled = true; + private boolean _rotateEnabled = true; + private boolean _enableOnRegionWillChange = false; + private boolean _enableOnRegionDidChange = false; + private int _paddingTop, _paddingRight, _paddingBottom, _paddingLeft; + + private boolean _recentlyChanged = false; + private boolean _willChangeThrottled = false; + private boolean _didChangeThrottled = false; + private boolean _changeWasAnimated = false; + + private Map _annotations = new HashMap(); + private Map _annotationIdsToName = new HashMap(); + private Map _annotationOptions = new HashMap(); + + private android.os.Handler _handler; + + @UiThread + public ReactNativeMapboxGLView(Context context, ReactNativeMapboxGLManager manager) { + super(context); + _handler = new android.os.Handler(); + _manager = manager; + _mapOptions = MapboxMapOptions.createFromAttributes(context, null); + _mapOptions.zoomGesturesEnabled(true); + _mapOptions.rotateGesturesEnabled(true); + _mapOptions.scrollGesturesEnabled(true); + _mapOptions.tiltGesturesEnabled(true); + } + + // Lifecycle methods + + public void onAfterUpdateTransaction() { + if (_mapView != null) { return; } + setupMapView(); + _paused = false; + _mapView.onResume(); + _manager.getContext().addLifecycleEventListener(this); + } + + public void onDrop() { + if (_mapView == null) { return; } + _manager.getContext().removeLifecycleEventListener(this); + if (!_paused) { + _paused = true; + _mapView.onPause(); + } + destroyMapView(); + _mapView = null; + } + + @Override + public void onHostResume() { + _paused = false; + _mapView.onResume(); + } + + @Override + public void onHostPause() { + _paused = true; + _mapView.onPause(); + } + + @Override + public void onHostDestroy() { + onDrop(); + } + + // Initialization + + private void setupMapView() { + _mapOptions.camera(_initialCamera.build()); + _mapView = new MapView(this.getContext(), _mapOptions); + this.addView(_mapView); + _mapView.addOnMapChangedListener(this); + _mapView.onCreate(null); + _mapView.getMapAsync(this); + } + + @Override + public void onMapReady(MapboxMap mapboxMap) { + if (_mapView == null) { return; } + _map = mapboxMap; + + // Configure map + _map.setMyLocationEnabled(_showsUserLocation); + _map.getTrackingSettings().setMyLocationTrackingMode(_locationTrackingMode); + _map.getTrackingSettings().setMyBearingTrackingMode(_bearingTrackingMode); + _map.setPadding(_paddingLeft, _paddingTop, _paddingRight, _paddingBottom); + UiSettings uiSettings = _map.getUiSettings(); + uiSettings.setZoomGesturesEnabled(_zoomEnabled); + uiSettings.setScrollGesturesEnabled(_scrollEnabled); + uiSettings.setRotateGesturesEnabled(_rotateEnabled); + + // If these settings changed between setupMapView() and onMapReady(), coerce them to their right values + // This doesn't happen in the current implementation of MapView, but let's be future proof + if (_map.isDebugActive() != _mapOptions.getDebugActive()) { + _map.setDebugActive(_mapOptions.getDebugActive()); + } + if (!_map.getStyleUrl().equals(_mapOptions.getStyle())) { + _map.setStyleUrl(_mapOptions.getStyle()); + } + if (uiSettings.isLogoEnabled() != _mapOptions.getLogoEnabled()) { + uiSettings.setLogoEnabled(_mapOptions.getLogoEnabled()); + } + if (uiSettings.isAttributionEnabled() != _mapOptions.getAttributionEnabled()) { + uiSettings.setAttributionEnabled(_mapOptions.getAttributionEnabled()); + } + if (uiSettings.isCompassEnabled() != _mapOptions.getCompassEnabled()) { + uiSettings.setCompassEnabled(_mapOptions.getCompassEnabled()); + } + + // Attach listeners + _map.setOnMapClickListener(this); + _map.setOnMapLongClickListener(this); + _map.setOnMyLocationTrackingModeChangeListener(this); + _map.setOnMyBearingTrackingModeChangeListener(this); + _map.setOnMyLocationChangeListener(this); + _map.setOnMarkerClickListener(this); + _map.setOnInfoWindowClickListener(this); + + // Create annotations + for (Map.Entry entry : _annotationOptions.entrySet()) { + Annotation annotation = entry.getValue().addToMap(_map); + _annotations.put(entry.getKey(), annotation); + _annotationIdsToName.put(annotation.getId(), entry.getKey()); + } + _annotationOptions.clear(); + } + + private void destroyMapView() { + _mapView.removeOnMapChangedListener(this); + if (_map != null) { + _map.setOnMapClickListener(null); + _map.setOnMapLongClickListener(null); + _map.setOnMyLocationTrackingModeChangeListener(null); + _map.setOnMyBearingTrackingModeChangeListener(null); + _map.setOnMyLocationChangeListener(null); + _map.setOnMarkerClickListener(null); + _map.setOnInfoWindowClickListener(null); + _map = null; + } + _mapView.onDestroy(); + } + + // Props + + public void setInitialZoomLevel(double value) { + _initialCamera.zoom(value); + } + + public void setInitialDirection(double value) { + _initialCamera.bearing(value); + } + + public void setInitialCenterCoordinate(double lat, double lon) { + _initialCamera.target(new LatLng(lat, lon)); + } + + public void setEnableOnRegionDidChange(boolean value) { + _enableOnRegionDidChange = value; + } + + public void setEnableOnRegionWillChange(boolean value) { + _enableOnRegionWillChange = value; + } + + public void setShowsUserLocation(boolean value) { + if (_showsUserLocation == value) { return; } + _showsUserLocation = value; + if (_map != null) { _map.setMyLocationEnabled(value); } + } + + public void setRotateEnabled(boolean value) { + if (_rotateEnabled == value) { return; } + _rotateEnabled = value; + if (_map != null) { + _map.getUiSettings().setRotateGesturesEnabled(value); + } + } + + public void setScrollEnabled(boolean value) { + if (_scrollEnabled == value) { return; } + _scrollEnabled = value; + if (_map != null) { + _map.getUiSettings().setScrollGesturesEnabled(value); + } + } + + public void setZoomEnabled(boolean value) { + if (_zoomEnabled == value) { return; } + _zoomEnabled = value; + if (_map != null) { + _map.getUiSettings().setZoomGesturesEnabled(value); + } + } + + public void setStyleURL(String styleURL) { + if (styleURL.equals(_mapOptions.getStyle())) { return; } + _mapOptions.styleUrl(styleURL); + if (_map != null) { _map.setStyleUrl(styleURL); } + } + + public void setDebugActive(boolean value) { + if (_mapOptions.getDebugActive() == value) { return; } + _mapOptions.debugActive(value); + if (_map != null) { _map.setDebugActive(value); }; + } + + public void setLocationTracking(int value) { + if (_locationTrackingMode == value) { return; } + _locationTrackingMode = value; + if (_map != null) { _map.getTrackingSettings().setMyLocationTrackingMode(value); }; + } + + public void setBearingTracking(int value) { + if (_bearingTrackingMode == value) { return; } + _bearingTrackingMode = value; + if (_map != null) { _map.getTrackingSettings().setMyBearingTrackingMode(value); }; + } + + public void setAttributionButtonIsHidden(boolean value) { + if (_mapOptions.getAttributionEnabled() == !value) { return; } + _mapOptions.attributionEnabled(!value); + if (_map != null) { + _map.getUiSettings().setAttributionEnabled(!value); + } + } + + public void setLogoIsHidden(boolean value) { + if (_mapOptions.getLogoEnabled() == !value) { return; } + _mapOptions.logoEnabled(!value); + if (_map != null) { + _map.getUiSettings().setLogoEnabled(!value); + } + } + + public void setCompassIsHidden(boolean value) { + if (_mapOptions.getCompassEnabled() == !value) { return; } + _mapOptions.compassEnabled(!value); + if (_map != null) { + _map.getUiSettings().setCompassEnabled(!value); + } + } + + public void setContentInset(int top, int right, int bottom, int left) { + if (top == _paddingTop && + bottom == _paddingBottom && + left == _paddingLeft && + right == _paddingRight) { return; } + _paddingTop = top; + _paddingRight = right; + _paddingBottom = bottom; + _paddingLeft = left; + if (_map != null) { _map.setPadding(left, top, right, bottom); } + } + + // Events + + void emitEvent(String name, @Nullable WritableMap event) { + if (event == null) { + event = Arguments.createMap(); + } + ((ReactContext)getContext()) + .getJSModule(RCTEventEmitter.class) + .receiveEvent(getId(), name, event); + } + + WritableMap serializePoint(LatLng point) { + PointF screenCoords = _map.getProjection().toScreenLocation(point); + + WritableMap event = Arguments.createMap(); + WritableMap src = Arguments.createMap(); + src.putDouble("latitude", point.getLatitude()); + src.putDouble("longitude", point.getLongitude()); + src.putDouble("screenCoordX", screenCoords.x); + src.putDouble("screenCoordY", screenCoords.y); + event.putMap("src", src); + return event; + } + + @Override + public void onMapClick(LatLng point) { + emitEvent("onTap", serializePoint(point)); + } + + @Override + public void onMapLongClick(@NonNull LatLng point) { + emitEvent("onLongPress", serializePoint(point)); + } + + @Override + public void onMyLocationChange(@Nullable Location location) { + WritableMap event = Arguments.createMap(); + WritableMap src = Arguments.createMap(); + + if (location == null) { + src.putString("message", "Could not get user location"); + event.putMap("src", src); + emitEvent("onLocateUserFailed", event); + return; + } + + src.putDouble("latitude", location.getLatitude()); + src.putDouble("longitude", location.getLongitude()); + + if (location.hasAccuracy()) { + src.putDouble("verticalAccuracy", location.getAccuracy()); + src.putDouble("horizontalAccuracy", location.getAccuracy()); + } + + GeomagneticField geoField = new GeomagneticField( + (float)location.getLatitude(), + (float)location.getLongitude(), + location.hasAltitude() ? (float)location.getAltitude() : 0.0f, + System.currentTimeMillis() + ); + + src.putDouble("magneticHeading", location.getBearing()); + src.putDouble("trueHeading", location.getBearing() + geoField.getDeclination()); + + event.putMap("src", src); + emitEvent("onUpdateUserLocation", event); + } + + class TrackingModeChangeRunnable implements Runnable { + ReactNativeMapboxGLView target; + TrackingModeChangeRunnable(ReactNativeMapboxGLView target) { + this.target = target; + } + @Override + public void run() { + target.onTrackingModeChange(); + } + } + + public void onTrackingModeChange() { + if (!_trackingModeUpdateScheduled) { return; } + _trackingModeUpdateScheduled = false; + + for (int mode = 0; mode < ReactNativeMapboxGLModule.locationTrackingModes.length; mode++) { + if (_locationTrackingMode == ReactNativeMapboxGLModule.locationTrackingModes[mode] && + _bearingTrackingMode == ReactNativeMapboxGLModule.bearingTrackingModes[mode]) { + WritableMap event = Arguments.createMap(); + event.putInt("src", mode); + emitEvent("onChangeUserTrackingMode", event); + break; + } + } + } + + @Override + @UiThread + public void onMyBearingTrackingModeChange(int myBearingTrackingMode) { + if (_bearingTrackingMode == myBearingTrackingMode) { return; } + _bearingTrackingMode = myBearingTrackingMode; + _trackingModeUpdateScheduled = true; + _handler.post(new TrackingModeChangeRunnable(this)); + + } + + @Override + @UiThread + public void onMyLocationTrackingModeChange(int myLocationTrackingMode) { + if (_locationTrackingMode == myLocationTrackingMode) { return; } + _locationTrackingMode = myLocationTrackingMode; + _trackingModeUpdateScheduled = true; + _handler.post(new TrackingModeChangeRunnable(this)); + } + + WritableMap serializeCurrentRegion(boolean animated) { + CameraPosition camera = _map == null + ? _initialCamera.build() + : _map.getCameraPosition(); + + WritableMap event = Arguments.createMap(); + WritableMap src = Arguments.createMap(); + src.putDouble("longitude", camera.target.getLongitude()); + src.putDouble("latitude", camera.target.getLatitude()); + src.putDouble("zoomLevel", camera.zoom); + src.putDouble("direction", camera.bearing); + src.putDouble("pitch", camera.tilt); + src.putBoolean("animated", animated); + event.putMap("src", src); + return event; + } + + class RegionChangedThrottleRunnable implements Runnable { + ReactNativeMapboxGLView target; + RegionChangedThrottleRunnable(ReactNativeMapboxGLView target) { + this.target = target; + } + @Override + public void run() { + target.flushRegionChangedThrottle(true); + } + } + + private void flushRegionChangedThrottle(boolean fireAgain) { + _recentlyChanged = false; + if (_willChangeThrottled) { + emitEvent("onRegionWillChange", serializeCurrentRegion(_changeWasAnimated)); + } + if (_didChangeThrottled) { + emitEvent("onRegionDidChange", serializeCurrentRegion(_changeWasAnimated)); + } + + if (fireAgain && _didChangeThrottled) { + _recentlyChanged = true; + _handler.postDelayed(new RegionChangedThrottleRunnable(this), 100); + } + _willChangeThrottled = false; + _didChangeThrottled = false; + } + + private void onRegionWillChange(boolean animated) { + if (animated) { + flushRegionChangedThrottle(false); + } + + if (_recentlyChanged) { + _willChangeThrottled = true; + _changeWasAnimated = animated; + } else { + emitEvent("onRegionWillChange", serializeCurrentRegion(animated)); + } + } + + private void onRegionDidChange(boolean animated) { + if (animated) { + flushRegionChangedThrottle(false); + } + + if (_recentlyChanged) { + _didChangeThrottled = true; + _changeWasAnimated = animated; + } else { + emitEvent("onRegionDidChange", serializeCurrentRegion(animated)); + _recentlyChanged = true; + _handler.postDelayed(new RegionChangedThrottleRunnable(this), 100); + } + } + + @Override + public void onMapChanged(int change) { + switch (change) { + case MapView.REGION_WILL_CHANGE: + case MapView.REGION_WILL_CHANGE_ANIMATED: + if (_enableOnRegionWillChange) { + onRegionWillChange(change == MapView.REGION_WILL_CHANGE_ANIMATED); + } + break; + case MapView.REGION_DID_CHANGE: + case MapView.REGION_DID_CHANGE_ANIMATED: + if (_enableOnRegionDidChange) { + onRegionDidChange(change == MapView.REGION_DID_CHANGE_ANIMATED); + } + break; + case MapView.WILL_START_LOADING_MAP: + emitEvent("onStartLoadingMap", null); + break; + case MapView.DID_FINISH_LOADING_MAP: + emitEvent("onFinishLoadingMap", null); + break; + } + } + + WritableMap serializeMarker(Marker marker) { + WritableMap event = Arguments.createMap(); + WritableMap src = Arguments.createMap(); + + src.putString("id", _annotationIdsToName.get(marker.getId())); + src.putDouble("longitude", marker.getPosition().getLongitude()); + src.putDouble("latitude", marker.getPosition().getLatitude()); + src.putString("title", marker.getTitle()); + src.putString("subtitle", marker.getSnippet()); + + event.putMap("src", src); + return event; + } + + @Override + public boolean onInfoWindowClick(@NonNull Marker marker) { + emitEvent("onRightAnnotationTapped", serializeMarker(marker)); + return false; + } + + @Override + public boolean onMarkerClick(@NonNull Marker marker) { + emitEvent("onOpenAnnotation", serializeMarker(marker)); + + // Due to a bug, we need to force a relayout on the _mapView + _handler.post(new Runnable() { + @Override + public void run() { + _mapView.measure( + View.MeasureSpec.makeMeasureSpec(_mapView.getMeasuredWidth(), View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(_mapView.getMeasuredHeight(), View.MeasureSpec.EXACTLY)); + _mapView.layout(_mapView.getLeft(), _mapView.getTop(), _mapView.getRight(), _mapView.getBottom()); + } + }); + + return false; + } + + // Getters + + public CameraPosition getCameraPosition() { + if (_map == null) { return _initialCamera.build(); } + return _map.getCameraPosition(); + } + + public LatLngBounds getBounds() { + if (_map == null) { return new LatLngBounds.Builder().build(); }; + return _map.getProjection().getVisibleRegion().latLngBounds; + } + + // Camera setters + + public void setCameraPosition(CameraPosition position, int duration, @Nullable Runnable callback) { + if (_map == null) { + _initialCamera = new CameraPosition.Builder(position); + if (callback != null) { callback.run(); } + return; + } + + CameraUpdate update = CameraUpdateFactory.newCameraPosition(position); + setCameraUpdate(update, duration, callback); + } + + public void setCameraUpdate(CameraUpdate update, int duration, @Nullable Runnable callback) { + if (_map == null) { + return; + } + + if (duration == 0) { + _map.moveCamera(update); + if (callback != null) { callback.run(); } + } else { + // Ugh... Java callbacks suck + class CameraCallback implements MapboxMap.CancelableCallback { + Runnable callback; + CameraCallback(Runnable callback) { + this.callback = callback; + } + @Override + public void onCancel() { + if (callback != null) { callback.run(); } + } + + @Override + public void onFinish() { + if (callback != null) { callback.run(); } + } + } + + _map.animateCamera(update, duration, new CameraCallback(callback)); + } + } + + // Annotations + + @Nullable Annotation _removeAnnotation(String name, boolean keep) { + if (_map == null) { + _annotationOptions.remove(name); + return null; + } + Annotation annotation = _annotations.remove(name); + if (annotation == null) { return null; } + _annotationIdsToName.remove(annotation.getId()); + + if (keep) { return annotation; } + _map.removeAnnotation(annotation); + return null; + } + + public void removeAnnotation(String name) { + _removeAnnotation(name, false); + } + + public void removeAllAnnotations() { + _annotationOptions.clear(); + _annotations.clear(); + _annotationIdsToName.clear(); + if (_map != null) { + _map.removeAnnotations(); + } + } + + public void setAnnotation(String name, RNMGLAnnotationOptions options) { + Annotation removed = _removeAnnotation(name, true); + + if (_map == null) { + _annotationOptions.put(name, options); + } else { + Annotation annotation = options.addToMap(_map); + _annotations.put(name, annotation); + _annotationIdsToName.put(annotation.getId(), name); + } + + if (removed != null) { _map.removeAnnotation(removed); } + } + + public void selectAnnotation(String name, boolean animated) { + if (_map == null) { return; } + Annotation annotation = _annotations.get(name); + if (annotation == null) { return; } + if (!(annotation instanceof Marker)) { return; } + Marker marker = (Marker)annotation; + _map.selectMarker(marker); + } +} diff --git a/example.js b/example.js new file mode 100644 index 000000000..809145817 --- /dev/null +++ b/example.js @@ -0,0 +1,310 @@ +'use strict'; + +import React, { Component } from 'react'; +import Mapbox, { MapView } from 'react-native-mapbox-gl'; +import { + AppRegistry, + StyleSheet, + Text, + StatusBar, + View, + ScrollView +} from 'react-native'; + +const accessToken = 'your-mapbox.com-access-token'; +Mapbox.setAccessToken(accessToken); + +class MapExample extends Component { + state = { + center: { + latitude: 40.72052634, + longitude: -73.97686958312988 + }, + zoom: 11, + userTrackingMode: Mapbox.userTrackingMode.none, + annotations: [{ + coordinates: [40.72052634, -73.97686958312988], + type: 'point', + title: 'This is marker 1', + subtitle: 'It has a rightCalloutAccessory too', + rightCalloutAccessory: { + source: { uri: 'https://cldup.com/9Lp0EaBw5s.png' }, + height: 25, + width: 25 + }, + annotationImage: { + source: { uri: 'https://cldup.com/CnRLZem9k9.png' }, + height: 25, + width: 25 + }, + id: 'marker1' + }, { + coordinates: [40.714541341726175,-74.00579452514648], + type: 'point', + title: 'Important!', + subtitle: 'Neat, this is a custom annotation image', + annotationImage: { + source: { uri: 'https://cldup.com/7NLZklp8zS.png' }, + height: 25, + width: 25 + }, + id: 'marker2' + }, { + coordinates: [[40.76572150042782,-73.99429321289062],[40.743485405490695, -74.00218963623047],[40.728266950429735,-74.00218963623047],[40.728266950429735,-73.99154663085938],[40.73633186448861,-73.98983001708984],[40.74465591168391,-73.98914337158203],[40.749337730454826,-73.9870834350586]], + type: 'polyline', + strokeColor: '#00FB00', + strokeWidth: 4, + strokeAlpha: .5, + id: 'foobar' + }, { + coordinates: [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], + type: 'polygon', + fillAlpha: 1, + strokeColor: '#ffffff', + fillColor: '#0000ff', + id: 'zap' + }] + }; + + onRegionDidChange = (location) => { + this.setState({ currentZoom: location.zoomLevel }); + console.log('onRegionDidChange', location); + }; + onRegionWillChange = (location) => { + console.log('onRegionWillChange', location); + }; + onUpdateUserLocation = (location) => { + console.log('onUpdateUserLocation', location); + }; + onOpenAnnotation = (annotation) => { + console.log('onOpenAnnotation', annotation); + }; + onRightAnnotationTapped = (e) => { + console.log('onRightAnnotationTapped', e); + }; + onLongPress = (location) => { + console.log('onLongPress', location); + }; + onTap = (location) => { + console.log('onTap', location); + }; + onChangeUserTrackingMode = (userTrackingMode) => { + this.setState({ userTrackingMode }); + console.log('onChangeUserTrackingMode', userTrackingMode); + }; + + componentWillMount() { + this._offlineProgressSubscription = Mapbox.addOfflinePackProgressListener(progress => { + console.log('offline pack progress', progress); + }); + this._offlineMaxTilesSubscription = Mapbox.addOfflineMaxAllowedTilesListener(tiles => { + console.log('offline max allowed tiles', tiles); + }); + this._offlineErrorSubscription = Mapbox.addOfflineErrorListener(error => { + console.log('offline error', error); + }); + }; + + componentWillUnmount() { + this._offlineProgressSubscription.remove(); + this._offlineMaxTilesSubscription.remove(); + this._offlineErrorSubscription.remove(); + } + + addNewMarkers = () => { + // Treat annotations as immutable and create a new one instead of using .push() + this.setState({ + annotations: [ ...this.state.annotations, { + coordinates: [40.73312,-73.989], + type: 'point', + title: 'This is a new marker', + id: 'foo' + }, { + 'coordinates': [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], + 'type': 'polygon', + 'fillAlpha': 1, + 'fillColor': '#000000', + 'strokeAlpha': 1, + 'id': 'new-black-polygon' + }] + }); + }; + + updateMarker2 = () => { + // Treat annotations as immutable and use .map() instead of changing the array + this.setState({ + annotations: this.state.annotations.map(annotation => { + if (annotation.id !== 'marker2') { return annotation; } + return { + coordinates: [40.714541341726175,-74.00579452514648], + 'type': 'point', + title: 'New Title!', + subtitle: 'New Subtitle', + annotationImage: { + source: { uri: 'https://cldup.com/7NLZklp8zS.png' }, + height: 25, + width: 25 + }, + id: 'marker2' + }; + }) + }); + }; + + removeMarker2 = () => { + this.setState({ + annotations: this.state.annotations.filter(a => a.id !== 'marker2') + }); + }; + + render() { + StatusBar.setHidden(true); + return ( + + { this._map = map; }} + style={styles.map} + initialCenterCoordinate={this.state.center} + initialZoomLevel={this.state.zoom} + initialDirection={0} + rotateEnabled={true} + scrollEnabled={true} + zoomEnabled={true} + showsUserLocation={false} + styleURL={Mapbox.mapStyles.dark} + userTrackingMode={this.state.userTrackingMode} + annotations={this.state.annotations} + annotationsAreImmutable + onChangeUserTrackingMode={this.onChangeUserTrackingMode} + onRegionDidChange={this.onRegionDidChange} + onRegionWillChange={this.onRegionWillChange} + onOpenAnnotation={this.onOpenAnnotation} + onRightAnnotationTapped={this.onRightAnnotationTapped} + onUpdateUserLocation={this.onUpdateUserLocation} + onLongPress={this.onLongPress} + onTap={this.onTap} + /> + + {this._renderButtons()} + + + ); + } + + _renderButtons() { + return ( + + this._map && this._map.setDirection(0)}> + Set direction to 0 + + this._map && this._map.setZoomLevel(6)}> + Zoom out to zoom level 6 + + this._map && this._map.setCenterCoordinate(48.8589, 2.3447)}> + Go to Paris at current zoom level {parseInt(this.state.currentZoom)} + + this._map && this._map.setCenterCoordinateZoomLevel(35.68829, 139.77492, 14)}> + Go to Tokyo at fixed zoom level 14 + + this._map && this._map.easeTo({ pitch: 30 })}> + Set pitch to 30 degrees + + + Add new marker + + + Update marker2 + + this._map && this._map.selectAnnotation('marker1')}> + Open marker1 popup + + + Remove marker2 annotation + + this.setState({ annotations: [] })}> + Remove all annotations + + this._map && this._map.setVisibleCoordinateBounds(40.712, -74.227, 40.774, -74.125, 100, 0, 0, 0)}> + Set visible bounds to 40.7, -74.2, 40.7, -74.1 + + this.setState({ userTrackingMode: Mapbox.userTrackingMode.followWithHeading })}> + Set userTrackingMode to followWithHeading + + this._map && this._map.getCenterCoordinateZoomLevel((location)=> { + console.log(location); + })}> + Get location + + this._map && this._map.getDirection((direction)=> { + console.log(direction); + })}> + Get direction + + this._map && this._map.getBounds((bounds)=> { + console.log(bounds); + })}> + Get bounds + + { + Mapbox.addOfflinePack({ + name: 'test', + type: 'bbox', + bounds: [0, 0, 0, 0], + minZoomLevel: 0, + maxZoomLevel: 0, + metadata: { anyValue: 'you wish' }, + styleURL: Mapbox.mapStyles.dark + }).then(() => { + console.log('Offline pack added'); + }).catch(err => { + console.log(err); + }); + }}> + Create offline pack + + { + Mapbox.getOfflinePacks() + .then(packs => { + console.log(packs); + }) + .catch(err => { + console.log(err); + }); + }}> + Get offline packs + + { + Mapbox.removeOfflinePack('test') + .then(info => { + if (info.deleted) { + console.log('Deleted', info.deleted); + } else { + console.log('No packs to delete'); + } + }) + .catch(err => { + console.log(err); + }); + }}> + Remove pack with name 'test' + + User tracking mode is {this.state.userTrackingMode} + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'stretch' + }, + map: { + flex: 1 + }, + scrollView: { + flex: 1 + } +}); + +AppRegistry.registerComponent('YourAppName', () => MapExample); diff --git a/index.android.js b/index.android.js deleted file mode 100644 index 9c3deaee1..000000000 --- a/index.android.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict' - -var React = require('react'); -var { PropTypes } = React; -var ReactNative = require('react-native'); -var { NativeModules, requireNativeComponent, findNodeHandle } = ReactNative; -var { MapboxGLManager } = NativeModules; - -var MapMixins = { - setDirectionAnimated(mapRef, heading) { - MapboxGLManager.setDirectionAnimated(findNodeHandle(this.refs[mapRef]), heading); - }, - setZoomLevelAnimated(mapRef, zoomLevel) { - MapboxGLManager.setZoomLevelAnimated(findNodeHandle(this.refs[mapRef]), zoomLevel); - }, - setCenterCoordinateAnimated(mapRef, latitude, longitude) { - MapboxGLManager.setCenterCoordinateAnimated(findNodeHandle(this.refs[mapRef]), latitude, longitude); - }, - setCenterCoordinateZoomLevelAnimated(mapRef, latitude, longitude, zoomLevel) { - MapboxGLManager.setCenterCoordinateZoomLevelAnimated(findNodeHandle(this.refs[mapRef]), latitude, longitude, zoomLevel); - }, - addAnnotations(mapRef, annotations) { - MapboxGLManager.addAnnotations(findNodeHandle(this.refs[mapRef]), annotations); - }, - selectAnnotationAnimated(mapRef, selectedIdentifier) { - MapboxGLManager.selectAnnotationAnimated(findNodeHandle(this.refs[mapRef]), selectedIdentifier); - }, - removeAnnotation(mapRef, selectedIdentifier) { - MapboxGLManager.removeAnnotation(findNodeHandle(this.refs[mapRef]), selectedIdentifier); - }, - removeAllAnnotations(mapRef) { - MapboxGLManager.removeAllAnnotations(findNodeHandle(this.refs[mapRef])); - }, - setVisibleCoordinateBoundsAnimated(mapRef, latitudeSW, longitudeSW, latitudeNE, longitudeNE, paddingTop, paddingRight, paddingBottom, paddingLeft) { - MapboxGLManager.setVisibleCoordinateBoundsAnimated(findNodeHandle(this.refs[mapRef]), latitudeSW, longitudeSW, latitudeNE, longitudeNE, paddingTop, paddingRight, paddingBottom, paddingLeft); - }, - setUserTrackingMode(mapRef, userTrackingMode) { - MapboxGLManager.setUserTrackingMode(findNodeHandle(this.refs[mapRef]), userTrackingMode); - }, - setTilt(mapRef, tilt) { - MapboxGLManager.setTilt(findNodeHandle(this.refs[mapRef]), tilt); - }, - getCenterCoordinateZoomLevel(mapRef, callback) { - MapboxGLManager.getCenterCoordinateZoomLevel(findNodeHandle(this.refs[mapRef]), callback); - }, - getDirection(mapRef, callback) {; - MapboxGLManager.getDirection(findNodeHandle(this.refs[mapRef]), callback); - }, - getBounds(mapRef, callback) { - MapboxGLManager.getBounds(findNodeHandle(this.refs[mapRef]), callback); - }, - mapStyles: MapboxGLManager.mapStyles, - userTrackingMode: MapboxGLManager.userTrackingMode -}; - -var ReactMapViewWrapper = React.createClass({ - statics: { - Mixin: MapMixins - }, - getDefaultProps() { - return { - centerCoordinate: { - latitude: 0, - longitude: 0 - }, - debugActive: false, - direction: 0, - rotateEnabled: true, - scrollEnabled: true, - showsUserLocation: false, - styleURL: MapboxGLManager.mapStyles.streets, - userTrackingMode: MapboxGLManager.userTrackingMode.none, - zoomEnabled: true, - zoomLevel: 0, - tilt: 0, - compassIsHidden: false - }; - }, - propTypes: { - accessToken: PropTypes.string.isRequired, - attributionButtonIsHidden: PropTypes.bool, - logoIsHidden: PropTypes.bool, - annotations: PropTypes.arrayOf(PropTypes.shape({ - title: PropTypes.string, - subtitle: PropTypes.string, - coordinates: PropTypes.array.isRequired, - alpha: PropTypes.number, - fillColor: PropTypes.string, - strokeColor: PropTypes.string, - strokeWidth: PropTypes.number - })), - centerCoordinate: PropTypes.shape({ - latitude: PropTypes.number.isRequired, - longitude: PropTypes.number.isRequired - }), - centerCoordinateZoom: PropTypes.shape(), - debugActive: PropTypes.bool, - direction: PropTypes.number, - rotateEnabled: PropTypes.bool, - scrollEnabled: PropTypes.bool, - showsUserLocation: PropTypes.bool, - styleURL: PropTypes.string, - userTrackingMode: PropTypes.number, - zoomEnabled: PropTypes.bool, - zoomLevel: PropTypes.number, - tilt: PropTypes.number, - compassIsHidden: PropTypes.bool, - onRegionChange: PropTypes.func, - onOpenAnnotation: PropTypes.func, - onLongPress: PropTypes.func, - onUserLocationChange: PropTypes.func - }, - handleOnChange(event) { - if (this.props.onRegionChange) this.props.onRegionChange(event); - }, - handleUserLocation(event) { - if (this.props.onUserLocationChange) this.props.onUserLocationChange(event); - }, - handleOnOpenAnnotation(event) { - if (this.props.onOpenAnnotation) this.props.onOpenAnnotation(event); - }, - handleOnLongPress(event) { - if (this.props.onLongPress) this.props.onLongPress(event); - }, - render() { - return ( - - ); - } -}); - -var ReactMapView = requireNativeComponent('RCTMapbox'); - -module.exports = ReactMapViewWrapper; diff --git a/index.ios.js b/index.ios.js deleted file mode 100644 index 148fe984c..000000000 --- a/index.ios.js +++ /dev/null @@ -1,215 +0,0 @@ -'use strict'; - -var React = require('react'); -var { PropTypes } = React; - -var ReactNative = require('react-native'); -var { NativeModules, requireNativeComponent, findNodeHandle } = ReactNative; - -var { MapboxGLManager } = NativeModules; - -var MapMixins = { - addPackForRegion(mapRef, options) { - MapboxGLManager.addPackForRegion(findNodeHandle(this.refs[mapRef]), options); - }, - getPacks(mapRef, callback) { - MapboxGLManager.getPacks(findNodeHandle(this.refs[mapRef]), callback); - }, - removePack(mapRef, packName, callback) { - MapboxGLManager.removePack(findNodeHandle(this.refs[mapRef]), packName, callback); - }, - setDirectionAnimated(mapRef, heading) { - MapboxGLManager.setDirectionAnimated(findNodeHandle(this.refs[mapRef]), heading); - }, - setZoomLevelAnimated(mapRef, zoomLevel) { - MapboxGLManager.setZoomLevelAnimated(findNodeHandle(this.refs[mapRef]), zoomLevel); - }, - setCenterCoordinateAnimated(mapRef, latitude, longitude) { - return MapboxGLManager.setCenterCoordinateAnimated(findNodeHandle(this.refs[mapRef]), latitude, longitude); - }, - setCenterCoordinateZoomLevelAnimated(mapRef, latitude, longitude, zoomLevel) { - MapboxGLManager.setCenterCoordinateZoomLevelAnimated(findNodeHandle(this.refs[mapRef]), latitude, longitude, zoomLevel); - }, - setCameraAnimated(mapRef, latitude, longitude, fromDistance, pitch, heading, duration) { - MapboxGLManager.setCameraAnimated(findNodeHandle(this.refs[mapRef]), latitude, longitude, fromDistance, pitch, heading, duration); - }, - addAnnotations(mapRef, annotations) { - MapboxGLManager.addAnnotations(findNodeHandle(this.refs[mapRef]), annotations); - }, - updateAnnotation(mapRef, annotation) { - MapboxGLManager.updateAnnotation(findNodeHandle(this.refs[mapRef]), annotation); - }, - selectAnnotationAnimated(mapRef, selectedIdentifier) { - MapboxGLManager.selectAnnotationAnimated(findNodeHandle(this.refs[mapRef]), selectedIdentifier); - }, - removeAnnotation(mapRef, selectedIdentifier) { - MapboxGLManager.removeAnnotation(findNodeHandle(this.refs[mapRef]), selectedIdentifier); - }, - removeAllAnnotations(mapRef) { - MapboxGLManager.removeAllAnnotations(findNodeHandle(this.refs[mapRef])); - }, - setVisibleCoordinateBoundsAnimated(mapRef, latitudeSW, longitudeSW, latitudeNE, longitudeNE, paddingTop, paddingRight, paddingBottom, paddingLeft) { - MapboxGLManager.setVisibleCoordinateBoundsAnimated(findNodeHandle(this.refs[mapRef]), latitudeSW, longitudeSW, latitudeNE, longitudeNE, paddingTop, paddingRight, paddingBottom, paddingLeft); - }, - setUserTrackingMode(mapRef, userTrackingMode) { - MapboxGLManager.setUserTrackingMode(findNodeHandle(this.refs[mapRef]), userTrackingMode); - }, - getCenterCoordinateZoomLevel(mapRef, callback) { - MapboxGLManager.getCenterCoordinateZoomLevel(findNodeHandle(this.refs[mapRef]), callback); - }, - getDirection(mapRef, callback) { - MapboxGLManager.getDirection(findNodeHandle(this.refs[mapRef]), callback); - }, - getBounds(mapRef, callback) { - MapboxGLManager.getBounds(findNodeHandle(this.refs[mapRef]), callback); - }, - mapStyles: MapboxGLManager.mapStyles, - userTrackingMode: MapboxGLManager.userTrackingMode, - userLocationVerticalAlignment: MapboxGLManager.userLocationVerticalAlignment, - unknownResourceCount: MapboxGLManager.unknownResourceCount -}; - -var MapView = React.createClass({ - statics: { - Mixin: MapMixins - }, - _onRegionChange(event: Event) { - if (this.props.onRegionChange) this.props.onRegionChange(event.nativeEvent.src); - }, - _onRegionWillChange(event: Event) { - if (this.props.onRegionWillChange) this.props.onRegionWillChange(event.nativeEvent.src); - }, - _onOpenAnnotation(event: Event) { - if (this.props.onOpenAnnotation) this.props.onOpenAnnotation(event.nativeEvent.src); - }, - _onRightAnnotationTapped(event: Event) { - if (this.props.onRightAnnotationTapped) this.props.onRightAnnotationTapped(event.nativeEvent.src); - }, - _onUpdateUserLocation(event: Event) { - if (this.props.onUpdateUserLocation) this.props.onUpdateUserLocation(event.nativeEvent.src); - }, - _onLongPress(event: Event) { - if (this.props.onLongPress) this.props.onLongPress(event.nativeEvent.src); - }, - _onTap(event: Event) { - if (this.props.onTap) this.props.onTap(event.nativeEvent.src); - }, - _onFinishLoadingMap(event: Event) { - if (this.props.onFinishLoadingMap) this.props.onFinishLoadingMap(event.nativeEvent.src); - }, - _onStartLoadingMap(event: Event) { - if (this.props.onStartLoadingMap) this.props.onStartLoadingMap(event.nativeEvent.src); - }, - _onLocateUserFailed(event: Event) { - if (this.props.onLocateUserFailed) this.props.onLocateUserFailed(event.nativeEvent.src); - }, - _onOfflineProgressDidChange(event: Event) { - if (this.props.onOfflineProgressDidChange) this.props.onOfflineProgressDidChange(event.nativeEvent.src); - }, - _onOfflineMaxAllowedMapboxTiles(event: Event) { - if (this.props.onOfflineMaxAllowedMapboxTiles) this.props.onOfflineMaxAllowedMapboxTiles(event.nativeEvent.src); - }, - _onOfflineDidRecieveError(event: Event) { - if (this.props.onOfflineDidRecieveError) this.props.onOfflineDidRecieveError(event.nativeEvent.src); - }, - propTypes: { - showsUserLocation: PropTypes.bool, - rotateEnabled: PropTypes.bool, - scrollEnabled: PropTypes.bool, - zoomEnabled: PropTypes.bool, - accessToken: PropTypes.string.isRequired, - zoomLevel: PropTypes.number, - direction: PropTypes.number, - styleURL: PropTypes.string, - clipsToBounds: PropTypes.bool, - debugActive: PropTypes.bool, - userTrackingMode: PropTypes.number, - attributionButton: PropTypes.bool, - centerCoordinate: PropTypes.shape({ - latitude: PropTypes.number.isRequired, - longitude: PropTypes.number.isRequired - }), - annotations: PropTypes.arrayOf(PropTypes.shape({ - coordinates: PropTypes.array.isRequired, - title: PropTypes.string, - subtitle: PropTypes.string, - fillAlpha: PropTypes.number, - fillColor: PropTypes.string, - strokeAlpha: PropTypes.number, - strokeColor: PropTypes.string, - strokeWidth: PropTypes.number, - id: PropTypes.string, - type: PropTypes.string.isRequired, - rightCalloutAccessory: PropTypes.object({ - height: PropTypes.number, - width: PropTypes.number, - url: PropTypes.string - }), - annotationImage: PropTypes.object({ - height: PropTypes.number, - width: PropTypes.number, - url: PropTypes.string - }) - })), - attributionButtonIsHidden: PropTypes.bool, - logoIsHidden: PropTypes.bool, - compassIsHidden: PropTypes.bool, - onRegionChange: PropTypes.func, - onRegionWillChange: PropTypes.func, - onOpenAnnotation: PropTypes.func, - onUpdateUserLocation: PropTypes.func, - onRightAnnotationTapped: PropTypes.func, - onFinishLoadingMap: PropTypes.func, - onStartLoadingMap: PropTypes.func, - onLocateUserFailed: PropTypes.func, - onLongPress: PropTypes.func, - onTap: PropTypes.func, - contentInset: PropTypes.array, - userLocationVerticalAlignment: PropTypes.number, - onOfflineProgressDidChange: PropTypes.func, - onOfflineMaxAllowedMapboxTiles: PropTypes.func, - onOfflineDidRecieveError: PropTypes.func - }, - getDefaultProps() { - return { - centerCoordinate: { - latitude: 0, - longitude: 0 - }, - debugActive: false, - direction: 0, - rotateEnabled: true, - scrollEnabled: true, - showsUserLocation: false, - styleURL: this.Mixin.mapStyles.streets, - zoomEnabled: true, - zoomLevel: 0, - attributionButtonIsHidden: false, - logoIsHidden: false, - compassIsHidden: false - }; - }, - render() { - return ( - - ); - } -}); - -var MapboxGLView = requireNativeComponent('RCTMapboxGL', MapView); - -module.exports = MapView; diff --git a/index.js b/index.js new file mode 100644 index 000000000..2cb9cb904 --- /dev/null +++ b/index.js @@ -0,0 +1,429 @@ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import ReactNative, { + View, + NativeModules, + NativeAppEventEmitter, + requireNativeComponent, + findNodeHandle, + Platform +} from 'react-native'; + +import cloneDeep from 'lodash/cloneDeep'; +import clone from 'lodash/clone'; +import isEqual from 'lodash/isEqual'; + +const { MapboxGLManager } = NativeModules; +const { mapStyles, userTrackingMode, userLocationVerticalAlignment, unknownResourceCount } = MapboxGLManager; + +// Deprecation + +function deprecated(obj, key) { + const value = obj[key]; + let warned = false; + Object.defineProperty(obj, key, { + get() { + if (!warned) { + console.warn(`${key} is deprecated`); + warned = true; + } + return value; + } + }); +} + +deprecated(mapStyles, 'emerald'); + +// Monkeypatch Android commands + +if (Platform.OS === 'android') { + const RCTUIManager = NativeModules.UIManager; + const commands = RCTUIManager.RCTMapboxGL.Commands; + + // Since we cannot pass functions to dispatchViewManagerCommand, we keep a + // map of callbacks and send an int instead + const callbackMap = new Map(); + let nextCallbackId = 0; + + Object.keys(commands).forEach(command => { + MapboxGLManager[command] = (handle, ...rawArgs) => { + const args = rawArgs.map(arg => { + if (typeof arg === 'function') { + callbackMap.set(nextCallbackId, arg); + return nextCallbackId++; + } + return arg; + }); + RCTUIManager.dispatchViewManagerCommand(handle, commands[command], args); + }; + }); + + NativeAppEventEmitter.addListener('MapboxAndroidCallback', ([ callbackId, args ]) => { + const callback = callbackMap.get(callbackId); + if (!callback) { + throw new Error(`Native is calling a callbackId ${callbackId}, which is not registered`); + } + callbackMap.delete(callbackId); + callback.apply(null, args); + }); +} + +// Metrics + +let _metricsEnabled = MapboxGLManager.metricsEnabled; + +function setMetricsEnabled(enabled: boolean) { + _metricsEnabled = enabled; + MapboxGLManager.setMetricsEnabled(enabled); +} + +function getMetricsEnabled() { + return _metricsEnabled; +} + +// Access token +function setAccessToken(token: string) { + MapboxGLManager.setAccessToken(token); +} + +// Offline +function bindCallbackToPromise(callback, promise) { + if (callback) { + promise.then(value => { + callback(null, value); + }).catch(err => { + callback(err); + }) + } +} + +function addOfflinePack(options, callback) { + let _options = options; + // Workaround the fact that RN Android can't serialize JSON correctly + if (Platform.OS === 'android') { + _options = { + ...options, + metadata: JSON.stringify({ v: options.metadata }) + }; + } + const promise = MapboxGLManager.addOfflinePack(_options); + bindCallbackToPromise(callback, promise); + return promise; +} + +function getOfflinePacks(callback) { + let promise = MapboxGLManager.getOfflinePacks(); + if (Platform.OS === 'android') { + promise = promise.then(packs => { + packs.forEach(progress => { + if (progress.metadata) { + progress.metadata = JSON.parse(progress.metadata).v; + } + }); + return packs; + }); + } + bindCallbackToPromise(callback, promise); + return promise; +} + +function removeOfflinePack(packName, callback) { + const promise = MapboxGLManager.removeOfflinePack(packName); + bindCallbackToPromise(callback, promise); + return promise; +} + +function setOfflinePackProgressThrottleInterval(milis) { + MapboxGLManager.setOfflinePackProgressThrottleInterval(milis); +} + +function addOfflinePackProgressListener(handler) { + let _handler = handler; + if (Platform.OS === 'android') { + _handler = (progress) => { + if (progress.metadata) { + progress.metadata = JSON.parse(progress.metadata).v; + } + handler(progress); + }; + } + return NativeAppEventEmitter.addListener('MapboxOfflineProgressDidChange', _handler); +} + +function addOfflineMaxAllowedTilesListener(handler) { + return NativeAppEventEmitter.addListener('MapboxOfflineMaxAllowedTiles', handler); +} + +function addOfflineErrorListener(handler) { + return NativeAppEventEmitter.addListener('MapboxOfflineError', handler); +} + +class MapView extends Component { + + // Viewport setters + setDirection(direction, animated = true, callback) { + return this.easeTo({ direction }, animated, callback); + } + setZoomLevel(zoomLevel, animated = true, callback) { + return this.easeTo({ zoomLevel }, animated, callback); + } + setCenterCoordinate(latitude, longitude, animated = true, callback) { + return this.easeTo({ latitude, longitude }, animated, callback); + } + setCenterCoordinateZoomLevel(latitude, longitude, zoomLevel, animated = true, callback) { + return this.easeTo({ latitude, longitude, zoomLevel }, animated, callback); + } + setCenterCoordinateZoomLevelPitch(latitude, longitude, zoomLevel, pitch, animated = true, callback) { + return this.easeTo({ latitude, longitude, zoomLevel, pitch }, animated, callback); + } + setPitch(pitch, animated = true, callback) { + return this.easeTo({ pitch }, animated, callback); + } + easeTo(options, animated = true, callback) { + let _resolve; + const promise = new Promise(resolve => _resolve = resolve); + MapboxGLManager.easeTo(findNodeHandle(this), options, animated, () => { + callback && callback(); + _resolve(); + }); + return promise; + } + + setVisibleCoordinateBounds(latitudeSW, longitudeSW, latitudeNE, longitudeNE, paddingTop = 0, paddingRight = 0, paddingBottom = 0, paddingLeft = 0, animated = true) { + MapboxGLManager.setVisibleCoordinateBounds(findNodeHandle(this), latitudeSW, longitudeSW, latitudeNE, longitudeNE, paddingTop, paddingRight, paddingBottom, paddingLeft, animated); + } + + // Getters + getCenterCoordinateZoomLevel(callback) { + MapboxGLManager.getCenterCoordinateZoomLevel(findNodeHandle(this), callback); + } + getDirection(callback) { + MapboxGLManager.getDirection(findNodeHandle(this), callback); + } + getBounds(callback) { + MapboxGLManager.getBounds(findNodeHandle(this), callback); + } + getPitch(callback) { + MapboxGLManager.getPitch(findNodeHandle(this), callback); + } + + // Others + selectAnnotation(annotationId, animated = true) { + MapboxGLManager.selectAnnotation(findNodeHandle(this), annotationId, animated); + } + + // Events + _onRegionDidChange = (event: Event) => { + if (this.props.onRegionDidChange) this.props.onRegionDidChange(event.nativeEvent.src); + }; + _onRegionWillChange = (event: Event) => { + if (this.props.onRegionWillChange) this.props.onRegionWillChange(event.nativeEvent.src); + }; + _onOpenAnnotation = (event: Event) => { + if (this.props.onOpenAnnotation) this.props.onOpenAnnotation(event.nativeEvent.src); + }; + _onRightAnnotationTapped = (event: Event) => { + if (this.props.onRightAnnotationTapped) this.props.onRightAnnotationTapped(event.nativeEvent.src); + }; + _onChangeUserTrackingMode = (event: Event) => { + if (this.props.onChangeUserTrackingMode) this.props.onChangeUserTrackingMode(event.nativeEvent.src); + }; + _onUpdateUserLocation = (event: Event) => { + if (this.props.onUpdateUserLocation) this.props.onUpdateUserLocation(event.nativeEvent.src); + }; + _onLongPress = (event: Event) => { + if (this.props.onLongPress) this.props.onLongPress(event.nativeEvent.src); + }; + _onTap = (event: Event) => { + if (this.props.onTap) this.props.onTap(event.nativeEvent.src); + }; + _onFinishLoadingMap = (event: Event) => { + if (this.props.onFinishLoadingMap) this.props.onFinishLoadingMap(event.nativeEvent.src); + }; + _onStartLoadingMap = (event: Event) => { + if (this.props.onStartLoadingMap) this.props.onStartLoadingMap(event.nativeEvent.src); + }; + _onLocateUserFailed = (event: Event) => { + if (this.props.onLocateUserFailed) this.props.onLocateUserFailed(event.nativeEvent.src); + }; + + static propTypes = { + ...View.propTypes, + + initialZoomLevel: PropTypes.number, + initialDirection: PropTypes.number, + initialCenterCoordinate: PropTypes.shape({ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired + }), + clipsToBounds: PropTypes.bool, + debugActive: PropTypes.bool, + rotateEnabled: PropTypes.bool, + scrollEnabled: PropTypes.bool, + zoomEnabled: PropTypes.bool, + showsUserLocation: PropTypes.bool, + styleURL: PropTypes.string.isRequired, + userTrackingMode: PropTypes.number, + attributionButtonIsHidden: PropTypes.bool, + logoIsHidden: PropTypes.bool, + compassIsHidden: PropTypes.bool, + userLocationVerticalAlignment: PropTypes.number, + contentInset: PropTypes.arrayOf(PropTypes.number), + + annotations: PropTypes.arrayOf(PropTypes.shape({ + coordinates: PropTypes.array.isRequired, + title: PropTypes.string, + subtitle: PropTypes.string, + fillAlpha: PropTypes.number, + fillColor: PropTypes.string, + strokeAlpha: PropTypes.number, + strokeColor: PropTypes.string, + strokeWidth: PropTypes.number, + id: PropTypes.string, + type: PropTypes.string.isRequired, + rightCalloutAccessory: PropTypes.object({ + height: PropTypes.number, + width: PropTypes.number, + url: PropTypes.string + }), + annotationImage: PropTypes.object({ + height: PropTypes.number, + width: PropTypes.number, + url: PropTypes.string + }) + })), + annotationsAreImmutable: PropTypes.bool, + + onRegionDidChange: PropTypes.func, + onRegionWillChange: PropTypes.func, + onOpenAnnotation: PropTypes.func, + onUpdateUserLocation: PropTypes.func, + onRightAnnotationTapped: PropTypes.func, + onFinishLoadingMap: PropTypes.func, + onStartLoadingMap: PropTypes.func, + onLocateUserFailed: PropTypes.func, + onLongPress: PropTypes.func, + onTap: PropTypes.func, + onChangeUserTrackingMode: PropTypes.func, + }; + + static defaultProps = { + initialCenterCoordinate: { + latitude: 0, + longitude: 0 + }, + initialDirection: 0, + initialZoomLevel: 0, + debugActive: false, + rotateEnabled: true, + scrollEnabled: true, + showsUserLocation: false, + styleURL: mapStyles.streets, + userTrackingMode: userTrackingMode.none, + zoomEnabled: true, + attributionButtonIsHidden: false, + logoIsHidden: false, + compassIsHidden: false, + annotationsAreImmutable: false, + annotations: [], + contentInset: [0, 0, 0, 0] + }; + + componentWillReceiveProps(newProps) { + const oldKeys = clone(this._annotations); + const itemsToAdd = []; + const itemsToRemove = []; + + const isImmutable = newProps.annotationsAreImmutable; + if (isImmutable && this.props.annotations === newProps.annotations) { + return; + } + + newProps.annotations.forEach(annotation => { + const id = annotation.id; + if (!isEqual(this._annotations[id], annotation)) { + this._annotations[id] = isImmutable ? annotation : cloneDeep(annotation); + itemsToAdd.push(annotation); + } + oldKeys[id] = null; + }); + + for (let key in oldKeys) { + if (oldKeys[key]) { + delete this._annotations[key]; + itemsToRemove.push(key); + } + } + + MapboxGLManager.spliceAnnotations(findNodeHandle(this), false, itemsToRemove, itemsToAdd); + } + + _native = null; + + _onNativeComponentMount = (ref) => { + if (this._native === ref) { return; } + this._native = ref; + + MapboxGLManager.spliceAnnotations(findNodeHandle(this), true, [], this.props.annotations); + + const isImmutable = this.props.annotationsAreImmutable; + + this._annotations = this.props.annotations.reduce((acc, annotation) => { + acc[annotation.id] = isImmutable ? annotation : cloneDeep(annotation); + return acc; + }, {}); + }; + + setNativeProps(nativeProps) { + this._native && this._native.setNativeProps(nativeProps); + } + + componentWillUnmount() { + this._native = null; + } + + render() { + return ( + + ); + } +} + +const MapboxGLView = requireNativeComponent('RCTMapboxGL', MapView, { + nativeOnly: { + onChange: true, + enableOnRegionDidChange: true, + enableOnRegionWillChange: true + } +}); + +const Mapbox = { + MapView, + mapStyles, userTrackingMode, userLocationVerticalAlignment, unknownResourceCount, + getMetricsEnabled, setMetricsEnabled, + setAccessToken, + addOfflinePack, getOfflinePacks, removeOfflinePack, + addOfflinePackProgressListener, + addOfflineMaxAllowedTilesListener, + addOfflineErrorListener, + setOfflinePackProgressThrottleInterval +}; + +module.exports = Mapbox; diff --git a/ios/API.md b/ios/API.md deleted file mode 100644 index 94c4e04b6..000000000 --- a/ios/API.md +++ /dev/null @@ -1,212 +0,0 @@ -# iOS API Docs - -## Options - -| Option | Type | Opt/Required | Default | Note | -|---|---|---|---|---| -| `accessToken` | `string` | Required | NA |Mapbox access token. Sign up for a [Mapbox account here](https://www.mapbox.com/signup). -| `centerCoordinate` | `object` | Optional | `0,0`| Initial `latitude`/`longitude` the map will load at, defaults to `0,0`. -| `zoomLevel` | `double` | Optional | `0` | Initial zoom level the map will load at. 0 is the entire world, 18 is rooftop level. Defaults to 0. -| `rotateEnabled` | `bool` | Optional | `true` | Whether the map can rotate | -| `scrollEnabled` | `bool` | Optional | `true` | Whether the map can be scrolled | -| `zoomEnabled` | `bool` | Optional | `true` | Whether the map zoom level can be changed | -|`showsUserLocation` | `bool` | Optional | `false` | Whether the user's location is shown on the map. Note - the map will not zoom to their location.| -| `styleURL` | `string` | required | Mapbox Streets | A Mapbox style. Defaults to `streets`. -| `annotations` | `array` | Optional | NA | An array of annotation objects. See [annotation detail](https://github.com/bsudekum/react-native-mapbox-gl/blob/master/ios/API.md#annotations) -| `direction` | `double` | Optional | `0` | Heading of the map in degrees where 0 is north and 180 is south | -| `debugActive` | `bool` | Optional | `false` | Turns on debug mode. | -| `style` | flexbox `view` | Optional | NA | Styles the actual map view container | -| `userTrackingMode` | `int` | Optional | `this.userTrackingMode.none` | Must add `mixins` to use. Valid values are `this.userTrackingMode.none`, `this.userTrackingMode.follow`, `this.userTrackingMode.followWithCourse`, `this.userTrackingMode.followWithHeading` | -| `attributionButtonIsHidden` | `bool` | Optional | `false` | Whether attribution button is visible in lower right corner. *If true you must still attribute OpenStreetMap in your app. [Ref](https://www.mapbox.com/about/maps/)* | -| `logoIsHidden` | `bool` | Optional | `false` | Whether logo is visible in lower left corner. | -| `compassIsHidden` | `bool` | Optional | `false` | Whether compass is visible when map is rotated. | -| `contentInset` | `array` | Optional | `[0, 0, 0, 0]` | Change the center point of the map. Offset is in pixels. `[top, right, bottom, left]` -| `userLocationVerticalAlignment` | `enum` | Optional | `userLocationVerticalAlignment.center` | Change the alignment of where the user location shows on the screen. Valid values: `userLocationVerticalAlignment.top`, `userLocationVerticalAlignment.center`, `userLocationVerticalAlignment.bottom` - -## Event listeners - -| Event Name | Returns | Notes -|---|---|---| -| `onRegionChange` | `{latitude: 0, longitude: 0, zoom: 0}` | Fired when the map ends panning or zooming. -| `onRegionWillChange` | `{latitude: 0, longitude: 0, zoom: 0}` | Fired when the map begins panning or zooming. -| `onOpenAnnotation` | `{title: null, subtitle: null, latitude: 0, longitude: 0}` | Fired when focusing a an annotation. -| `onUpdateUserLocation` | `{latitude: 0, longitude: 0, headingAccuracy: 0, magneticHeading: 0, trueHeading: 0, isUpdating: false}` | Fired when the users location updates. -| `onRightAnnotationTapped` | `{title: null, subtitle: null, latitude: 0, longitude: 0}` | Fired when user taps `rightCalloutAccessory` -| `onTap` | `{latitude: 0, longitude: 0}` | Fired when the users taps the screen. -| `onLongPress` | `{latitude: 0, longitude: 0, screenCoordY, screenCoordX}` | Fired when the user taps and holds screen for 1 second. -| `onFinishLoadingMap` | does not return an object | Fired once the map has loaded the style | -| `onStartLoadingMap` | does not return an object | Fired once the map begins loading the style | -| `onLocateUserFailed` | `{message: message}` | Fired when there is an error getting the users location. Do not rely on the string that is returned for determining what kind of error it is. | -| `getCenterCoordinateZoomLevel` | `mapViewRef`, `callback` | Gets the current center location and zoom level. Returns a single callback object. | -| `getDirection` | `mapViewRef`, `callback` | Gets the current direction. Returns a single callback object. | -| `getBounds` | `mapViewRef`, `callback` | Gets the bounds of the current view. Returns array [latitudeSW, longitudeSW, latitudeNE, longitudeNE]. | -| `onOfflineProgressDidChange` | `{countOfResourcesCompleted: 7, countOfResourcesExpected: 1284, name: "test", countOfBytesCompleted: 306543, maximumResourcesExpected: 1284}` | Event fired when the progress of an offline pack changes while downloading. | -| `onOfflineMaxAllowedMapboxTiles` | `{maximumCount: number}` | Event fired when the maximum number of tiles has been hit. | -| `onOfflineDidRecieveError` | `{error: error}` | Event fired when there is an error while downloading a pack. | - -## Methods for Modifying the Map State - -These methods require you to use `MapboxGLMap.Mixin` to access the methods. Each method also requires you to pass in a string as the first argument which is equal to the `ref` on the map view you wish to modify. See the [example](https://github.com/mapbox/react-native-mapbox-gl/blob/master/ios/example.js) on how this is implemented. - -| Method Name | Arguments | Notes -|---|---|---| -| `setDirectionAnimated` | `mapViewRef`, `heading` | Rotates the map to a new heading -| `setZoomLevelAnimated` | `mapViewRef`, `zoomLevel` | Zooms the map to a new zoom level -| `setCenterCoordinateAnimated` | `mapViewRef`, `latitude`, `longitude` | Moves the map to a new coordinate. Note, the zoom level stay at the current zoom level. Returns a promise for handling completion. -| `setCenterCoordinateZoomLevelAnimated` | `mapViewRef`, `latitude`, `longitude`, `zoomLevel` | Moves the map to a new coordinate and zoom level -| `setCameraAnimated` | `mapViewRef`, `latitude`, `longitude`, `fromDistance`, `pitch`, `heading`, `duration` | Sets viewing angle on the map -| `addAnnotations` | `mapViewRef`, `` (array of annotation objects, see [#annotations](https://github.com/bsudekum/react-native-mapbox-gl/blob/master/API.md#annotations)) | Adds annotation(s) to the map without redrawing the map. Note, this will remove all previous annotations from the map. -| `selectAnnotationAnimated` | `mapViewRef`, `marker id` | Open the callout of the selected annotation. This method requires that you supply an id to an annotation when creating. If 2 annotations have the same id, only the first annotation will be selected. Only works on annotation `type = 'point'``. -| `updateAnnotation` | `mapViewRef`, `annotation object` | Replace annotation if it exists on the map. This check happens based on the `id` of the object being passed in. The annotation will still be added if no previous one exists. -| `removeAnnotation` | `mapViewRef`, `marker id` | Removes annotation from map. This method requires that you supply an id to an annotation when creating. If 2 annotations have the same id, only the first will be removed. -| `removeAllAnnotations` | `mapViewRef`| Removes all annotations from the map. -| `setVisibleCoordinateBoundsAnimated` | `mapViewRef`, `latitude1`, `longitude1`, `latitude2`, `longitude2`, `padding top`, `padding right`, `padding bottom`, `padding left` | Changes the viewport to fit the given coordinate bounds and some additional padding on each side. -| `setUserTrackingMode` | `mapViewRef`, `userTrackingMode` | Modifies the tracking mode. Valid args: `this.userTrackingMode.none`, `this.userTrackingMode.follow`, `this.userTrackingMode.followWithCourse`, `this.userTrackingMode.followWithHeading` -| `addPackForRegion` | `mapRef` `{name, type, bounds, minZoomLevel, maxZoomLevel, style}` | Adds an offline region for a given bounding box. `name` is a string to represent an offline pack. `type` must be of type `bbox`. `bounds` is an array. `minZoomLevel` is an number representing the minimum zoom level of the offline pack. `maxZoomLevel` is an number representing the maximum zoom level of the offline pack. `style` is a style url to download. `metadata` is an object you can use metadata about the pack. -| `getPacks` | `mapRef` `callback` | Returns a callback with an array of all offline packs on device. If the downloaded pack was not downloaded during the current session, the size will be 0. -| `removePack` | `mapRef` `name-of-pack` `callback` | Removes a pack from the device. The name corresponds to the `name` of the pack used when calling `addPackForRegion` - -## Styles - -This ships with 6 styles included: - -* `streets` -* `emerald` -* `dark` -* `light` -* `satellite` -* `hybrid` - -To use one of these, make you add mixins: - -```js -mixins: [Mapbox.Mixin] -``` - -Then you can access each style by: - -```jsx -styleURL={this.mapStyles.emerald} -``` - -## Custom styles - -You can also create a custom style in [Mapbox Studio](https://www.mapbox.com/studio/) and add it your map. Simply grab the style url. It should look something like: - -``` -mapbox://styles/bobbysud/cigtw1pzy0000aam2346f7ex0 -``` - -## Annotations -```json -[{ - "coordinates": "required. For type polyline and polygon must be an array of arrays. For type point, single array", - "type": "required: point, polyline or polygon", - "title": "optional string", - "subtitle": "optional string", - "fillAlpha": "optional, only used for type=polygon. Controls the opacity of polygon", - "fillColor": "optional string hex color including #, only used for type=polygon", - "strokeAlpha": "optional number from 0-1. Only used for type=poyline. Controls opacity of line", - "strokeColor": "optional string hex color including #, used for type=polygon and type=polyline", - "strokeWidth": "optional number. Only used for type=poyline. Controls line width", - "id": "required string, unique identifier. Used for adding or selecting an annotation.", - "rightCalloutAccessory": { - "url": "Optional. Either remote image or specify via 'image!yourImage.png'", - "height": "required if url specified", - "width": "required if url specified" - }, - "annotationImage": { - "url": "Optional. Either remote image or specify via 'image!yourImage.png'", - "height": "required if url specified", - "width": "required if url specified" - }, -}] -``` -**For adding local images via `image!yourImage.png` see [adding static resources to your app using Images.xcassets docs](https://facebook.github.io/react-native/docs/image.html#adding-static-resources-to-your-app-using-images-xcassets)**. - -#### Example -```json -annotations: [{ - "coordinates": [40.72052634, -73.97686958312988], - "type": "point", - "title": "This is marker 1", - "subtitle": "It has a rightCalloutAccessory too", - "rightCalloutAccessory": { - "url": "https://cldup.com/9Lp0EaBw5s.png", - "height": 25, - "width": 25 - }, - "annotationImage": { - "url": "https://cldup.com/CnRLZem9k9.png", - "height": 25, - "width": 25 - }, - "id": "marker1" -}, { - "coordinates": [40.714541341726175,-74.00579452514648], - "type": "point", - "title": "Important", - "subtitle": "Neat, this is a custom annotation image", - "annotationImage": { - "url": "https://cldup.com/7NLZklp8zS.png", - "height": 25, - "width": 25 - }, - "id": "marker2" -}, { - "coordinates": [[40.76572150042782,-73.99429321289062],[40.743485405490695, -74.00218963623047],[40.728266950429735,-74.00218963623047],[40.728266950429735,-73.99154663085938],[40.73633186448861,-73.98983001708984],[40.74465591168391,-73.98914337158203],[40.749337730454826,-73.9870834350586]], - "type": "polyline", - "strokeColor": "#00FB00", - "strokeWidth": 3, - "strokeAlpha": 0.5, - "id": "line" -}, { - "coordinates": [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], - "type": "polygon", - "fillAlpha":1, - "fillColor": "#C32C2C", - "strokeColor": "#DDDDD", - "id": "route" -}] -``` - - -### Offline - -There are 3 main methods for interacting with the offline API: -* `addPackForRegion` - creates an offline pack -* `getPacks` - returns an array of all offline packs on the device -* `removePack` - removes a single pack - -To create a pack: - -```js -this.addPackForRegion(mapRef, { - name: 'test', //required - type: 'bbox', // required, only type currently supported` - metadata: { // required. You can put any information in here that may be useful to you. Can be empty if no metadata is needed - date: new Date(), - foo: 'bar' - }, - bounds: bounds, // latitudeSW, longitudeSW, latitudeNE, longitudeNE - minZoomLevel: 10, - maxZoomLevel: 13, - styleURL: this.mapStyles.emerald // valid styleURL -}); -``` - -You can view the progress of a pack that is downloading by listening on `onOfflineProgressDidChange`. - -To delete a pack, provide the `name` of the pack to delete -```js -this.removePack(mapRef, 'test', (err, info)=> { - if (err) console.log(err); - if (info) { - console.log('Deleted', info.deleted); - } else { - console.log('No packs to delete'); // There are no packs on the device - } -}); -``` - -Check out our [help page](https://www.mapbox.com/help/mobile-offline/) for more information on offline. diff --git a/ios/RCTMapboxGL.xcodeproj/project.pbxproj b/ios/RCTMapboxGL.xcodeproj/project.pbxproj index 3eea7e86b..e8b3e7d99 100644 --- a/ios/RCTMapboxGL.xcodeproj/project.pbxproj +++ b/ios/RCTMapboxGL.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; C5DBB34C1AF2EF2B00E611A9 /* libRCTMapboxGL.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C5DBB3401AF2EF2B00E611A9 /* libRCTMapboxGL.a */; }; @@ -37,6 +38,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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; }; C5DBB3431AF2EF2B00E611A9 /* RCTMapboxGL.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTMapboxGL.h; sourceTree = ""; }; C5DBB3451AF2EF2B00E611A9 /* RCTMapboxGL.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTMapboxGL.m; sourceTree = ""; }; @@ -90,6 +93,8 @@ C5DBB3451AF2EF2B00E611A9 /* RCTMapboxGL.m */, C5DBB3641AF2EFB500E611A9 /* RCTMapboxGLManager.h */, C5DBB3651AF2EFB500E611A9 /* RCTMapboxGLManager.m */, + C167F89A1D18111F007C7A42 /* RCTMapboxGLConversions.h */, + C167F89B1D18112B007C7A42 /* RCTMapboxGLConversions.m */, ); path = RCTMapboxGL; sourceTree = ""; @@ -200,6 +205,7 @@ files = ( C5DBB3461AF2EF2B00E611A9 /* RCTMapboxGL.m in Sources */, C5DBB3661AF2EFB500E611A9 /* RCTMapboxGLManager.m in Sources */, + C167F89C1D18112B007C7A42 /* RCTMapboxGLConversions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/RCTMapboxGL/RCTMapboxGL.h b/ios/RCTMapboxGL/RCTMapboxGL.h index 46b1a352b..6b65d3582 100644 --- a/ios/RCTMapboxGL/RCTMapboxGL.h +++ b/ios/RCTMapboxGL/RCTMapboxGL.h @@ -11,43 +11,59 @@ #import "RCTEventDispatcher.h" #import "RCTBridgeModule.h" -@interface RCTMapboxGL : RCTView +@interface RCTMapboxGL : RCTView - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; -- (void)setAccessToken:(NSString *)accessToken; -- (void)setAnnotations:(NSArray *)annotations; -- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate; +// React props +- (void)setInitialCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate; +- (void)setInitialZoomLevel:(double)zoomLevel; +- (void)setInitialDirection:(double)direction; - (void)setClipsToBounds:(BOOL)clipsToBounds; - (void)setDebugActive:(BOOL)debugActive; -- (void)setDirection:(double)direction; - (void)setRotateEnabled:(BOOL)rotateEnabled; - (void)setScrollEnabled:(BOOL)scrollEnabled; - (void)setZoomEnabled:(BOOL)zoomEnabled; - (void)setShowsUserLocation:(BOOL)showsUserLocation; - (void)setStyleURL:(NSURL *)styleURL; -- (void)setZoomLevel:(double)zoomLevel; - (void)setUserTrackingMode:(int)userTrackingMode; -- (void)setZoomLevelAnimated:(double)zoomLevel; -- (void)setDirectionAnimated:(int)heading; -- (void)setCenterCoordinateAnimated:(CLLocationCoordinate2D)coordinates resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; -- (void)setCenterCoordinateZoomLevelAnimated:(CLLocationCoordinate2D)coordinates zoomLevel:(double)zoomLevel; -- (void)setCameraAnimated:(MGLMapCamera*)camera withDuration:(int)duration animationTimingFunction:(CAMediaTimingFunction*)function; -- (void)selectAnnotationAnimated:(NSString*)selectedId; -- (void)addAnnotation:(NSObject *)annotation; +- (void)setAttributionButtonIsHidden:(BOOL)isHidden; +- (void)setLogoIsHidden:(BOOL)isHidden; +- (void)setCompassIsHidden:(BOOL)isHidden; +- (void)setUserLocationVerticalAlignment:(MGLAnnotationVerticalAlignment) aligment; +- (void)setContentInset:(UIEdgeInsets)contentInset; + +// Imperative methods +- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinates zoomLevel:(double)zoomLevel direction:(double)direction animated:(BOOL)animated completionHandler:(void (^)())callback; +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))handler; +- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)padding animated:(BOOL)animated; +- (void)selectAnnotation:(NSString*)selectedId animated:(BOOL)animated; + +// Annotation management +- (void)upsertAnnotation:(NSObject *)annotation; - (void)removeAnnotation:(NSString*)selectedIdentifier; - (void)removeAllAnnotations; + +// Getters - (MGLCoordinateBounds)visibleCoordinateBounds; -- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)padding animated:(BOOL)animated; -- (void)setAttributionButtonVisibility:(BOOL)isVisible; -- (void)setLogoVisibility:(BOOL)isVisible; -- (void)setCompassVisibility:(BOOL)isVisible; - (double)zoomLevel; - (double)direction; -- (void) createOfflinePack:(MGLCoordinateBounds)bounds styleURL:(NSURL*)styleURL fromZoomLevel:(double)fromZoomLevel toZoomLevel:(double)toZoomLevel name:(NSString*)name type:(NSString*)type metadata:(NSDictionary*)metadata; +- (double)pitch; +- (MGLMapCamera*)camera; - (CLLocationCoordinate2D)centerCoordinate; -@property (nonatomic) MGLAnnotationVerticalAlignment userLocationVerticalAlignment; -@property (nonatomic) UIEdgeInsets contentInset; + +// Events +@property (nonatomic, copy) RCTDirectEventBlock onRegionDidChange; +@property (nonatomic, copy) RCTDirectEventBlock onRegionWillChange; +@property (nonatomic, copy) RCTDirectEventBlock onChangeUserTrackingMode; +@property (nonatomic, copy) RCTDirectEventBlock onOpenAnnotation; +@property (nonatomic, copy) RCTDirectEventBlock onRightAnnotationTapped; +@property (nonatomic, copy) RCTDirectEventBlock onUpdateUserLocation; +@property (nonatomic, copy) RCTDirectEventBlock onTap; +@property (nonatomic, copy) RCTDirectEventBlock onLongPress; +@property (nonatomic, copy) RCTDirectEventBlock onFinishLoadingMap; +@property (nonatomic, copy) RCTDirectEventBlock onStartLoadingMap; +@property (nonatomic, copy) RCTDirectEventBlock onLocateUserFailed; @end @@ -55,7 +71,7 @@ @property (nonatomic, strong) UIButton *rightCalloutAccessory; @property (nonatomic) NSString *id; -@property (nonatomic) NSString *annotationImageURL; +@property (nonatomic) NSDictionary *annotationImageSource; @property (nonatomic) CGSize annotationImageSize; + (instancetype)annotationWithLocation:(CLLocationCoordinate2D)coordinate title:(NSString *)title subtitle:(NSString *)subtitle id:(NSString *)id; diff --git a/ios/RCTMapboxGL/RCTMapboxGL.m b/ios/RCTMapboxGL/RCTMapboxGL.m index db276f23e..1534c159b 100644 --- a/ios/RCTMapboxGL/RCTMapboxGL.m +++ b/ios/RCTMapboxGL/RCTMapboxGL.m @@ -11,10 +11,7 @@ #import "RCTEventDispatcher.h" #import "UIView+React.h" #import "RCTLog.h" - -@interface RCTMapboxGL () -@property (nonatomic) MGLOfflinePack *pack; -@end +#import "RCTMapboxGLConversions.h" @implementation RCTMapboxGL { /* Required to publish events */ @@ -24,27 +21,30 @@ @implementation RCTMapboxGL { MGLMapView *_map; /* Map properties */ - NSString *_accessToken; NSMutableDictionary *_annotations; - CLLocationCoordinate2D _centerCoordinate; + CLLocationCoordinate2D _initialCenterCoordinate; + double _initialDirection; + double _initialZoomLevel; + BOOL _zoomEnabled; BOOL _clipsToBounds; BOOL _debugActive; - double _direction; BOOL _finishedLoading; BOOL _rotateEnabled; BOOL _scrollEnabled; - BOOL _zoomEnabled; BOOL _showsUserLocation; NSURL *_styleURL; - double _zoomLevel; - UIButton *_rightCalloutAccessory; int _userTrackingMode; BOOL _attributionButton; BOOL _logo; BOOL _compass; + UIEdgeInsets _contentInset; + MGLAnnotationVerticalAlignment _userLocationVerticalAlignment; + + /* So we don't fire onChangeUserTracking mode when triggered by props */ + BOOL _isChangingUserTracking; } -RCT_EXPORT_MODULE(); +// View creation - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { @@ -58,47 +58,21 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher return self; } -- (void)setAccessToken:(NSString *)accessToken +- (void)createMapIfNeeded { - if ([accessToken isEqualToString:@"your-mapbox.com-access-token"] || [accessToken length] == 0) { - RCTLogError(@"No access token specified. Go to mapbox.com to signup and get an access token."); - } else { - _accessToken = accessToken; - [self updateMap]; + CGRect bounds = self.bounds; + if (_map || + !_styleURL || + bounds.size.width <= 0 || bounds.size.height <= 0 + ) { + return; } -} - -- (void)updateMap -{ - if (_map) { - _map.centerCoordinate = _centerCoordinate; - _map.clipsToBounds = _clipsToBounds; - _map.debugActive = _debugActive; - _map.direction = _direction; - _map.rotateEnabled = _rotateEnabled; - _map.scrollEnabled = _scrollEnabled; - _map.zoomEnabled = _zoomEnabled; - _map.showsUserLocation = _showsUserLocation; - _map.styleURL = _styleURL; - _map.zoomLevel = _zoomLevel; - _map.contentInset = _contentInset; - [_map.attributionButton setHidden:_attributionButton]; - [_map.logoView setHidden:_logo]; - [_map.compassView setHidden:_compass]; - _map.userLocationVerticalAlignment = _userLocationVerticalAlignment; - _map.userTrackingMode = _userTrackingMode; - } else { - /* We need to have a height/width specified in order to render */ - if (_accessToken && _styleURL && self.bounds.size.height > 0 && self.bounds.size.width > 0) { - [self createMap]; - } + + if (![MGLAccountManager accessToken]) { + RCTLogError(@"You need an access token to use Mapbox. Register to mapbox.com to obtain one, then run Mapbox.setAccessToken(yourToken) before mounting this component"); + return; } -} - - -- (void)createMap -{ - [MGLAccountManager setAccessToken:_accessToken]; + _map = [[MGLMapView alloc] initWithFrame:self.bounds]; _map.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _map.delegate = self; @@ -110,34 +84,32 @@ - (void)createMap singleTap.delegate = self; [_map addGestureRecognizer:singleTap]; - // Setup offline pack notification handlers. - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackProgressDidChange:) name:MGLOfflinePackProgressChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveError:) name:MGLOfflinePackErrorNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveMaximumAllowedMapboxTiles:) name:MGLOfflinePackMaximumMapboxTilesReachedNotification object:nil]; - - [self updateMap]; + _map.centerCoordinate = _initialCenterCoordinate; + _map.clipsToBounds = _clipsToBounds; + _map.debugActive = _debugActive; + _map.direction = _initialDirection; + _map.rotateEnabled = _rotateEnabled; + _map.scrollEnabled = _scrollEnabled; + _map.zoomEnabled = _zoomEnabled; + _map.showsUserLocation = _showsUserLocation; + _map.styleURL = _styleURL; + _map.zoomLevel = _initialZoomLevel; + _map.contentInset = _contentInset; + [_map.attributionButton setHidden:_attributionButton]; + [_map.logoView setHidden:_logo]; + [_map.compassView setHidden:_compass]; + _map.userLocationVerticalAlignment = _userLocationVerticalAlignment; + _isChangingUserTracking = YES; + _map.userTrackingMode = _userTrackingMode; + _isChangingUserTracking = NO; + for (NSString * annotationId in _annotations) { + [_map addAnnotation:_annotations[annotationId]]; + } + [self addSubview:_map]; [self layoutSubviews]; } --(void)createOfflinePack:(MGLCoordinateBounds)bounds styleURL:(NSURL*)styleURL fromZoomLevel:(double)fromZoomLevel toZoomLevel:(double)toZoomLevel name:(NSString*)name type:(NSString*)type metadata:(NSDictionary *)metadata -{ - - id region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL bounds:bounds fromZoomLevel:fromZoomLevel toZoomLevel:toZoomLevel]; - - NSMutableDictionary *userInfo = [metadata mutableCopy]; - userInfo[@"name"] = name; - NSData *context = [NSKeyedArchiver archivedDataWithRootObject:userInfo]; - - [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack *pack, NSError *error) { - if (error != nil) { - RCTLogError(@"Error: %@", error.localizedFailureReason); - } else { - [pack resume]; - } - }]; -} - - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; @@ -145,32 +117,41 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecogni - (void)layoutSubviews { - if (_annotations.count == 0) { - [self updateMap]; + if (!_map) { + [self createMapIfNeeded]; } _map.frame = self.bounds; } -- (void)setAnnotations:(NSMutableArray *)annotations -{ - [self performSelector:@selector(updateAnnotations:) withObject:annotations afterDelay:0.5]; -} +// Annotation management -- (void)updateAnnotations:(NSMutableArray *) annotations { - for (RCTMGLAnnotation *annotation in annotations) { - NSString *id = [annotation id]; - if ([id length] != 0) { - [_annotations setObject:annotation forKey:id]; - } else { - RCTLogError(@"field `id` is required on all annotations"); - } - [_map addAnnotation:annotation]; +- (void)upsertAnnotation:(RCTMGLAnnotation *) annotation { + NSString * identifier = [annotation id]; + if (!identifier || [identifier length] == 0) { + RCTLogError(@"field `id` is required on all annotations"); + return; + } + + RCTMGLAnnotation * oldAnnotation = [_annotations objectForKey:identifier]; + [_annotations setObject:annotation forKey:identifier]; + [_map addAnnotation:annotation]; + if (oldAnnotation) { + [_map removeAnnotation:oldAnnotation]; } } -- (void)addAnnotation:(RCTMGLAnnotation *) annotation { - [_annotations setObject:annotation forKey:[annotation id]]; - [_map addAnnotation:annotation]; +- (void)removeAnnotation:(NSString*)selectedIdentifier +{ + RCTMGLAnnotation * annotation = [_annotations objectForKey:selectedIdentifier]; + if (!annotation) { return; } + [_map removeAnnotation:annotation]; + [_annotations removeObjectForKey:selectedIdentifier]; +} + +- (void)removeAllAnnotations +{ + [_map removeAnnotations:_map.annotations]; + [_annotations removeAllObjects]; } - (CGFloat)mapView:(MGLMapView *)mapView alphaForShapeAnnotation:(RCTMGLAnnotationPolyline *)shape @@ -205,396 +186,363 @@ - (UIColor *)mapView:(MGLMapView *)mapView fillColorForPolygonAnnotation:(RCTMGL return [self getUIColorObjectFromHexString:shape.fillColor alpha:1]; } -- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate -{ - _centerCoordinate = centerCoordinate; - [self updateMap]; +- (BOOL)mapView:(RCTMapboxGL *)mapView annotationCanShowCallout:(id )annotation { + NSString *title = [(RCTMGLAnnotation *) annotation title]; + NSString *subtitle = [(RCTMGLAnnotation *) annotation subtitle]; + return ([title length] != 0 || [subtitle length] != 0); } - -- (void)setDebugActive:(BOOL)debugActive +- (UIButton *)mapView:(MGLMapView *)mapView rightCalloutAccessoryViewForAnnotation:(id )annotation; { - _debugActive = debugActive; - [self updateMap]; + if ([annotation isKindOfClass:[RCTMGLAnnotation class]]) { + UIButton *accessoryButton = [(RCTMGLAnnotation *) annotation rightCalloutAccessory]; + return accessoryButton; + } + return nil; } -- (void)setRotateEnabled:(BOOL)rotateEnabled +- (void)mapView:(MGLMapView *)mapView annotation:(id)annotation calloutAccessoryControlTapped:(UIControl *)control { - _rotateEnabled = rotateEnabled; - [self updateMap]; + if (annotation.title && annotation.subtitle) { + + NSString *id = [(RCTMGLAnnotation *) annotation id]; + + NSDictionary *event = @{ @"target": self.reactTag, + @"src": @{ @"title": annotation.title, + @"subtitle": annotation.subtitle, + @"id": id, + @"latitude": @(annotation.coordinate.latitude), + @"longitude": @(annotation.coordinate.longitude)} }; + + [_eventDispatcher sendInputEventWithName:@"onRightAnnotationTapped" body:event]; + } } -- (void)setScrollEnabled:(BOOL)scrollEnabled +- (MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id)annotation { - _scrollEnabled = scrollEnabled; - [self updateMap]; + NSDictionary *source = [(RCTMGLAnnotation *) annotation annotationImageSource]; + if (!source) { return nil; } + + CGSize imageSize = [(RCTMGLAnnotation *) annotation annotationImageSize]; + NSString *reuseIdentifier = source[@"uri"]; + MGLAnnotationImage *annotationImage = [mapView dequeueReusableAnnotationImageWithIdentifier:reuseIdentifier]; + + if (!annotationImage) { + UIImage *image = imageFromSource(source); + UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0); + [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)]; + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + annotationImage = [MGLAnnotationImage annotationImageWithImage:newImage reuseIdentifier:reuseIdentifier]; + } + + return annotationImage; } -- (void)setZoomEnabled:(BOOL)zoomEnabled +// React props + +- (void)setInitialCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate { - _zoomEnabled = zoomEnabled; - [self updateMap]; + _initialCenterCoordinate = centerCoordinate; } -- (void)setShowsUserLocation:(BOOL)showsUserLocation +- (void)setInitialZoomLevel:(double)zoomLevel { - _showsUserLocation = showsUserLocation; - [self updateMap]; + _initialZoomLevel = zoomLevel; } -- (void)setClipsToBounds:(BOOL)clipsToBounds +- (void)setInitialDirection:(double)direction { - _clipsToBounds = clipsToBounds; - [self updateMap]; + _initialDirection = direction; } -- (void)setDirection:(double)direction + +- (void)setClipsToBounds:(BOOL)clipsToBounds { - _direction = direction; - [self updateMap]; + if (_clipsToBounds == clipsToBounds) { return; } + _clipsToBounds = clipsToBounds; + if (_map) { _map.clipsToBounds = clipsToBounds; } } -- (void)setZoomLevel:(double)zoomLevel +- (void)setDebugActive:(BOOL)debugActive { - _zoomLevel = zoomLevel; - [self updateMap]; + if (_debugActive == debugActive) { return; } + _debugActive = debugActive; + if (_map) { _map.debugActive = debugActive; } } -- (void)setStyleURL:(NSURL *)styleURL +- (void)setRotateEnabled:(BOOL)rotateEnabled { - _styleURL = styleURL; - [self updateMap]; + if (_rotateEnabled == rotateEnabled) { return; } + _rotateEnabled = rotateEnabled; + if (_map) { _map.rotateEnabled = rotateEnabled; } } -- (void)setAttributionButtonVisibility:(BOOL)isVisible +- (void)setScrollEnabled:(BOOL)scrollEnabled { - _attributionButton = isVisible; - [self updateMap]; + if (_scrollEnabled == scrollEnabled) { return; } + _scrollEnabled = scrollEnabled; + if (_map) { _map.scrollEnabled; } } -- (void)setLogoVisibility:(BOOL)isVisible +- (void)setZoomEnabled:(BOOL)zoomEnabled { - _logo = isVisible; - [self updateMap]; + if (_zoomEnabled == zoomEnabled) { return; } + _zoomEnabled = zoomEnabled; + if (_map) { _map.zoomEnabled; } } -- (void)setCompassVisibility:(BOOL)isVisible +- (void)setShowsUserLocation:(BOOL)showsUserLocation { - _compass = isVisible; - [self updateMap]; + if (_showsUserLocation == showsUserLocation) { return; } + _showsUserLocation = showsUserLocation; + if (_map) { _map.showsUserLocation = showsUserLocation; } } -- (MGLCoordinateBounds) visibleCoordinateBounds +- (void)setStyleURL:(NSURL *)styleURL { - return [_map visibleCoordinateBounds]; + if (_styleURL && [styleURL isEqual:_styleURL]) { return; } + _styleURL = styleURL; + if (_map) { + _map.styleURL = styleURL; + } else { + [self createMapIfNeeded]; + } } - (void)setUserTrackingMode:(int)userTrackingMode { + if (_userTrackingMode == userTrackingMode) { return; } if (userTrackingMode > 3 || userTrackingMode < 0) { _userTrackingMode = 0; } else { _userTrackingMode = userTrackingMode; } - [self performSelector:@selector(updateMap) withObject:nil afterDelay:0.2]; + if (_map) { + _isChangingUserTracking = YES; + _map.userTrackingMode = _userTrackingMode; + _isChangingUserTracking = NO; + } } -- (void)setRightCalloutAccessory:(UIButton *)rightCalloutAccessory +- (void)setAttributionButtonIsHidden:(BOOL)isHidden { - _rightCalloutAccessory = rightCalloutAccessory; + if (_attributionButton == isHidden) { return; } + _attributionButton = isHidden; + if (_map) { _map.attributionButton.hidden = isHidden; } } --(void)setDirectionAnimated:(int)heading +- (void)setLogoIsHidden:(BOOL)isHidden { - [_map setDirection:heading animated:YES]; + if (_logo == isHidden) { return; } + _logo = isHidden; + if (_map) { _map.logoView.hidden = isHidden; } } --(void)setZoomLevelAnimated:(double)zoomLevel +- (void)setCompassIsHidden:(BOOL)isHidden { - [_map setZoomLevel:zoomLevel animated:YES]; + if (_compass == isHidden) { return; } + _compass = isHidden; + if (_map) { _map.compassView.hidden = isHidden; } } --(void)setCenterCoordinateAnimated:(CLLocationCoordinate2D)coordinates resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject +- (void)setContentInset:(UIEdgeInsets)inset { - [_map setCenterCoordinate:coordinates zoomLevel:_map.zoomLevel direction:_map.direction animated:YES completionHandler:^{ - resolve(@"DONE"); - }]; + _contentInset = inset; + if (_map) { _map.contentInset = inset; } } --(void)setCenterCoordinateZoomLevelAnimated:(CLLocationCoordinate2D)coordinates zoomLevel:(double)zoomLevel +- (void)setUserLocationVerticalAlignment:(MGLAnnotationVerticalAlignment)alignment { - [_map setCenterCoordinate:coordinates zoomLevel:zoomLevel animated:YES]; + if (_userLocationVerticalAlignment == alignment) { return; } + _userLocationVerticalAlignment = alignment; + if (_map) { _map.userLocationVerticalAlignment = alignment; } } --(void)setCameraAnimated:(MGLMapCamera *)camera withDuration:(int)duration animationTimingFunction:(CAMediaTimingFunction *)function -{ - [_map setCamera:camera withDuration:duration animationTimingFunction:function]; -} +// Getters -- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)padding animated:(BOOL)animated +- (MGLCoordinateBounds) visibleCoordinateBounds { - [_map setVisibleCoordinateBounds:bounds edgePadding:padding animated:animated]; + return [_map visibleCoordinateBounds]; } -- (void)mapView:(MGLMapView *)mapView didUpdateUserLocation:(MGLUserLocation *)userLocation; -{ - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ @"latitude": @(userLocation.coordinate.latitude), - @"longitude": @(userLocation.coordinate.longitude), - @"headingAccuracy": @(userLocation.heading.headingAccuracy), - @"magneticHeading": @(userLocation.heading.magneticHeading), - @"trueHeading": @(userLocation.heading.trueHeading), - @"isUpdating": [NSNumber numberWithBool:userLocation.isUpdating]} }; - - [_eventDispatcher sendInputEventWithName:@"onUpdateUserLocation" body:event]; +-(CLLocationCoordinate2D)centerCoordinate { + if (!_map) { return _initialCenterCoordinate; } + return _map.centerCoordinate; } +-(double)direction { + if (!_map) { return _initialDirection; } + return _map.direction; +} --(void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id)annotation -{ - if (annotation.title && annotation.subtitle) { - - NSString *id = [(RCTMGLAnnotation *) annotation id]; +-(double)pitch { + if (!_map) { return 0; } + return _map.camera.pitch; +} - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ @"title": annotation.title, - @"subtitle": annotation.subtitle, - @"id": id, - @"latitude": @(annotation.coordinate.latitude), - @"longitude": @(annotation.coordinate.longitude)} }; +-(double)zoomLevel { + if (!_map) { return _initialZoomLevel; } + return _map.zoomLevel; +} - [_eventDispatcher sendInputEventWithName:@"onOpenAnnotation" body:event]; - } +-(MGLMapCamera*)camera { + if (!_map) { return nil; } + return _map.camera; } +// Imperative methods -- (void)mapView:(RCTMapboxGL *)mapView regionDidChangeAnimated:(BOOL)animated +- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate zoomLevel:(double)zoomLevel direction:(double)direction animated:(BOOL)animated completionHandler:(void (^)())callback { - - CLLocationCoordinate2D region = _map.centerCoordinate; - - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ @"latitude": @(region.latitude), - @"longitude": @(region.longitude), - @"zoom": [NSNumber numberWithDouble:_map.zoomLevel] } }; - - [_eventDispatcher sendInputEventWithName:@"onRegionChange" body:event]; + if (!_map) { + _initialCenterCoordinate = coordinate; + _initialZoomLevel = zoomLevel; + _initialDirection = direction; + callback(); + return; + } + [_map setCenterCoordinate:coordinate + zoomLevel:zoomLevel + direction:direction + animated:animated + completionHandler:callback]; } - -- (void)mapView:(RCTMapboxGL *)mapView regionWillChangeAnimated:(BOOL)animated +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))handler { - - CLLocationCoordinate2D region = _map.centerCoordinate; - - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ @"latitude": @(region.latitude), - @"longitude": @(region.longitude), - @"zoom": [NSNumber numberWithDouble:_map.zoomLevel] } }; - - [_eventDispatcher sendInputEventWithName:@"onRegionWillChange" body:event]; + [_map setCamera: camera withDuration:duration animationTimingFunction:function completionHandler:handler]; } -- (BOOL)mapView:(RCTMapboxGL *)mapView annotationCanShowCallout:(id )annotation { - NSString *title = [(RCTMGLAnnotation *) annotation title]; - NSString *subtitle = [(RCTMGLAnnotation *) annotation subtitle]; - return ([title length] != 0 || [subtitle length] != 0); +- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)padding animated:(BOOL)animated +{ + [_map setVisibleCoordinateBounds:bounds edgePadding:padding animated:animated]; } --(CLLocationCoordinate2D)centerCoordinate { - return _map.centerCoordinate; +- (void)selectAnnotation:(NSString*)selectedId animated:(BOOL)animated; +{ + RCTMGLAnnotation * annotation = [_annotations objectForKey:selectedId]; + if (!annotation) { return; } + [_map selectAnnotation:annotation animated:animated]; } --(double)direction { - return _map.direction; -} --(double)zoomLevel { - return _map.zoomLevel; -} +// Events -- (void)selectAnnotationAnimated:(NSString*)selectedIdentifier +-(void)mapView:(MGLMapView *)mapView didChangeUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated { - [_map selectAnnotation:[_annotations objectForKey:selectedIdentifier] animated:YES]; + if (_isChangingUserTracking) { return; } + if (!_onChangeUserTrackingMode) { return; } + + _onChangeUserTrackingMode(@{ @"target": self.reactTag, + @"src": @(mode) }); } -- (void)removeAnnotation:(NSString*)selectedIdentifier +- (void)mapView:(MGLMapView *)mapView didUpdateUserLocation:(MGLUserLocation *)userLocation; { - [_map removeAnnotation:[_annotations objectForKey:selectedIdentifier]]; - [_annotations removeObjectForKey:selectedIdentifier]; + if (!_onUpdateUserLocation) { return; } + _onUpdateUserLocation(@{ @"target": self.reactTag, + @"src": @{ @"latitude": @(userLocation.coordinate.latitude), + @"longitude": @(userLocation.coordinate.longitude), + @"verticalAccuracy": @(userLocation.location.verticalAccuracy), + @"horizontalAccuracy": @(userLocation.location.horizontalAccuracy), + @"headingAccuracy": @(userLocation.heading.headingAccuracy), + @"magneticHeading": @(userLocation.heading.magneticHeading), + @"trueHeading": @(userLocation.heading.trueHeading), + @"isUpdating": [NSNumber numberWithBool:userLocation.isUpdating]} }); } -- (void) setContentInset:(UIEdgeInsets)inset +- (void)mapView:(MGLMapView *)mapView didFailToLocateUserWithError:(NSError *)error { - _contentInset = inset; - [self updateMap]; + if (!_onLocateUserFailed) { return; } + _onLocateUserFailed(@{ @"target": self.reactTag, + @"src": @{ @"message": [error localizedDescription] } }); } -- (void)removeAllAnnotations +-(void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id)annotation { - [_map removeAnnotations:_map.annotations]; - [_annotations removeAllObjects]; + if (!annotation.title || !annotation.subtitle) { return; } + if (!_onOpenAnnotation) { return; } + _onOpenAnnotation(@{ @"target": self.reactTag, + @"src": @{ @"title": annotation.title, + @"subtitle": annotation.subtitle, + @"id": [(RCTMGLAnnotation *) annotation id], + @"latitude": @(annotation.coordinate.latitude), + @"longitude": @(annotation.coordinate.longitude)} }); } -- (UIButton *)mapView:(MGLMapView *)mapView rightCalloutAccessoryViewForAnnotation:(id )annotation; -{ - if ([annotation isKindOfClass:[RCTMGLAnnotation class]]) { - UIButton *accessoryButton = [(RCTMGLAnnotation *) annotation rightCalloutAccessory]; - return accessoryButton; - } - return nil; -} -- (void)mapView:(MGLMapView *)mapView annotation:(id)annotation calloutAccessoryControlTapped:(UIControl *)control +- (void)mapView:(RCTMapboxGL *)mapView regionDidChangeAnimated:(BOOL)animated { - if (annotation.title && annotation.subtitle) { - - NSString *id = [(RCTMGLAnnotation *) annotation id]; + if (!_onRegionDidChange) { return; } - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ @"title": annotation.title, - @"subtitle": annotation.subtitle, - @"id": id, - @"latitude": @(annotation.coordinate.latitude), - @"longitude": @(annotation.coordinate.longitude)} }; - - [_eventDispatcher sendInputEventWithName:@"onRightAnnotationTapped" body:event]; - } + CLLocationCoordinate2D region = _map.centerCoordinate; + _onRegionDidChange(@{ @"target": self.reactTag, + @"src": @{ @"latitude": @(region.latitude), + @"longitude": @(region.longitude), + @"zoomLevel": @(_map.zoomLevel), + @"direction": @(_map.direction), + @"pitch": @(_map.camera.pitch), + @"animated": @(animated) } }); } -- (MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id)annotation -{ - NSString *url = [(RCTMGLAnnotation *) annotation annotationImageURL]; - if (!url) { return nil; } - - CGSize imageSize = [(RCTMGLAnnotation *) annotation annotationImageSize]; - MGLAnnotationImage *annotationImage = [mapView dequeueReusableAnnotationImageWithIdentifier:url]; - if (!annotationImage) { - UIImage *image = nil; - if ([url hasPrefix:@"image!"]) { - NSString* localImagePath = [url substringFromIndex:6]; - image = [UIImage imageNamed:localImagePath]; - } else { - image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:url]]]; - } - UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0); - [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)]; - UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - annotationImage = [MGLAnnotationImage annotationImageWithImage:newImage reuseIdentifier:url]; - } +- (void)mapView:(RCTMapboxGL *)mapView regionWillChangeAnimated:(BOOL)animated +{ + if (!_onRegionWillChange) { return; } - return annotationImage; + CLLocationCoordinate2D region = _map.centerCoordinate; + _onRegionWillChange(@{ @"target": self.reactTag, + @"src": @{ @"latitude": @(region.latitude), + @"longitude": @(region.longitude), + @"zoomLevel": @(_map.zoomLevel), + @"direction": @(_map.direction), + @"pitch": @(_map.camera.pitch), + @"animated": @(animated) } }); } - (void)handleSingleTap:(UITapGestureRecognizer *)sender { + if (!_onTap) { return; } + CLLocationCoordinate2D location = [_map convertPoint:[sender locationInView:_map] toCoordinateFromView:_map]; CGPoint screenCoord = [sender locationInView:_map]; - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ - @"latitude": @(location.latitude), - @"longitude": @(location.longitude), - @"screenCoordY": @(screenCoord.y), - @"screenCoordX": @(screenCoord.x) - } - }; - - [_eventDispatcher sendInputEventWithName:@"onTap" body:event]; + _onTap(@{ @"target": self.reactTag, + @"src": @{ @"latitude": @(location.latitude), + @"longitude": @(location.longitude), + @"screenCoordY": @(screenCoord.y), + @"screenCoordX": @(screenCoord.x) } }); } - (void)handleLongPress:(UITapGestureRecognizer *)sender { - if (sender.state == UIGestureRecognizerStateBegan) { - CLLocationCoordinate2D location = [_map convertPoint:[sender locationInView:_map] toCoordinateFromView:_map]; - CGPoint screenCoord = [sender locationInView:_map]; + if (!_onLongPress) { return; } + if (sender.state != UIGestureRecognizerStateBegan) { return; } + + CLLocationCoordinate2D location = [_map convertPoint:[sender locationInView:_map] toCoordinateFromView:_map]; + CGPoint screenCoord = [sender locationInView:_map]; - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ - @"latitude": @(location.latitude), - @"longitude": @(location.longitude), - @"screenCoordY": @(screenCoord.y), - @"screenCoordX": @(screenCoord.x) - } - }; - - [_eventDispatcher sendInputEventWithName:@"onLongPress" body:event]; - } + _onLongPress(@{ @"target": self.reactTag, + @"src": @{ @"latitude": @(location.latitude), + @"longitude": @(location.longitude), + @"screenCoordY": @(screenCoord.y), + @"screenCoordX": @(screenCoord.x) } }); } - (void)mapViewDidFinishLoadingMap:(MGLMapView *)mapView { - NSDictionary *event = @{ @"target": self.reactTag }; - - [_eventDispatcher sendInputEventWithName:@"onFinishLoadingMap" body:event]; + if (!_onFinishLoadingMap) { return; } + _onFinishLoadingMap(@{ @"target": self.reactTag }); } + - (void)mapViewWillStartLoadingMap:(MGLMapView *)mapView { - NSDictionary *event = @{ @"target": self.reactTag }; - - [_eventDispatcher sendInputEventWithName:@"onStartLoadingMap" body:event]; -} - -- (void)offlinePackProgressDidChange:(NSNotification *)notification { - - MGLOfflinePack *pack = notification.object; - NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; - MGLOfflinePackProgress progress = pack.progress; - - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ - @"name": userInfo[@"name"], - @"countOfResourcesCompleted": @(progress.countOfResourcesCompleted), - @"countOfResourcesExpected": @(progress.countOfResourcesExpected), - @"countOfBytesCompleted": @(progress.countOfBytesCompleted), - @"maximumResourcesExpected": @(progress.maximumResourcesExpected) - } - }; - - [_eventDispatcher sendInputEventWithName:@"onOfflineProgressDidChange" body:event]; + if (!_onStartLoadingMap) { return; } + _onStartLoadingMap(@{ @"target": self.reactTag }); } -- (void)offlinePackDidReceiveMaximumAllowedMapboxTiles:(NSNotification *)notification { - MGLOfflinePack *pack = notification.object; - NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; - uint64_t maximumCount = [notification.userInfo[MGLOfflinePackMaximumCountUserInfoKey] unsignedLongLongValue]; - - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ - @"name": userInfo[@"name"], - @"maxTiles": @(maximumCount) - } - }; - [_eventDispatcher sendInputEventWithName:@"onOfflineMaxAllowedMapboxTiles" body:event]; -} - -- (void)offlinePackDidReceiveError:(NSNotification *)notification { - MGLOfflinePack *pack = notification.object; - NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; - NSError *error = notification.userInfo[MGLOfflinePackErrorUserInfoKey]; - - NSDictionary *event = @{ @"target": self.reactTag, - @"src": @{ - @"name": userInfo[@"name"], - @"error": [error localizedDescription] - } - }; - [_eventDispatcher sendInputEventWithName:@"onOfflineDidRecieveError" body:event]; -} - - -- (void)mapView:(MGLMapView *)mapView didFailToLocateUserWithError:(NSError *)error -{ - NSDictionary *event = @{ @"target": mapView.reactTag, - @"src": @{ - @"message": [error localizedDescription] - } - }; - - [_eventDispatcher sendInputEventWithName:@"onLocateUserFailed" body:event]; -} +// Utils - (unsigned int)intFromHexString:(NSString *)hexStr { diff --git a/ios/RCTMapboxGL/RCTMapboxGLConversions.h b/ios/RCTMapboxGL/RCTMapboxGLConversions.h new file mode 100644 index 000000000..cb7d1c3fb --- /dev/null +++ b/ios/RCTMapboxGL/RCTMapboxGLConversions.h @@ -0,0 +1,13 @@ +// +// RCTMapboxGLConversions.h +// RCTMapboxGL +// +// Created by Marius Petcu on 20/06/16. +// Copyright © 2016 Mapbox. All rights reserved. +// + +UIImage *imageFromSource (NSDictionary *source); +NSObject *convertObjectToPoint (NSObject *annotationObject); +NSObject *convertObjectToPolyline (NSObject *annotationObject); +NSObject *convertObjectToPolygon (NSObject *annotationObject); +NSObject *convertToMGLAnnotation (NSDictionary *annotationObject); \ No newline at end of file diff --git a/ios/RCTMapboxGL/RCTMapboxGLConversions.m b/ios/RCTMapboxGL/RCTMapboxGLConversions.m new file mode 100644 index 000000000..5d09397ab --- /dev/null +++ b/ios/RCTMapboxGL/RCTMapboxGLConversions.m @@ -0,0 +1,231 @@ +// +// RCTMapboxGLConversions.m +// RCTMapboxGL +// +// Created by Marius Petcu on 20/06/16. +// Copyright © 2016 Mapbox. All rights reserved. +// + +#import +#import "RCTConvert+CoreLocation.h" +#import "RCTConvert+MapKit.h" +#import "RCTMapboxGL.h" + +UIImage *imageFromSource (NSDictionary *source) +{ + if (!source) { return nil; } + NSString *uri = source[@"uri"]; + if (!uri) { return nil; } + + NSURL* checkURL = [NSURL URLWithString:uri]; + if (checkURL && checkURL.scheme && checkURL.host) { + return [UIImage imageWithData:[NSData dataWithContentsOfURL:checkURL]]; + } + + return [UIImage imageNamed:uri]; +} + +NSObject *convertObjectToPoint (NSObject *annotationObject) +{ + NSString *title = @""; + if ([annotationObject valueForKey:@"title"]) { + title = [RCTConvert NSString:[annotationObject valueForKey:@"title"]]; + } + + NSString *subtitle = @""; + if ([annotationObject valueForKey:@"subtitle"]) { + subtitle = [RCTConvert NSString:[annotationObject valueForKey:@"subtitle"]]; + } + + NSString *id = @""; + if ([annotationObject valueForKey:@"id"]) { + id = [RCTConvert NSString:[annotationObject valueForKey:@"id"]]; + } + + RCTMGLAnnotation *point; + + if ([annotationObject valueForKey:@"rightCalloutAccessory"]) { + NSDictionary *rightCalloutAccessory = [annotationObject valueForKey:@"rightCalloutAccessory"]; + NSDictionary *imageSource = (NSDictionary*)rightCalloutAccessory[@"source"]; + CGFloat height = (CGFloat)[[rightCalloutAccessory valueForKey:@"height"] doubleValue]; + CGFloat width = (CGFloat)[[rightCalloutAccessory valueForKey:@"width"] doubleValue]; + + UIImage *image = imageFromSource(imageSource); + + UIButton *imageButton = [UIButton buttonWithType:UIButtonTypeCustom]; + imageButton.frame = CGRectMake(0, 0, height, width); + [imageButton setImage:image forState:UIControlStateNormal]; + + NSArray *coordinate = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; + CLLocationDegrees lat = [coordinate[0] doubleValue]; + CLLocationDegrees lng = [coordinate[1] doubleValue]; + + point = [[RCTMGLAnnotation alloc] initWithLocationRightCallout:CLLocationCoordinate2DMake(lat, lng) title:title subtitle:subtitle id:id rightCalloutAccessory:imageButton]; + + } else { + + NSArray *coordinate = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; + CLLocationDegrees lat = [coordinate[0] doubleValue]; + CLLocationDegrees lng = [coordinate[1] doubleValue]; + + point = [[RCTMGLAnnotation alloc] initWithLocation:CLLocationCoordinate2DMake(lat, lng) title:title subtitle:subtitle id:id]; + } + + if ([annotationObject valueForKey:@"annotationImage"]) { + NSDictionary *annotationImage = [annotationObject valueForKey:@"annotationImage"]; + NSDictionary *imageSource = (NSDictionary*)annotationImage[@"source"]; + CGFloat height = (CGFloat)[[annotationImage valueForKey:@"height"] doubleValue]; + CGFloat width = (CGFloat)[[annotationImage valueForKey:@"width"] doubleValue]; + if (!height || !width) { + RCTLogError(@"Height and width for image required"); + return nil; + } + CGSize annotationImageSize = CGSizeMake(width, height); + point.annotationImageSource = imageSource; + point.annotationImageSize = annotationImageSize; + } + + return point; +} + +NSObject *convertObjectToPolyline (NSObject *annotationObject) +{ + + NSString *title = @""; + if ([annotationObject valueForKey:@"title"]) { + title = [RCTConvert NSString:[annotationObject valueForKey:@"title"]]; + } + + NSString *subtitle = @""; + if ([annotationObject valueForKey:@"subtitle"]) { + subtitle = [RCTConvert NSString:[annotationObject valueForKey:@"subtitle"]]; + } + + NSString *id = @""; + if ([annotationObject valueForKey:@"id"]) { + id = [RCTConvert NSString:[annotationObject valueForKey:@"id"]]; + } + + NSString *type = @""; + if ([annotationObject valueForKey:@"type"]) { + type = [RCTConvert NSString:[annotationObject valueForKey:@"type"]]; + } + + CGFloat strokeAlpha = 1.0; + if ([annotationObject valueForKey:@"strokeAlpha"]) { + strokeAlpha = [RCTConvert CGFloat:[annotationObject valueForKey:@"strokeAlpha"]]; + } + + NSString *strokeColor = nil; + if ([annotationObject valueForKey:@"strokeColor"]) { + strokeColor = [RCTConvert NSString:[annotationObject valueForKey:@"strokeColor"]]; + } + + double strokeWidth = 3; + if ([annotationObject valueForKey:@"strokeWidth"]) { + strokeWidth = [RCTConvert double:[annotationObject valueForKey:@"strokeWidth"]]; + } + + NSArray *coordinates = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; + NSUInteger numberOfPoints = coordinates.count; + int count = 0; + CLLocationCoordinate2D *coord = malloc(sizeof(CLLocationCoordinate2D) * numberOfPoints); + + if ([annotationObject valueForKey:@"coordinates"]) { + for (int i = 0; i < [coordinates count]; i++) { + CLLocationDegrees lat = [coordinates[i][0] doubleValue]; + CLLocationDegrees lng = [coordinates[i][1] doubleValue]; + coord[count] = CLLocationCoordinate2DMake(lat, lng); + count++; + } + } + RCTMGLAnnotationPolyline *polyline = [RCTMGLAnnotationPolyline polylineAnnotation:coord strokeAlpha:strokeAlpha strokeColor:strokeColor strokeWidth:strokeWidth id:id type:@"polyline" count:count]; + free(coord); + return polyline; +} + +NSObject *convertObjectToPolygon (NSObject *annotationObject) +{ + NSString *title = @""; + if ([annotationObject valueForKey:@"title"]) { + title = [RCTConvert NSString:[annotationObject valueForKey:@"title"]]; + } + + NSString *subtitle = @""; + if ([annotationObject valueForKey:@"subtitle"]) { + subtitle = [RCTConvert NSString:[annotationObject valueForKey:@"subtitle"]]; + } + + NSString *id = @""; + if ([annotationObject valueForKey:@"id"]) { + id = [RCTConvert NSString:[annotationObject valueForKey:@"id"]]; + } + + NSString *type = @""; + if ([annotationObject valueForKey:@"type"]) { + type = [RCTConvert NSString:[annotationObject valueForKey:@"type"]]; + } + + CGFloat fillAlpha = 1.0; + if ([annotationObject valueForKey:@"fillAlpha"]) { + fillAlpha = [RCTConvert CGFloat:[annotationObject valueForKey:@"fillAlpha"]]; + } + + NSString *fillColor = @""; + if ([annotationObject valueForKey:@"fillColor"]) { + fillColor = [RCTConvert NSString:[annotationObject valueForKey:@"fillColor"]]; + } + + CGFloat strokeAlpha = 1.0; + if ([annotationObject valueForKey:@"strokeAlpha"]) { + strokeAlpha = [RCTConvert CGFloat:[annotationObject valueForKey:@"strokeAlpha"]]; + } + + NSString *strokeColor = @""; + if ([annotationObject valueForKey:@"strokeColor"]) { + strokeColor = [RCTConvert NSString:[annotationObject valueForKey:@"strokeColor"]]; + } + + NSArray *coordinates = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; + NSUInteger numberOfPoints = coordinates.count; + int count = 0; + CLLocationCoordinate2D *coord = malloc(sizeof(CLLocationCoordinate2D) * numberOfPoints); + + if ([annotationObject valueForKey:@"coordinates"]) { + for (int i = 0; i < [coordinates count]; i++) { + CLLocationDegrees lat = [coordinates[i][0] doubleValue]; + CLLocationDegrees lng = [coordinates[i][1] doubleValue]; + coord[count] = CLLocationCoordinate2DMake(lat, lng); + count++; + } + } + RCTMGLAnnotationPolygon *polygon = [RCTMGLAnnotationPolygon polygonAnnotation:coord fillAlpha:fillAlpha fillColor:fillColor strokeColor:strokeColor strokeAlpha:strokeAlpha id:id type:@"polygon" count:count]; + free(coord); + return polygon; +} + +NSObject *convertToMGLAnnotation (NSDictionary *annotationObject) +{ + if (![annotationObject valueForKey:@"type"]) { + RCTLogError(@"type point, polyline or polygon required"); + return nil; + } + + NSString *type = [RCTConvert NSString:[annotationObject valueForKey:@"type"]]; + + if ([type isEqual: @"point"]) { + return convertObjectToPoint(annotationObject); + + } else if ([type isEqual: @"polyline"]) { + return convertObjectToPolyline(annotationObject); + + + } else if ([type isEqual: @"polygon"]) { + return convertObjectToPolygon(annotationObject); + + } else { + RCTLogError(@"type point, polyline or polygon required"); + return nil; + } + +} diff --git a/ios/RCTMapboxGL/RCTMapboxGLManager.h b/ios/RCTMapboxGL/RCTMapboxGLManager.h index b89dd2f57..cb95d4b9c 100644 --- a/ios/RCTMapboxGL/RCTMapboxGLManager.h +++ b/ios/RCTMapboxGL/RCTMapboxGLManager.h @@ -8,6 +8,13 @@ #import "RCTViewManager.h" -@interface RCTMapboxGLManager : RCTViewManager - +@interface RCTMapboxGLManager : RCTViewManager { + NSMutableSet * _recentPacks; + NSMutableSet * _throttledPacks; + NSMutableArray * _packRequests; + NSMutableSet * _removedPacks; + int _throttleInterval; + BOOL _loadedPacks; + NSMutableSet * _loadingPacks; +} @end \ No newline at end of file diff --git a/ios/RCTMapboxGL/RCTMapboxGLManager.m b/ios/RCTMapboxGL/RCTMapboxGLManager.m index 6e863f693..e48a5b731 100644 --- a/ios/RCTMapboxGL/RCTMapboxGLManager.m +++ b/ios/RCTMapboxGL/RCTMapboxGLManager.m @@ -15,42 +15,66 @@ #import "RCTEventDispatcher.h" #import "UIView+React.h" #import "RCTUIManager.h" +#import "RCTMapboxGLConversions.h" @implementation RCTMapboxGLManager - -RCT_EXPORT_MODULE(); -@synthesize bridge = _bridge; - - (UIView *)view { return [[RCTMapboxGL alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } +@synthesize bridge = _bridge; + - (dispatch_queue_t)methodQueue { return _bridge.uiManager.methodQueue; } -- (NSArray *)customDirectEventTypes +RCT_EXPORT_MODULE(); + +// Props + +RCT_EXPORT_VIEW_PROPERTY(initialCenterCoordinate, CLLocationCoordinate2D); +RCT_EXPORT_VIEW_PROPERTY(initialZoomLevel, double); +RCT_EXPORT_VIEW_PROPERTY(initialDirection, double); +RCT_EXPORT_VIEW_PROPERTY(clipsToBounds, BOOL); +RCT_EXPORT_VIEW_PROPERTY(debugActive, BOOL); +RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL); +RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL); +RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL); +RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL); +RCT_EXPORT_VIEW_PROPERTY(styleURL, NSURL); +RCT_EXPORT_VIEW_PROPERTY(userTrackingMode, int); +RCT_EXPORT_VIEW_PROPERTY(attributionButtonIsHidden, BOOL); +RCT_EXPORT_VIEW_PROPERTY(logoIsHidden, BOOL); +RCT_EXPORT_VIEW_PROPERTY(compassIsHidden, BOOL); +RCT_EXPORT_VIEW_PROPERTY(userLocationVerticalAlignment, int); + +RCT_EXPORT_VIEW_PROPERTY(onRegionDidChange, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onRegionWillChange, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onChangeUserTrackingMode, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onOpenAnnotation, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onRightAnnotationTapped, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onUpdateUserLocation, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onTap, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onFinishLoadingMap, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onStartLoadingMap, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onLocateUserFailed, RCTDirectEventBlock); + +RCT_CUSTOM_VIEW_PROPERTY(contentInset, UIEdgeInsetsMake, RCTMapboxGL) { - return @[ - @"onRegionChange", - @"onRegionWillChange", - @"onOpenAnnotation", - @"onRightAnnotationTapped", - @"onUpdateUserLocation", - @"onTap", - @"onLongPress", - @"onFinishLoadingMap", - @"onStartLoadingMap", - @"onLocateUserFailed", - @"onOfflineProgressDidChange", - @"onOfflineMaxAllowedMapboxTiles", - @"onOfflineDidRecieveError" - ]; + int top = [json[0] doubleValue]; + int left = [json[3] doubleValue]; + int bottom = [json[2] doubleValue]; + int right = [json[1] doubleValue]; + UIEdgeInsets inset = UIEdgeInsetsMake(top, left, bottom, right); + view.contentInset = inset; } +// Constants + - (NSDictionary *)constantsToExport { return @{ @@ -73,292 +97,482 @@ - (NSDictionary *)constantsToExport @"center": @(MGLAnnotationVerticalAlignmentCenter), @"bottom": @(MGLAnnotationVerticalAlignmentBottom) }, - @"unknownResourceCount": @(UINT64_MAX) + @"unknownResourceCount": @(UINT64_MAX), + @"metricsEnabled": @([RCTMapboxGLManager metricsEnabled]) }; }; -RCT_EXPORT_VIEW_PROPERTY(accessToken, NSString); -RCT_EXPORT_VIEW_PROPERTY(centerCoordinate, CLLocationCoordinate2D); -RCT_EXPORT_VIEW_PROPERTY(clipsToBounds, BOOL); -RCT_EXPORT_VIEW_PROPERTY(debugActive, BOOL); -RCT_EXPORT_VIEW_PROPERTY(direction, double); -RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL); -RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL); -RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL); -RCT_EXPORT_VIEW_PROPERTY(styleURL, NSURL); -RCT_EXPORT_VIEW_PROPERTY(userTrackingMode, int); -RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL); -RCT_EXPORT_VIEW_PROPERTY(zoomLevel, double); -RCT_EXPORT_VIEW_PROPERTY(userLocationVerticalAlignment, int); +// Metrics -RCT_EXPORT_METHOD(getCenterCoordinateZoomLevel:(nonnull NSNumber *)reactTag - callback:(RCTResponseSenderBlock)callback) ++ (BOOL)metricsEnabled { - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - NSMutableDictionary* callbackDict = [NSMutableDictionary new]; - CLLocationCoordinate2D region = [mapView centerCoordinate]; - double zoom = [mapView zoomLevel]; - - [callbackDict setValue:@(region.latitude) forKey:@"latitude"]; - [callbackDict setValue:@(region.longitude) forKey:@"longitude"]; - [callbackDict setValue:@(region.longitude) forKey:@"longitude"]; - [callbackDict setValue:@(zoom) forKey:@"zoom"]; - - callback(@[callbackDict]); - }]; + NSUserDefaults * ud = [NSUserDefaults standardUserDefaults]; + NSNumber * nr = [ud valueForKey:@"MGLMapboxMetricsEnabled"]; + if (!nr || ![nr isKindOfClass:[NSNumber class]]) { + return YES; + } + return nr.boolValue; } -RCT_EXPORT_METHOD(getBounds:(nonnull NSNumber *)reactTag - callback:(RCTResponseSenderBlock)callback) +RCT_EXPORT_METHOD(setMetricsEnabled:(BOOL)enabled) { - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - MGLCoordinateBounds bounds = [mapView visibleCoordinateBounds]; - NSMutableArray *callbackArray = [[NSMutableArray alloc] init]; - - [callbackArray addObject:@(bounds.sw.latitude)]; - [callbackArray addObject:@(bounds.sw.longitude)]; - [callbackArray addObject:@(bounds.ne.latitude)]; - [callbackArray addObject:@(bounds.ne.longitude)]; - - callback(@[callbackArray]); - }]; + [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:@"MGLMapboxMetricsEnabled"]; } -RCT_EXPORT_METHOD(getDirection:(nonnull NSNumber *)reactTag - callback:(RCTResponseSenderBlock)callback) -{ - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - NSMutableDictionary* callbackDict = [NSMutableDictionary new]; - double direction = [mapView direction]; - - [callbackDict setValue:@(direction) forKey:@"direction"]; +// Access token - callback(@[callbackDict]); - }]; -} - -RCT_CUSTOM_VIEW_PROPERTY(annotations, CLLocationCoordinate2D, RCTMapboxGL) +RCT_EXPORT_METHOD(setAccessToken:(nonnull NSString *)accessToken) { - if ([json isKindOfClass:[NSArray class]]) { - NSMutableArray* annotations = [NSMutableArray array]; - id annotationObject; - NSEnumerator *enumerator = [json objectEnumerator]; - [view removeAllAnnotations]; - - while (annotationObject = [enumerator nextObject]) { - CLLocationCoordinate2D coordinate = [RCTConvert CLLocationCoordinate2D:annotationObject]; - if (CLLocationCoordinate2DIsValid(coordinate)){ - [annotations addObject:convertToMGLAnnotation(annotationObject)]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if (!accessToken || ![accessToken length] || [accessToken isEqual:@"your-mapbox.com-access-token"]) { + return; } - - view.annotations = annotations; - } + [MGLAccountManager setAccessToken:accessToken]; + }); } -RCT_CUSTOM_VIEW_PROPERTY(contentInset, UIEdgeInsetsMake, RCTMapboxGL) +// Offline + +- (id)init { - int top = [json[0] doubleValue]; - int left = [json[3] doubleValue]; - int bottom = [json[2] doubleValue]; - int right = [json[1] doubleValue]; - UIEdgeInsets inset = UIEdgeInsetsMake(top, left, bottom, right); - view.contentInset = inset; + if (!(self = [super init])) { return nil; } + + _recentPacks = [NSMutableSet new]; + _throttledPacks = [NSMutableSet new]; + _removedPacks = [NSMutableSet new]; + _throttleInterval = 300; + + _loadingPacks = [NSMutableSet new]; + _loadedPacks = NO; + + // Setup pack array loading notifications + [[MGLOfflineStorage sharedOfflineStorage] addObserver:self forKeyPath:@"packs" options:0 context:NULL]; + _packRequests = [NSMutableArray new]; + + // Setup offline pack notification handlers. + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackProgressDidChange:) name:MGLOfflinePackProgressChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveError:) name:MGLOfflinePackErrorNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveMaximumAllowedMapboxTiles:) name:MGLOfflinePackMaximumMapboxTilesReachedNotification object:nil]; + + return self; } -RCT_CUSTOM_VIEW_PROPERTY(attributionButtonIsHidden, BOOL, RCTMapboxGL) +- (void)dealloc { - BOOL value = [json boolValue]; - [view setAttributionButtonVisibility:value ? true : false]; + [[MGLOfflineStorage sharedOfflineStorage] removeObserver:self forKeyPath:@"packs"]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; } -RCT_CUSTOM_VIEW_PROPERTY(logoIsHidden, BOOL, RCTMapboxGL) +- (void)offlinePacksDidFinishLoading { - BOOL value = [json boolValue]; - [view setLogoVisibility:value ? true : false]; + _loadedPacks = YES; + + NSArray * packs = [MGLOfflineStorage sharedOfflineStorage].packs; + + if ([_packRequests count]) { + NSArray * callbackArray = [self serializePacksArray:packs]; + for (RCTPromiseResolveBlock callback in _packRequests) { + callback(callbackArray); + } + [_packRequests removeAllObjects]; + } + + for (MGLOfflinePack * pack in packs) { + [pack resume]; + } } -RCT_CUSTOM_VIEW_PROPERTY(compassIsHidden, BOOL, RCTMapboxGL) +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { - BOOL value = [json boolValue]; - [view setCompassVisibility:value ? true : false]; + NSNumber * changeKind = change[NSKeyValueChangeKindKey]; + if (changeKind == [NSNull null]) { return; } + if ([changeKind integerValue] != NSKeyValueChangeSetting) { return; } + + NSArray * packs = [[MGLOfflineStorage sharedOfflineStorage] packs]; + + if (!packs) { return; } + if (_loadedPacks) { return; } + + [_loadingPacks addObjectsFromArray:packs]; + + for (MGLOfflinePack * pack in packs) { + [pack requestProgress]; + } + + if (!packs.count) { + [self offlinePacksDidFinishLoading]; + } } -RCT_EXPORT_METHOD(addPackForRegion:(nonnull NSNumber *)reactTag - options:(NSDictionary*)options) -{ +- (void)firePackProgress:(MGLOfflinePack*)pack { + NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; + MGLOfflinePackProgress progress = pack.progress; + + NSDictionary *event = @{ @"name": userInfo[@"name"], + @"metadata": userInfo[@"metadata"], + @"countOfResourcesCompleted": @(progress.countOfResourcesCompleted), + @"countOfResourcesExpected": @(progress.countOfResourcesExpected), + @"countOfBytesCompleted": @(progress.countOfBytesCompleted), + @"maximumResourcesExpected": @(progress.maximumResourcesExpected) }; + + [_bridge.eventDispatcher sendAppEventWithName:@"MapboxOfflineProgressDidChange" body:event]; + + [_recentPacks addObject:pack]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, _throttleInterval * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + [_recentPacks removeObject:pack]; + if ([_throttledPacks containsObject:pack]) { + [_throttledPacks removeObject:pack]; + [self firePackProgress:pack]; + } + }); +} - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { +- (void)flushThrottleForPack:(MGLOfflinePack*)pack { + if ([_throttledPacks containsObject:pack]) { + [_throttledPacks removeObject:pack]; + [self firePackProgress:pack]; + } +} - if ([options objectForKey:@"name"] == nil) { - return RCTLogError(@"Name is required."); - } - if ([options objectForKey:@"minZoomLevel"] == nil) { - return RCTLogError(@"minZoomLevel is required."); - } - if ([options objectForKey:@"maxZoomLevel"] == nil) { - return RCTLogError(@"maxZoomLevel is required."); - } - if ([options objectForKey:@"bounds"] == nil) { - return RCTLogError(@"bounds is required."); - } - if ([options objectForKey:@"styleURL"] == nil) { - return RCTLogError(@"styleURL is required."); - } - if ([options objectForKey:@"metadata"] == nil) { - return RCTLogError(@"metadata is required."); - } - if (!([[options objectForKey:@"type"] isEqualToString:@"bbox"])) { - return RCTLogError(@"Offline type %@ not supported. Only type `bbox` supported.", [options valueForKey:@"type"]); - } +- (void)discardThrottleForPack:(MGLOfflinePack*)pack { + if ([_throttledPacks containsObject:pack]) { + [_throttledPacks removeObject:pack]; + } +} - NSArray *b = [options valueForKey:@"bounds"]; - MGLCoordinateBounds bounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake([b[0] floatValue], [b[1] floatValue]), CLLocationCoordinate2DMake([b[2] floatValue], [b[3] floatValue])); - [mapView createOfflinePack:bounds styleURL:[NSURL URLWithString:[options valueForKey:@"styleURL"]] fromZoomLevel:[[options valueForKey:@"minZoomLevel"] floatValue] toZoomLevel:[[options valueForKey:@"maxZoomLevel"] floatValue] name:[options valueForKey:@"name"] type:[options valueForKey:@"type"] metadata:[options valueForKey:@"metadata"]]; +- (void)offlinePackProgressDidChange:(NSNotification *)notification { + MGLOfflinePack *pack = notification.object; + + if (!_loadedPacks && [_loadingPacks containsObject:pack]) { + [_loadingPacks removeObject:pack]; + if ([_loadingPacks count] == 0) { + [self offlinePacksDidFinishLoading]; } - }]; + } + + if ([_removedPacks containsObject:pack]) { + return; + } + + if ([_recentPacks containsObject:pack]) { + [_throttledPacks addObject:pack]; + return; + } + + [self firePackProgress:pack]; } -RCT_EXPORT_METHOD(getPacks:(nonnull NSNumber *)reactTag - callback:(RCTResponseSenderBlock)callback) -{ - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - NSMutableArray* callbackArray = [NSMutableArray new]; +- (void)offlinePackDidReceiveMaximumAllowedMapboxTiles:(NSNotification *)notification { + MGLOfflinePack *pack = notification.object; + [self flushThrottleForPack:pack]; + NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; + uint64_t maximumCount = [notification.userInfo[MGLOfflinePackMaximumCountUserInfoKey] unsignedLongLongValue]; + + NSDictionary *event = @{ @"name": userInfo[@"name"], + @"maxTiles": @(maximumCount) }; + + [_bridge.eventDispatcher sendAppEventWithName:@"MapboxOfflineMaxAllowedTiles" body:event]; +} - MGLOfflinePack *packs = [MGLOfflineStorage sharedOfflineStorage].packs; +- (void)offlinePackDidReceiveError:(NSNotification *)notification { + MGLOfflinePack *pack = notification.object; + [self flushThrottleForPack:pack]; + NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; + NSError *error = notification.userInfo[MGLOfflinePackErrorUserInfoKey]; + + NSDictionary *event = @{ @"name": userInfo[@"name"], + @"error": [error localizedDescription] }; + + [_bridge.eventDispatcher sendAppEventWithName:@"MapboxOfflineError" body:event]; +} - for (MGLOfflinePack *pack in packs) { - NSMutableDictionary *packDict = [NSMutableDictionary new]; - NSMutableDictionary *userInfo = [[NSKeyedUnarchiver unarchiveObjectWithData:pack.context] mutableCopy]; - [packDict setObject:userInfo[@"name"] forKey:@"name"]; - [userInfo removeObjectForKey:@"name"]; - [packDict setObject:userInfo forKey:@"metadata"]; - [packDict setObject:@(pack.progress.countOfBytesCompleted) forKey:@"countOfBytesCompleted"]; - [packDict setObject:@(pack.progress.countOfResourcesCompleted) forKey:@"countOfResourcesCompleted"]; - [callbackArray addObject:packDict]; +RCT_REMAP_METHOD(addOfflinePack, + pack:(NSDictionary*)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + if (options[@"name"] == nil) { + reject(@"invalid_arguments", @"addOfflinePack(): name is required.", nil); + return; + } + if (options[@"minZoomLevel"] == nil) { + reject(@"invalid_arguments", @"addOfflinePack(): minZoomLevel is required.", nil); + return; + } + if (options[@"maxZoomLevel"] == nil) { + reject(@"invalid_arguments", @"addOfflinePack(): maxZoomLevel is required.", nil); + return; + } + if (options[@"bounds"] == nil) { + reject(@"invalid_arguments", @"addOfflinePack(): bounds is required.", nil); + return; + } + if (options[@"styleURL"] == nil) { + reject(@"invalid_arguments", @"addOfflinePack(): styleURL is required.", nil); + return; + } + if (!([options[@"type"] isEqualToString:@"bbox"])) { + reject(@"invalid_arguments", + [NSString stringWithFormat:@"addOfflinePack(): Offline type %@ not supported. Only type \"bbox\" supported.", options[@"type"]] + , nil); + return; + } + + NSArray *b = [options valueForKey:@"bounds"]; + MGLCoordinateBounds bounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake([b[0] floatValue], [b[1] floatValue]), CLLocationCoordinate2DMake([b[2] floatValue], [b[3] floatValue])); + + NSURL * styleURL = [NSURL URLWithString:[options valueForKey:@"styleURL"]]; + float fromZoomLevel = [[options valueForKey:@"minZoomLevel"] floatValue]; + float toZoomLevel = [[options valueForKey:@"maxZoomLevel"] floatValue]; + NSString * name = [options valueForKey:@"name"]; + NSString * type = [options valueForKey:@"type"]; + NSDictionary * metadata = [options valueForKey:@"metadata"]; + + dispatch_async(dispatch_get_main_queue(), ^{ + id region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL bounds:bounds fromZoomLevel:fromZoomLevel toZoomLevel:toZoomLevel]; + + NSMutableDictionary *userInfo = @{ @"name": name, + @"metadata": metadata ? metadata : [NSNull null] }; + NSData *context = [NSKeyedArchiver archivedDataWithRootObject:userInfo]; + + [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack *pack, NSError *error) { + if (error != nil) { + reject(@"add_pack_failed", error.localizedFailureReason, error); + } else { + [pack resume]; + resolve([NSNull null]); } - - callback(@[[NSNull null], callbackArray]); - } - }]; + }]; + }); } -RCT_EXPORT_METHOD(removePack:(nonnull NSNumber *)reactTag - packName:(NSString*)packName - callback:(RCTResponseSenderBlock)callback) +- (NSArray*)serializePacksArray:(NSArray*)packs { - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; + NSMutableArray* callbackArray = [NSMutableArray new]; + + for (MGLOfflinePack *pack in packs) { + NSMutableDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; + [callbackArray addObject:@{ @"name": userInfo[@"name"], + @"metadata": userInfo[@"metadata"], + @"countOfBytesCompleted": @(pack.progress.countOfBytesCompleted), + @"countOfResourcesCompleted": @(pack.progress.countOfResourcesCompleted), + @"countOfResourcesExpected": @(pack.progress.countOfResourcesExpected), + @"maximumResourcesExpected": @(pack.progress.maximumResourcesExpected) }]; + } + + return callbackArray; +} +RCT_REMAP_METHOD(getOfflinePacks, + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray* callbackArray = [NSMutableArray new]; + + if (!_loadedPacks) { + [_packRequests addObject:resolve]; + } else { MGLOfflinePack *packs = [MGLOfflineStorage sharedOfflineStorage].packs; - MGLOfflinePack *tempPack; - - for (MGLOfflinePack *pack in packs) { - NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; - if ([packName isEqualToString:userInfo[@"name"]]) { - tempPack = pack; - break; - } - } + resolve([self serializePacksArray:packs]); + } + }); +} - if (tempPack == nil) { - return callback(@[[NSNull null]]); +RCT_REMAP_METHOD(removeOfflinePack, + name:(NSString*)packName + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + MGLOfflinePack *packs = [MGLOfflineStorage sharedOfflineStorage].packs; + MGLOfflinePack *tempPack; + + for (MGLOfflinePack *pack in packs) { + NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; + if ([packName isEqualToString:userInfo[@"name"]]) { + tempPack = pack; + break; } - - NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:tempPack.context]; - + } + + if (tempPack == nil) { + return resolve(@{}); + } + + NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:tempPack.context]; + + + // Workaround for https://github.com/mapbox/mapbox-gl-native/issues/5508 + + [_removedPacks addObject:tempPack]; + [self discardThrottleForPack:tempPack]; + [tempPack suspend]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + [_removedPacks removeObject:tempPack]; [[MGLOfflineStorage sharedOfflineStorage] removePack:tempPack withCompletionHandler:^(NSError * _Nullable error) { if (error != nil) { - RCTLogError(@"Error: %@", error.localizedFailureReason); + reject(@"remove_pack_failed", error.localizedFailureReason, error); } else { - NSMutableDictionary *deletedObject = [NSMutableDictionary new]; - [deletedObject setObject:userInfo[@"name"] forKey:@"deleted"]; - callback(@[[NSNull null], deletedObject]); + resolve(@{ @"deleted": userInfo[@"name"] }); } }]; - } - }]; + }); + }); } +RCT_EXPORT_METHOD(setOfflinePackProgressThrottleInterval:(nonnull NSNumber *)milis) +{ + _throttleInterval = [milis intValue]; +} +// View methods -RCT_EXPORT_METHOD(setZoomLevelAnimated:(nonnull NSNumber *)reactTag - zoomLevel:(double)zoomLevel) +RCT_EXPORT_METHOD(spliceAnnotations:(nonnull NSNumber *)reactTag + deleteAll:(BOOL)deleteAll + toDelete:(nonnull NSArray *)toDelete + toAdd:(nonnull NSArray *)toAdd) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView setZoomLevelAnimated:zoomLevel]; + + if (deleteAll) { + [mapView removeAllAnnotations]; + } else { + for (NSString * key in toDelete) { + [mapView removeAnnotation:key]; + } + } + + for (NSObject * annotationObject in toAdd) { + [mapView upsertAnnotation:convertToMGLAnnotation(annotationObject)]; } }]; } -RCT_EXPORT_METHOD(setDirectionAnimated:(nonnull NSNumber *)reactTag - heading:(float)heading) + +RCT_EXPORT_METHOD(getCenterCoordinateZoomLevel:(nonnull NSNumber *)reactTag + callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView setDirectionAnimated:heading]; - } + CLLocationCoordinate2D region = [mapView centerCoordinate]; + double zoom = [mapView zoomLevel]; + + callback(@[ @{ @"latitude": @(region.latitude), + @"longitude": @(region.longitude), + @"zoomLevel": @(zoom) } ]); }]; } -RCT_EXPORT_METHOD(setCenterCoordinateAnimated:(nonnull NSNumber *)reactTag - latitude:(float) latitude - longitude:(float) longitude - resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(getBounds:(nonnull NSNumber *)reactTag + callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView setCenterCoordinateAnimated:CLLocationCoordinate2DMake(latitude, longitude) resolver:resolve rejecter:reject]; - } + MGLCoordinateBounds bounds = [mapView visibleCoordinateBounds]; + NSMutableArray *callbackArray = [[NSMutableArray alloc] init]; + + [callbackArray addObject:@(bounds.sw.latitude)]; + [callbackArray addObject:@(bounds.sw.longitude)]; + [callbackArray addObject:@(bounds.ne.latitude)]; + [callbackArray addObject:@(bounds.ne.longitude)]; + + callback(@[callbackArray]); }]; } -RCT_EXPORT_METHOD(setCenterCoordinateZoomLevelAnimated:(nonnull NSNumber *)reactTag - latitude:(float) latitude - longitude:(float) longitude - zoomLevel:(double)zoomLevel) +RCT_EXPORT_METHOD(getDirection:(nonnull NSNumber *)reactTag + callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView setCenterCoordinateZoomLevelAnimated:CLLocationCoordinate2DMake(latitude, longitude) zoomLevel:zoomLevel]; - } + double direction = [mapView direction]; + + callback(@[ @(direction) ]); }]; } -RCT_EXPORT_METHOD(setCameraAnimated:(nonnull NSNumber *)reactTag - latitude:(float) latitude - longitude:(float) longitude - fromDistance:(int) fromDistance - pitch:(int) pitch - heading:(int) heading - duration:(int) duration) +RCT_EXPORT_METHOD(getPitch:(nonnull NSNumber *)reactTag + callback:(RCTResponseSenderBlock)callback) { - CLLocationCoordinate2D center = CLLocationCoordinate2DMake(latitude, longitude); - MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:center fromDistance:fromDistance pitch:pitch heading:heading]; + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTMapboxGL *mapView = viewRegistry[reactTag]; + double pitch = [mapView pitch]; + + callback(@[ @(pitch) ]); + }]; +} +RCT_EXPORT_METHOD(easeTo:(nonnull NSNumber *)reactTag + options:(NSDictionary *)options + animated:(BOOL)animated + callback:(RCTResponseSenderBlock)callback) +{ [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView setCameraAnimated:camera withDuration:duration animationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]]; + + NSNumber * latitude = options[@"latitude"]; + NSNumber * longitude = options[@"longitude"]; + NSNumber * zoom = options[@"zoomLevel"]; + NSNumber * direction = options[@"direction"]; + NSNumber * pitch = options[@"pitch"]; + NSNumber * altitude = options[@"altitude"]; + + if (pitch && zoom) { + RCTLogError(@"Pitch and zoomLevel can't be set together with MapView.easeTo() on iOS. Use altitude instead of zoomLevel"); + return; + } + + if (zoom && altitude) { + RCTLogError(@"Altitude and zoomLevel are mutually exclusive with MapView.easeTo()"); + return; + } + + CLLocationCoordinate2D _center = (latitude && longitude) + ? CLLocationCoordinate2DMake([latitude doubleValue], [longitude doubleValue]) + : mapView.centerCoordinate; + + double _direction = direction ? [direction doubleValue] : mapView.direction; + + if (pitch || altitude) { + MGLMapCamera * oldCamera = (!pitch || !altitude) ? mapView.camera : nil; + double _altitude = altitude ? [altitude doubleValue] : oldCamera ? oldCamera.altitude : 0; + double _pitch = pitch ? [pitch doubleValue] : oldCamera ? oldCamera.pitch : 0; + + MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:_center + fromDistance:_altitude + pitch:_pitch + heading:_direction]; + + [mapView setCamera: camera + withDuration: 0.3 + animationTimingFunction: [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] + completionHandler: ^{ + callback(@[[NSNull null]]); + }]; + + } else { + double _zoomLevel = zoom ? [zoom doubleValue] : mapView.zoomLevel; + + [mapView setCenterCoordinate: _center + zoomLevel: _zoomLevel + direction: _direction + animated: animated + completionHandler: ^{ + callback(@[[NSNull null]]); + }]; + } } }]; } -RCT_EXPORT_METHOD(setVisibleCoordinateBoundsAnimated:(nonnull NSNumber *)reactTag +RCT_EXPORT_METHOD(setVisibleCoordinateBounds:(nonnull NSNumber *)reactTag latitudeSW:(float) latitudeSW longitudeSW:(float) longitudeSW latitudeNE:(float) latitudeNE @@ -366,325 +580,28 @@ - (NSDictionary *)constantsToExport paddingTop:(double) paddingTop paddingRight:(double) paddingRight paddingBottom:(double) paddingBottom - paddingLeft:(double) paddingLeft) + paddingLeft:(double) paddingLeft + animated:(BOOL) animated) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { MGLCoordinateBounds coordinatesBounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake(latitudeSW, longitudeSW), CLLocationCoordinate2DMake(latitudeNE, longitudeNE)); - [mapView setVisibleCoordinateBounds:coordinatesBounds edgePadding:UIEdgeInsetsMake(paddingTop, paddingLeft, paddingBottom, paddingRight) animated:YES]; + [mapView setVisibleCoordinateBounds:coordinatesBounds edgePadding:UIEdgeInsetsMake(paddingTop, paddingLeft, paddingBottom, paddingRight) animated:animated]; } }]; } -RCT_EXPORT_METHOD(selectAnnotationAnimated:(nonnull NSNumber *) reactTag - selectedIdentifier:(NSString*)selectedIdentifier) +RCT_EXPORT_METHOD(selectAnnotation:(nonnull NSNumber *) reactTag + selectedIdentifier:(NSString*)selectedIdentifier + animated:(BOOL)animated) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView selectAnnotationAnimated:selectedIdentifier]; + [mapView selectAnnotation:selectedIdentifier animated:animated]; } }]; } -RCT_EXPORT_METHOD(removeAnnotation:(nonnull NSNumber *) reactTag - selectedIdentifier:(NSString*)selectedIdentifier) -{ - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView removeAnnotation:selectedIdentifier]; - } - }]; -} - -RCT_EXPORT_METHOD(removeAllAnnotations:(nonnull NSNumber *) reactTag) -{ - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView removeAllAnnotations]; - } - }]; -} - -RCT_EXPORT_METHOD(updateAnnotation:(nonnull NSNumber *) reactTag - annotation:(NSDictionary *) annotation) -{ - NSString *id = [annotation valueForKey:@"id"]; - - if ([id length] != 0) { - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView removeAnnotation:id]; - [mapView addAnnotation:convertToMGLAnnotation(annotation)]; - } - }]; - } else { - RCTLogError(@"field `id` is required on all annotation"); - } -} - -RCT_EXPORT_METHOD(setUserTrackingMode:(nonnull NSNumber *) reactTag - userTrackingMode:(int)userTrackingMode) -{ - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if ([mapView isKindOfClass:[RCTMapboxGL class]]) { - [mapView setUserTrackingMode:userTrackingMode]; - } - }]; -} - -RCT_EXPORT_METHOD(addAnnotations:(nonnull NSNumber *)reactTag - annotations:(NSArray *) annotations) -{ - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTMapboxGL *mapView = viewRegistry[reactTag]; - if([mapView isKindOfClass:[RCTMapboxGL class]]) { - NSMutableArray* annotationsArray = [NSMutableArray array]; - id annotationObject; - NSEnumerator *enumerator = [annotations objectEnumerator]; - - while (annotationObject = [enumerator nextObject]) { - CLLocationCoordinate2D coordinate = [RCTConvert CLLocationCoordinate2D:annotationObject]; - if (CLLocationCoordinate2DIsValid(coordinate)){ - [annotationsArray addObject:convertToMGLAnnotation(annotationObject)]; - } - } - mapView.annotations = annotationsArray; - } - }]; -} - -NSObject *convertToMGLAnnotation (NSDictionary *annotationObject) -{ - if (![annotationObject valueForKey:@"type"]) { - RCTLogError(@"type point, polyline or polygon required"); - return nil; - } - - NSString *type = [RCTConvert NSString:[annotationObject valueForKey:@"type"]]; - - if ([type isEqual: @"point"]) { - return convertObjectToPoint(annotationObject); - - } else if ([type isEqual: @"polyline"]) { - return convertObjectToPolyline(annotationObject); - - - } else if ([type isEqual: @"polygon"]) { - return convertObjectToPolygon(annotationObject); - - } else { - RCTLogError(@"type point, polyline or polygon required"); - return nil; - } - -} - -NSObject *convertObjectToPoint (NSObject *annotationObject) -{ - NSString *title = @""; - if ([annotationObject valueForKey:@"title"]) { - title = [RCTConvert NSString:[annotationObject valueForKey:@"title"]]; - } - - NSString *subtitle = @""; - if ([annotationObject valueForKey:@"subtitle"]) { - subtitle = [RCTConvert NSString:[annotationObject valueForKey:@"subtitle"]]; - } - - NSString *id = @""; - if ([annotationObject valueForKey:@"id"]) { - id = [RCTConvert NSString:[annotationObject valueForKey:@"id"]]; - } - - if ([annotationObject valueForKey:@"rightCalloutAccessory"]) { - NSObject *rightCalloutAccessory = [annotationObject valueForKey:@"rightCalloutAccessory"]; - NSString *url = [rightCalloutAccessory valueForKey:@"url"]; - CGFloat height = (CGFloat)[[rightCalloutAccessory valueForKey:@"height"] floatValue]; - CGFloat width = (CGFloat)[[rightCalloutAccessory valueForKey:@"width"] floatValue]; - - UIImage *image = nil; - - if ([url hasPrefix:@"image!"]) { - NSString* localImagePath = [url substringFromIndex:6]; - image = [UIImage imageNamed:localImagePath]; - } - - NSURL* checkURL = [NSURL URLWithString:url]; - if (checkURL && checkURL.scheme && checkURL.host) { - image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:url]]]; - } - - UIButton *imageButton = [UIButton buttonWithType:UIButtonTypeCustom]; - imageButton.frame = CGRectMake(0, 0, height, width); - [imageButton setImage:image forState:UIControlStateNormal]; - - NSArray *coordinate = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; - CLLocationDegrees lat = [coordinate[0] doubleValue]; - CLLocationDegrees lng = [coordinate[1] doubleValue]; - - RCTMGLAnnotation *pin = [[RCTMGLAnnotation alloc] initWithLocationRightCallout:CLLocationCoordinate2DMake(lat, lng) title:title subtitle:subtitle id:id rightCalloutAccessory:imageButton]; - - if ([annotationObject valueForKey:@"annotationImage"]) { - NSObject *annotationImage = [annotationObject valueForKey:@"annotationImage"]; - NSString *annotationImageURL = [annotationImage valueForKey:@"url"]; - CGFloat height = (CGFloat)[[annotationImage valueForKey:@"height"] floatValue]; - CGFloat width = (CGFloat)[[annotationImage valueForKey:@"width"] floatValue]; - if (!height || !width) { - RCTLogError(@"Height and width for image required"); - return nil; - } - CGSize annotationImageSize = CGSizeMake(width, height); - pin.annotationImageURL = annotationImageURL; - pin.annotationImageSize = annotationImageSize; - } - - return pin; - - } else { - - NSArray *coordinate = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; - CLLocationDegrees lat = [coordinate[0] doubleValue]; - CLLocationDegrees lng = [coordinate[1] doubleValue]; - - RCTMGLAnnotation *point = [[RCTMGLAnnotation alloc] initWithLocation:CLLocationCoordinate2DMake(lat, lng) title:title subtitle:subtitle id:id]; - - if ([annotationObject valueForKey:@"annotationImage"]) { - NSObject *annotationImage = [annotationObject valueForKey:@"annotationImage"]; - NSString *annotationImageURL = [annotationImage valueForKey:@"url"]; - CGFloat height = (CGFloat)[[annotationImage valueForKey:@"height"] floatValue]; - CGFloat width = (CGFloat)[[annotationImage valueForKey:@"width"] floatValue]; - if (!height || !width) { - RCTLogError(@"Height and width for image required"); - return nil; - } - CGSize annotationImageSize = CGSizeMake(width, height); - point.annotationImageURL = annotationImageURL; - point.annotationImageSize = annotationImageSize; - } - - return point; - } -} - -NSObject *convertObjectToPolyline (NSObject *annotationObject) -{ - - NSString *title = @""; - if ([annotationObject valueForKey:@"title"]) { - title = [RCTConvert NSString:[annotationObject valueForKey:@"title"]]; - } - - NSString *subtitle = @""; - if ([annotationObject valueForKey:@"subtitle"]) { - subtitle = [RCTConvert NSString:[annotationObject valueForKey:@"subtitle"]]; - } - - NSString *id = @""; - if ([annotationObject valueForKey:@"id"]) { - id = [RCTConvert NSString:[annotationObject valueForKey:@"id"]]; - } - - NSString *type = @""; - if ([annotationObject valueForKey:@"type"]) { - type = [RCTConvert NSString:[annotationObject valueForKey:@"type"]]; - } - - CGFloat strokeAlpha = 1.0; - if ([annotationObject valueForKey:@"strokeAlpha"]) { - strokeAlpha = [RCTConvert CGFloat:[annotationObject valueForKey:@"strokeAlpha"]]; - } - - NSString *strokeColor = nil; - if ([annotationObject valueForKey:@"strokeColor"]) { - strokeColor = [RCTConvert NSString:[annotationObject valueForKey:@"strokeColor"]]; - } - - double strokeWidth = 3; - if ([annotationObject valueForKey:@"strokeWidth"]) { - strokeWidth = [RCTConvert double:[annotationObject valueForKey:@"strokeWidth"]]; - } - - NSArray *coordinates = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; - NSUInteger numberOfPoints = coordinates.count; - int count = 0; - CLLocationCoordinate2D *coord = malloc(sizeof(CLLocationCoordinate2D) * numberOfPoints); - - if ([annotationObject valueForKey:@"coordinates"]) { - for (int i = 0; i < [coordinates count]; i++) { - CLLocationDegrees lat = [coordinates[i][0] doubleValue]; - CLLocationDegrees lng = [coordinates[i][1] doubleValue]; - coord[count] = CLLocationCoordinate2DMake(lat, lng); - count++; - } - } - RCTMGLAnnotationPolyline *polyline = [RCTMGLAnnotationPolyline polylineAnnotation:coord strokeAlpha:strokeAlpha strokeColor:strokeColor strokeWidth:strokeWidth id:id type:@"polyline" count:count]; - free(coord); - return polyline; -} - -NSObject *convertObjectToPolygon (NSObject *annotationObject) -{ - NSString *title = @""; - if ([annotationObject valueForKey:@"title"]) { - title = [RCTConvert NSString:[annotationObject valueForKey:@"title"]]; - } - - NSString *subtitle = @""; - if ([annotationObject valueForKey:@"subtitle"]) { - subtitle = [RCTConvert NSString:[annotationObject valueForKey:@"subtitle"]]; - } - - NSString *id = @""; - if ([annotationObject valueForKey:@"id"]) { - id = [RCTConvert NSString:[annotationObject valueForKey:@"id"]]; - } - - NSString *type = @""; - if ([annotationObject valueForKey:@"type"]) { - type = [RCTConvert NSString:[annotationObject valueForKey:@"type"]]; - } - - CGFloat fillAlpha = 1.0; - if ([annotationObject valueForKey:@"fillAlpha"]) { - fillAlpha = [RCTConvert CGFloat:[annotationObject valueForKey:@"fillAlpha"]]; - } - - NSString *fillColor = @""; - if ([annotationObject valueForKey:@"fillColor"]) { - fillColor = [RCTConvert NSString:[annotationObject valueForKey:@"fillColor"]]; - } - - CGFloat strokeAlpha = 1.0; - if ([annotationObject valueForKey:@"strokeAlpha"]) { - strokeAlpha = [RCTConvert CGFloat:[annotationObject valueForKey:@"strokeAlpha"]]; - } - - NSString *strokeColor = @""; - if ([annotationObject valueForKey:@"strokeColor"]) { - strokeColor = [RCTConvert NSString:[annotationObject valueForKey:@"strokeColor"]]; - } - - NSArray *coordinates = [RCTConvert NSArray:[annotationObject valueForKey:@"coordinates"]]; - NSUInteger numberOfPoints = coordinates.count; - int count = 0; - CLLocationCoordinate2D *coord = malloc(sizeof(CLLocationCoordinate2D) * numberOfPoints); - - if ([annotationObject valueForKey:@"coordinates"]) { - for (int i = 0; i < [coordinates count]; i++) { - CLLocationDegrees lat = [coordinates[i][0] doubleValue]; - CLLocationDegrees lng = [coordinates[i][1] doubleValue]; - coord[count] = CLLocationCoordinate2DMake(lat, lng); - count++; - } - } - RCTMGLAnnotationPolygon *polygon = [RCTMGLAnnotationPolygon polygonAnnotation:coord fillAlpha:fillAlpha fillColor:fillColor strokeColor:strokeColor strokeAlpha:strokeAlpha id:id type:@"polygon" count:count]; - free(coord); - return polygon; -} @end diff --git a/ios/example.js b/ios/example.js deleted file mode 100644 index 91cb9c590..000000000 --- a/ios/example.js +++ /dev/null @@ -1,230 +0,0 @@ -'use strict'; - -import React, { Component } from 'react'; -var Mapbox = require('react-native-mapbox-gl'); -var mapRef = 'mapRef'; -import { - AppRegistry, - StyleSheet, - Text, - StatusBar, - View -} from 'react-native'; - -var MapExample = React.createClass({ - mixins: [Mapbox.Mixin], - getInitialState() { - return { - center: { - latitude: 40.72052634, - longitude: -73.97686958312988 - }, - zoom: 11, - annotations: [{ - coordinates: [40.72052634, -73.97686958312988], - 'type': 'point', - title: 'This is marker 1', - subtitle: 'It has a rightCalloutAccessory too', - rightCalloutAccessory: { - url: 'https://cldup.com/9Lp0EaBw5s.png', - height: 25, - width: 25 - }, - annotationImage: { - url: 'https://cldup.com/CnRLZem9k9.png', - height: 25, - width: 25 - }, - id: 'marker1' - }, { - coordinates: [40.714541341726175,-74.00579452514648], - 'type': 'point', - title: 'Important!', - subtitle: 'Neat, this is a custom annotation image', - annotationImage: { - url: 'https://cldup.com/7NLZklp8zS.png', - height: 25, - width: 25 - }, - id: 'marker2' - }, { - 'coordinates': [[40.76572150042782,-73.99429321289062],[40.743485405490695, -74.00218963623047],[40.728266950429735,-74.00218963623047],[40.728266950429735,-73.99154663085938],[40.73633186448861,-73.98983001708984],[40.74465591168391,-73.98914337158203],[40.749337730454826,-73.9870834350586]], - 'type': 'polyline', - 'strokeColor': '#00FB00', - 'strokeWidth': 4, - 'strokeAlpha': .5, - 'id': 'foobar' - }, { - 'coordinates': [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], - 'type': 'polygon', - 'fillAlpha':1, - 'strokeColor': '#fffff', - 'fillColor': 'blue', - 'id': 'zap' - }] - }; - }, - onRegionChange(location) { - this.setState({ currentZoom: location.zoom }); - }, - onRegionWillChange(location) { - console.log(location); - }, - onUpdateUserLocation(location) { - console.log(location); - }, - onOpenAnnotation(annotation) { - console.log(annotation); - }, - onRightAnnotationTapped(e) { - console.log(e); - }, - onLongPress(location) { - console.log('long pressed', location); - }, - onTap(location) { - console.log('tapped', location); - }, - onOfflineProgressDidChange(progress) { - console.log(progress); - }, - onOfflineMaxAllowedMapboxTiles(hitLimit) { - console.log(hitLimit); - }, - render() { - StatusBar.setHidden(true); - return ( - - this.setDirectionAnimated(mapRef, 0)}> - Set direction to 0 - - this.setZoomLevelAnimated(mapRef, 6)}> - Zoom out to zoom level 6 - - this.setCenterCoordinateAnimated(mapRef, 48.8589, 2.3447)}> - Go to Paris at current zoom level {parseInt(this.state.currentZoom)} - - this.setCenterCoordinateZoomLevelAnimated(mapRef, 35.68829, 139.77492, 14)}> - Go to Tokyo at fixed zoom level 14 - - this.addAnnotations(mapRef, [{ - coordinates: [40.73312,-73.989], - type: 'point', - title: 'This is a new marker', - id: 'foo' - }, { - 'coordinates': [[40.749857912194386, -73.96820068359375], [40.741924698522055,-73.9735221862793], [40.735681504432264,-73.97523880004883], [40.7315190495212,-73.97438049316406], [40.729177554196376,-73.97180557250975], [40.72345355209305,-73.97438049316406], [40.719290332250544,-73.97455215454102], [40.71369559554873,-73.97729873657227], [40.71200407096382,-73.97850036621094], [40.71031250340588,-73.98691177368163], [40.71031250340588,-73.99154663085938]], - 'type': 'polygon', - 'fillAlpha': 1, - 'fillColor': '#000', - 'strokeAlpha': 1, - 'id': 'new-black-polygon' - }])}> - Add new marker - - this.updateAnnotation(mapRef, { - coordinates: [40.714541341726175,-74.00579452514648], - 'type': 'point', - title: 'New Title!', - subtitle: 'New Subtitle', - annotationImage: { - url: 'https://cldup.com/7NLZklp8zS.png', - height: 25, - width: 25 - }, - id: 'marker2' - })}> - Update marker2 - - this.selectAnnotationAnimated(mapRef, 'marker1')}> - Open marker1 popup - - this.removeAnnotation(mapRef, 'marker2')}> - Remove marker2 annotation - - this.removeAllAnnotations(mapRef)}> - Remove all annotations - - this.setVisibleCoordinateBoundsAnimated(mapRef, 40.712, -74.227, 40.774, -74.125, 100, 0, 0, 0)}> - Set visible bounds to 40.7, -74.2, 40.7, -74.1 - - this.setUserTrackingMode(mapRef, this.userTrackingMode.follow)}> - Set userTrackingMode to follow - - this.getCenterCoordinateZoomLevel(mapRef, (location)=> { - console.log(location); - })}> - Get location - - this.getDirection(mapRef, (direction)=> { - console.log(direction); - })}> - Get direction - - this.getBounds(mapRef, (bounds)=> { - console.log(bounds); - })}> - Get bounds - - this.addPackForRegion(mapRef, { - name: 'test', - type: 'bbox', - bounds: [0, 0, 0, 0], - minZoomLevel: 0, - maxZoomLevel: 0, - metadata: {}, - styleURL: this.mapStyles.emerald - })}> - Create offline pack - - this.getPacks(mapRef, (err, packs)=> { - if (err) console.log(err); - console.log(packs); - })}> - Get offline packs - - this.removePack(mapRef, 'test', (err, info)=> { - if (err) console.log(err); - if (info) { - console.log('Deleted', info.deleted); - } else { - console.log('No packs to delete'); - } - })}> - Remove pack with name 'test' - - - - ); - } -}); - -var styles = StyleSheet.create({ - container: { - flex: 1 - } -}); - -AppRegistry.registerComponent('your-app-name', () => MapExample); diff --git a/ios/install.md b/ios/install.md index 2268b2b89..cc129f657 100644 --- a/ios/install.md +++ b/ios/install.md @@ -55,6 +55,6 @@ React Native Mapbox GL doesn't support iOS version less than 8.0. Under **Target ![](https://dl.dropboxusercontent.com/s/yu3zyjy59p44cxb/2016-03-14%20at%201.15%20PM.png) -### 6: Add to project, [see example](./example.js) +### 6: Add to project, [see example](../example.js) If you already have an iOS Simulator running from before you followed these steps, you'll need to rebuild the project from XCode - automatic refresh won't bring in the changes you made to this build process. diff --git a/package.json b/package.json index c8cf83ca1..9fc48af52 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-mapbox-gl", "description": "A Mapbox GL react native module for creating custom maps", - "version": "4.1.1", + "version": "5.0.0", "author": "Bobby Sudekum", "keywords": [ "gl", @@ -22,9 +22,9 @@ "url": "https://github.com/mapbox/react-native-mapbox-gl" }, "scripts": { - "preinstall": "node ./scripts/download-mapbox-gl-native-ios-if-on-mac.js 3.2.0", + "preinstall": "node ./scripts/download-mapbox-gl-native-ios-if-on-mac.js 3.3.1", "test": "npm run lint", - "lint": "eslint --no-eslintrc -c .eslintrc index.ios.js index.android.js ios/example.js android/example.js" + "lint": "eslint --no-eslintrc -c .eslintrc index.js example.js" }, "devDependencies": { "babel-eslint": "^4.1.6", @@ -33,6 +33,7 @@ "eslint-plugin-react": "^3.11.3" }, "dependencies": { - "babel-eslint": "^4.1.6" + "babel-eslint": "^4.1.6", + "lodash": "^4.13.1" } } diff --git a/readme.md b/readme.md index 32b58c855..3e87f5ab9 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ This project is **experimental**. Mapbox does not officially support React Nativ * node * npm -* [React Native](https://facebook.github.io/react-native/) >= 0.15.0 +* [React Native](https://facebook.github.io/react-native/) >= 0.19.0 ``` npm install react-native-mapbox-gl --save @@ -25,12 +25,10 @@ npm install react-native-mapbox-gl --save or with [CocoaPods](/ios/install-cocoapods.md) ## API -* [Android](/android/API.md) -* [iOS](/ios/API.md) +* [API Documentation](/API.md) ## Example -* [Android](/android/example.js) -* [iOS](/ios//example.js) +* [See example](/example.js) ![](http://i.imgur.com/I8XkXcS.jpg) ![](https://cldup.com/A8S_7rLg1L.png) diff --git a/scripts/download-mapbox-gl-native-ios-if-on-mac.js b/scripts/download-mapbox-gl-native-ios-if-on-mac.js index 0cf0e37fa..6cae70885 100644 --- a/scripts/download-mapbox-gl-native-ios-if-on-mac.js +++ b/scripts/download-mapbox-gl-native-ios-if-on-mac.js @@ -1,9 +1,12 @@ +#!/usr/bin/env node + var version = process.argv[2]; +var path = require('path'); // only download iOS SDK if on Mac OS if (process.platform === 'darwin') { var exec = require('child_process').exec; - var cmd = './scripts/download-mapbox-gl-native-ios.sh ' + version; + var cmd = path.join(__dirname, 'download-mapbox-gl-native-ios.sh') + ' ' + version; exec(cmd, function(error, stdout, stderr) { if (error) { console.error(error);