Integrate Mapbox into your Web App using only Python
Tags: webdev, python, tutorial
Published: 2023-05-24 15:35:26


# Add powerful Mapbox maps into your Anvil app


> Watch me [live code this app](https://www.youtube.com/watch?v=A0QSCoQ6ozs&t=1072s) during a Mapbox webinar.


[Mapbox](https://mapbox.com) is a powerful tool for building interactive customizable maps and integrating location and navigation data into your apps and websites. With Anvil, you can add Mapbox to your web applications entirely in Python - no JavaScript needed.

[Anvil](https://anvil.works?utm_source=crosspost:dev.to:/articles/mapbox-isochrone) is a platform for building full-stack web applications with nothing but Python. In this tutorial, I'm going to show you how to build an Anvil app, which uses Mapbox's [Isochrone API](https://docs.mapbox.com/api/navigation/isochrone/) to estimate how far you can travel given a starting point and a certain amount of time. My app build is based on [this tutorial](https://docs.mapbox.com/help/tutorials/get-started-isochrone-api/), but requires no JS.


![Screenshot of the final app](https://anvil.works/articles/img/mapbox-isochrone/mapbox-app.png)

First, we'll [set up our app](#1-set-up-the-app-and-mapbox-map) by creating a blank Anvil app, getting a Mapbox access key and adding a `Map` object to our new app. Next, we'll [add a geocoder](#2-add-a-geocoder) to the app so that users can search for a location, and then we'll use [the Isochrone API](#3-add-the-isochrone-api) estimate how far we can travel in a certain amount of time. Finally, we'll [build the UI](#4-build-the-front-end) for our app.

Let's get started!  

---

## 1. Set up the App and Mapbox Map

### Create an Anvil app

We first need to create a blank Anvil app: 

![Location of the Create Blank App button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yz4kbcifnocgir5h4eh4.png)

### Get a Mapbox access key
Next, we need to create a Mapbox account and get an access key. You can sign up for a free account at [mapbox.com](https://mapbox.com), which will give you a public access token. 

Now that we have a Mapbox account and an access token, we can add a basic map to our Anvil app. This step is based on Mapbox's [quickstart tutorial](https://docs.mapbox.com/mapbox-gl-js/api/). 

We first need to add the Mapbox GL JS library and CSS file into our Anvil app. Add the following code to the [Native Libraries](https://anvil.works/docs/client/javascript#using-native-javascript-libraries?utm_source=crosspost:dev.to:/articles/mapbox-isochrone) section of your app:

```html
<script src='https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css' rel='stylesheet' />
```

The blank Anvil app we created already has a Form (`Form1`) that will load when someone visits our app. In our `Form1` code, we need to add the following import statements:

```python
from anvil.js.window import mapboxgl
import anvil.js
import anvil.http
```

[`anvil.js.window`](https://anvil.works/docs/client/javascript/accessing-javascript?utm_source=crosspost:dev.to:/articles/mapbox-isochrone) allows us to access JS libraries in our Anvil app and interact with JS objects from Python. This way, we can still make use of Mapbox's powerful JS library, but we can do it with Python.

### Add a Map object

We need to add a placeholder component to our app that will hold our map. A `Spacer` works nicely because we can easily set the height. Drag and drop one from the Toolbox onto the form, and set its height to something reasonable.

We want the map to be displayed when the form is opened, so select the form and scroll down to the bottom of the Properties panel to create an event handler for the `show` event:

![Adding a Spacer component and the `form_show` event](https://anvil-website-static.s3.eu-west-2.amazonaws.com/blog/mapbox-isochrone/spacer-and-form-show.gif)

Anvil has created the `form_show` event handler for you, and this is where we want to write code to display our map. 

In `form_show`, we first need to set our Mapbox access token then create a [Mapbox `Map` object](https://docs.mapbox.com/mapbox-gl-js/api/map/). We'll pass in a dictionary that tells the `Map` object where it should be displayed (the `Spacer` component we added), how the map should be styled, where to center the map (as `[longitude, latitude]` coordinates) and how zoomed in the map should be. 

```python
def form_show(self, **event_args):
    """This method is called when the HTML panel is shown on the screen"""
    #I defined my access token in the __init__
    mapboxgl.accessToken = self.token 

    #put the map in the spacer 
    self.mapbox = mapboxgl.Map({'container': anvil.js.get_dom_node(self.spacer_1), 
                                'style': 'mapbox://styles/mapbox/streets-v11', #use the standard Mapbox style
                                'center': [0.1218, 52.2053], #center on Cambridge
                                'zoom': 11})
```


>   **Why are we using the Show event rather than setting this up in the constructor?**
>   We need our spacer component to be on the page before we initialise Mapbox's API. `__init__` is called before adding the Form to the page, but the show event is triggered afterwards. ([See more in the Anvil docs](https://anvil.works/docs/client/components#the-show-and-hide-events?utm_source=crosspost:dev.to:/articles/mapbox-isochrone))


### Add a Marker object

Finally, we can add a [`Marker` object](https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker) to our map. We first need to create the object, give it a color and set `draggable` to `True`. Then we can set its `[longitude, latitude]` coordinates and add it to the map. This is added to the `form_show` event:

```python
    self.marker = mapboxgl.Marker({'color': '#5a3fc0', 'draggable': True})
    self.marker.setLngLat([0.1218, 52.2053]).addTo(self.mapbox)
```
![Mapbox map with a Marker added](https://anvil.works/articles/img/mapbox-isochrone/map-with-marker.png)

---

## 2. Add a Geocoder

Mapbox's [Geocoding API](https://docs.mapbox.com/api/search/geocoding/) allows us to easily turn location searches into longitude and latitude coordinates. We can use the [Mapbox GL JS Geocoder plugin](https://docs.mapbox.com/mapbox-gl-js/plugins/#mapbox-gl-geocoder) to add a search bar to our map. When a users searches for and selects a place, the map will fly over to this new location. 

### Import the geocoder

We first need to add the Mapbox Geocoder. In Native Libraries, add the following lines:

```html
<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' />
```

Next, change the import statement you added in the previous step to also import `MapboxGeocoder`:

```python
from anvil.js.window import mapboxgl, MapboxGeocoder
```

### Add a geocoder to the map

Next we need to instantiate a [`MapboxGeocoder` object](https://github.com/mapbox/mapbox-gl-geocoder/blob/master/API.md) by passing in our access token. Then we can add it to our map. The following code should be added to the `form_show` event:

```python
    self.geocoder = MapboxGeocoder({'accessToken': mapboxgl.accessToken,
                                    'marker': False}) #we've already added a marker
    self.mapbox.addControl(self.geocoder)
```

### Move the marker

We now have a geocoder added to our map. It will center the map to the location we search for, but it doesn't move the marker. Let's change that. We'll write a function that gets the new coordinates from the geocoder and sets the marker to these coordinates after the user selects a location from the geocoder:

```python
def form_show(self, **event_args):

    ...

    #when the 'result' event is triggered, call self.move_marker
    self.geocoder.on('result', self.move_marker)

def move_marker(self, result):
    #get the [longitude, latitude] coordinates from the JS object returned from 'result'
    lnglat = result['result']['geometry']['coordinates']
    self.marker.setLngLat(lnglat)
```


<img src="https://anvil-website-static.s3.eu-west-2.amazonaws.com/blog/mapbox-isochrone/geocoder-search.gif">

---

## 3. Add the Isochrone API

Now that we have a map, a marker and a geocoder, we can calculate the isochrone lines that tell us how far we can travel given a specified amount of time.

To make the request to the Isochrone API, we need to pass in the `profile` ('walking', 'cycling' or 'driving'), the coordinates of our starting point (as `[longitude, latitude]`) and the number of minutes we want to calculate the isochrone for (`contours_minutes`). We can get the longitude and latitude coordinates from our marker and collect the `profile` and minutes from the front-end of our app (we'll build this in the next step).

```python
def get_iso(self, profile, contours_minutes):
    lnglat = self.marker.getLngLat()
    response = anvil.http.request(f"https://api.mapbox.com/isochrone/v1/mapbox/{profile}/{lnglat.lng},{lnglat.lat}?contours_minutes={contours_minutes}&polygons=true&access_token={self.token}", json=True)
```

`response` will be a JSON object representing the isochrone lines we want to add to our map. In order to add these, we first need to add a [Source](https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/), which tells the map which data it should display, and a [Layer](https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/), which defines how to style the source data.

We should set the `source` and `layer` at the beginning of our `get_iso` function. Then after we get the response from the API, we can set that as the data for the source to display:

```python
def get_iso(self, profile, contours_minutes):
    #we only want to create the source and layer once
    if not self.mapbox.getSource('iso'):
        self.mapbox.addSource('iso', {'type': 'geojson',
                                        'data': {'type': 'FeatureCollection',
                                                        'features': []}})
        self.mapbox.addLayer({'id': 'isoLayer',
                                'type': 'fill',
                                'source': 'iso', #the id of the source we just defined
                                'layout': {},
                                'paint': {
                                'fill-color': '#5a3fc0',
                                'fill-opacity': 0.3}})

    lnglat = self.marker.getLngLat()
    response = anvil.http.request(f"https://api.mapbox.com/isochrone/v1/mapbox/{profile}/{lnglat.lng},{lnglat.lat}?contours_minutes={contours_minutes}&polygons=true&access_token={self.token}", json=True)

    self.mapbox.getSource('iso').setData(response)
```

Let's now build a front-end for our app so that users can specify an amount of time and a mode of travel.

---

## 4. Build the Front End

We can add two DropDown menus to our app so that users can select an amount of time and a mode of travel. Name the first DropDown `time_dropdown` and the other `profile_dropdown`.

![Adding DropDown menus to the form](https://anvil-website-static.s3.eu-west-2.amazonaws.com/blog/mapbox-isochrone/add-dropdowns.gif)

When we pass an amount of time as the `contours_minutes` argument to our `get_iso` function, we need it to be a number as a string (e.g. '10'). If we set `time_dropdown.items` from code, we can use a list of 2-tuples where the the first element of each tuple will be displayed in the DropDown and the second element will be the `selected_value` of the DropDown:

```python
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.    
    self.time_dropdown.items = [('10 minutes', '10'), ('20 minutes', '20'), ('30 minutes', '30')]
```

The items for the `profile_dropdown` should be 'walking', 'cycling' and 'driving'. These can be set directly in the Properties panel of the Editor or via code:

```python
self.profile_dropdown.items = ['walking', 'cycling', 'driving']
```

We now need to add event handlers so that `get_iso` is called whenever the value of a DropDown menu changes. For each DropDown on your form, scroll down to the bottom of the Properties panel to create an event handler for the `change` event. Then call `get_iso` with the selected values of the DropDowns as arguments to the function:

```python
  def time_dropdown_change(self, **event_args):
      """This method is called when an item is selected"""
      self.get_iso(self.profile_dropdown.selected_value, self.time_dropdown.selected_value)

  def profile_dropdown_change(self, **event_args):
      """This method is called when an item is selected"""
      self.get_iso(self.profile_dropdown.selected_value, self.time_dropdown.selected_value)
```

Finally, we can also call `get_iso` when the Marker is dragged. We'll do this just like we did in [Step 2](#2-add-a-geocoder) when we caught the `result` event being triggered on the geocoder and moved the Marker. This time, we need to catch when the `drag` event is triggered on the Marker, then call `get_iso`:

```python
  def form_show(self, **event_args):
      ...

      self.marker.on('drag', self.marker_dragged)

  def marker_dragged(self, drag):
      self.get_iso(self.profile_dropdown.selected_value, self.time_dropdown.selected_value)   
```

And that's it! We now have a fully working app built from the Mapbox Isochrone API that shows us how far we can travel in a specific amount of time. I made some changes to the style of my map and app, which I'll show you how to do in the next (optional) step.

![The finished app in action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5e2alv1ybabsfzgsr2v7.gif)

You can check out the full source code of the app [here](https://anvil.works/build?utm_source=crosspost:dev.to:/articles/mapbox-isochrone#clone:HYM75SW2AAJQ74SJ%3dXVRPOQL2FURO7SEWXB5B6SES)

---

## 5. Optional: Style your app (Advanced)

To make my app look a bit nicer, I first changed the style of the Mapbox map. 

[This page](https://docs.mapbox.com/mapbox-gl-js/api/map/) lists Mapbox's predefined styles, but you can also design your own in the [Studio](https://www.mapbox.com/mapbox-studio) or choose from the [gallery](https://www.mapbox.com/gallery/). I chose the style [Frank](https://www.mapbox.com/gallery/#frank) from the gallery and added this to my account. After adding the style, you'll be in Mapbox Studio. Click "Share" in the top menu and copy the 'Style URL'. When instantiating your map, pass this URL as the map's 'style'. 

I also changed the color of the Marker and the Isochrone fill to better match the 'Frank' style. I also set the height of the Spacer holding the map to `100vh` so that the map would fill the entire page. 

I then edited the app's HTML to remove the app bar and add an extra `<div>` element that floats on top of the map. At the top of `standard-page.html`, I commented out the `app-bar` and added a `floating-menu`.

```html
 <div class="structure">
<!--   <div class="app-bar" anvil-drop-container=".anvil-container" anvil-drop-redirect=".placeholder">
    <a class="sidebar-toggle" anvil-if-slot-empty="top-left-btn" anvil-hide-if-slot-empty="left-nav" anvil-drop-slot="top-left-btn" href="javascript:void(0)"><i class="fa fa-bars"></i></a>
    <a class="sidebar-toggle anvil-designer-only" anvil-if-slot-empty="top-left-btn" anvil-if-slot-empty="left-nav" anvil-drop-slot="top-left-btn"><i class="fa fa-blank"></i></a>
    <div class="top-left-btn" anvil-slot="top-left-btn"></div>
    <div class="title" anvil-slot="title">
      <div class="placeholder anvil-designer-only" anvil-if-slot-empty="title" anvil-drop-here>Drop title here</div>
    </div>
    <div class="app-bar-nav" anvil-slot="nav-right">
      <div class="placeholder anvil-designer-only" anvil-if-slot-empty="nav-right" anvil-drop-here>Drop a FlowPanel here</div>
    </div>
    <div style="clear:both"></div>
  </div> -->
  <div class="floating-menu" anvil-drop-here anvil-slot=".floating-menu">
  </div>
```

I added my DropDown menus to this floating element and edited its class in `theme.css` so that it's background is transparent, and it stays fixed to the map:

```css
.floating-menu {
  position: absolute;
  z-index: 100;
  min-height: 250px;
  min-width: 400px;
  margin: 50px 100px;
  padding: 30px 40px;
  background-color: rgba(229, 213, 194, 0.7);
}
```

---

## More about Anvil

If you're new here, welcome! [Anvil](https://anvil.works?utm_source=crosspost:dev.to:/articles/mapbox-isochrone) is a platform for building full-stack web apps with nothing but Python. No need to wrestle with JS, HTML, CSS, Python, SQL and all their frameworks – just **build it all in Python**.