Adding MapBox Blocks to Wagtail Stream Fields

Introduction

This began as a (relatively) simple idea to be able to add maps as Wagtail blocks on a page. Drop some pins onto a MapBox map and optionally create a route with your choice of transport means.

As I got further in, I realised there were quite a few hurdles to get across to do this:

  1. I needed to create a nested stream block that could store an unknown number of GPS points
  2. Since this block could be repeated on a page, the component ID’s that the MapBox JavaScript would be writing back to needed to be unique.
  3. I needed a way to send a complex data structure from the Django backend to the JavaScript securely and with minimum fuss. Because of the unique ID requirement, this ended up needing a small hack for one of Django’s default filters.
Note: All Wagtail code detailed below is 3.x.

Blocks

Imports

Imports you’ll need for the blocks:

from django.forms.utils import ErrorList
from django.utils.translation import gettext_lazy as _
from wagtail.blocks import (BooleanBlock, ChoiceBlock, StreamBlock, StructBlock, TextBlock)
from wagtail.blocks.field_block import IntegerBlock
from wagtail.blocks.struct_block import StructBlockValidationError

Waypoints

What information do we need to construct the map?

The map would have some display properties and a collection of waypoints (GPS coordinates) to add to the map. Since we don’t know the number of waypoints we’ll be adding to each map, we’ll need to create a stream of waypoints within the map block.

What do we need for a waypoint?

  • GPS coordinates. For ease of processing, these should be supplied in decimal notation.
  • Do we want to show the waypoint on the map? Definitely, this should be available, but there may be times you don’t want it (maybe you’re just adding one to force the route along a certain path). Option to display the waypoint then.
  • If we're displaying a pin, chances are you'll also want a label to display on the pin. This should also be optional.

I could have added the latitude/longitude as separate numeric fields – it would have made numeric checking easier, and possibly more robust for locales that use different number formats. For this instance, I left it as one text field so that it could be quickly pasted in from Google Maps - there's a small trade-off between robustness and usability. I do a custom check (clean()) before saving the waypoint to catch any obvious errors. If you split this into two numeric fields, you'll need to amend the clean() method, and update the waypoint constructor in the template tag.

On the subject of the clean(), Python's isnumeric() string method will just tell you if every character is numeric and will return false if it contains a decimal point. The following method can be used to check if a string can be cast as a float:

def isfloat(element: str) -> bool:
    try:
        float(element)
        return True
    except ValueError:
        return False

The waypoint block:

class MapWaypointBlock(StructBlock):
    gps_coord = TextBlock(
        label=_('GPS Coordinates (Latitude, Longtitude)'),
        help_text=_('Ensure latitude followed by longitude separated by a comma (e.g. 42.597486, 1.429252).')
        )
    pin_label = TextBlock(
        label=_('Map Pin Label (optional)'),
        help_text=_('Text for map pin pop-up (if used).'),
        required=False
    )
    show_pin = BooleanBlock(
        label=_('Show Pin on Map'),
        default=True,
        required=False
    )
    class Meta:
        icon = 'plus-inverse'
        label = _("Map Waypoint")
        
    def clean(self, value):
        errors = {}
        gps = value.get('gps_coord')

        if gps.count(',') != 1:
            errors['gps_coord'] = ErrorList([_("Please enter latitude followed by longitude, separated by a comma.")])
            raise StructBlockValidationError(block_errors=errors)

        lat, lng = gps.split(',')

        if not(isfloat(lat) and isfloat(lng)):
            errors['gps_coord'] = ErrorList([_("Please enter latitude and longitude in numeric format (e.g. 42.603552, 1.442655 not 42°36'12.8\"N 1°26'33.6\"E).")])
            raise StructBlockValidationError(block_errors=errors)

        if (float(lat) < -90 or float(lat) > 90 or float(lng) < -180 or float(lng) > 360):
            errors['gps_coord'] = ErrorList([_("Please enter latitude between -90 and 90 and longitude between -180 and 360.")])
            raise StructBlockValidationError(block_errors=errors)

        return super().clean(value)

