All docsHelpTutorialsSort stores by distance

Sort stores by distance

Tutorial series: Store locator

This series of tutorials teaches you how to create a store locator using Mapbox GL JS then sort the stores by their distance from a given location using the Mapbox GL Geocoder plugin and Turf.js:

This tutorial shows you how to sort store locations based on their distance from a geocoded point, using Mapbox GL JS, the Mapbox GL Geocoder plugin, and Turf.js.

This tutorial extends the map created in the Build a store locator using Mapbox GL JS tutorial. If you haven't completed that tutorial yet, you can do so before starting this project, or you can download the starter code in the Getting started section of this tutorial.

Getting started

To complete this tutorial, you will need:

  • Store locator final project. This tutorial builds off of the code created in the Build a store locator using Mapbox GL JS tutorial. Create a copy of the final version of that code for this new project, or you can download the starter code here.
Download the starter code
  • A Mapbox access token. Your Mapbox access tokens are on your Account page.
  • Mapbox GL JS. Mapbox GL JS is a JavaScript API for building web maps.
  • The Mapbox GL Geocoder plugin. The Mapbox GL Geocoder plugin is the Mapbox GL JS wrapper library for the Mapbox Geocoding API.
  • Turf.js. Turf is an open-source analysis library that performs spatial analysis in the browser and in Node.js.
  • A text editor. Use the text editor of your choice for writing HTML, CSS, and JavaScript.

Add plugins and initialize the map

For this project, we recommend that you create a local folder called "sort-store-locator" to house your project files. You'll see this folder referred to as your project folder.

Download the starter-code zip file. Inside you'll find an index.html file and an img folder that contains the custom marker you'll be using to show store locations. Open the index.html file in a text editor. Make sure you use your own access token and set it equal to mapboxgl.accessToken.

Add Mapbox GL geocoder plugin and Turf.js

Next, set up your document by adding the Mapbox GL Geocoder plug-in and Turf.js library links to your HTML file.

Copy the following code and paste it inside your <head> tags after your links to mapbox-gl.js and mapbox-gl.css.

<!-- Geocoder plugin -->
<script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.0/mapbox-gl-geocoder.min.js'></script>
<link rel='stylesheet' href='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.0/mapbox-gl-geocoder.css' type='text/css' />

<!-- Turf.js plugin -->
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>

Add geocoder control

Add the geocoder control to your JavaScript code using the constructor new mapboxgl.Geocoder. In this case, you'll limit the search results to the Washington DC area using the bbox parameter. There are several other parameters you can specify. You can read more about the available parameters in the documentation on GitHub.

Copy the code below and paste it inside your <script> tags, inside your map.on('load', function (e) { ... }) after map.addSource().

var geocoder = new MapboxGeocoder({
  accessToken: mapboxgl.accessToken, // Set the access token
  mapboxgl: mapboxgl, // Set the mapbox-gl instance
  marker: true, // Use the geocoder's default marker style
  bbox: [-77.210763, 38.803367, -76.853675, 39.052643] // Set the bounding box coordinates
});

map.addControl(geocoder, 'top-left');

Now add some CSS to style your new geocoding search bar. You can add this code right before your closing </style> tag.

.mapboxgl-ctrl-geocoder {
  border: 0;
  border-radius: 0;
  position: relative;
  top: 0;
  width: 800px;
  margin-top: 0;
}

.mapboxgl-ctrl-geocoder > div {
  min-width: 100%;
  margin-left: 0;
}

Save your HTML document, and refresh the page in your web browser. The result should look like this, with a new geocoder control appearing on the map:

Notice what happens when you search for an address using the geocoding form you have created. The map flies to the location you specified and adds a marker at the matching location.

Sort store list by distance

Next, you will calculate the distance between the searched location and the stores, add the results to your GeoJSON data, and sort the store listings by distance from the searched point.

Listen for a geocoder result

Create an event listener that fires when the user selects a geocoder result. When the user selects a place from the list of returned locations, save the coordinates in a variable called searchResult. Copy the code below and paste it after your map.addSource() function, before the closing bracket (}) of your map.on() function.

