Build an Interactive Peak Map with Mapbox and AscenDB

This tutorial will develop a basic use-case for AscenDB: displaying a few peaks on a map as the user navigates the map in their browser. We'll keep it simple, using the excellent Mapbox GL JS API to do the heavy lifting around map rendering and interactivity. When the user single-clicks the map, up to 25 of the most important peaks in their current viewport will appear on the map as markers. We will also color those markers by prominence, so that more prominent peaks are easier to pick out when zoomed in on the map.

Note that you will need a Mapbox access token to complete this tutorial. We will be using the free (but rate-limited) version of the AscenDB API, so a token is not required there. However, if you want to use the non-rate-limited version of the API, you can sign up for a StdLib account from their homepage. The AscenDB API service is hosted and managed through StdLib.

Finally, you can find the general documentation for the AscenDB API at its StdLib home.

Step 1: Tiny Mapbox Template

We'll start with a simple Mapbox HTML template. Our map will use the Outdoor rendering style and enable the zoom controls, so the user can more effectively narrow their area of interest. The map will be initialized will the full Earth in view. Here's the HTML code, complete with the initial JavaScript, for this template. Remember to replace <YOUR_ACCESS_TOKEN_HERE> with your own Mapbox access token:

<!doctype html>
<html>
    <head>
        <title>Peaks on Mapbox</title>
        <script src='https://api.mapbox.com/mapbox-gl-js/v0.44.2/mapbox-gl.js'></script>
        <script src='https://api.mapbox.com/mapbox.js/plugins/turf/v2.0.2/turf.min.js'></script>
        <script src='./lib.js'></script>
        <link href='https://api.mapbox.com/mapbox-gl-js/v0.44.2/mapbox-gl.css' rel='stylesheet' />
        <style>
        body { margin:0; padding:0; }
        #map { position:absolute; top:0; bottom:0; width:100%; }
        </style>
    </head>
    <body>
        <div id='map'></div>
        <script>
        mapboxgl.accessToken = '<YOUR_ACCESS_TOKEN_HERE>';
        let map = new mapboxgl.Map({
            container: 'map',
            style: 'mapbox://styles/mapbox/outdoors-v9'
        });

        map.addControl(new mapboxgl.NavigationControl());
        </script>
    </body>
</html>

Step 2: Connecting to AscenDB

AscenDB has a simple but flexible JavaScript API provided through the StdLib service directory. The easiest way to connect with AscenDB is to include StdLib's lib.js file in the same folder as your template html file, then include it like so in your head tag:

    <script src='./lib.js'></script>

At the end of the inline JavaScript section, declare a reference to the AscenDB peaks service:

    const peaks = lib.ascendb.peaks['1.1.2'];

And just like that, you can call the service! The peaks variable is, essentially, a function that returns a promise with a JSON array payload. We will now utilize this function to make our map interactive.

Step 3: Click to Get Peaks

The Mapbox API provides a way to capture single-click events on the map. Our intention is to get the location and prominence of peaks within the viewport, but to start as simple as possible we'll just grab the location of the first 25 that the AscenDB service brings back when called with no filters. The next snippet will provide the outline for the full click event handler:

let markers = [];
map.on('click', event => {
  let peakFields = ['lat', 'lon'];
  peaks({ fields: peakFields }).then(peaks => {
    markers.forEach(m => m.remove());
    markers = [];
    peaks.forEach(p => {
      let marker = new mapboxgl.Marker();
      marker.setLngLat([p.lon, p.lat]);
      marker.addTo(map);
      markers.push(marker);
    });
  });
});

This is a bit more involved than the previous code, so let's walk through it. We start with an empty list of map 'markers' upon map initialization. Each time we click the map, we call the peaks service, asking for both the lat and lon fields for each peak it returns (representing latitude and longitude). We then remove any existing markers from the map, clear the list, then add a new marker to the map at each peak's location.

At this point, you should be able to click the map and see the 25 of the most prominent peaks in the world appear on your map. Here's an example of what it should look like so far:

global-peak-map.png

Step 4: Focusing on Local Peaks

Now we want to bring back only peaks that fit in the user's current map viewport. AscenDB allows the specification of search filters, such as searching for peaks by name or above a certain elevation. The one we're interested in is the near filter, which finds peaks within a radius around a search location. The location is given in latitude and longitude, and the search radius is specified in meters.

How do we determine what the location and search radius should be? Mapbox GL JS provides many useful functions for getting the screen coordinates of the viewport, and we will use the center of the screen as the center of our search circle. Calculating the radius is a little bit trickier, and will require us to use the turf.js plugin for Mapbox (which we already included in the template section). Using turf.js, we will make our search radius the arc distance between the center location of the screen and the center of the northern edge of the viewport. If your map is longer vertically than horizontally, e.g. on a mobile device, you may want to use the eastern or western edge instead to ensure that all returned peaks will be within the user's viewport.