We have the three fields for our information decided on, some basic meta, then onto the clean():

  • Check there’s only one comma, if not, raise an error.
  • If so, split on that comma, which will give us two values – check they’re both floats. If not, the coordinates may have been written in non-decimal format (i.e. 42°36'12.8"N instead of 42.603552), an error is raised.
  • Finally, a small check that may catch additional input errors – is the latitude within the range of -90 to 90? Is the longitude within the range of -180 to 360?
  • Note that we need to use StructBlockValidationError here to be able to highlight the fields in error as we’re raising errors inside StructBlocks. Using Django’s ValidationError will raise the error but not highlight the field.

fa-solid fa-triangle-exclamation Multi-lingual sites

The above clean method relies on a numerical format similar to English (i.e. a dot for the decimal point rather than a comma). If you're on a site with locales that use commas, or mixed, it would be better to have separate numeric fields for latitude and longitude and let the operating system take care of the formatting.

To create the StreamBlock of waypoints is straightforward:

class MapWayPointStreamBlock(StreamBlock):
    waypoint = MapWaypointBlock()

Map Block

On to the map block itself.

We know we’ll need a collection of waypoints. We can set a minimum and maximum since we need at least two to create a route and can only add up to 25 as a limit set by MapBox.

MapBox offers a selection of travel methods – we’ll add these as options in a ChoiceBlock, as well as the option to not display a route at all.

class RouteOptionChoiceBlock(ChoiceBlock):
     choices=[
        ('no-route', "None"),
        ('walking', "Walking"),
        ('cycling', "Cycling"),
        ('driving', "Driving"),
        ('driving-traffic', "Driving (with traffic conditions)")
     ]

More options:

  • We can pull route length and estimated time from MapBox. We'll add this as an option.
  • Map height is a good option – for this, I’ll just use a percentage of the viewport height rather than hardcode pixel sizes.
  • Routes can be sinuous and can extend well outside of the rectangle formed by the extreme points on the map. Add some padding options to allow the whole route to be seen in tricky cases.

There are other options that I could have added but aren't included here – map style (I’m just using the default outdoor map here), adding navigation steps either to the map or to a separate adjacent table, title, etc..

class MapBlock(StructBlock):
    waypoints = MapWayPointStreamBlock(min_num=2, max_num=25, 
                  label=_("Add Waypoints (minimum 2, maximum 25)"))
    route_type = RouteOptionChoiceBlock(default='walking')
    show_route_info = BooleanBlock(
        label=_("Show Route Distance and Duration on Map"),
        default=True,
        required=False
    )
    height = IntegerBlock(default=70, min_value=20,
                    help_text=_("Height of map (% of viewport)."))
    padding_top = IntegerBlock(default=50, min_value=0)
    padding_right = IntegerBlock(default=50, min_value=0)
    padding_bottom = IntegerBlock(default=50, min_value=0)
    padding_left = IntegerBlock(default=50, min_value=0,
                    help_text=_("Pixels from edge of map to closest waypoint."))

    class Meta:
        template='blocks/map_block.html'
        icon="site"
        label = _("Interactive Map")

Site Setting

Before we go to the template tag, MapBox requires an API token which you’ll need to register for.

Rather than hard coding this into the site, it's better to add a site setting to allow it to be updated from the admin site without needing to edit any code:

from wagtail.contrib.settings.models import register_setting
@register_setting(icon='password')
class MapBoxToken(BaseSetting):
    key = models.CharField(
        max_length=100,
        null=True,
        blank=False,
        verbose_name=_("Mapbox Access Token")
    )

I have a site settings app to keep all these in one place so I import site_settings.models in the template tag below – adjust to your own site.

Template Tags

To pass information easily and securely into JavaScript, we’re going to create a JSON script from a dictionary object containing all of our values. To do this, we’ll create a template tag that takes the block as a parameter and creates the dictionary.

Back at the beginning, I mentioned I would need a unique identifier to distinguish one map from another on the same page. For this, I use the block ID. If you want to use a different UID, you just need to change this definition once here.

This is also where we’ll split that GPS field into latitude and longitude. Mapbox actually uses long/lat for some reason.

from django import template
from site_settings.models import MapBoxToken

register = template.Library()

@register.simple_tag()
def get_map_settings(block):
    try:
        token = getattr(MapBoxToken.objects.first(), 'key')
    except:
        token = ''
        print('MapBox key not found in site settings')
    
    map_settings = {
        'uid': block.id,
        'token': token,
        'route_type' : block.value['route_type'], 
        'show_route_info' : block.value['show_route_info'], 
        'padding' : [
            block.value['padding_top'], 
            block.value['padding_right'], 
            block.value['padding_bottom'], 
            block.value['padding_left']
            ],
        'waypoints' : []
        }
    
    waypoints = block.value['waypoints']
    for waypoint in waypoints:
        latitude, longitude = [round(float(x.strip()),6) for x in waypoint.value['gps_coord'].split(',')]
        map_settings['waypoints'].append({
            'longitude' : longitude,
            'latitude' : latitude,
            'pin_label' : waypoint.value['pin_label'],
            'show_pin' : waypoint.value['show_pin']
        })

    return(map_settings)

There’s a second tag needed, which is the small hack I mentioned at the beginning.

Django includes a default filter to convert dictionaries into JSON scripts:

{{value|json_script:"hello-data"}}

If value is the dictionary {'hello':'world'}, the output will be:

<script id="hello-data" type="application/json">{"hello": "world"}</script>

Unfortunately, there’s no way to parametrise the script ID with Django's template language. Looking at the filter definition, it calls a json_script() function from django.utils.html with the needed parameters. We can do this from a custom tag instead, which allows us to supply an ID at run-time.

from django.utils.html import json_script
    
@register.simple_tag()
def add_json_script(value, element_id):
    return json_script(value, element_id)

I’ve saved this to map_block_tags.py in the appropriate folder which I call in the template below.

If this seems like a lot to do each time you load a map, it is, but you should be using template caching before doing this kind of pre-processing so that it only gets performed once.

You should get a dictionary with a structure similar to the following:

{
      "uid": "34f2110b-778b-48a5-9653-62607f4c96d2",
      "token": "your.mapbox.token",
      "route_type": "walking",
      "show_route_info": true,
      "padding": [
          50,
          50,
          50,
          50
      ],
      "waypoints": [
          {
              "longitude": 11.77624,
              "latitude": 42.1541,
              "pin_label": "a",
              "show_pin": true
          },
          {
              "longitude": 12.128261,
              "latitude": 42.168219,
              "pin_label": "b",
              "show_pin": true
          }
      ]
  }

Template

Before I start on this, a note that I’m writing templates for multi-lingual sites. For text appearing directly in templates, I use a system I created called template sets. If you’re not using multi-language, or using a different system, drop the core_tags and anything calling trans in the below code. If you’d like to know more about using the template tags, you can read about it in this blog.

Stepping through the template below:

  • Load the template tags.
  • Load Mapbox styling and JavaScript. These are the current versions at the time of writing. Check these if you’re using this at a later date.
  • Use the template tag to create the settings dictionary, then make a JSON script from that with the UID as the component ID.
  • Set up the map container with the UID as a suffix (the map will be drawn to this container). Note: it needs to be empty, otherwise MapBox will throw an error. If you’re wondering about setting the height with an inline style, for some reason, setting the height as a bootstrap class would just get ignored.
  • Underneath the container, we display the route summary if required. The initial display style is none to hide the text while the route is processed. It will be set to block once the info has been returned from MapBox .
  • Finally, we call our custom JavaScript function, which is detailed later. The function will take the UID so that it can pull in the correct data set. I’ve added this to my site's JavaScript file so it is already loaded when the block is loaded. If you keep it separate, you will need to make sure it gets loaded in the template.
{%load map_block_tags core_tags%}
{%get_template_set 'maps' as trans%}

<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v2.8.1/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v2.8.1/mapbox-gl.js"></script>

{%get_map_settings block as map_settings%} 
{%add_json_script map_settings map_settings.uid%} 

<div class="block-container">
  <div id="map-{{map_settings.uid}}" class="map-block" style="height:{{self.height}}vh;"></div>
  {%if self.show_route_info and self.route_type != 'no-route'%}
    <div id="routesummary-{{map_settings.uid}}" style="display:none;text-align:left;">
      {{trans.route_length}}: <span id="distance-{{map_settings.uid}}"></span>{{trans.km}}, 
      {{trans.approximate_time}} <span id="hours-{{map_settings.uid}}"></span> {{trans.hours}}.
    </div>
  {%endif%}
</div>

<script>draw_mapblock("{{map_settings.uid}}")</script>

You'll need the following class to add to your CSS:

.map-block {
  position: relative;
  top: 0px;
  right: 0px;
  width: 100%;
}

JavaScript

Last but not least, the JavaScript. This is where the meat of the map functionality happens.

Much of the following came from ideas I scraped from the MapBox help section on directions - mostly the first example where the user can click two points to draw a map route.

We start off by just retrieving the data from the JSON script and then passing this on to the main function. Why do this? Well, maybe you’re not coming from Django/Wagtail and already have the data ready to go. Less recoding that way. If that’s the case, be sure to look at the JSON object structure above first.

Back in the template, we added the JSON script with the tag ID set to the UID of the block. We just need to retrieve that data using the same UID.

// get the map block settings
let draw_mapblock = (uid) => {
  const map_settings = JSON.parse(
    document.getElementById(`${uid}`).textContent
  );
  add_mapbox(map_settings);
};

I’ll put the full add_mapbox function below and then break it up to step through it afterwards:

let add_mapbox = (map_settings) => {

  // create base map object
  mapboxgl.accessToken = map_settings.token;
  const map = new mapboxgl.Map({
    container: `map-${map_settings.uid}`,
    style: "mapbox://styles/mapbox/outdoors-v11",
  });
  map.addControl(new mapboxgl.NavigationControl());
  map.addControl(new mapboxgl.ScaleControl({ position: "bottom-right" }));
  //   map.scrollZoom.disable();

  // set the initial bounds of the map, bound set again after route loads
  const arrayColumn = (arr, n) => arr.map((x) => x[n]);
  const min_lat = Math.min(...arrayColumn(map_settings.waypoints, "latitude"));
  const max_lat = Math.max(...arrayColumn(map_settings.waypoints, "latitude"));
  const min_lng = Math.min(...arrayColumn(map_settings.waypoints, "longitude"));
  const max_lng = Math.max(...arrayColumn(map_settings.waypoints, "longitude"));
  map.fitBounds(
    [
      [min_lng, min_lat],
      [max_lng, max_lat],
    ],
    {
      padding: {
        top: map_settings.padding[0],
        right: map_settings.padding[1],
        bottom: map_settings.padding[2],
        left: map_settings.padding[3],
      },
    }
  );

  // create a function to make a directions request
  async function getRoute(coord_list) {
    // build the gps points query string
    let points = [];
    for (let i = 0; i < coord_list.length; i++) {
      points.push([coord_list[i].longitude, coord_list[i].latitude].join());
    }
    let gps_list = points.join(";");
    const query = await fetch(
      `https://api.mapbox.com/directions/v5/mapbox/${map_settings.route_type}/${gps_list}?steps=false&geometries=geojson&access_token=${mapboxgl.accessToken}`,
      { method: "GET" }
    );
    // request json data
    const json = await query.json();
    const data = json.routes[0];
    const route = data.geometry.coordinates;
    const geojson = {
      type: "Feature",
      properties: {},
      geometry: {
        type: "LineString",
        coordinates: route,
      },
    };
    map.addLayer({
      id: `route-${map_settings.uid}`,
      type: "line",
      source: {
        type: "geojson",
        data: geojson,
      },
      layout: {
        "line-join": "round",
        "line-cap": "round",
      },
      paint: {
        "line-color": "#3887be",
        "line-width": 5,
        "line-opacity": 0.75,
      },
    });
    // send route length info back to page
    if (map_settings.show_route_info) {
        document.getElementById(`distance-${map_settings.uid}`).innerText = (
          Math.round(data.distance / 100) / 10
        ).toFixed(1);
        document.getElementById(`hours-${map_settings.uid}`).innerText = (
          Math.round(data.duration / 360) / 10
        ).toFixed(1);
        document.getElementById(`routesummary-${map_settings.uid}`).style.display =
          "block";
      }

    // set map bounds to fit route
    const bounds = new mapboxgl.LngLatBounds(route[0], route[0]);
    for (const coord of route) {
      bounds.extend(coord);
    }
    map.fitBounds(bounds, {
      padding: {
        top: map_settings.padding[0],
        right: map_settings.padding[1],
        bottom: map_settings.padding[2],
        left: map_settings.padding[3],
      },
    });
  }

  map.on("load", () => {
    if (map_settings.method != "no-route") {
      getRoute(map_settings.waypoints);
      // Add starting and end points to the map
      map.addLayer({
        id: "start",
        type: "circle",
        source: {
          type: "geojson",
          data: {
            type: "FeatureCollection",
            features: [
              {
                type: "Feature",
                properties: {},
                geometry: {
                  type: "Point",
                  coordinates: [
                    map_settings.waypoints[0].longitude,
                    map_settings.waypoints[0].latitude,
                  ],
                },
              },
            ],
          },
        },
        paint: {
          "circle-radius": 10,
          "circle-color": "#3887be",
        },
      });
      let end_index = map_settings.waypoints.length - 1;
      map.addLayer({
        id: "end",
        type: "circle",
        source: {
          type: "geojson",
          data: {
            type: "FeatureCollection",
            features: [
              {
                type: "Feature",
                properties: {},
                geometry: {
                  type: "Point",
                  coordinates: [
                    map_settings.waypoints[end_index].longitude,
                    map_settings.waypoints[end_index].latitude,
                  ],
                },
              },
            ],
          },
        },
        paint: {
          "circle-radius": 10,
          "circle-color": "#f30",
        },
      });
    }

    // add markers with Google Maps links
    map_settings.waypoints.forEach(function (waypoint) {
      if (waypoint.show_pin) {
        const marker = new mapboxgl.Marker()
          .setLngLat([waypoint.longitude, waypoint.latitude])
          .setPopup(
            new mapboxgl.Popup().setHTML(
              "<b>" +
                waypoint.pin_label +
                "</b><br/>" +
                `<a href="https://www.google.com/maps?q=${waypoint.latitude},${waypoint.longitude}" 
                       target="_blank">${waypoint.latitude}, ${waypoint.longitude}</a>`
            )
          ) // add popup
          .addTo(map);
      }
    });
  });
};

Breaking this into chunks, we’ll start off with the initialisation:

  • Set the token from the value in the settings.
  • Create the Map object with the target container ID (‘map-UID’) and map style. Again, v11 is the current version at the time of writing. Check this before using it. I’ve hardcoded the default outdoors map here. You could change this to another map or to a parametrised value. You can also use MapBox Studio to create your own maps and call them here.
  • Add navigation and scale buttons.
  • A commented out command to disable mouse wheel zooming is left in (map.scrollZoom.disable();), in case you prefer to do this.
  • Next, set the initial bounds of the map based on the waypoints. Take the minimum and maximum of both latitude and longitude to set this. Doing this means the map is more or less in the right place while waiting for the route to load. Without it, you have a world map for a couple of seconds beforehand.
mapboxgl.accessToken = map_settings.token;
  const map = new mapboxgl.Map({
    container: `map-${map_settings.uid}`,
    style: "mapbox://styles/mapbox/outdoors-v11",
  });
  map.addControl(new mapboxgl.NavigationControl());
  map.addControl(new mapboxgl.ScaleControl({ position: "bottom-right" }));
  //   map.scrollZoom.disable();

  // set the initial bounds of the map, bound set again after route loads
  const arrayColumn = (arr, n) => arr.map((x) => x[n]);
  const min_lat = Math.min(...arrayColumn(map_settings.waypoints, "latitude"));
  const max_lat = Math.max(...arrayColumn(map_settings.waypoints, "latitude"));
  const min_lng = Math.min(...arrayColumn(map_settings.waypoints, "longitude"));
  const max_lng = Math.max(...arrayColumn(map_settings.waypoints, "longitude"));
  map.fitBounds(
    [
      [min_lng, min_lat],
      [max_lng, max_lat],
    ],
    {
      padding: {
        top: map_settings.padding[0],
        right: map_settings.padding[1],
        bottom: map_settings.padding[2],
        left: map_settings.padding[3],
      },
    }
  );

I’ll skip over the getRoute subroutine for the moment and get to the next loading point, which is the map on load event code:

  • If showing some type of route, call the getRoute subroutine (more details later) and add blue and red spots to mark the first and last waypoints on the route.
  • For each waypoint where show_pin was selected, add the pin on the map with its label. I’m also adding a Google Map link to that GPS coordinate.
map.on("load", () => {
    if (map_settings.method != "no-route") {
      getRoute(map_settings.waypoints);
      // Add starting and end points to the map
      // See following section for code and explanation 
    }

    // add markers with Google Maps links
    map_settings.waypoints.forEach(function (waypoint) {
      if (waypoint.show_pin) {
        const marker = new mapboxgl.Marker()
          .setLngLat([waypoint.longitude, waypoint.latitude])
          .setPopup(
            new mapboxgl.Popup().setHTML(
              "<b>" +
                waypoint.pin_label +
                "</b><br/>" +
                `<a href="https://www.google.com/maps?q=${waypoint.latitude},${waypoint.longitude}" 
                       target="_blank">${waypoint.latitude}, ${waypoint.longitude}</a>`
            )
          ) // add popup
          .addTo(map);
      }
    });
  });

The final part of the jigsaw is the getRoute function which is an async call that sets up a promise and returns all the route info:

  • Build a GPS list for the API query string (a list of lng/lat pairs separated by semicolons)
  • Make the API call
    • check the version if you do this (currently v5)
    • steps=false is hardcoded here, change this if you want to access the route steps
  • The first route is used and assigned to the data variable – MapBox actually returns a list of routes which you can take advantage of
  • The route layer is added (note I suffix 'route' with the UID to make sure it’s unique on the page)
  • If the option to show the route summary was selected, send this data to the appropriate elements and display the summary container.
  • Lastly, gather all the step coordinates from the route and fit the map to show all of these
  • Note, this needs to be declared before the map load event and is just described last here to keep it in chronological order
async function getRoute(coord_list) {
    // build the gps points query string
    let points = [];
    for (let i = 0; i < coord_list.length; i++) {
      points.push([coord_list[i].longitude, coord_list[i].latitude].join());
    }
    let gps_list = points.join(";");
    const query = await fetch(
      `https://api.mapbox.com/directions/v5/mapbox/${map_settings.route_type}/${gps_list}?steps=false&geometries=geojson&access_token=${mapboxgl.accessToken}`,
      { method: "GET" }
    );
    // request json data
    const json = await query.json();
    const data = json.routes[0];
    const route = data.geometry.coordinates;
    const geojson = {
      type: "Feature",
      properties: {},
      geometry: {
        type: "LineString",
        coordinates: route,
      },
    };
    map.addLayer({
      id: `route-${map_settings.uid}`,
      type: "line",
      source: {
        type: "geojson",
        data: geojson,
      },
      layout: {
        "line-join": "round",
        "line-cap": "round",
      },
      paint: {
        "line-color": "#3887be",
        "line-width": 5,
        "line-opacity": 0.75,
      },
    });

    // send route length info back to page
    if (map_settings.show_route_info) {
        document.getElementById(`distance-${map_settings.uid}`).innerText = (
          Math.round(data.distance / 100) / 10
        ).toFixed(1);
        document.getElementById(`hours-${map_settings.uid}`).innerText = (
          Math.round(data.duration / 360) / 10
        ).toFixed(1);
        document.getElementById(`routesummary-${map_settings.uid}`).style.display =
          "block";
      }

    // set map bounds to fit route
    const bounds = new mapboxgl.LngLatBounds(route[0], route[0]);
    for (const coord of route) {
      bounds.extend(coord);
    }
    map.fitBounds(bounds, {
      padding: {
        top: map_settings.padding[0],
        right: map_settings.padding[1],
        bottom: map_settings.padding[2],
        left: map_settings.padding[3],
      },
    });
  }

The Finished Product

Well, nothing is "finished" if you're truly Agile right? Here's the map block in action, the first leg of a 10-day hike I made last year. This is also a good example of when it's a good idea to not trust calculated estimates of hiking times at face value. 3 hours is plenty of time for 8km on gentle terrain; this took me a solid 6 hours, and I'm a fast hiker. Terrain makes all the difference. That's another subject altogether ...

Conclusion

Simple ideas can become surprisingly complex when you dig a bit deeper. This exercise threw up quite a few more challenges than expected, but was also a good exercise in thinking through workarounds to unusual problems.

  • Passing data structures from Django's backend to JavaScript securely
  • Passing extra parameters into Django's default filters (in this case, json_script to achieve dynamic component ID's)
  • Nesting stream blocks within stream blocks
  • Adding site settings to avoid hard coding values

The example here is a fairly basic use of MapBox, to display a route between waypoints. It could easily be adapted to show the route to your business from the customer's current location for example or other types of dynamic maps with user interaction.

MapBox is also very good for displaying GIS data. The Earthquake app demonstrated on a previous blog was written with MapBox to display earthquake size and location. Maybe you can use MapBox to display live location-based data or showcase a venue with a fly-through. There are many uses.

While making a block like this that allows you to add multiple maps to a single page, take care not to slow your page loading too much. The outdoor map style used here is quite heavy. You can use a lighter style depending on your purpose.


  Please feel free to leave any questions or comments below, or send me a message here