NativeScript Mapbox plugin
Awesome native OpenGL-powered maps - by Mapbox
There is a NativeScript Core Modules bug under Android that causes random crashes on navigation. See ./demo-angular/README.md for a workaround.
https://github.com/NativeScript/NativeScript/issues/7954
https://github.com/NativeScript/NativeScript/issues/7867
Before you begin - Prerequisites
You either need your own tile server such as the one provided by openmaptiles.org or a Mapbox API access token (they have a 🆓 Starter plan!), so sign up with Mapbox.
Once you've registered go to your Account > Apps > New token. The 'Default Secret Token' is what you'll need.
You will also need to set up your development environment. Please refer to the NativeScript documentation.
Installation
$ tns plugin install nativescript-mapbox
DEMOS
Two demo applications are available in the repository.
To run them, you'll need to clone the github repository and build the plugin. See below.
You will also need an access token. Your access_token can then be set in the top level mapbox_config.ts file.
The style can be set to one of the Mapbox style names or it can be the URL of your own hosted tile server.
NOTE: As of this writing, the NativeScript demo only works with a mapbox token. The demo-angular will work with either a self hosted tile server or a mapbox token.
To run the Angular demo
cd src
npm run build.release
cd ../demo-angular
tns run <platform>
To run the plain Nativescript demo
cd src
npm run build.release
cd ../demo
tns run <platform>
Debug Build
To come up to speed on the plugin, I added extensive trace messages.
These can be turned on by replacing 'npm run build.release' with 'npm run build.debug' in the commands above.
Breaking Changes
This version includes breaking API changes.
The intent moving forward is to mirror, to the maximum extent practical, the Mapbox GL JS API to enable
sharing of mapping code between browser based and native applications.
Issues
If you get an error during iOS build related to Podspec versions, probably the easiest fix is:
tns platform remove ios
and tns platform add ios
.
On Android the plugin adds this to the <application>
node of app/App_Resources/Android/AndroidManifest.xml
(the plugin already attempts to do so):
<service android:name="com.mapbox.services.android.telemetry.service.TelemetryService" />
If you get an error related to TelemetryService
then please check it's there.
Usage
Demo app (XML + TypeScript)
If you want a quickstart, see the demo in this repository.
It shows you how to draw a map in XML and JS with almost all possible options.
Demo app (Angular)
There is also the beginnings of an Angular demo in demo-angular in this repository.
Declaring a map in the view
XML
You can instantiate a map from JS or TS. As the map is yet another view component it will play nice with any NativeScript layout you throw it in. You can also easily add multiple maps to the same page or to different pages in any layout you like.
A simple layout could look like this:
Could be rendered by a definition like this:
<Page xmlns="http://schemas.nativescript.org/tns.xsd" xmlns:map="nativescript-mapbox" navigatingTo="navigatingTo">
<StackLayout>
<Label text="Nice map, huh!" class="title"/>
<ContentView height="240" width="240">
<map:MapboxView
accessToken="your_token"
mapStyle="traffic_night"
latitude="52.3702160"
longitude="4.8951680"
zoomLevel="3"
showUserLocation="true"
mapReady="onMapReady">
</map:MapboxView>
</ContentView>
</StackLayout>
</Page>
Angular
Component:
import { registerElement } from "nativescript-angular/element-registry";
registerElement("Mapbox", () => require("nativescript-mapbox").MapboxView);
View:
<ContentView height="100%" width="100%">
<Mapbox
accessToken="your_token"
mapStyle="traffic_day"
latitude="50.467735"
longitude="13.427718"
hideCompass="true"
zoomLevel="18"
showUserLocation="false"
disableZoom="false"
disableRotation="false"
disableScroll="false"
disableTilt="false"
(mapReady)="onMapReady($event)">
</Mapbox>
</ContentView>
Available XML/Angular options
All currently supported options for your XML based map are (don't use other properties - if you need styling wrap the map in a ContentView
and apply things like width
to that container!):
option | default | description |
---|
accesstoken | - | see 'Prerequisites' above |
delay | 0 | A delay in milliseconds - you can set this to have better control over when Mapbox is invoked so it won't clash with other computations your app may need to perform. |
mapStyle | streets | streets, light, dark, satellite_streets, satellite, traffic_day, traffic_night, an URL starting with mapbox:// or pointing to a custom JSON definition (http://, https://, or local relative to nativescript app path ~/) |
latitude | - | Set the center of the map by passing this in |
longitude | - | .. and this as well |
zoomLevel | 0 | 0-20 |
showUserLocation | false | Requires location permissions on Android which you can remove from AndroidManifest.xml if you don't need them |
hideCompass | false | Don't show the compass in the top right corner during rotation of the map |
hideLogo | false | Mapbox requires false if you're on a free plan |
hideAttribution | true | Mapbox requires false if you're on a free plan |
disableZoom | false | Don't allow the user to zoom in or out (pinch and double-tap) |
disableRotation | false | Don't allow the user to rotate the map (two finger gesture) |
disableScroll | false | Don't allow the user to move the center of the map (one finger drag) |
disableTilt | false | Don't allow the user to tilt the map (two finger drag up or down) |
mapReady | - | The name of a callback function you can declare to interact with the map after it has been drawn |
moveBeginEvent | - | The name of a function to be called when the map is moved. |
locationPermissionGranted | - | The name of a callback function you can declare to get notified when the user granted location permissions |
locationPermissionDenied | - | The name of a callback function you can declare to get notified when the user denied location permissions (will never fire on iOS because there's nothing to deny) |
Want to add markers?
This is where that last option in the table above comes in - mapReady
.
It allows you to interact with the map after it has been drawn to the page.
Open main-page.[js|ts]
and add this (see addMarkers
further below for the full marker API):
var mapbox = require("nativescript-mapbox");
function onMapReady(args) {
var nativeMapView = args.ios ? args.ios : args.android;
console.log("Mapbox onMapReady for " + (args.ios ? "iOS" : "Android") + ", native object received: " + nativeMapView);
args.map.addMarkers([
{
lat: 52.3602160,
lng: 4.8891680,
title: 'One-line title here',
subtitle: 'Really really nice location',
selected: true,
onCalloutTap: function(){console.log("'Nice location' marker callout tapped");}
}]
);
}
exports.onMapReady = onMapReady;
.. or want to set the viewport bounds?
var mapbox = require("nativescript-mapbox");
function onMapReady(args) {
args.map.setViewport(
{
bounds: {
north: 52.4820,
east: 5.1087,
south: 52.2581,
west: 4.6816
},
animated: true
}
);
}
exports.onMapReady = onMapReady;
The methods you can invoke like this from an XML-declared map are:
addMarkers
, setViewport
, removeMarkers
, getCenter
, setCenter
, getZoomLevel
, setZoomLevel
, getViewport
, getTilt
, setTilt
, setMapStyle
, animateCamera
, addPolygon
, removePolygons
, addPolyline
, removePolylines
, getUserLocation
, trackUser
, setOnMapClickListener
, setOnMapLongClickListener
and destroy
.
Check out the usage details on the functions below.
Declaring a map programmatically
Add a container to your view XML where you want to programmatically add the map. Give it an id.
<ContentView id="mapContainer" />
show
const contentView : ContentView = <ContentView>page.getViewById( 'mapContainer' );
const settings = {
container: contentView,
accessToken: ACCESS_TOKEN,
style: MapStyle.LIGHT,
margins: {
left: 18,
right: 18,
top: isIOS ? 390 : 454,
bottom: isIOS ? 50 : 8
},
center: {
lat: 52.3702160,
lng: 4.8951680
},
zoomLevel: 9,
showUserLocation: true,
hideAttribution: true,
hideLogo: true,
hideCompass: false,
disableRotation: false,
disableScroll: false,
disableZoom: false,
disableTilt: false,
markers: [
{
id: 1,
lat: 52.3732160,
lng: 4.8941680,
title: 'Nice location',
subtitle: 'Really really nice location',
iconPath: 'res/markers/green_pin_marker.png',
onTap: () => console.log("'Nice location' marker tapped"),
onCalloutTap: () => console.log("'Nice location' marker callout tapped")
}
]
};
console.log( "main-view-model:: doShow(): creating new MapboxView." );
const mapView = new MapboxView();
mapView.on( 'mapReady', ( args : any ) => {
console.log( "main-view-model: onMapReady fired." );
this.mapboxView = args.map;
this.mapbox = this.mapboxView.getMapboxApi();
this.mapbox.setOnMapClickListener( point => {
console.log(`>> Map clicked: ${JSON.stringify(point)}`);
return true;
});
this.mapbox.setOnMapLongClickListener( point => {
console.log(`>> Map longpressed: ${JSON.stringify(point)}`);
return true;
});
this.mapbox.setOnScrollListener((point: LatLng) => {
});
this.mapbox.setOnFlingListener(() => {
console.log(`>> Map flinged"`);
}).catch( err => console.log(err) );
});
mapView.setConfig( settings );
contentView.content = mapView;
hide
All further examples assume mapbox
has been required.
Also, all functions support promises, but we're leaving out the .then()
stuff for brevity where it doesn't add value.
mapbox.hide();
unhide
If you previously called hide()
you can quickly unhide the map,
instead of redrawing it (which is a lot slower and you loose the viewport position, etc).
mapbox.unhide();
destroy 💥
To clean up the map entirely you can destroy instead of hide it:
mapbox.destroy();
setMapStyle
You can update the map style after you've loaded it.
With Mapbox Android SDK 6.1.x (used in plugin version 4.1.0) I've seen Android crash a few seconds after this has been used, so test this well and perhaps don't use it when in doubt.
mapbox.setMapStyle(mapbox.MapStyle.DARK);
addMarkers
import { MapboxMarker } from "nativescript-mapbox";
const firstMarker = <MapboxMarker>{
id: 2,
lat: 52.3602160,
lng: 4.8891680,
title: 'One-line title here',
subtitle: 'Infamous subtitle!',
icon: 'http(s)://website/coolimage.png',
iconPath: 'res/markers/home_marker.png',
selected: true,
onTap: marker => console.log("Marker tapped with title: '" + marker.title + "'"),
onCalloutTap: marker => alert("Marker callout tapped with title: '" + marker.title + "'")
};
mapbox.addMarkers([
firstMarker,
{
}
])
Updating markers
Plugin version 4.2.0 added the option to update makers. Just call update
on the MapboxMarker
reference you created above.
You can update the following properties (all but the icon really):
firstMarker.update({
lat: 52.3622160,
lng: 4.8911680,
title: 'One-line title here (UPDATE)',
subtitle: 'Updated subtitle',
selected: true,
onTap: (marker: MapboxMarker) => console.log(`UPDATED Marker tapped with title: ${marker.title}`),
onCalloutTap: (marker: MapboxMarker) => alert(`UPDATED Marker callout tapped with title: ${marker.title}`)
})
removeMarkers
You can either remove all markers by not passing in an argument,
or remove specific marker id's (which you specified previously).
mapbox.removeMarkers();
mapbox.removeMarkers([1, 2]);
setViewport
If you want to for instance make the viewport contain all markers you
can set the bounds to the lat/lng of the outermost markers using this function.
mapbox.setViewport(
{
bounds: {
north: 52.4820,
east: 5.1087,
south: 52.2581,
west: 4.6816
},
animated: true
}
)
getViewport
mapbox.getViewport().then(
function(result) {
console.log("Mapbox getViewport done, result: " + JSON.stringify(result));
}
)
setCenter
mapbox.setCenter(
{
lat: 52.3602160,
lng: 4.8891680,
animated: false
}
)
getCenter
Here the promise callback makes sense, so adding it to the example:
mapbox.getCenter().then(
function(result) {
console.log("Mapbox getCenter done, result: " + JSON.stringify(result));
},
function(error) {
console.log("mapbox getCenter error: " + error);
}
)
setZoomLevel
mapbox.setZoomLevel(
{
level: 6.5,
animated: true
}
)
getZoomLevel
mapbox.getZoomLevel().then(
function(result) {
console.log("Mapbox getZoomLevel done, result: " + JSON.stringify(result));
},
function(error) {
console.log("mapbox getZoomLevel error: " + error);
}
)
animateCamera
mapbox.animateCamera({
target: {
lat: 52.3732160,
lng: 4.8941680
},
zoomLevel: 17,
altitude: 2000,
bearing: 270,
tilt: 50,
duration: 5000
})
setTilt (Android only)
mapbox.setTilt(
{
tilt: 40,
duration: 4000
}
)
getTilt (Android only)
mapbox.getTilt().then(
function(tilt) {
console.log("Current map tilt: " + tilt);
}
)
getUserLocation
If the user's location is shown on the map you can get their coordinates and speed:
mapbox.getUserLocation().then(
function(userLocation) {
console.log("Current user location: " + userLocation.location.lat + ", " + userLocation.location.lng);
console.log("Current user speed: " + userLocation.speed);
}
)
trackUser
In case you're showing the user's location, you can have the map track the position.
The map will continuously move along with the last known location.
mapbox.trackUser({
mode: "FOLLOW_WITH_HEADING",
animated: true
});
addSource
https://docs.mapbox.com/mapbox-gl-js/api/#map#addsource
Adds a vector to GeoJSON source to the map.
mapbox.addSource( id, {
type: 'vector',
url: 'url to source'
} );
-or-
mapbox.addSource( id, {
'type': 'geojson',
'data': {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [ [ lng, lat ], [ lng, lat ], ..... ]
}
}
}
);
removeSource
Remove a source by id
mapbox.removeSource( id );
addLayer
NOTE: For version 5 the API for addLayer() has changed and is now a subset of the web-gl-js API.
https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers
To add a line:
mapbox.addLayer({
'id': someid,
'type': 'line',
'source': {
'type': 'geojson',
'data': {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [ [ lng, lat ], [ lng, lat ], ..... ]
}
}
}
},
'layout': {
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': '#ed6498',
'line-width': 5,
'line-opacity': .8,
'line-dash-array': [ 1, 1, 1, ..]
}
});
To add a circle:
mapbox.addLayer({
"id": someid,
"type": 'circle',
"radius-meters": 500,
"source": {
"type": 'geojson',
"data": {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [ lng, lat ]
}
}
},
"paint": {
"circle-radius": {
"stops": [
[0, 0],
[20, 8000 ]
],
"base": 2
},
'circle-opacity': 0.05,
'circle-color': '#ed6498',
'circle-stroke-width': 2,
'circle-stroke-color': '#ed6498'
}
});
Source may be a geojson or vector source description or may be
the id of a source added using addSource()
removeLayer
Remove a layer added with addLayer() by id.
mapbox.removeLayer( id );
addLinePoint
Dynamically add a point to a line.
mapbox.addLinePoint( <id of line layer>, lnglat )
where lnglat is an array of two points, a longitude and a latitude.
addPolygon
Draw a shape. Just connect the dots like we did as a toddler.
The first person to tweet a snowman drawn with this function gets a T-shirt.
mapbox.addPolygon(
{
id: 1,
fillColor: new Color("red"),
fillOpacity: 0.7,
strokeColor: new Color("green"),
strokeWidth: 8,
strokeOpacity: 0.5,
points: [
{
lat: 52.3923633970718,
lng: 4.902648925781249
},
{
lat: 52.35421556258807,
lng: 4.9308013916015625
},
{
lat: 52.353796172573944,
lng: 4.8799896240234375
},
{
lat: 52.3864966440161,
lng: 4.8621368408203125
},
{
lat: 52.3923633970718,
lng: 4.902648925781249
}
]
})
.then(result => console.log("Mapbox addPolygon done"))
.catch((error: string) => console.log("mapbox addPolygon error: " + error));
removePolygons
You can either remove all polygons by not passing in an argument,
or remove specific polygon id's (which you specified previously).
mapbox.removePolygons();
mapbox.removePolygons([1, 2]);
addPolyline
Deprecated. Use addLayer() instead.
Draw a polyline. Connect the points given as parameters.
mapbox.addPolyline({
id: 1,
color: '#336699',
width: 7,
opacity: 0.6,
points: [
{
'lat': 52.3833160,
'lng': 4.8991780
},
{
'lat': 52.3834160,
'lng': 4.8991880
},
{
'lat': 52.3835160,
'lng': 4.8991980
}
]
});
removePolylines
Deprecated. Use removeLayer() instead.
You can either remove all polylines by not passing in an argument,
or remove specific polyline id's (which you specified previously).
mapbox.removePolylines();
mapbox.removePolylines([1, 2]);
addSource
Add a source that can be used by addLayer
. Note only vector
type is currently supported.
mapbox.addSource(
id: "terrain-source",
type: "vector",
url: "mapbox://mapbox.mapbox-terrain-v2"
);
removeSource
Remove a source by id
.
mapbox.removeSource("terrain-source");
addLayer
Add a layer from a source to the map. Note only circle
, fill
and line
types are currently supported.
mapbox.addLayer(
id: "terrain-data",
source: "terrain-source",
sourceLayer: "contour",
type: "line",
lineJoin: "round",
lineCap: "round",
lineColor: "#ff69b4",
lineWidth: 1,
);
removeLayer
Remove a layer by id
.
mapbox.removeLayer("terrain-data");
setOnMapClickListener
Add a listener to retrieve lat and lng of where the user taps the map (not a marker).
mapbox.setOnMapClickListener((point: LatLng) => {
console.log("Map clicked at latitude: " + point.lat + ", longitude: " + point.lng);
});
setOnMapLongClickListener
Add a listener to retrieve lat and lng of where the user longpresses the map (not a marker).
mapbox.setOnMapLongClickListener((point: LatLng) => {
console.log("Map longpressed at latitude: " + point.lat + ", longitude: " + point.lng);
});
setOnScrollListener
Add a listener to retrieve lat and lng of where the user scrolls to on the map.
mapbox.setOnScrollListener((point?: LatLng) => {
console.log("Map scrolled to latitude: " + point.lat + ", longitude: " + point.lng);
});
Offline maps
For situations where you want the user to pre-load certain regions you can use these methods to create and remove offline regions.
Important read: the offline maps documentation by Mapbox.
downloadOfflineRegion
This example downloads the region 'Amsterdam' on zoom levels 9, 10 and 11 for map style 'outdoors'.
mapbox.downloadOfflineRegion(
{
accessToken: accessToken,
name: "Amsterdam",
style: mapbox.MapStyle.OUTDOORS,
minZoom: 9,
maxZoom: 11,
bounds: {
north: 52.4820,
east: 5.1087,
south: 52.2581,
west: 4.6816
},
onProgress: function (progress) {
console.log("Download progress: " + JSON.stringify(progress));
}
}
).then(
function() {
console.log("Offline region downloaded");
},
function(error) {
console.log("Download error: " + error);
}
);
Advanced example: download the current viewport
Grab the viewport with the mapbox.getViewport()
function and download it at various zoom levels:
mapbox.getViewport().then(function(viewport) {
mapbox.downloadOfflineRegion(
{
name: "LastViewport",
style: mapbox.MapStyle.LIGHT,
minZoom: viewport.zoomLevel,
maxZoom: viewport.zoomLevel + 2,
bounds: viewport.bounds,
onProgress: function (progress) {
console.log("Download %: " + progress.percentage);
}
}
);
});
listOfflineRegions
To help you manage offline regions there's a listOfflineRegions
function you can use. You can then fi. call deleteOfflineRegion
(see below) and pass in the name
to remove any cached region(s) you like.
mapbox.listOfflineRegions({
accessToken: accessToken
}).then(
function(regions) {
console.log(JSON.stringify(JSON.stringify(regions));
},
function(error) {
console.log("Error while listing offline regions: " + error);
}
);
deleteOfflineRegion
You can remove regions you've previously downloaded. Any region(s) matching the name
param will be removed locally.
mapbox.deleteOfflineRegion({
name: "Amsterdam"
}).then(
function() {
console.log("Offline region deleted");
},
function(error) {
console.log("Error while deleting an offline region: " + error);
}
);
Permissions
hasFineLocationPermission / requestFineLocationPermission
On Android 6 you need to request permission to be able to show the user's position on the map at runtime when targeting API level 23+.
Even if the uses-permission
tag for ACCESS_FINE_LOCATION
is present in AndroidManifest.xml
.
You don't need to do this with plugin version 2.4.0+ as permission is request when required while rendering the map. You're welcome :)
Note that hasFineLocationPermission
will return true when:
- You're running this on iOS, or
- You're targeting an API level lower than 23, or
- You're using Android < 6, or
- You've already granted permission.
mapbox.hasFineLocationPermission().then(
function(granted) {
console.log("Has Location Permission? " + granted);
}
);
mapbox.requestFineLocationPermission().then(
function() {
console.log("Location permission requested");
}
);
Note that the show
function will also check for permission if you passed in showUserLocation : true
.
If you didn't request permission before showing the map, and permission was needed, the plugin will ask the user permission while rendering the map.
Using marker images from the internet
If you specify icon: 'http(s)://some-remote-image'
, then on iOS you'll need to whitelist
the domain. Google for iOS ATS for detailed options, but for a quick test you can add this to
app/App_Resources/iOS/Info.plist
:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>