Let's change our click event handler to the following:

map.on('click', event => {
  let bounds = map.getBounds();
  let center = bounds.getCenter();
  let distance = turf.distance(turf.point(center.toArray()), turf.point([center.lng, bounds.getNorth()])) * 1000;

  let peakFields = ['lat', 'lon'];
  let peakFilter = { near: { lat: center.lat, lon: center.lng, rad: distance } };
  peaks({ fields: peakFields, filters: peakFilter }).then(peaks => {
    markers.forEach(m => m.remove());
    markers = [];
    peaks.forEach(p => {
        let marker = new mapboxgl.Marker();
        marker.setLngLat([p.lon, p.lat]);
        marker.addTo(map);
        markers.push(marker);
    });
  });
 });

There's a couple new things to note here. The first is three new lines near the top that calculate the Earth-scale arc distance from the center of the viewport to the north edge of the viewport. This will be our search radius, but turf.js returns the value in kilometers. Easy enough to fix, just multiply the distance by 1000 to get the meters that AscenDB expects. The second is the creation of the peakFilter object just below the peakFields object. This is the format AscenDB expects the 'nearby search' filter to be in. We pass this object in the parameter object of the peaks function, and AscenDB will then limit the returned peaks to be a subset of those found in the specified area.

Now we can zoom in and navigate the map to find prominent peaks in interesting locations!  Only one step left to complete our interactive peak map.

Step 5: Paint by Prominence

Now let's use AscenDB for what it was meant to do: accessing the stats of peaks across the world. We'll focus on prominence here, but you can change this example to use any of the other stats provided by AscenDB, such as elevation, isolation, geocentric distance, omnidirectional relief and steepness, or others.

The first thing to decide on is the color scheme. We will be using a simple greyscale color band, with darker peaks having lower prominence and brighter having greater. The second thing to decide is the intervals. AscenDB stores prominence values in feet (since it is the more fine-grained measurement). Peaks considered ultra-prominent have prominence greater than 1500m, which is about 4921 feet. We'll make this our first cutoff, so that any peaks with prominence greater than 4921 ft. (about 1500 worldwide, oddly enough) will be the brightest possible on the map, although not pure white because it looks funky with the Mapbox marker icon. Next up will be P2Ks (peaks with more than 2000 ft. of prominence), which include a lot of well-known peaks worldwide. After that will be 1000 ft., 600 ft., and finally 300 ft. AscenDB includes several million peaks with less than 300 ft. of prominence, and our map will color them jet black. Here's a simple function that encodes our color scheme:

let getColor = prom => {
    if (prom >= 4921) {
        return '#eeeeee';
    } else if (prom >= 2000) {
        return '#cccccc';
    } else if (prom >= 1000) {
        return '#999999';
    } else if (prom >= 600) {
        return '#666666';
    } else if (prom >= 300) {
        return '#333333';
    } else {
        return '#000000';
    }
};

There's certainly more interesting color schemes to choose from, and perhaps more elegant ways to code them, but this gets the job done. Now, we have to specify to AscenDB that we want prominence values for every peak in addition to latitude and longitude, and make use of our color function on the results. Here's the final version of our click handler:

let markers = [];
map.on('click', event => {
    let bounds = map.getBounds();
    let center = bounds.getCenter();
    let distance = turf.distance(turf.point(center.toArray()), turf.point([center.lng, bounds.getNorth()])) * 1000;

    let peakFields = ['lat', 'lon', 'prom'];
    let peakFilter = { near: { lat: center.lat, lon: center.lng, rad: distance } };
    peaks({ fields: peakFields, filters: peakFilter }).then(peaks => {
        markers.forEach(m => m.remove());
        markers = [];
        peaks.forEach(p => {
            let marker = new mapboxgl.Marker({color: getColor(p.prom)});
            marker.setLngLat([p.lon, p.lat]);
            marker.addTo(map);
            markers.push(marker);
        });
    });
});

Notice the call to getColor in the Marker constructor, and the addition of 'prom' to the peakFields array.

And that's all! You should now have a functional interactive peak map, complete with peak markers colored according to their prominence. A Gist containing the final code can be found on GitHub. As a last treat, here's a snapshot of the region around Mount Borah in AscenDB's home state of Idaho!

I hope this tutorial works as a good introduction to AscenDB. Until next time, see you at the summit!

TutorialRob Kleffner