How to get an accurate position estimate from the Geolocation API in JavaScript
Michał Gacka
Posted on February 8, 2021
The Geolocation API has been introduced into modern browsers many years ago and hasn't changed much since yet it still can waste many hours of your time if you don't know how to work with it. There's a lot of magic happening behind the scenes that isn't properly explained in the documentation. Here's a straightforward way to get an accurate estimate without you having to spend the 2 days I've spent figuring out why my location estimates look like out of a random number generator.
There are 2 functions you can use to ask the browser for location estimates: getCurrentPosition() and watchPosition(). So far so good: the first will spit out 1 position on success, while the second will keep throwing new positions at you as they are updated. It's important to note that the GeolocationCoordinates
object that we get as a result of either of these 2 functions contains the estimated position and accuracy of the measurement in meters.
For my application, where the user was supposed to trigger a location measurement it seemed obvious to use the getCurrentPosition()
since in that case, I wouldn't have to take care of storing the state of change coming from watchPosition()
and having to use clearWatch()
to stop listening at an appropriate time. It seemed perfect. And turned out to be completely useless.
The getCurrentPosition()
accepts an options
object where you can turn enableHighAccuracy
boolean to true. It comes with high hopes and an even larger disappointment. Even with the boolean, the measurements I'd get from my phone would have an accuracy of thousands of meters which rendered them virtually useless for what I needed.
Enter watchPosition()
. After reading some obscure blog I don't remember the name of that went into details of how the GPS module might work in the phone, I learned that it might take a few seconds to warm up and spit out a correct position. And that is the crucial piece of knowledge you need to solve this problem. One that should definitely be explained in more depth in some of the official sources that explain how to use this API.
Knowing that I implemented my logic using watchPosition()
instead and it turned out that indeed, magically the accuracy again starts at thousands of meters but, after a few seconds of these bad measurements, the GPS kicks in and provides estimates with a few meters of accuracy. These, finally, make sense for my application.
Here's an example of a function I use within the React's useEffect()
hook. Note the returned function that allows me to clear the watch by returning it from the hook.
const readLocation = (
setLocation: (location: ILocation) => void,
setError: (errorMessage: string) => void,
setAccuracy: (acc: number) => void
) => {
if (navigator.geolocation) {
const geoId = navigator.geolocation.watchPosition(
(position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
setLocation({ lat, lng });
setAccuracy(position.coords.accuracy);
console.log({ lat, lng }, position.coords.accuracy);
if (position.coords.accuracy > 10) {
showErrorSnackBar("The GPS accuracy isn't good enough");
}
},
(e) => {
showErrorSnackBar(e.message);
setError(e.message);
},
{ enableHighAccuracy: true, maximumAge: 2000, timeout: 5000 }
);
return () => {
console.log('Clear watch called');
window.navigator.geolocation.clearWatch(geoId);
};
}
return;
};
That's all you need to get accurate estimates from the Geolocation API. Let me know in the comments if this worked for you ☀️
EDIT:
Here's also a React hook version of a similar functionality (still imperfect but a good starting point for your own Geolocation hook):
const useLocation = (
enabled: boolean,
accuracyThreshold?: number,
accuracyThresholdWaitTime?: number,
options?: PositionOptions
): [ILocation | undefined, number | undefined, string | undefined] => {
const [accuracy, setAccuracy] = React.useState<number>();
const [location, setLocation] = React.useState<ILocation>();
const [error, setError] = React.useState<string>();
React.useEffect(() => {
if (!enabled) {
setAccuracy(undefined);
setError(undefined);
setLocation(undefined);
return;
}
if (navigator.geolocation) {
let timeout: NodeJS.Timeout | undefined;
const geoId = navigator.geolocation.watchPosition(
(position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
setAccuracy(position.coords.accuracy);
if (accuracyThreshold == null || position.coords.accuracy < accuracyThreshold) {
setLocation({ lat, lng });
}
},
(e) => {
setError(e.message);
},
options ?? { enableHighAccuracy: true, maximumAge: 2000, timeout: 5000 }
);
if (accuracyThreshold && accuracyThresholdWaitTime) {
timeout = setTimeout(() => {
if (!accuracy || accuracy < accuracyThreshold) {
setError('Failed to reach desired accuracy');
}
}, accuracyThresholdWaitTime * 1000);
}
return () => {
window.navigator.geolocation.clearWatch(geoId);
if (timeout) {
clearTimeout(timeout);
}
};
}
setError('Geolocation API not available');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled, accuracyThresholdWaitTime, accuracyThreshold, options]);
if (!enabled) {
return [undefined, undefined, undefined];
}
return [location, accuracy, error];
};
It lets you specify an accuracyThresholdWaitTime
which decides how long the watchLocation
will listen for before deciding that the accuracy is not good enough (for example when someone is indoors the accuracy will never get better than ~10m and you might need it to reach ~3m to serve your purpose).
Posted on February 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 8, 2021