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

-### 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)


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);