Geolocation & Compass Heading in web applications
How to implement user location markers with a compass heading in your web application
Location-based functions are not only an important topic in native applications. User location can also be implemented with a compass display in web-based applications. In this article, Martin Schuhfuss, Creative Developer at Ubilabs, describes what to consider when implementing, where the location data comes from and how to implement a reliable highlighting of the user location with a compass heading in your own web application.
In a lot of map based applications, it can be very beneficial for users to see their own location in relation to whatever information is being presented on the map or even use the user’s location to provide features like walking directions and similar.
Features like this are not exclusive to native apps. Web-based applications can make use of special browser APIs (Application Programming Interfaces) to implement the same features. Due to security considerations, this isn’t quite straightforward to do and there are some compatibility issues to consider. In this article, we show you how to implement these features.
Click here to go to our demoHow user locations are captured
All modern browsers implement the Geolocation API, which allows websites to request the current location of the user’s device. For smartphones, this is typically either a location from the cellular network and WiFi signals for low precision and a GNSS (Global Navigation Satellite System) receiver for high accuracy. Desktop browsers will use corresponding location services offered by the operating system if available.
These readings will provide the coordinates (latitude and longitude) as well as the current accuracy in meters. This is already enough to display the user’s location on the map just like you know it from native map applications. The Geolocation API also provides a value for the heading, and we might think we can just take that to also display the direction the phone is currently facing.
In a lot of cases this won’t work though, as the given value is the direction measured between multiple location-readings. This works well when moving at a constant speed, but fails miserably when standing still. For this we have to look at the values provided by another API, DeviceOrientation events.
DeviceOrientation events are a way for web applications to access readings from the IMU (Inertial Measurement Unit) built into almost every smartphone in the market today. The readings provided by this API represent the orientation of the device relative to earth’s magnetic field and allow us to compute the compass heading of the device.
Privacy Considerations and Permissions
Geolocation data has to be considered one of the most sensitive pieces of information a web application can obtain about their users, and it is for this reason that it is also heavily guarded against possibly malicious use. As such, browsers will display a prominent permission dialog before a web application can receive geolocation information for the first time. In addition to this, the operating system might also show a similar dialog about allowing the browser access to the device’s location service.
When the user denies the permission, the application isn’t only blocked from receiving the geolocation coordinates, it will also no longer be possible to start the permissions prompt. Without the user actively allowing it in the website settings of the browser (and rarely do users even know how that works), there is no way for the application to enable that feature again.
For this reason – and as a general courtesy, it is important to only ever ask for permission when the user actually expects it. We think there should always be a separate button that triggers the permission dialog (so the user experiences this as “I made the site ask for my location”) instead of automatically starting the prompts (which might read as “This site wants to know my location”). There should ideally never be a situation where a user clicks ‘deny’ because the permission-dialog was unexpected or they were missing information about what was about to happen.
For iOS devices (and possibly also for future Android devices), there is another permission we need to ask for so we can get access to the DeviceOrientation events we need to display the compass. But this permission isn’t as important as the geolocation and could even be left away if there’s no need to show a compass.
Implementation based on our demo
Our example is a simple Google map that shows the user’s location. It consists of just two parts: the actual map, and a small panel containing some information as well as the buttons to enable the geolocation features.
The most interesting aspect about this, and the thing we are going to focus on, is the handling of the permissions and actually retrieving values from the two APIs.
There are a couple of different cases we need to cover in the behavior of our application, depending on the current status of the permissions:
- If the user has previously granted the required permissions, we can start requesting the geolocation right away and won’t have to trigger the permissions prompt.
- When the permission has been denied during an earlier visit, we can either just accept this as the user's choice (this is good when the geolocation isn’t an integral part of the user-experience) or show some UI with instructions on how to access and change the website settings (if having the user’s location is essential to the application).
- The third and most common option is that the permission-status is ‘prompt’, which means the user hasn’t made a choice and needs to be asked. The UI in this case should explain what the geolocation will be used for and offer a button to start the permissions-process.
Permissions
To get the information about the current status, you can query the Permissions API, which is available in most modern browsers (in Safari starting with version 16):
async queryPermissionsState(): Promise<PermissionState> {
try {
const {state} = await navigator.permissions.query({name: 'geolocation'});
return state; // can be ‘prompt’, ‘granted’, or ‘denied’
} catch (err) {
// when the permissions-API isn't available (e.g. Safari prior to 16.0),
// we have to assume we have to ask.
return 'prompt';
}
}
If the permission state returned by this function is ‘granted’, the application can start right away by requesting the user’s location. If it is ‘prompt’, the application should wait for the user to activate the feature, for example when a button is pressed. Once that happens, we can first ask for the permission to use the device-orientation, which is done with
try {
permissionState = await DeviceOrientationEvent.requestPermission();
} catch() {
permissionState = ‘granted’;
}
This has to be wrapped in an exception-handler since some browsers don’t implement the DeviceOrientationEvent or the requestPermission method. If it doesn’t exist, we won’t need any permission to use it, so any exception can be ignored. Important for this to work is that the requestPermission call has to happen within the scope of a trusted event, so an event that has been generated by a user-action.
The Geolocation API doesn’t provide separate functions to ask for permission. Instead, the permission prompts will be triggered when the application first accesses the Geolocation API:
const permissionState = await new Promise<PermissionState>(
(resolve, reject) => {
navigator.geolocation.getCurrentPosition(
() => resolve('granted'),
err => {
if (!(err instanceof GeolocationPositionError)) {
reject(err);
return;
}
if (err.code === err.PERMISSION_DENIED) {
resolve('denied');
} else if (err.code === err.TIMEOUT) {
// when a timeout occurs, we know that the permission had been
// granted, otherwise we would get the 'permission denied' error.
resolve('granted');
}
},
{enableHighAccuracy: true, timeout: 10, maximumAge: Infinity}
);
}
);
At this point we are only interested in triggering the permissions dialog and not in the returned location, which is why we can set a very short timeout, so the call will return almost immediately after the permission has been granted.
Getting the position
For the position on the map, we want to receive a continually updated position, which is where the other two methods of the Geolocation API come into play: watchPosition and clearWatch.
Like the getCurrentPosition-method used above, watchPosition receives three parameters – a success callback, an error callback, and an options object. It returns a watcher ID which is needed to stop the watcher once it is no longer needed.
const watcherId = navigator.geolocation.watchPosition(
positionResult => {
const {latitude, longitude, accuracy} = positionResult.coords;
},
err => {},
{enableHighAccuracy: true}
);
// once the location is no longer needed
navigator.geolocation.clearWatch(watcherId);
And that’s basically it. This is most of what you need to know when you want to implement geolocation and device-orientation in your own application. If you want to have a look at the full code for the demo, and the actual implementation of the position/compass/accuracy-marker we’re using, head on over to the github-repository and feel free to reuse it:
https://github.com/ubilabs/geolocation-deviceorientation-demo