Make a healthy food finder with the Tilequery API
This tutorial demonstrates how you can combine your own custom geospatial data, the Tilequery API, and Mapbox's global address and place data using the Geocoding API to create a cohesive, custom reverse search experience for your users.
In this tutorial, you will create a web app that shows all stores that sell food that are located within one mile of a query point. You will use Mapbox GL JS to create the interface, the Mapbox Geocoding API (in the form of the Mapbox GL JS Geocoder plugin) to set a query point, and the Tilequery API to find stores that are within one mile of that query point.
A person using this app could search for a location, then use the results to determine whether the area is a food desert. A food desert is an urban area in which at least 33% of the population live more than one mile from a supermarket or large grocery store. (For rural areas, the distance is more than 10 miles.) People who live in these food desert areas may have difficulty finding healthy foods. Identifying a food desert can be the first step toward solving the issue.
Getting started
To complete this tutorial, you will need:
- A Mapbox access token. Your Mapbox access tokens are on your Account page.
- Mapbox GL JS. Mapbox GL JS is a JavaScript API for building web maps.
- A text editor. Use the text editor of your choice for writing HTML, CSS, and JavaScript.
- Data. In this tutorial, you will use food store data from the City of Denver Open Data Catalog. The original file has been simplified by removing columns that are not relevant to this tutorial.
Upload the data as a tileset
A tileset is a collection of raster or vector data broken up into a uniform grid of square tiles. Tilesets are highly cacheable and load quickly, and help Mapbox maps load quickly. In this step, you will upload the food store location to Mapbox as a new tileset so that you can access it in later steps.
- Log into Mapbox Studio and navigate to the Tilesets page.
- Click the New tileset button.
- Click the Select a file button and navigate to the location in which you saved the
food_stores.csv
file. - Select
food_stores.csv
, then click Confirm. - When the upload successfully finishes, it will appear at the top of the custom tilesets list on your Tilesets page.
- Click on the Menu button next to the new tileset's name. Find the tileset ID, which is the tileset's unique identifier. You will need to use this in a later step, so remember where you found it!
Create a map
Next, you will create a map using Mapbox GL JS.
Open your text editor and create a new file named index.html
. Set up this new HTML file by pasting the following code into your text editor. This code creates the structure of the page. This code also imports Mapbox GL JS in the <head>
of the page. The Mapbox GL JS JavaScript and CSS files allow you to use Mapbox GL JS functionality and style.
There is a <div>
element with the ID map
in the <body>
of the page. This <div>
is the container in which the map will be displayed on the page.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Healthy food finder</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/* Import Mapbox GL JS */}
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/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_MAPBOX_ACCESS_TOKEN '; // set the access token
const map = new mapboxgl.Map({
container: 'map', // The container ID
style: 'mapbox://styles/mapbox/light-v11', // The map style to use
center: [-105.0178157, 39.737925], // Starting position [lng, lat]
zoom: 12 // Starting zoom level
});
</script>
</body>
</html>
This Mapbox GL JS code sets a style for the map, gives it coordinates on which to center, and sets a zoom level.
Save your changes. Open the HTML file in your browser to see the rendered map, which is centered on the city of Denver.
Add the geocoder
The next step is to add a geocoder to the map using the Mapbox GL JS Geocoder plugin. This plugin allows you to take advantage of the Mapbox Geocoding API within the context of Mapbox GL JS.
To add the geocoder to your map, first add the links to the geocoder plugin's JavaScript and CSS to the head of the HTML file:
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.min.js"></script>
<link
rel="stylesheet"
href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.css"
type="text/css"
/>
Once these links have been added, you will be able to use the geocoder plugin in your app. Next, add the following code above the closing </script>
tag in your HTML file.
map.on('load', () => {
const geocoder = new MapboxGeocoder({
// Initialize the geocoder
accessToken: mapboxgl.accessToken, // Set the access token
mapboxgl: mapboxgl, // Set the mapbox-gl instance
zoom: 13, // Set the zoom level for geocoding results
placeholder: 'Enter an address or place name', // This placeholder text will display in the search bar
bbox: [-105.116, 39.679, -104.898, 39.837] // Set a bounding box
});
// Add the geocoder to the map
map.addControl(geocoder, 'top-left'); // Add the search box to the top left
});
The bbox
parameter in this code sets a bounding box, which means that the geocoder will not return any results that are outside of the specified area. The coordinates of this bounding box roughly describe the city of Denver. Learn more about the bbox
parameter in the Geocoding API documentation.
Save your changes. Refresh the page in your browser, and you will see that a geocoder search box with custom text has been added to the map. When you type a search term into the box and select a result, the map flies to that location.
Place a marker on the map
Now you have a web app that flies to a result location when a user enters a location in the geocoder. Next, you will add a marker to the result location to show where this center point is.
Paste the following code into your file, right below map.addControl(geocoder, 'top-left');
:
const marker = new mapboxgl.Marker({ color: '#008000' }); // Create a new green marker
geocoder.on('result', async (event) => {
// When the geocoder returns a result
const point = event.result.center; // Capture the result coordinates
marker.setLngLat(point).addTo(map); // Add the marker to the map at the result coordinates
});
This code snippet initializes a new marker after a result is returned, then adds it to the map at search result's coordinates. Learn more about markers in the Mapbox GL JS documentation.
Save your changes. Refresh the page in your browser and enter a location in the search box. When you choose a result, the map will fly to that location and add a green marker.
Add the Tilequery API
In the last step, you created a constant named point
that is the coordinates returned by a user's request to the Geocoding API. In this step, you will use this constant in a call to the Tilequery API.
Tilequery API request format
The Mapbox Tilequery API allows you to retrieve data about specific features from a vector tileset, based on a given latitude and longitude.
A Tilequery request requires two parameters: the tileset_id
of the tileset being queried, and the {lon, lat}
coordinates of the query point. The Tilequery API accepts an optional radius
parameter, which is a distance in meters from the query point in which to search for features. It also accepts a limit
parameter so that you can specify the maximum number of results that a query can return.
An example call to the Tilequery API that had a radius of 1,000 meters and a limit of 10 would look like:
https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/tilequery/-105.01109,39.75953.json?radius=1000&limit=10&access_token=YOUR_MAPBOX_ACCESS_TOKEN
A Tilequery API request returns a GeoJSON FeatureCollection
of features at or near the geographic point described by {lon},{lat}
and within the distance described by the radius
parameter.
Set up the Tilequery API call
In the geocoder.on('result')
method you wrote earlier, you will create three more constants, use them to create the Tilequery API request, and then use fetch to make the Tilequery API call.
Now you will create constants for the tileset that the Tilequery API will query, the radius in which it should search, and the maximum number of results to return. Go back to your Tilesets page and find the tileset ID of the tileset you created. After the point
constants that you declared earlier, add these additional constants:
const tileset = 'examples.dl46ljcs'; // replace this with the ID of the tileset you created
const radius = 1609; // 1609 meters is roughly equal to one mile
const limit = 50; // The maximum amount of results to return
Next, use these new constants to create a new constant for the Tilequery API request.
const query = `https://api.mapbox.com/v4/${tileset}/tilequery/${point[0]},${point[1]}.json?radius=${radius}&limit= ${limit} &access_token=${mapboxgl.accessToken}`;
Next, you will use fetch to make the Tilequery API call. Add the following code inside of the geocoder.on('result')
call, before the closing curly brace. In the next step you will add a circle to the map for each store in the radius area, but for now you can view the results using console.log()
.
geocoder.on('result', async (event) => {
// When the geocoder returns a result
const point = event.result.center; // Capture the result coordinates
const tileset = 'examples.dl46ljcs'; // replace this with the ID of the tileset you created
const radius = 1609; // 1609 meters is roughly equal to one mile
const limit = 50; // The maximum amount of results to return
marker.setLngLat(point).addTo(map); // Add the marker to the map at the result coordinates
const query = await fetch(
`https://api.mapbox.com/v4/${tileset}/tilequery/${point[0]},${point[1]}.json?radius=${radius}&limit=${limit}&access_token=${mapboxgl.accessToken}`,
{ method: 'GET' }
);
const json = await query.json();
console.log(json);
});
Save your changes and open your developer tools. Refresh the page in your browser and enter a location in the search box. When you select a result, the results of the call to the Tilequery API will print out to the console.
Display store locations
In the last step, you set up a Tilequery API call that sets the result coordinates of a request to the Geocoding API as the query point. Now that your query returns store locations as a result, you will use Mapbox GL JS to add a visual representation of each store's location to the map.
Add the following code to the end of your JavaScript, before the closing curly brace of the map.on('load')
function.
map.addSource('tilequery', {
// Add a new source to the map style: https://docs.mapbox.com/mapbox-gl-js/api/#map#addsource
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
map.addLayer({
// Add a new layer to the map style: https://docs.mapbox.com/mapbox-gl-js/api/#map#addlayer
id: 'tilequery-points',
type: 'circle',
source: 'tilequery', // Set the layer source
paint: {
'circle-stroke-color': 'white',
'circle-stroke-width': {
// Set the stroke width of each circle: https://docs.mapbox.com/style-spec/reference/layers/#paint-circle-circle-stroke-width
stops: [
[0, 0.1],
[18, 3]
],
base: 5
},
'circle-radius': {
// Set the radius of each circle, as well as its size at each zoom level: https://docs.mapbox.com/style-spec/reference/layers/#paint-circle-circle-radius
stops: [
[12, 5],
[22, 180]
],
base: 5
},
'circle-color': [
// Specify the color each circle should be
'match', // Use the 'match' expression: https://docs.mapbox.com/style-spec/reference/expressions/#match
['get', 'STORE_TYPE'], // Use the result 'STORE_TYPE' property
'Small Grocery Store',
'#008000',
'Supercenter',
'#008000',
'Superette',
'#008000',
'Supermarket',
'#008000',
'Warehouse Club Store',
'#008000',
'Specialty Food Store',
'#9ACD32',
'Convenience Store',
'#FF8C00',
'Convenience Store With Gas',
'#FF8C00',
'Pharmacy',
'#FF8C00',
'#FF0000' // any other store type
]
}
});
This code uses a Mapbox GL JS match
expression to set the color of each circle based on the STORE_TYPE
property:
#008000
for grocery stores of any size that are likely to have fresh produce.#9ACD32
for specialty food stores which may not have fresh produce.#FF8C00
for convenience stores, which rarely have fresh produce.#FF0000
for stores of any otherSTORE_TYPE
.
Learn more about expressions and how to use them in the Mapbox style specification.
Now that you have the tilequery
source set up and have added rules for the appearance of the points, you will reference this in the fetch call you created in the last step. You will replace console.log(data);
so that the entire function is:
geocoder.on('result', async (event) => {
// When the geocoder returns a result
const point = event.result.center; // Capture the result coordinates
const tileset = 'examples.dl46ljcs'; // replace this with the ID of the tileset you created
const radius = 1609; // 1609 meters is roughly equal to one mile
const limit = 50; // The maximum amount of results to return
marker.setLngLat(point).addTo(map); // Add the marker to the map at the result coordinates
const query = await fetch(
`https://api.mapbox.com/v4/${tileset}/tilequery/${point[0]},${point[1]}.json?radius=${radius}&limit=${limit}&access_token=${mapboxgl.accessToken}`,
{ method: 'GET' }
);
const json = await query.json();
// Use the response to populate the 'tilequery' source
map.getSource('tilequery').setData(json);
});
Save your changes and refresh the page in your browser. When you search for a location, you will see the store locations populate on the map, colored according to the match
expression.
Add popups for each store
The last step is to add popups to each store that list the store's name, type, address, and distance from the query point. Mapbox GL JS provides a popup component that allows you to customize popups according to your needs.
Add the following code to the end of your JavaScript, before the closing curly brace of the map.on('load')
function:
const popup = new mapboxgl.Popup(); // Initialize a new popup
map.on('mouseenter', 'tilequery-points', (event) => {
map.getCanvas().style.cursor = 'pointer'; // When the cursor enters a feature, set it to a pointer
const properties = event.features[0].properties;
const obj = JSON.parse(properties.tilequery); // Get the feature's tilequery object (https://docs.mapbox.com/api/maps/#response-retrieve-features-from-vector-tiles)
const coordinates = new mapboxgl.LngLat(
properties.longitude,
properties.latitude
); // Create a new LngLat object (https://docs.mapbox.com/mapbox-gl-js/api/#lnglatlike)
const content = `<h3>${properties.STORE_NAME}</h3><h4>${
properties.STORE_TYPE
}</h4><p>${properties.ADDRESS_LINE1}</p><p>${(
obj.distance / 1609.344
).toFixed(2)} mi. from location</p>`;
popup
.setLngLat(coordinates) // Set the popup at the given coordinates
.setHTML(content) // Set the popup contents equal to the HTML elements you created
.addTo(map); // Add the popup to the map
});
map.on('mouseleave', 'tilequery-points', () => {
map.getCanvas().style.cursor = ''; // Reset the cursor when it leaves the point
popup.remove(); // Remove the popup when the cursor leaves the point
});
Save your changes and refresh the page in your browser. When the store locations populate on the map, a popup will display for each location when you mouse over it.
Final product
You created an app that uses the Tilequery API to augment the Mapbox geocoder to identify potential food deserts in the city of Denver.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Make a healthy food finder with the Tilequery API</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.min.js"></script>
<link
rel="stylesheet"
href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.1-dev/mapbox-gl-geocoder.css"
type="text/css"
/>
<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 = '{{MAPBOX_ACCESS_TOKEN}}';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [-105.0178157, 39.737925],
zoom: 12
});
map.on('load', () => {
const geocoder = new MapboxGeocoder({
accessToken: mapboxgl.accessToken,
mapboxgl: mapboxgl,
zoom: 13,
placeholder: 'Enter an address or place name',
bbox: [-105.116, 39.679, -104.898, 39.837]
});
map.addControl(geocoder, 'top-left');
const marker = new mapboxgl.Marker({
'color': '#008000'
});
geocoder.on('result', async (event) => {
const point = event.result.center;
const tileset = 'examples.dl46ljcs';
const radius = 1609;
const limit = 50;
marker.setLngLat(point).addTo(map);
const query = await fetch(
`https://api.mapbox.com/v4/${tileset}/tilequery/${point[0]},${point[1]}.json?radius=${radius}&limit=${limit}&access_token=${mapboxgl.accessToken}`,
{ method: 'GET' }
);
const json = await query.json();
map.getSource('tilequery').setData(json);
});
map.addSource('tilequery', {
type: 'geojson',
data: {
'type': 'FeatureCollection',
'features': []
}
});
map.addLayer({
id: 'tilequery-points',
type: 'circle',
source: 'tilequery',
paint: {
'circle-stroke-color': 'white',
'circle-stroke-width': {
stops: [
[0, 0.1],
[18, 3]
],
base: 5
},
'circle-radius': {
stops: [
[12, 5],
[22, 180]
],
base: 5
},
'circle-color': [
'match',
['get', 'STORE_TYPE'],
'Convenience Store',
'#FF8C00',
'Convenience Store With Gas',
'#FF8C00',
'Pharmacy',
'#FF8C00',
'Specialty Food Store',
'#9ACD32',
'Small Grocery Store',
'#008000',
'Supercenter',
'#008000',
'Superette',
'#008000',
'Supermarket',
'#008000',
'Warehouse Club Store',
'#008000',
'#FF0000' // any other store type
]
}
});
const popup = new mapboxgl.Popup();
map.on('mouseenter', 'tilequery-points', (event) => {
map.getCanvas().style.cursor = 'pointer';
const properties = event.features[0].properties;
const obj = JSON.parse(properties.tilequery);
const coordinates = new mapboxgl.LngLat(
properties.longitude,
properties.latitude
);
const content = `<h3>${properties.STORE_NAME}</h3><h4>${
properties.STORE_TYPE
}</h4><p>${properties.ADDRESS_LINE1}</p><p>${(
obj.distance / 1609.344
).toFixed(2)} mi. from location</p>`;
popup.setLngLat(coordinates).setHTML(content).addTo(map);
});
map.on('mouseleave', 'tilequery-points', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});
});
</script>
</body>
</html>
Next steps
There are a lot of things you could do to build this app out more. You could:
- Add another custom tileset that has population-level data, which would allow you to investigate whether an area without many stores is truly underserved or if it doesn't have a large population. (See the Visualize population density example.)
- Experiment with expressions in Mapbox GL JS to customize the store location results based on store type even further. (See the Style circles with a data-driven property example.)
- Create a toggle function that would allow you to only display results that are a certain store type, like "Supermarket" or "Convenience Store with Gas". (See the Filter symbols by toggling a list example.)