Mapbox GL JS Bearing - mapbox

Is it possible in Mapbox GL JS to get the users bearing?
I would like to show the direction in which the user is facing, to assist them in navigating to nearby POI.
I understand that it is possible to set the bearing of the map and also get the current bearing of it, but i need the actual real life bearing of the user.
Kind of the same thing as on Google Maps:
The service is intended to run as an Ionic app on iOS and Android, and the assistance in bearing is a key feature in helping them locate nearby POI on a well populated map.

You can get the user's bearing (if their device has such a sensor) by obtaining a Coordinates object from Gelocation#getCurrentPosition() and reading Coordinates#heading.
Mapbox GL JS has no built-in user interface for displaying a user's heading. Building your own user interface is easy. See this example which uses the symbol-rotation property.

So, after some time spend on this, i thought I'd show how i ended up doing this, in case someone else needs it or have a better solution.
It seems cordova has a built in "heading" property in the position object.
https://github.com/apache/cordova-plugin-geolocation
var heading = $rootScope.position.heading;
First, i make sure that the marker is always pointing in the heading direction, even when the user turns the map, by subtracting the mapBearing(degrees the map has turned from North), from the user heading.
map.on('rotate', function(){
map.setLayoutProperty('drone', 'icon-rotate', heading - map.getBearing())
});
I create an icon, at the users position, add the source and add the layer with the source.
map.on('load', function () {
var point = {"type": "Point", "coordinates": [$rootScope.position.long, $rootScope.position.lat]};
map.addSource('drone', {type: 'geojson', data: point });
map.addLayer({
"id": "drone",
"type": "symbol",
"source": "drone"
}
});
Next i check that heading is actually available, since it only appears to return a value, when the user is moving(only tested on Android so far), and if it is, update the heading of the point.
if($rootScope.position.heading){
var heading = $rootScope.position.heading;
map.setLayoutProperty('drone', 'icon-rotate', $rootScope.position.heading);
};
Finally i update the position of the point, in a "$watch" position.
map.getSource('drone').setData(point);
This way, i can watch the users heading, and the point keeps on track, even when the user rotates the map.

For the users coming here after 2020 (what an year lol), mapbox gl js now supports geolocation which not only provides user's heading but also a bunch of other helpful data:
const geolocate = map.addControl(
new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
})
)
then listen for geolocate event:
geolocate.on('geolocate', (e) => {
console.log(e);
});
this will give you following object:
{
coords: {
accuracy: number;
altitude: number;
altitudeAccuracy: number;
heading: number;
latitude: number;
longitude: number;
speed: number;
};
timestamp: number;
heading will give you direction of the user. As the geolocate control keeps triggering automatically so can get the user's direction as well as speed and altitude etc in real time and use that to display data driven symbols on map.

Related

What is the difference between Exif GPS DestLatitude vs Latitude

In the EXIF metadata GPS schema there are two places to store GPS data:
#1-4 Latitude, LatitudeRef, Longitude and LongitudeRef
#19-23 DestLatitude, DestLatitudeRef, DestLongitude and DestLongitudeRef
In theory the first is where the photo was taken, so an iPhone will populate this data. The second are the coordinates of the object in the photo. So if you are on Westminster Bridge and taking a photo of the London Eye, you'd have two slightly different values.
Does anyone know if there's accepted usage of these properties?
Specifically, should Latitude only be set if you have GPS data from either the camera or an external logger, so this could be considered optional? But the DestLatitude would be always set on any, well organized photo collection?
The second is referenced as "destination point".
You can see those in W3 exif, prefixed by gps, which suggests it makes only sense when you record a position relative to a fixed destination point.
You can see that field used in tomchentw/react-google-maps for example:
withScriptjs, withGoogleMap,
lifecycle({
componentDidMount() {
this.setState({
onDirectionChange: () => {
const DirectionsService = new google.maps.DirectionsService();
DirectionsService.route({
origin: new google.maps.LatLng(this.props.latitude, this.props.longitude),
destination: new google.maps.LatLng(this.props.destLatitude, this.props.destLongitude),
travelMode: google.maps.TravelMode.DRIVING,
}, (result, status) => {
if (status === google.maps.DirectionsStatus.OK) {
this.setState({
directions: result
});
} else {
console.error(`error fetching directions ${result}`);
}
});
}
});
const DirectionsService = new google.maps.DirectionsService();
In that case, this is to call Google Map in order to launch an itinerary computation.

MapboxGL: querying rendered features after multiple geocodes

Situation: I have a working site where upon entering an address, MapboxGL marks a point on the map and queries a polygon layer (queryRenderedFeatures) and displays the polygon feature containing the point.
This works; however, if I then want to geocode a second address that changes the map view, it fails the second time because map.queryRenderedFeatures returns an empty array.
var userDistrictsGeoJson;
map.on('load', function() {
//add layers from Mapbox account
addLayers(); //details left out of example, but this works.
// Listen for geocoding result
// This works the first time through, but fails if the user searchs for a second address because queryRenderedFeatures is working with a smaller set of features
geocoder.on('result', function(e) {
//need to clear geojson layer and
userDistrictsGeoJson = {
"type": "FeatureCollection",
"features": []
};
map.getSource('single-point').setData(e.result.geometry);
//project to use (pixel xy coordinates instead of lat/lon for WebGL)
var point = map.project([e.result.center[0], e.result.center[1]]);
var features = map.queryRenderedFeatures(point, { layers: ['congress-old'] });
var filter = featuresOld.reduce(function(memo, feature){
// console.log(feature.properties);
memo.push(feature.properties.GEOID);
return memo;
}, ['in', 'GEOID']);
map.setFilter('user-congress-old', filter);
var userCongressOldGeoJson = map.querySourceFeatures('congressional-districts', {
sourceLayer: 'congress_old',
filter: map.getFilter('user-congress-old')
});
userDistrictsGeoJson.features.push(userCongressOldGeoJson[0]);
var bbox = turf.bbox(userDistrictsGeoJson);
var bounds = [[bbox[0], bbox[1]], [bbox[2], bbox[3]]];
map.fitBounds(bounds, {
padding: 40
});
}); //geocoder result
}); //map load
So like I said, everything that runs on the geocodes 'result' event works the first time through, but it seems that on the second time through (user searches new address, but doesn't reload map) queryRenderedFeatures returns a smaller subset of features that doesn't include the tiles where the geocoder lands.
Any suggestions are much appreciated.
I ended up solving this by triggering the querying code once on 'moveend' event.
So now the syntax is:
geocoder.on('result', function(e){
map.once('moveend', function(){
.... rest of code
}
}
I thought I had tried this before posting the question, but seems to be working for me now.

Leaflet.js time slider/animation with Mapbox TileLayers

I've got a RESTful API pushing .png-based TileLayers and associated metadata (datetime, lat/lon, etc.)
I'd like to create a time slider or animation to display them as per the user's inputs.
What is a good starting point? I'm seeing some libraries which do not seem to have support for TileLayers/raster overlays. Can anyone recommend a better choice?
After perusing the literature, this is the solution I chose to start from:
http://fiddle.jshell.net/nathansnider/260hffor/
We basically build an array of tile layer strings and then use a slider to load them dynamically.
.ajax({
type: "GET",
url: {{GET ROUTE GOES HERE}},
success: function (data) {
imageLayers = [];
$.each(data, function (k, v) {
imageLayers.push(L.tileLayer(mbUrl, { id: {{TILE LAYER ID GOES HERE}}, token: {{TOKEN GOES HERE}}, format: 'png', time: {{IMAGE
DATE TIME GOES HERE}}.substr(0, 10)}))
});
map.setView([data[0].{{LATITUDE PROPERTY}}, data[0].{{LONGITUDE PROPERTY}}], 14);
layerGroup = L.layerGroup(imageLayers);
var sliderControl = L.control.sliderControl({
layer: layerGroup,
follow: null
});
map.addControl(sliderControl);
sliderControl.startSlider();
$('#slider-timestamp').html(options.markers[ui.value].feature.properties.time.substr(0, 10));

Mapbox GL JS: Control max zoom with geolocation control?

I am using Mapbox GL JS v0.32.1 and I have a geolocation control on my map.
I would like to make it so that the maximum zoom when the user geolocates is 8, so that the map zooms to the user's approximate location, not street location.
According to the documentation there should be a geolocate event available, but this isn't working for me:
var geoLocate = map.addControl(new mapboxgl.GeolocateControl());
geoLocate.on('geolocate', function(e) {
console.log('geolocated');
map.setZoom(8);
})
Geolocating (in Chrome at least) still zooms to the maximum zoom level available, and I don't see a console log message when it happens.
Map#addControl returns Map. The geolocate event is fired on GeolocateControl. The following should work:
var geoLocate = new mapboxgl.GeolocateControl();
map.addControl(geoLocate);
geoLocate.on('geolocate', function(e) {
console.log('geolocated');
map.setZoom(8);
});
Try this way. smooth zoom to your location
var geoLocate = new mapboxgl.GeolocateControl();
map.addControl(geoLocate);
geoLocate.on('geolocate', function(e) {
map.flyTo({
center:[e.coords.longitude, e.coords.latitude],
zoom:16 //set zoom
});
});

Updating layers in Leaflet / Mapbox

I'm trying to make a mapping visualization in realtime, where I keep getting new points via websockets. The initial plotting these markers on the map seems simple, but I'm not sure what's the right way of updating a layer on Mapbox.
As of now, whenever I get a new point, I remove the old layer, create a new one and then add it on the map. The problem with this approach is that it is slow and for high number of points (>5000) it starts lagging.
// remove layer
if (this.pointsLayer != null) {
map.removeLayer(this.pointsLayer);
}
// build geoJSON
var geoJSON = { "type": "FeatureCollection", "features": [] };
geoJSON["features"] = tweets.map(function(tweet) {
return this.getGeoPoint(tweet);
}.bind(this));
// add geoJSON to layer
this.pointsLayer = L.mapbox.featureLayer(geoJSON, {
pointToLayer: function(feature, latlon) {
return L.circleMarker(latlon, {
fillColor: '#AA5042',
fillOpacity: 0.7,
radius: 3,
stroke: false
});
}
}).addTo(map);
Is there a better way?
You can create an empty GeoJSON layer by passing it a false instead of real data:
//create empty layer
this.pointsLayer = L.mapbox.featureLayer(false, {
pointToLayer: function(feature, latlon) {
return L.circleMarker(latlon, {
fillColor: '#AA5042',
fillOpacity: 0.7,
radius: 3,
stroke: false
});
}
}).addTo(map);
then use .addData to update it as new tweets come in. Something like:
// build geoJSON
var geoJSON = { "type": "FeatureCollection", "features": [] };
geoJSON["features"] = /**whatever function you use to build a single tweet's geoJSON**/
// add geoJSON to layer
this.pointsLayer.addData(geoJSON);
For a single tweet, I guess you could just create a Feature instead of a FeatureCollection, though I don't know whether that extra layer of abstraction would make any difference in terms of performance.
EDIT: Here is an example fiddle showing the .addData method at work:
http://jsfiddle.net/nathansnider/4mwrwo0t/
It does slow down noticeably if you add 10,000 points, and for 15,000 points, it's really sluggish, but I suspect that has less to do with how the points are added that the demands of rendering so many circleMarkers.
If you aren't already, you may want to try using the new Leaflet 1.0 beta, which redraws vector layers faster and is generally much more responsive with large datasets. Compare this 15,000-point example using Leaflet 0.7.5 to the same code using Leaflet 1.0.0b2. Not everything is fixed (popups take a long time to open in both), but the difference in lag time when trying to drag the map is pretty dramatic.
There's no reason to go through the intermediate step of construction a GeoJSON object just so you can add it to the map. Depending on your exact needs, you can do something like this:
tweets.forEach(function(t) {
L.marker(this.getGeoPoint(t)).addTo(map);
}, this);
You should manage the tweets object so it only contains points that are not already visible on the map, though. Deleting all the old markers, just so you can add them again, is of course going to be very slow.
I would take a look at Leaflet Realtime:
Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything.
https://github.com/perliedman/leaflet-realtime