geocoder.on('result', function(ev) {
  var searchResult = ev.result.geometry;
  // Code for the next step will go here
});

Find distance from all locations

Next you'll use Turf.js to find the distances between your new point and each of the restaurant locations. Turf.js can do a wide variety of spatial analysis functions, which you can read about in the Turf documentation. In this tutorial you will use the distance method.

Within your geocoder.on('result', function(){...}); function, use a forEach loop to iterate through all the store locations in your GeoJSON (remember, you stored these in the stores variable earlier), define a new property for each object called distance, and set the value of that property to the distance between the coordinates stored in the searchResult and the coordinates of each store location. You will do this using the turf.distance() method, which accepts three arguments: from, to, options.

var options = { units: 'miles' };
stores.features.forEach(function(store) {
  Object.defineProperty(store.properties, 'distance', {
    value: turf.distance(searchResult, store.geometry, options),
    writable: true,
    enumerable: true,
    configurable: true
  });
});
// Code for the next step will go here

For each feature in your GeoJSON, a distance property is applied or will be updated each time a new geocoder result is selected.

Sort store list by distance

Now that you have the distance value for each store location, you can use it to sort the list of stores by distance.

First, sort the objects in the stores array by the distance property you added earlier. Copy and paste the following code snippet inside the geocoder.on('result', function(){...}); function.

stores.features.sort(function(a, b) {
  if (a.properties.distance > b.properties.distance) {
    return 1;
  }
  if (a.properties.distance < b.properties.distance) {
    return -1;
  }
  return 0; // a must be equal to b
});
// Code for the next step will go here

Then, remove the current list of stores and rebuild the list using the reordered array you created. The individual listings are nested within the div with id listings.

var listings = document.getElementById('listings');
while (listings.firstChild) {
  listings.removeChild(listings.firstChild);
}
buildLocationList(stores);

Now the listing for each store will be in ascending order of distance from the point that was searched. To make the new list of locations more useful to your viewers, add text that describes each listing's distance from the point they searched for. When you built your initial interactive store locator in the previous tutorial, you created a buildLocationList() function. You will need to find and change that function to check if there is a distance property, and if there is, add the value of that property to each listing. Copy and paste the following code before the link.addEventListener() function within the buildLocationList() function.

if (prop.distance) {
  var roundedDistance = Math.round(prop.distance * 100) / 100;
  details.innerHTML += '<p><strong>' + roundedDistance + ' miles away</strong></p>';
}

Save your work. If you refresh your browser page now and do a search in the geocoder, you will see that the restaurant list in the sidebar reorders itself so that the closest restaurant to the query point is on top. To make this listing stand out, we will apply a CSS class to it in the next step.

Highlight the closest store's listing

To clarify that the first item in the reordered list is the restaurant closest to the query point, you will highlight its address in the listing by adding and applying the active CSS class. Add the following code to geocoder.on('result', function(){...}) before the final }.

var activeListing = document.getElementById('listing-' + stores.features[0].properties.id);
activeListing.classList.add('active');

Save your changes. Now when you refresh the page in your browser and search for a location, the top result in the restaurant listings will change color from dark green to light green.

Fit bounds to search result and closest store

Finally, when you search for a location, you can change the view to include both the location that was searched and the closest store to show more context. You can do this with map.fitBounds().

Determine bounding box

First, create a function that determines a bounding box that will contain both the geocoder location result and the closest store.

The bounds need to be in a specific order. The first point you specify is the lower left corner of the bounding box, and the second is the upper right corner. You will create a function called getBbox that takes the coordinates of the search result and store closest to it and creates a bounding box based on those coordinates. Add the following code to your JavaScript, before the closing </script> tag:

