Mapbox GL JS Tutorial: Part One
Notes: This tutorial was written for those with some experience coding. As such I won’t be explaining basic concepts or terms but should still be pretty understandable for beginners. I will also include links when necessary for further reading up on concept that are too in depth for this post.
You will also need to have a local dev environment set up along with accounts with Github, Mapbox, and Netlify.
This tutorial will walk you through setting up a web map for the trails and trailheads of Great Smokey Mountain National Park and publishing it to the web using Webpack and Netlify. I suggest perusing the beginner tutorials from Mapbox if you are new to web mapping. The basic Mapbox tutorials use a single HTML template with code in a script tag - but once you get a few dozen lines of JavaScript going I find it easier to split up the files. The main difference with my tutorial will be that we will be splitting up the HTML, CSS, and JavaScript files which will require some bundling and configuration in order to work.
Step One - Getting Started:
I am going to walk through this like you would be starting from scratch but you can also checkout the getting_starting branch of my repo to follow along with.
To start make a repo on Github - I am going to call it mapbox_template. Then inside your terminal follow the step to set up your local environment and connect it to the remote repo.
Open up your code editor and navigate to the appropriate folder - if you made a .gitignore and README file you should see them in the explorer panel. In .gitignore you can add the node modules and dist folder that will be created shortly to the list. And anything you want to add to the README.
#.gitignore node_modules dist
Next we are going to set up our repo to be able to take packages from npm. So run ‘npm init’ and follow the commands to create your package.json file. It should look something like this when you are done, though will we make a few changes to that.
{ "name": "mapbox_template", "version": "0.0.1", "description": "", "main": "index.js", "scripts": { "test": "" }, "repository": { "type": "git", "url": "" }, "author": "You", "license": "ISC" }
I add “private” to be true (since this won’t be getting published on NPM) and remove the main line item, and you can update the scripts and dependencies to look like below. Webpack is a powerful bundling tool that does have a bit of a learn curve. That being said I am not going to go into a full description of how it works and each item in the configuration. The simplest explanation being that we are going to use Webpack to bundle our HTML, CSS, JavaScript, and other assets so that they can reference each other and function in the browser as a whole. We are also including some loading dependencies for assets and most importantly the Mapbox GL JS API to let us build our web map!
"scripts": { "build": "webpack", "start": "webpack serve", "watch": "webpack --watch", "test": "echo \"Error: no test specified\" && exit 1" }, "devDependencies": { "css-loader": "^6.7.3", "html-webpack-plugin": "^5.5.0", "style-loader": "^3.3.1", "webpack": "^5.76.2", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.13.1" }, "dependencies": { "mapbox-gl": "^2.13.0" }
After you’ve update that, run ‘npm install’ to get the packages listed. Now we can quickly set up our source code folder and the webpack config file before we run our local. If you’d like to get more in depth with Webpack checkout their getting started tutorial, but for our tutorial purposes you can just copy and paste the files from the getting_started branch in my repo to make this a little quicker.
A few small notes on the items in Webpack config. We are telling Webpack our main JS file is the index.js, that for development to use port 9000, and we’ve got an output path for our bundle.
Add a ‘src’ folder and include the index.html, index.js, and main.css files. You can also add the 404.html file and copy over the webpack.config.js file. Your file structure should now look like this:
|- index.js
|- index.html
|- main.css
|- .gitignore
|- 404.html
|- package.json
|- package-lock.json
|- README.md
|- webpack.config.js
Finally let’s take a peek at the basic source code we have now before run it. Let’s start with the HTML file. You’ll see it has all the normal html head and body tags. There is also a stylesheet link to Mapbox and inside the body we have a simple div with the id of ‘map’. This id will give our Mapbox webmap a place to load.
<!doctype html> <html class="no-js" lang=""> <head> <meta charset="utf-8"> <!-- Change Title for browser tab --> <title>Mapbox GL JS</title> ... <link href="https://api.mapbox.com/mapbox-gl-js/v2.13.0/mapbox-gl.css" rel="stylesheet"> </head> <body> <div id="map" class="map"></div> </body> </html>
Next we will check in on the main.css which has a few basic items for html & body and then a few for our map class. This is to ensure we see the map when we load up the page, if you don’t give it any height or width you will not see anything.
html, body { color: #404040; font-family: 'Open Sans', sans-serif; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; overscroll-behavior: contain; } .map { width: 100vw; position: fixed; top: 0; bottom: 0; }
Finally that brings us to our index.js file and the first two lines are very important and part of why we are using webpack. We are listing these two items, Mapbox GL JS API package and the main CSS file, so that they can be used by the web map. Webpack will bundle everything together like it is all in one file but we can keep our files separate for ease of reading and coding. You will need to go to your Mapbox account and use a public API key to fill in the access token location for anything to work.
After that we can see that we are creating a new map and giving it the ID for it to load into. We are also using a mapbox style, with a starting latitude and longitude, and a starting zoom. We are also adding basic navigation controls to the map that should appear in the bottom right of the screen.
We should have the map load with the park at the center of the map, so we will need the correct longitude and latitude.
We can use this site for a quick lat long look up, then adjust the zoom to 9 so we are maintaining some reference of where we are.
import mapboxgl from 'mapbox-gl'; import './main.css'; // TO MAKE THE MAP APPEAR YOU MUST ADD YOUR ACCESS TOKEN FROM // https://account.mapbox.com mapboxgl.accessToken = 'XXXX-XXXX-XXXX'; const map = new mapboxgl.Map({ container: 'map', // container ID to match the html style: 'mapbox://styles/mapbox/streets-v11', // style URL center: [-83.5, 35.5], // starting position [lng, lat] zoom: 9, // starting zoom, higher is more zoomed in customAttribution: 'Alicea Halsted' //Attribute your work and your data }); //Add navigation controls map.addControl(new mapboxgl.NavigationControl(), 'bottom-right');
Now that we have our base set up we can do our first ‘npm run start’, then go to http://localhost:9000/ and see if you see a map. If you've followed all the steps correctly you should see this map with the center on Great Smokey Mountain National Park.
Step Two - Adding Data to our Map:
If you aren’t seeing the map from step one you’ll need to walk back through and see what you missed. You’ll need that working before you get started on adding data.
For our tutorial we are going to be adding data from the National Park Service to show the trails and trail heads inside the park. You can checkout all the public open data from the park service here.
We are going to be downloading two files from the site. For both use the GeoJSON option, which is similar to regular JSON as it is a set of name/value pairs but in this case it is in a specific format for encoding a variety of geographic data structures such as points, lines, and polygons.
First the trailheads and then the trails. Once you’ve got the files downloaded make a new folder under ‘src’ called ‘data’ and load the files in there.
|- index.js
|- index.html
|- main.css
|- data
|- GRSM_TRAILHEADS.geojson
|- GRSM_TRAILS.geojson
It is helpful to take a quick look at the JSON structure so you can see how it is set up since we will be reference the properties when working with the data. All GeoJSONs are an feature collection object with an array of features. Each feature is an object and at the very least will contain the geometry of the feature - for example the simple point feature below. As you can see our GeoJSON data has many more properties than this - such as the location name, elevation, and county.
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {}, "geometry": { "coordinates": [ -83.53179236554101, 35.40183343980445 ], "type": "Point" } } ] }
Once you are done looking at the GeoJSONs we will be adding that data to our map. In your index.js file after adding the map we are going to use the map lifecycle event of ‘load’ to then add our data and layers. The load event fires immediately after the necessary resources have been downloaded and the first visually complete rendering of the map has occurred. Since we want our data to be displayed immediately this a great time to fetch that information.
There are two steps we need to do inside load for each data layer - adding the source and adding the layer. Adding the source we will need to create a source ID, list the type as GeoJSON and then point to the file in our data folder. Once you’ve added a source it can be used multiple times as different layers but always needs to be added as a source first.
//Load files to map map.on('load', () =>{ //Add GeoJSON point with simple circles map.addSource('trailheads',{ type: 'geojson', data:'../data/GRSM_TRAILHEADS.geojson' }), map.addLayer({ id: 'trailheads', type: 'circle', source: 'trailheads', paint: { 'circle-radius': 5, 'circle-color': 'blue' } }) //Add GeoJSON lines with simple lines map.addSource('trails',{ type: 'geojson', data:'../data/GRSM_TRAILS.geojson', generateId: true //Add ids to each feature }) map.addLayer({ id: 'trails', type: 'line', source: 'trails', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#7e9480', 'line-width': 4 }, }) });
When you add a layer you will need to give it it’s own ID (here I just matched the source ID for the initial load of that layer), the type (point or line for these files), and some basic layout and paint fields. For example we are telling the trailheads layer to paint blue circles with a radius of 5px.
Now if you still have your code running in localhost you may have gone to that tab and seen that no data is showing on the screen - what is happening here?!
If you had your console open you should see two 404 errors message that Mapbox can’t find those files. This is a spot where using webpack can seem tricky. Since we aren’t importing the files directly at the top of our JavaScript file Webpack isn’t looking for the files to be bundled in. The easiest way to ensure that these files are available is to directly tell Webpack in the config file to copy over all of the data folder. For this we will need to install a new plugin called ‘copy-webpack-plugin’.
After installing you can update the plugins section of your config to include this new one.
//In your terminal npm install copy-webpack-plugin --save-dev //In your webpack.config.js const CopyPlugin = require("copy-webpack-plugin"); module.exports = { plugins: [ ... new CopyPlugin({ patterns: [ { from: "src/data", to: "data" }, ], }), ], };
Now end the current start process and rerun to take in the new webpack changes, and you should see the data on your screen. At this point it would be good to play with the layers so you can see how they update when you make changes to colors and sizes. You can also add the trailheads in after the trails so that it gets painted on top of them instead of below, like you can see in this screenshot. The order you list them in the load function matters.
Now that’s we’ve got some data on the map don’t forget to attribute that data from the National Park Service into the custom attribution attribute when you create your map.
Check out part two to learn how to add interactions to the map so our user can get more information.