If you build it, will they come? If you work on web applications, you are likely in the business of attracting people to your site and providing an engaging experience that will keep them coming back for more.
To make users feel cared for, a good approach is to customize the way your site behaves for them individually, as opposed to other users. One way to get an easy win catering to a user's specific needs is to ask for their location and use it to show things that might interest them that are related to nearby points of interest.
Luckily, this is a pretty easy thing to achieve with HTML5 Geolocation. The Geolocation API is getting better all the time. For example, you can now use it to track a user's location as they move. Great for mobile web applications!
In this post, we'll go over the basics of how to get a user's location, place a pin on a map, and show them museums nearby using JavaScript. We'll also cover some of the gotchas that you'll need to work around when integrating geolocation into your app.
If you want to follow along, you can check out my sample project code.
The HTML5 Geolocation API
To find out where a user is located, we'll need to ask them, using their browser's implementation of the HTML Geolocation API.
As a user, providing your location is always optional, so users will often see a dialog box pop up in the browser with a message like "Example website wants to: Know your location", and buttons to either block or accept the request.
Assuming the user accepts the request, the browser will somehow figure out the current location and provide it back to our JavaScript code.
Under the hood, desktop Firefox and Chrome do this by sending local Wi-Fi network information to Google's Location Service database, while mobile browsers might use Wi-Fi, cellular triangulation, or GPS (source) depending on what’s available.
Let's take a look at the process of setting up geolocation using ES2015.
Usage
Imagine that we've built a page with a button on it, and we want to find a user's coordinates when they click it. How would we go about doing that?
First, we set up an event handler on the button that triggers a function called geolocate
.
const $geolocateButton = document.getElementById('geolocation-button');
$geolocateButton.addEventListener('click', geolocate);
Next, we'll define the actual geolocate
function.
It's important that we check that the browser actually supports geolocation before we move forward, so that we don't end up erroring out. Even though geolocation is supported in all modern browsers except Opera Mini, it's still a good idea to check.
Our handler will call the getCurrentPosition
method of the window.navigator.geolocation
object. This method is the magic sauce that pops up the dialog box prompting the user to block or accept our request. It takes a success and an error callback.
js
function geolocate() {
if (window.navigator && window.navigator.geolocation) {
navigator.geolocation.getCurrentPosition(onGeolocateSuccess, onGeolocateError);
}
}
Here's what it looks like:
Now that we've determined the user's location, what will we do with it? Also, what should we do if something goes wrong? Let's flesh out our success and error callbacks.
For now, we'll just log the coordinates to the console if we succeed. They'll be found in the coords
property of the response object. If there's an error, we'll also log its associated code and message. I've left some comments showing what the error codes mean too, in case we want to handle these different cases later.
function onGeolocateSuccess(coordinates) {
const { latitude, longitude } = coordinates.coords;
console.log('Found coordinates: ', latitude, longitude);
}
function onGeolocateError(error) {
console.warn(error.code, error.message);
if (error.code === 1) {
// they said no
} else if (error.code === 2) {
// position unavailable
} else if (error.code === 3) {
// timeout
}
}
Boom! Location found, and sad path covered. That was easier than it looked!
Gotchas
Before we go any further, it's worth pointing out that you can only use these features over HTTPS in Chrome. Localhost is considered secure over HTTP, so you'll be able to play with these features in a local development environment without SSL. But if you're planning to use them on a live site, you'll need to set up HTTPS.
If you were thinking about asking the user for their location as soon as the page loads, there's something you should note.
In the past, if a user declined geolocation on the first go-round, it was not possible to ask them again. The proper way to do this would be to use the HTML5 Permissions.request()
method, but it's not supported in any browser but Firefox 47 and greater, and even here the user has to explicitly enable it.
Because of this problem, it's typically been a best practice to NOT prompt users to allow geolocation on page load. When they see the unexpected dialog popup, many users typically click "block" without even reading what is being asked. Then they wouldn't have a future chance to accept future requests to geolocate. The only way around it was to show them a message asking them to go digging through their browser settings and re-enable geolocation for your site. For example, in Chrome, you can visit chrome://settings/contentExceptions#location to clear your selection.
This is no longer true in Chrome. Now, the user's geolocation decision is reset on page reload. There's also a little icon that appears in the address bar after a selection has been made that allows you to clear your choice. Thus, you can now ask visitors to geolocate each time they load the page if you like. But it still may not be a good idea if you expect them to be using browsers other than Chrome.
Integrating with Google Geocoder and Google Maps
Now that we've looked at how to get a user's location, it would be nice to show it on a map. Luckily, this is pretty straightforward using any of the off-the-shelf map solutions available on the web today. For our example, we'll use Google Maps.
Showing the User's Location
Before you can load a Google Map on your site, you'll need to set up an API key. You can do this in the developer's console. Create a new project and enable the JavaScript Maps API for it. Once you've done that, copy the API key to your clipboard. You'll need it to be able to load the Maps API script.
Also note that Google rate limits requests to this API. For the free tier, you get 25,000 map loads per day. If you’re setting up a production site, you may need to pay for a higher tier of service.
Add the following line to your HTML file, substituting your API key where indicated. I added it at the bottom of the <body>
section of my page so it would load after the DOM.
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=enableButtons"></script>
Note the callback=enableButtons
portion of that URL. You can enter whatever you want as the name of the callback.
When the script has loaded, it will look for a function by that name on the window
object and call it. For this example, we'll just use this callback to enable a button once Google maps has loaded.
window.enableButtons = () => {
const $geolocateButton = document.getElementById('geolocation-button');
$geolocateButton.disabled = false;
console.log('Google Maps API loaded');
}
Now that we've loaded the Google Maps library, we can use it to display a map with the user's location once the button is clicked. First, we'll add an event handler to trigger when the button is clicked.
document.addEventListener('DOMContentLoaded', () => {
const $geolocateButton = document.getElementById('geolocation-button');
$geolocateButton.addEventListener('click', geolocate);
});
Then, we'll set up the geolocate
handler to create a map, and show it in a div
with an ID of map
.
function geolocate() {
navigator.geolocation.getCurrentPosition(onGeolocateSuccess, onGeolocateError);
}
function onGeolocateSuccess(coordinates) {
const { latitude, longitude } = coordinates.coords;
showMap(latitude, longitude);
}
function showMap(lat, lng) {
const $map = document.getElementById('map');
const position = { lat, lng };
const map = new google.maps.Map($map, {
center: position,
zoom: 6
});
});
If you were to run this code now, you would see a map appear centered on the location found when the user agreed to geolocation.
We're only one step away from our goal of showing the user's location. All that's left is to create a marker and add it to the map at their coordinates.
We'll add that code to the showMap
function as well:
function showMap(lat, lng) {
const $map = document.getElementById('map');
const position = { lat, lng };
const map = new google.maps.Map($map, {
center: position,
zoom: 6
});
const marker = new google.maps.Marker({ map, position });
}
Voila!
Now, when the user clicks on the #geolocate
button, we'll ask them to geolocate. Once they allow it, we’ll show a map centered on their location!
Here's what it looks like from me:
Another option would be to show the map as soon as the Google Maps script is loaded in the initial callback (ours was called enableButtons
, but you could just as easily name it createMap
). Once the script is loaded, you have a lot of flexibility.
To see our example in action, download the source, add an API key to the script in index.html
and try it out!
You can take a look at Google's documentation to for more details on how to interface with their API.
Tracking A User's Location
If you're working on a site that you expect your users to visit on the go, you can also track their location.
To do so, you pretty much follow the same steps as you would to request the user's location once, only using the geolocation.watchPosition
function instead. There's also a geolocation.clearWatch
function that will remove the position watcher.
Let's look at how to work with that, too.
First, let’s add an event listener on a button to turn on geolocation tracking, and another on a button we'll use to clear the position watcher.
document.addEventListener('DOMContentLoaded', () => {
const $geolocateButton = document.getElementById('geolocation-button');
const $watchButton = document.getElementById('watch-button');
const $clearWatchButton = document.getElementById('clear-watch-button');
$geolocateButton.addEventListener('click', geolocate);
$watchButton.addEventListener('click', watchLocation);
$clearWatchButton.addEventListener('click', clearWatch);
});
Next, we’ll write the handler function for the watch button. It takes a callback to be fired whenever the device's position changes, and an error handler. We'll write an onLocationChange
handler for the first of these, and reuse the onGeolocateError
handler from above for the error case.
When you call watchPosition
, you get back a unique ID for the watch. We'll store it in localStorage for simplicity's sake.
function watchLocation() {
const watchId = navigator.geolocation.watchPosition(onLocationChange, onGeolocateError);
window.localStorage.setItem('lastWatch', watchId);
console.log('Set watchId', watchId);
}
function onLocationChange(coordinates) {
const { latitude, longitude } = coordinates.coords;
console.log('Changed coordinates: ', latitude, longitude);
}
Now we'll set up the function to clear the watch and remove its ID from localStorage.
function clearWatch() {
const watchId = window.localStorage.getItem('lastWatch');
navigator.geolocation.clearWatch(watchId);
console.log('Cleared watchId: ', watchId);
}
That's pretty much all there is to it. Now we can track our user’s location, or stop tracking them, at the click of a button.
In a real application, you would use the onLocationChange
callback as an entry point to executing the business logic of your app. Just don't forget to clean up your watches when you're done!
Showing Things Nearby
Now that we've shown our user's location, let's show them things nearby that they may be interested in. Imagine that you're building a site for finding museums. You have a database full of museum addresses, and you want to show the ones that are close by on the map.
To find out where the museums are by latitude and longitude, you will need to geocode them. It's really better to do that server-side, so we'll leave the details out of this post, but here are the docs for how you could do it on the front-end if you're interested in playing around with it.
Suffice to say, if you wanted to fetch nearby locations given a user's latitude and longitude, you'd pass them to your API server in some kind of POST request containing those coordinates. The server would then do some kind of query to find locations that match in the database. For example, with Ruby Geocoder you would call something like Museum.near(params[:latitude], params[:longitude])
.
If you wanted to store your location data in a scalable, high-availability SQL database, you could use CrateDB. It supports geospatial queries out of the box, so gathering and transforming data would be fast and easy. For example, you could find museums in Berlin-Mitte as shown in this previous post.
To do so, you would create a table of places, and a table of districts:
CREATE TABLE geo_shapes.places (
"types" array(string),
"name" string,
"geometry" geo_point
);
CREATE TABLE geo_shapes.districts (
geometry geo_shape INDEX USING geohash WITH (PRECISION='10m', distance_error_pct=0.025),
"type" string,
"name" string
);
Then you could select the geo_points:
select p.name as name, p.geometry as coordinates
from geo_shapes.places p, geo_shapes.districts
where d.properties['name'] = 'Mitte' and
'museum'= ANY( p.types) and
within(p.geometry, d.geometry)
order by 1;
After massaging this data with your server, you could send it back as a JSON array of museum objects like this:
{
museums: [
{
id: 1,
name: "Altes Museum",
lat: 52.5195,
lng:13.3987
},
{
id: 2,
name: "Berlin Wall Memorial",
lat: 52.4861,
lng: 13.4720
},
{
id: 3,
name: "DDR Museum",
lat: 52.5196,
lng: 13.4027
},
{
id: 4,
name: "Gemäldegalerie",
lat: 51.0534
lng: 13.7347
}
]
}
Since we've got a collection of museum locations with latitudes and longitudes here, all we have to do is loop through them and add markers with their location to the map. But first, we'll make a couple of small changes to our showMap
function from above, storing the Google maps map
instance in a global variable so we can reuse it. We'll also set a global window.markers
array in which we can store our markers (or remove them later).
function showMap(latitude, longitude) {
const $map = document.getElementById('map');
const position = { lat, lng };
const marker;
window.map = new google.maps.Map($map, {
center: position,
zoom: 6
});
window.markers = window.markers || [];
marker = new google.maps.Marker({ map, position });
window.markers.push(marker);
}
Now we simply loop through the museums and create a marker on the map for each one.
function showNearbyMuseums() {
if (!window.map || !window.markers) { return; }
mockMuseumResponse.museums.forEach((museum) => {
const { lat, lng, name } = museum;
const position = { lat, lng };
const title = name;
const marker = new google.maps.Marker({ map, position, title });
window.markers.push(marker);
});
}
We'll create another button to show the museums, and set up a click handler on it to call showNearbyMuseums
.
js
document.addEventListener('DOMContentLoaded', () => {
...
const $showNearbyButton = document.getElementById('show-nearby-button');
...
$showNearbyButton.addEventListener('click', showNearbyMuseums);
});
Then reload, click the button, and there they are!
Conclusion
Although you might suspect it would be difficult to get a user's location from the browser, it's actually a pretty straightforward process.
HTML5 geolocation is still evolving as browsers add more features, but it's becoming a more and more important component of the web for many companies (ever notice how often Google asks for your location?).
In this post, we looked at how to ask a user for their location, how to use it to put a pin on a Google map, how to track a user's location as they move, and how to show nearby locations.
If you're interested in learning more about geolocation, check out the Google Developer docs, and SitePoint. Learn more on how CrateDB does geo search.
I hope you enjoyed this post. If you end up using it to help you build something, I’d love to see it! Tweet us a link and show what you've made!
Until next time, happy hacking.