Tutorials
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 Mapbox GL JS, the Mapbox GL Geocoder plugin, and Turf.js to sort store locations based on distance from a geocoded point. This guide 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 read 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. You will use an access token to associate a map with your account. Your access token is on the 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/v4.2.0/mapbox-gl-geocoder.min.js'></script>
<link rel='stylesheet' href='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.2.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.

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

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.

Notice what happens when you search for an address using the geocoding form you have created. The map will fly to the location you've specified and add a marker at the matching location.

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.

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

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 buildLocationListing() 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 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 with map.fitBounds().

Determine bounding box

First, create a function that determines a bounding box containing both the geocoder result and the closets store.

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.

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

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.

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

createPopUp(stores.features[0]);

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

Finished product

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

The final code will look like:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8' />
    <title>Store Locator</title>
    <meta name='robots' content='noindex, nofollow'>
    <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
    <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/v1.11.1/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.11.1/mapbox-gl.css' rel='stylesheet' />
    <!-- Geocoder plugin -->
    <script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.2.0/mapbox-gl-geocoder.min.js'></script>
    <link rel='stylesheet' href='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.2.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_MAPBOX_ACCESS_TOKEN';

      /**
       * 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 += ' · ' + 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 our examples page and the Mapbox GL JS on the help page.

Was this page helpful?