function getBbox(sortedStores, storeIdentifier, searchResult) {
  var lats = [
    sortedStores.features[storeIdentifier].geometry.coordinates[1],
    searchResult.coordinates[1]
  ];
  var lons = [
    sortedStores.features[storeIdentifier].geometry.coordinates[0],
    searchResult.coordinates[0]
  ];
  var sortedLons = lons.sort(function(a,b) {
      if (a > b) { return 1; }
      if (a.distance < b.distance) { return -1; }
      return 0;
    });
  var sortedLats = lats.sort(function(a,b) {
      if (a > b) { return 1; }
      if (a.distance < b.distance) { return -1; }
      return 0;
    });
  return [
    [sortedLons[0], sortedLats[0]],
    [sortedLons[1], sortedLats[1]]
  ];
}

Update map view

Finally, add the following code inside your geocoder.on('result', function(){...}) function before the closing curly brace (}) to create a bbox using the getBbox() function you created in the last step.

var bbox = getBbox(stores, 0, searchResult);
map.fitBounds(bbox, {
  padding: 100
});

createPopUp(stores.features[0]);

Save your work and refresh the page in your browser. Now when you enter a location in the geocoder, the map view will adjust to contain both the searched location and the closest store, and will open the closest store's popup.

Finished product

You have created a store locator with geocoding and spatial analysis.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Sort stores by distance</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700"
rel="stylesheet"
/>
<!-- Mapbox GL JS -->
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.css"
rel="stylesheet"
/>
<!-- Geocoder plugin -->
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.0/mapbox-gl-geocoder.min.js"></script>
<link
rel="stylesheet"
href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.0/mapbox-gl-geocoder.css"
type="text/css"
/>
<!-- Turf.js plugin -->
<script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
<style>
body {
color: #404040;
font: 400 15px/22px 'Source Sans Pro', 'Helvetica Neue', Sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.sidebar {
position: absolute;
width: 33.3333%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
border-right: 1px solid rgba(0, 0, 0, 0.25);
}
.pad2 {
padding: 20px;
}
.map {
position: absolute;
left: 33.3333%;
width: 66.6666%;
top: 0;
bottom: 0;
}
h1 {
font-size: 22px;
margin: 0;
font-weight: 400;
line-height: 20px;
padding: 20px 2px;
}
a {
color: #404040;
text-decoration: none;
}
a:hover {
color: #101010;
}
.heading {
background: #fff;
border-bottom: 1px solid #eee;
min-height: 60px;
line-height: 60px;
padding: 0 10px;
background-color: #00853e;
color: #fff;
}
.listings {
height: 100%;
overflow: auto;
padding-bottom: 60px;
}
.listings .item {
display: block;
border-bottom: 1px solid #eee;
padding: 10px;
text-decoration: none;
}
.listings .item:last-child {
border-bottom: none;
}
.listings .item .title {
display: block;
color: #00853e;
font-weight: 700;
}
.listings .item .title small {
font-weight: 400;
}
.listings .item.active .title,
.listings .item .title:hover {
color: #8cc63f;
}
.listings .item.active {
background-color: #f8f8f8;
}
::-webkit-scrollbar {
width: 3px;
height: 3px;
border-left: 0;
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-track {
background: none;
}
::-webkit-scrollbar-thumb {
background: #00853e;
border-radius: 0;
}
.marker {
border: none;
cursor: pointer;
height: 56px;
width: 56px;
background-image: url(marker.png);
background-color: rgba(0, 0, 0, 0);
}
.clearfix {
display: block;
}
.clearfix:after {
content: '.';
display: block;
height: 0;
clear: both;
visibility: hidden;
}
/* Marker tweaks */
.mapboxgl-popup {
padding-bottom: 50px;
}
.mapboxgl-popup-close-button {
display: none;
}
.mapboxgl-popup-content {
font: 400 15px/22px 'Source Sans Pro', 'Helvetica Neue', Sans-serif;
padding: 0;
width: 180px;
}
.mapboxgl-popup-content-wrapper {
padding: 1%;
}
.mapboxgl-popup-content h3 {
background: #91c949;
color: #fff;
margin: 0;
display: block;
padding: 10px;
border-radius: 3px 3px 0 0;
font-weight: 700;
margin-top: -15px;
}
.mapboxgl-popup-content h4 {
margin: 0;
display: block;
padding: 10px 10px 10px 10px;
font-weight: 400;
}
.mapboxgl-popup-content div {
padding: 10px;
}
.mapboxgl-container .leaflet-marker-icon {
cursor: pointer;
}
.mapboxgl-popup-anchor-top > .mapboxgl-popup-content {
margin-top: 15px;
}
.mapboxgl-popup-anchor-top > .mapboxgl-popup-tip {
border-bottom-color: #91c949;
}
.mapboxgl-ctrl-geocoder {
border: 2px solid #00853e;
border-radius: 0;
position: relative;
top: 0;
width: 800px;
margin-top: 0;
border: 0;
}
.mapboxgl-ctrl-geocoder > div {
min-width: 100%;
margin-left: 0;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="heading">
<h1>Our locations</h1>
</div>
<div id="listings" class="listings"></div>
</div>
<div id="map" class="map"></div>
<script>
/* This will let you use the .remove() function later on */
if (!('remove' in Element.prototype)) {
Element.prototype.remove = function () {
if (this.parentNode) {
this.parentNode.removeChild(this);
}
};
}
mapboxgl.accessToken = '<your access token here>';
/**
* Add the map to the page
*/
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v10',
center: [-77.034084142948, 38.909671288923],
zoom: 13,
scrollZoom: false
});
var stores = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.034084142948, 38.909671288923]
},
'properties': {
'phoneFormatted': '(202) 234-7336',
'phone': '2022347336',
'address': '1471 P St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 15th St NW',
'postalCode': '20005',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.049766, 38.900772]
},
'properties': {
'phoneFormatted': '(202) 507-8357',
'phone': '2025078357',
'address': '2221 I St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 22nd St NW',
'postalCode': '20037',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.043929, 38.910525]
},
'properties': {
'phoneFormatted': '(202) 387-9338',
'phone': '2023879338',
'address': '1512 Connecticut Ave NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at Dupont Circle',
'postalCode': '20036',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.0672, 38.90516896]
},
'properties': {
'phoneFormatted': '(202) 337-9338',
'phone': '2023379338',
'address': '3333 M St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 34th St NW',
'postalCode': '20007',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.002583742142, 38.887041080933]
},
'properties': {
'phoneFormatted': '(202) 547-9338',
'phone': '2025479338',
'address': '221 Pennsylvania Ave SE',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'btwn 2nd & 3rd Sts. SE',
'postalCode': '20003',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-76.933492720127, 38.99225245786]
},
'properties': {
'address': '8204 Baltimore Ave',
'city': 'College Park',
'country': 'United States',
'postalCode': '20740',
'state': 'MD'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.097083330154, 38.980979]
},
'properties': {
'phoneFormatted': '(301) 654-7336',
'phone': '3016547336',
'address': '4831 Bethesda Ave',
'cc': 'US',
'city': 'Bethesda',
'country': 'United States',
'postalCode': '20814',
'state': 'MD'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.359425054188, 38.958058116661]
},
'properties': {
'phoneFormatted': '(571) 203-0082',
'phone': '5712030082',
'address': '11935 Democracy Dr',
'city': 'Reston',
'country': 'United States',
'crossStreet': 'btw Explorer & Library',
'postalCode': '20190',
'state': 'VA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.10853099823, 38.880100922392]
},
'properties': {
'phoneFormatted': '(703) 522-2016',
'phone': '7035222016',
'address': '4075 Wilson Blvd',
'city': 'Arlington',
'country': 'United States',
'crossStreet': 'at N Randolph St.',
'postalCode': '22203',
'state': 'VA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-75.28784, 40.008008]
},
'properties': {
'phoneFormatted': '(610) 642-9400',
'phone': '6106429400',
'address': '68 Coulter Ave',
'city': 'Ardmore',
'country': 'United States',
'postalCode': '19003',
'state': 'PA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-75.20121216774, 39.954030175164]
},
'properties': {
'phoneFormatted': '(215) 386-1365',
'phone': '2153861365',
'address': '3925 Walnut St',
'city': 'Philadelphia',
'country': 'United States',
'postalCode': '19104',
'state': 'PA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.043959498405, 38.903883387232]
},
'properties': {
'phoneFormatted': '(202) 331-3355',
'phone': '2023313355',
'address': '1901 L St. NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 19th St',
'postalCode': '20036',
'state': 'D.C.'
}
}
]
};
/**
* Assign a unique id to each store. You'll use this `id`
* later to associate each point on the map with a listing
* in the sidebar.
*/
stores.features.forEach(function (store, i) {
store.properties.id = i;
});
/**
* Wait until the map loads to make changes to the map.
*/
map.on('load', function (e) {
/**
* This is where your '.addLayer()' used to be, instead
* add only the source without styling a layer
*/
map.addSource('places', {
'type': 'geojson',
'data': stores
});
/**
* Create a new MapboxGeocoder instance.
*/
var geocoder = new MapboxGeocoder({
accessToken: mapboxgl.accessToken,
mapboxgl: mapboxgl,
marker: true,
bbox: [-77.210763, 38.803367, -76.853675, 39.052643]
});
/**
* Add all the things to the page:
* - The location listings on the side of the page
* - The search box (MapboxGeocoder) onto the map
* - The markers onto the map
*/
buildLocationList(stores);
map.addControl(geocoder, 'top-left');
addMarkers();
/**
* Listen for when a geocoder result is returned. When one is returned:
* - Calculate distances
* - Sort stores by distance
* - Rebuild the listings
* - Adjust the map camera
* - Open a popup for the closest store
* - Highlight the listing for the closest store.
*/
geocoder.on('result', function (ev) {
/* Get the coordinate of the search result */
var searchResult = ev.result.geometry;
/**
* Calculate distances:
* For each store, use turf.disance to calculate the distance
* in miles between the searchResult and the store. Assign the
* calculated value to a property called `distance`.
*/
var options = { units: 'miles' };
stores.features.forEach(function (store) {
Object.defineProperty(store.properties, 'distance', {
value: turf.distance(searchResult, store.geometry, options),
writable: true,
enumerable: true,
configurable: true
});
});
/**
* Sort stores by distance from closest to the `searchResult`
* to furthest.
*/
stores.features.sort(function (a, b) {
if (a.properties.distance > b.properties.distance) {
return 1;
}
if (a.properties.distance < b.properties.distance) {
return -1;
}
return 0; // a must be equal to b
});
/**
* Rebuild the listings:
* Remove the existing listings and build the location
* list again using the newly sorted stores.
*/
var listings = document.getElementById('listings');
while (listings.firstChild) {
listings.removeChild(listings.firstChild);
}
buildLocationList(stores);
/* Open a popup for the closest store. */
createPopUp(stores.features[0]);
/** Highlight the listing for the closest store. */
var activeListing = document.getElementById(
'listing-' + stores.features[0].properties.id
);
activeListing.classList.add('active');
/**
* Adjust the map camera:
* Get a bbox that contains both the geocoder result and
* the closest store. Fit the bounds to that bbox.
*/
var bbox = getBbox(stores, 0, searchResult);
map.fitBounds(bbox, {
padding: 100
});
});
});
/**
* Using the coordinates (lng, lat) for
* (1) the search result and
* (2) the closest store
* construct a bbox that will contain both points
*/
function getBbox(sortedStores, storeIdentifier, searchResult) {
var lats = [
sortedStores.features[storeIdentifier].geometry.coordinates[1],
searchResult.coordinates[1]
];
var lons = [
sortedStores.features[storeIdentifier].geometry.coordinates[0],
searchResult.coordinates[0]
];
var sortedLons = lons.sort(function (a, b) {
if (a > b) {
return 1;
}
if (a.distance < b.distance) {
return -1;
}
return 0;
});
var sortedLats = lats.sort(function (a, b) {
if (a > b) {
return 1;
}
if (a.distance < b.distance) {
return -1;
}
return 0;
});
return [
[sortedLons[0], sortedLats[0]],
[sortedLons[1], sortedLats[1]]
];
}
/**
* Add a marker to the map for every store listing.
**/
function addMarkers() {
/* For each feature in the GeoJSON object above: */
stores.features.forEach(function (marker) {
/* Create a div element for the marker. */
var el = document.createElement('div');
/* Assign a unique `id` to the marker. */
el.id = 'marker-' + marker.properties.id;
/* Assign the `marker` class to each marker for styling. */
el.className = 'marker';
/**
* Create a marker using the div element
* defined above and add it to the map.
**/
new mapboxgl.Marker(el, { offset: [0, -23] })
.setLngLat(marker.geometry.coordinates)
.addTo(map);
/**
* Listen to the element and when it is clicked, do three things:
* 1. Fly to the point
* 2. Close all other popups and display popup for clicked store
* 3. Highlight listing in sidebar (and remove highlight for all other listings)
**/
el.addEventListener('click', function (e) {
flyToStore(marker);
createPopUp(marker);
var activeItem = document.getElementsByClassName('active');
e.stopPropagation();
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
var listing = document.getElementById(
'listing-' + marker.properties.id
);
listing.classList.add('active');
});
});
}
/**
* Add a listing for each store to the sidebar.
**/
function buildLocationList(data) {
data.features.forEach(function (store, i) {
/**
* Create a shortcut for `store.properties`,
* which will be used several times below.
**/
var prop = store.properties;
/* Add a new listing section to the sidebar. */
var listings = document.getElementById('listings');
var listing = listings.appendChild(document.createElement('div'));
/* Assign a unique `id` to the listing. */
listing.id = 'listing-' + prop.id;
/* Assign the `item` class to each listing for styling. */
listing.className = 'item';
/* Add the link to the individual listing created above. */
var link = listing.appendChild(document.createElement('a'));
link.href = '#';
link.className = 'title';
link.id = 'link-' + prop.id;
link.innerHTML = prop.address;
/* Add details to the individual listing. */
var details = listing.appendChild(document.createElement('div'));
details.innerHTML = prop.city;
if (prop.phone) {
details.innerHTML += ' &middot; ' + prop.phoneFormatted;
}
if (prop.distance) {
var roundedDistance = Math.round(prop.distance * 100) / 100;
details.innerHTML +=
'<p><strong>' + roundedDistance + ' miles away</strong></p>';
}
/**
* Listen to the element and when it is clicked, do four things:
* 1. Update the `currentFeature` to the store associated with the clicked link
* 2. Fly to the point
* 3. Close all other popups and display popup for clicked store
* 4. Highlight listing in sidebar (and remove highlight for all other listings)
**/
link.addEventListener('click', function (e) {
for (var i = 0; i < data.features.length; i++) {
if (this.id === 'link-' + data.features[i].properties.id) {
var clickedListing = data.features[i];
flyToStore(clickedListing);
createPopUp(clickedListing);
}
}
var activeItem = document.getElementsByClassName('active');
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
this.parentNode.classList.add('active');
});
});
}
/**
* Use Mapbox GL JS's `flyTo` to move the camera smoothly
* a given center point.
**/
function flyToStore(currentFeature) {
map.flyTo({
center: currentFeature.geometry.coordinates,
zoom: 15
});
}
/**
* Create a Mapbox GL JS `Popup`.
**/
function createPopUp(currentFeature) {
var popUps = document.getElementsByClassName('mapboxgl-popup');
if (popUps[0]) popUps[0].remove();
var popup = new mapboxgl.Popup({ closeOnClick: false })
.setLngLat(currentFeature.geometry.coordinates)
.setHTML(
'<h3>Sweetgreen</h3>' +
'<h4>' +
currentFeature.properties.address +
'</h4>'
)
.addTo(map);
}
</script>
</body>
</html>

Next steps

After this guide, you should have everything you need to create your own store locator. You can complete the Create a custom style tutorial to create a branded map style or use Cartogram, a drag and drop tool, to create a custom style from your logo in minutes. To do more with Mapbox GL JS, explore the Mapbox GL JS API reference and examples