advanced
JavaScript
Sort stores by distance
Prerequisite
Familiarity with front-end development concepts. Some advanced JavaScript required.

This guide will walk you through how to use the Mapbox GL Geocoder plugin and Turf.js to sort store locations based on distance from a geocoded point. This guide takes a deep dive into JavaScript with Mapbox GL JS and extends the map created in the Build a store locator using Mapbox GL JS tutorial. If haven’t completed that tutorial yet, be sure to do so before starting this project. If you’re new to Mapbox GL JS, you may also want to check out our Web applications guide first.

Getting started

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.

There are a few resources you’ll need before getting started:

  • Store locator final project. This tutorial builds off of the code created in the Build a store locator using Mapbox GL JS tutorial. Make sure you’ve created a copy of the final version of that code for this new project or downloaded the starter code. Download starter code
  • An access token from your account. The access token is used to associate a map with your account and can be found on your Account page.
  • Mapbox GL JS. The Mapbox JavaScript library that uses WebGL to render interactive maps from Mapbox GL styles.
  • Mapbox GL Geocoder plug-in. The Mapbox GL JS wrapper library for the Mapbox Geocoding API.
  • Turf.js. An open-source analysis library that performs spatial analysis in the browser and in Node.js.
  • A text editor. You’ll be writing HTML, CSS, and JavaScript.

Add plugins and initialize the map

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 the head of your HTML file. Copy and paste the following code after your links to Mapbox GL JS.

  <!-- Geocoder plugin -->
  <script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v2.0.1/mapbox-gl-geocoder.js'></script>
  <link rel='stylesheet' href='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v2.0.1/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 in the JavaScript portion of your 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.

The code below should be added inside map.on('load', function (e) { ... }); in your script tags.

var geocoder = new MapboxGeocoder({
  accessToken: mapboxgl.accessToken,
  bbox: [-77.210763, 38.803367, -76.853675, 39.052643]
});

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.

View demo one.

Notice what happens when you search for an address using the geocoding form you’ve just created. The map will fly to the location you’ve specified, but doesn’t visualize the matching location. In the next step, you’ll add a point once you’ve successfully found a location using Mapbox GL Geocoder.

Now that your geocoder is working, you can write code to add a point to your map at the location you searched. All of this code will go inside map.on('load', function (e) { ... }); directly following the code you added in the previous step. First, you need to add an empty source using map.addSource() where you will store your geocoder result, and a styled layer from that source using map.addLayer().

map.addSource('single-point', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: [] // Notice that initially there are no features
  }
});

map.addLayer({
  id: 'point',
  source: 'single-point',
  type: 'circle',
  paint: {
    'circle-radius': 10,
    'circle-color': '#007cbf',
    'circle-stroke-width': 3,
    'circle-stroke-color': '#fff'
  }
});

Next, 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. Then, set the data in the source with the id single-point you declared above to searchResult. Copy and paste this code after the map.addSource() and map.addLayer() functions.

geocoder.on('result', function(ev) {
  var searchResult = ev.result.geometry;
  map.getSource('single-point').setData(searchResult);
});

The result should look like this:

View demo two.

Sort store list by distance

Next, 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.

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 perform a wide variety of spatial analysis functions, which you can read about in the documentation. In this tutorial you are going to use distance.

Within your geocoder.on('result', function(){...}); function, use a forEach loop to iterate through all of 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
  });
});

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;
  }
  // a must be equal to b
  return 0;
});

Then, remove the current list of stores and rebuild the list using the reordered array you just 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 buildLocationListing() function. You will need to find and modify 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 buildLocationListing() function.

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

The result should look like this:

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 by using map.fitBounds() and specifying a bounding box. However, the bounds need to be in a specific order. The first point you specify should be the lower left corner of the bounding box, and the second should be the upper right corner. Add the following code inside the geocoder.on() function to create a bbox with this syntax from the geocoded location and the closest store, fly to it, and open the closest store’s popup.

function sortLonLat(storeIdentifier) {
  var lats = [stores.features[storeIdentifier].geometry.coordinates[1], searchResult.coordinates[1]];
  var lons = [stores.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;
  });

  map.fitBounds([
    [sortedLons[0], sortedLats[0]],
    [sortedLons[1], sortedLats[1]]
  ], {
    padding: 100
  });
}

sortLonLat(0);
createPopUp(stores.features[0]);

Finished product

Check out your finished store locator with geocoding and spatial analysis. Great job!

View demo four.

Next steps

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