Web Mapping Tutorials

Alicea Halsted Alicea Halsted

ArcGIS Maps SDK for JavaScript

Welcome!

Today we are going to learn how to build this neat web map on American Terrestrial Species of Common Conservation Concern with Esri’s ArcGIS Maps SDK for JavaScript. To get started you’ll need a new GitHub repo (or simply clone my getting started branch from my repo to get the basics ready while you follow along) and an API key from Esri.

Note: 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.

You will also need to have a local dev environment set up along with accounts with GitHub, Esri, and Netlify.

To get the key go to developers.arcgis.com and sign up for a developers account. Once you’ve got that signed in go to your dashboard and you’ll see an API keys tab.

**Note: I’ve had a few Esri accounts over the years, from school to personal, and depending on what log in I use I am not able to change the account to a developers one for some reason. If you’ve had an account with them before and you don’t see the API keys tab, you may need a new one.

Step One - Setting up the base map

Feel free to simply clone my getting started branch from my repo to get the basics ready while you follow along.

For this map we aren’t going to use any local data and instead are going to rely on a layer already hosted by ArcGIS server. However because I like to be prepared and have the ability to change that later we are still going to set up using Webpack like I’ve done in the past before. It will also allow us to keep our HTML, CSS, and JavaScript separate, to be bundled together later when we publish our map using Netlify.

If you’ve cloned the getting started branch you should see the three Webpack config files, you’ll note in the common one I left in but commented out the CopyPlugin for data and an ArcGIS Plugin - you don’t need either for this tutorial but if you expand on it you may need them. Checkout the Mapbox GL JS Tutorial Part 1 and Part 2 for a more in-depth discussion of Webpack. There is a README file with starting instructions, a package.json and git ignore file.

In the package.json you’ll see we loaded in @arcgis/core which will give us all the features we’ll need for our map; everything else supports the Webpack loading and bundling.

In the src folder you’ll see our base HTML, CSS, and JavaScript files. Our index.html is pretty simple with just a div with the id and class “map” and our main.css has some attributes for that to give it height and width on the page.

Our index.js file imports the CSS file and three items from the @arcgis/core package. For this API to keep it small and only load what you need we will be adding to this import list as we add features. From there you’ve got your API key and your definition of the basemap. Then a view that tells it where to place the map(the div with the id “map”), where to center the map, and what zoom to start at.

//index.html
...
<body>
  <div id="map" class="map"></div>
</body>
...

//main.css
...
.map {
    padding: 0;
    margin: 0;
    height: 100%;
    width: 100%;
    position: absolute;
}

//index.js
import './main.css';
import Map from "@arcgis/core/Map.js";
import MapView from "@arcgis/core/views/MapView.js"
import esriConfig from "@arcgis/core/config.js";

esriConfig.apiKey = 'XXXX-XXXX-XXXX';

const map = new Map({
    basemap: "arcgis-topographic" // Basemap layer service
});

const view = new MapView({
    map: map,
    center: [-94.5, 38.7951], // Longitude, latitude
    zoom: 4, // Zoom level
    container: "map" // Div element
});

Go ahead and do npm install and npm run start and see what you get. You should see a basic map centered on North America - since that will the subject of our layers today.

Note: Unlike some other map APIs Esri “loads” a map even if you don’t have the API key correct or available. If you forgot to put in your unique key and did npm run start you’ll still see something on the page, a very basic hillshade in this case. If you open the console log you’ll see there was a failure to load the layers.

Step Two - Adding a Data Layer

For this map our main center of focus is going to be data from the Commission of Environmental Cooperation on North American Terrestrial Species of Common Conservation Concern which is available in Esri’s Living Atlas. You may find it useful to open the data up in the Map Viewer so you can can do some exploring before we start adding it to our own map. You can also view the service so you can see more details on the layer including the fields.

On the item’s page on the right hand side you’ll see a URL for the service on the server: ‘https://services7.arcgis.com/oF9CDB4lUYF7Um9q/arcgis/rest/services/NA_Terrestrial_Species_of_Common_Conservation_Concern/FeatureServer’

In order to use this we need to add the Feature Layer from the @arcgis/core, then create a new FeatureLayer and add it to the map.

//other imports
import FeatureLayer from "@arcgis/core/layers/FeatureLayer.js";
.
.
.
const terrestrialSCCCLayer = new FeatureLayer({
  url: "https://services7.arcgis.com/oF9CDB4lUYF7Um9q/arcgis/rest/services/NA_Terrestrial_Species_of_Common_Conservation_Concern/FeatureServer"
});
map.add(terrestrialSCCCLayer);

You should see some colored polygons added to the map using the pre-existing definitions provided by the service. Great but that’s not very helpful for our user - plus you definitely can’t see all the polygons or the map below them. Let’s add some attributes to our feature layer starting with opacity. We’ll make the opacity 0.7 which allows the base map below to show through a bit for some added context.

It would also be nice if we can add popups. This is a great feature of the FeatureLayer. First we need to look at the fields available to us and pick what we want outputted for us to access. Let’s choose the English name, the scientific name and the family.

const terrestrialSCCCLayer = new FeatureLayer({
  url: "https://services7.arcgis.com/oF9CDB4lUYF7Um9q/arcgis/rest/services/NA_Terrestrial_Species_of_Common_Conservation_Concern/FeatureServer",
  opacity: 0.7,
  outFields: ["ENGL_NAME", "SCI_NAME", "FAMILY"],
});

Then we need to create the popup template to add to the feature layer. Above the feature layer add a constant of popupTerrestrialSCCC which is going to contain all our information. We will start with the title being the English name (i.e. American black bear) and then an array of content which we will make as a fields content element which will give us a small table of the details.

const popupTerrestrialSCCC = {
  "title": "{ENGL_NAME}",
  "content": [{
    "type": "fields",
    "fieldInfos": [
      {
        "fieldName": "SCI_NAME",
        "label": "Scientific/Latin Name",
        "isEditable": false,
        "tooltip": "",
        "visible": true,
        "format": null,
        "stringFieldOption": "text-box"
      },
      {
        "fieldName": "FAMILY",
        "label": "Family",
        "isEditable": false,
        "tooltip": "",
        "visible": true,
        "format": null,
        "stringFieldOption": "text-box"
      },
    ]
  }]
};

Now when you open your map you should be able to click on the map and see a popup. Esri does some extra work for us here by checking if there are features on top of each other at your click spot and giving you a popup with multiple selections and highlighting the corresponding polygons.
If you want to style your popup differently checkout the API documentation for Popup Content for your options.

Step Three - Adding a Filter to the Data

The popup looks good and it helps a user visualize the range of the species but I don’t really like that the Monarch Butterfly range takes up most of North America. I’d like for a user to be able to filter the data to see just a single species range at a time. To do this we can use an Esri widget to create a drop down filter.

First we need to come up the list of queries we want to give the user, in our simple example it’s just going to be a list of species. You can then use document.createElement to create the select filter drop down and using the list of queries populate the filter choices. Then using the map view ui I add the element to my screen.

//SQL query array
const terrestrialSCCCSQL = ["Choose a species","All species","American black bear","Black-tailed prairie dog","Burrowing owl","California condor", "Ferruginous hawk","Southern long-nosed bat", "Peregrine falcon","Gray wolf","Golden-cheeked warbler","Loggerhead shrike", "Mexican long-nosed bat","Monarch butterfly", "Mountain plover", "Piping plover", "Sonoran pronghorn", "Spotted owl"];

//Add SQL UI
const selectFilter = document.createElement("select");
selectFilter.setAttribute("class", "esri-widget esri-select");
selectFilter.setAttribute("style", "width: 250px; font-family: 'Avenir Next'; font-size: 1em");
terrestrialSCCCSQL.forEach(function(query){
  let option = document.createElement("option");
  option.innerHTML = query;
  if(query==="Choose a species"){
    option.value = "1=0"
  } else if(query==="All species"){
      option.value = "1=1"
  } else {
    option.value = "ENGL_NAME='"+query+"'";
  }
  selectFilter.appendChild(option);
});

view.ui.add(selectFilter, "top-right");

You’ll notice that I’ve add some if’s to my forEach function. First I’d like the top “query” to be the users prompt - if they select that it will clear the map and if they select all species it will take them back to the original map load. The final option is for the query itself - to create the query for the species range in question.

Now we will add a function that creates a definition expression based on an expression query we send to it. This is a simple attribute update for the layer. As you can see below you can also add this directly to the feature layer on load if you wanted to load queried layer first. This new function needs to be called every time a user makes a change in the drop down. So we add an event listener to listen for the change and pass on our event target’s value, which we’ve already made to equal the query we want.

//Update the definition expression
const setFeatureLayerFilter = (expression) => {
  terrestrialSCCCLayer.definitionExpression = expression;
}

// Listen for changes
selectFilter.addEventListener('change', (event) => {
  setFeatureLayerFilter(event.target.value);

});

const terrestrialSCCCLayer = new FeatureLayer({
  url: "https://services7.arcgis.com/oF9CDB4lUYF7Um9q/arcgis/rest/services/NA_Terrestrial_Species_of_Common_Conservation_Concern/FeatureServer",
  ...
  definitionExpression: "1=1" //optional
});

Now our users can see exactly where the range of the Sonoran pronghorn (or any of the awesome species listed) is without any other species being displayed on the map.

Step Four - Adding a legend and basemap selections

The final step for this data layer I would like to see is adding a legend and options to change the basemap. Luckily Esri makes this part very simple. By importing the Legend widget from @arcgis/core we can quickly add it to our view and it will appear on our map with the title of the layer and all the species colors listed.

import Legend from "@arcgis/core/widgets/Legend.js";

let legend = new Legend({
  view: view
});

view.ui.add(legend, "bottom-left");

Adding the basemap toggle is also just as simple as importing some new widgets and and loading them up. We’ll choose the imagery options as our other basemap choice. You should see the toggle in the bottom right and being able to click between the two without losing the data on the map.

import BasemapToggle from "@arcgis/core/widgets/BasemapToggle";
import BasemapGallery from "@arcgis/core/widgets/BasemapGallery.js";

const basemapToggle = new BasemapToggle({
    view: view,
    nextBasemap: "arcgis-imagery"
});
view.ui.add(basemapToggle,"bottom-right");
 
const basemapGallery = new BasemapGallery({
    view: view,
    source: {
      query: {
        title: '"World Basemaps for Developers" AND owner:esri'
      }
    }
});

Step Five - Publishing to Netlify

After you’ve finished your own polishing touches you should have a pretty functional map, but it’s only local right now- you want to share your creation with others. There are a number of ways to get your code out on the web (for free) and I recommend you look into all your options, but for this tutorial we are going to use Netlify as it’s a very simple process. Hopefully as you’ve been following along you’ve been committing your code and pushing it to your GitHub repo. This is one of the ways you can publish to Netlify.

One of the most important items is having a build command available. By default Netlify is looking for ‘npm run build’ which we conveniently have in our package.json already.

Note: If you are planning on running your code from a public GitHub repo remember that your ArcGIS Maps API Access token will be accessible from there. You could run it from a private repo or you can, and should for good measure anyways, also go to your API key settings and make it only accessible to a specific website URL (look for the Referrers section).

Go to Netlify and create an account if you haven’t already. Click the Deploy to Netlify button and connect it to your GitHub account and your updated repository. You may need to include the build command (npm run build) and the publish directory (dist) if it doesn’t automatically include it. Click Save and Deploy and wait for the build to finish. To start Netlify will assign a random production URL to your site. For example when I first deployed this it was assigned the url ‘golden-wisp-b82cf6’. You can leave as is or you can change it in the Settings for Domain management.
If you do change the site name be sure to add it to the API key referrers list to protect your token.

Great job! Hopefully your deploy worked and you have a functioning site. You can visit the link below to see my working tutorial published via Netlify.

Read More
Alicea Halsted Alicea Halsted

OpenLayers Tutorial

Biodiversity Hotspot Map

We are going to make a quick OpenLayers map of biodiversity hotspots across the world. To get started you’ll need a Github account and eventually a Netlify account.

Step One - Create Ol-App

To start make a repo on Github - I am going to call it openlayers_tutorial. Then in your terminal run ‘npm create ol-app <foldername of choice>’. After that has installed you can follow the steps from Github to connect it to the remote repo.

Open up your code editor and navigate to the folder you just created. You’ll see the basic source code is set up by the create script and if you run ‘npm run start’ we will see a map in our localhost! That was pretty fast. Let’s explore what it created for us so far. We’ve got the basic .gitignore, readme.md, and a package.json. We’ve also got an index.html file with a div for our map. Go ahead and change the title to Biodiversity Hotspots while we are there.

Then we’ve got a style.css file that imports some styling from OpenLayers and also some basic CSS for our map so it shows up on the page. And finally a main JavaScript file that imports a few items from OpenLayers and sets up our map.

We can also see that there is a Vite config file and some Github workflows. Vite is a development server that helps connect all our files together, especially for importing CSS from JavaScript files.

Step Two - Add Data

Since we’ve got a simple map setup let’s work on adding the biodiversity hotspot data. I did some searching online and found this Biodiversity Hotspot Data from Conservation International. I download that GeoJSON and then ran it through MapShaper to come up with a simplified version.

Note: You don’t have to simplify it but I found that OpenLayers was struggling to paint the layer quickly and you can simplify it just enough to speed up the process but not loose too much quality.

Once you’ve got your data you can add it to your repo folder under a public folder. Now we need to set up a new layer constant using this data. We will need to use the Vector layer options for this one (unlike the OSM TileLayer that is currently our basemap). At the top of your file add these imports (note you need to restart the development server when you add new dependencies).

import GeoJSON from 'ol/format/GeoJSON';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import {Fill, Stroke, Style} from 'ol/style.js';

With those in place we can set up our new constant biodiversityLayer. We need to add it as a new layer with a new vector source specified as GeoJSON and pointing to our data folder. We can also add a style, though OpenLayers will do a basic paint of the layer for us if not. Once we’ve got that set up we can add this layer to the list of layers in the map function.

const biodiversityLayer = 
  new VectorLayer({
    source: new VectorSource({
      format: new GeoJSON(),
      url: '/Biodiversity_Hotspots_Simplify.geojson',
    }),
    style: 
        new Style({
          fill: new Fill({
            color: 'rgba(50, 123, 168, 0.5)',
          }),
          stroke: new Stroke({
            color: '#2d434f',
          }),
        })
  });

const map = new Map({
  target: 'map',
  layers: [layer, biodiversityLayer],//added new layer here
  overlays: [overlay],
  view: new View({
    center: [0, 0],
    zoom: 2
  })
});

If you rerun your development server you should see a map like this:

If you zoom into some areas the different polygons are hard to distinguish. Instead of painting them all one color we can paint them different colors based on their object ID. In order to not have to map all the object ids (mine has over 50) to a specific color we are going to use this colormap package to assist us in this. We can ‘npm install colormap’ and then import it at the top of our page.

Next we need to write a simple function we can call from our style section of the vector source to get the feature’s object ID and then match that to the index of the colormap ramp. We name the colormap we want (see the link above for the full list), I chose ‘viridis’ and then told it to divide the gradient up into 55 shades.

import colormap from 'colormap';
...
const ramp = colormap({
  colormap: 'viridis',
  nshades: 55,
});

function getColor(feature) {
  const index = feature.get('OBJECTID');
  return ramp[index];
}
const biodiversityLayer = 
  new VectorLayer({
    source: new VectorSource({
      format: new GeoJSON(),
      url: '/Biodiversity_Hotspots_Simplify.geojson',
    }),
    style: function (feature) {
        return new Style({
          fill: new Fill({
            color: getColor(feature),
          }),
          stroke: new Stroke({
            color: 'rgba(255,255,255,0.8)',
          }),
        });
    }
  });

So now our polygons areas stand out more from each other but the color ramp we chose doesn’t have any opacity to it so we can no longer see what’s under the polygons. We can simply set the opacity of the biodiversity layer with a ‘setOpacity’ function.

biodiversityLayer.setOpacity(0.6);

Step Three - Add Interactivity

If you noticed when looking at the GeoJSON these hotspots also have names - let’s add some popups to the map for the user. First in the html add a popup container for us to populate data in (it won’t show unless called).

<div id="popup" class="ol-popup">
      <a href="#" id="popup-closer" class="ol-popup-closer"></a>
      <div id="popup-content"></div>
</div>

We will add some CSS to our file to control how the popup looks (based on this great example of popups here)

.ol-popup {
  position: absolute;
  background-color: white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
  padding: 20px;
  border-radius: 10px;
  border: 1px solid #cccccc;
  bottom: 12px;
  left: -50px;
  min-width: 150px;
  text-align: center;
}
.ol-popup:after, .ol-popup:before {
  top: 100%;
  border: solid transparent;
  content: " ";
  height: 0;
  width: 0;
  position: absolute;
  pointer-events: none;
}
.ol-popup:after {
  border-top-color: white;
  border-width: 10px;
  left: 48px;
  margin-left: -10px;
}
.ol-popup:before {
  border-top-color: #cccccc;
  border-width: 11px;
  left: 48px;
  margin-left: -11px;
}
.ol-popup-closer {
  text-decoration: none;
  position: absolute;
  top: 2px;
  right: 8px;
}
.ol-popup-closer:after {
  content: "✖";
}

In our JavaScript we’ll need to import the Overlay code and then create an overlay. We’ll add some events to listen for a click on the map and also the close event for the popup. In the map click event we will need to use a getFeaturesAtPixel to determine what the biodiversity hotspot is under the mouse. Since parts of our map do not have this layer, and we only care about that layer for this popup, we will add a check to see if the hotspot exists. If it doesn’t we just do a return to exit out of the function and don’t display a popup at all.

import Overlay from 'ol/Overlay.js';
...
/Elements that make up the popup.
const container = document.getElementById('popup');
const content = document.getElementById('popup-content');
const closer = document.getElementById('popup-closer');

//Create an overlay
const overlay = new Overlay({
  element: container,
  autoPan: {
    animation: {
      duration: 250,
    },
  },
});

//Close click handler for overlay
closer.onclick = function () {
  overlay.setPosition(undefined);
  closer.blur();
  return false;
};

//Add a click handler to the map to render the popup.
map.on('singleclick', function (event) {
  const coordinate = event.coordinate;
  const features = map.getFeaturesAtPixel(event.pixel);
  let name = null;
  if(features[0].getProperties().NAME !== undefined){
    name = features[0].getProperties().NAME
  } else {
    return;
  }
  content.innerHTML = `<p>${name}</p>`;
  overlay.setPosition(coordinate);
});

If you’ve taken a look at your GeoJSON data you’ll see that we have two type of hotspots, the actually locations and the outer limits of those areas. These are most obvious in areas where the polygons stretch out into the ocean. I think those areas would be better to be mostly empty fill with just a stroke. We can update our style function with an if/else condition to look for the outer limit type specifically. I did add a little bit of fill color but put the opacity way down so they still stand out a tiny bit from the basemap and enable the popup to work on those polygons as well (no fill means no popup).

style: function (feature) {
      if(feature.get('Type')==='outer limit'){
        return new Style({
          fill: new Fill({
            color: 'rgba(222, 222, 222, 0.2)',
          }),
          stroke: new Stroke({
            color: '#2d434f',
          }),
        });
      } else {
        return new Style({
          fill: new Fill({
            color: getColor(feature),
          }),
          stroke: new Stroke({
            color: 'rgba(255,255,255,0.8)',
          }),
        });
      }
    }

Optional Step Four - Updating the basemap

The OSM layer isn’t terrible for this map but I wanted one that had more focus on terrain and nature. I choose a vector tile map focused on the Outdoors from MapTiler - you can create a free account with them to get your own API key. Pick the vector map tiles you would like to use and click on it. This will give you the api URL you will need.

We will also need to install ol-mapbox-style library to help with the vector tiles https://github.com/openlayers/ol-mapbox-style

Install that then import it into your main.js. We’ll need to call that after creating our map to add the vector tiles. You can also remove the OSM code as you won’t need that layer anymore. Since we can’t insert this layer into the layer attribute for the Map function we’ll need to control the order of the layers being painted a different way. You can add a ‘then’ on to the olms call and add the biodiversity layer after that.

import olms from 'ol-mapbox-style';
....
const map = new Map({
  target: 'map',
  overlays: [overlay],
  view: new View({
    center: [0, 0],
    zoom: 2
  })
});

olms(map, 'https://api.maptiler.com/maps/topo-v2/style.json?key=<Your API Key>').then(function(map){
  map.addLayer(biodiversityLayer)
});

Now depending on what basemap you should have something like this showing:

For a final finishing touch I would like to give users the option to change the basemap to a satellite view. I picked one with the labels still, so its considered a hybrid satellite view. In our html we’ll need to add a select with two options. In our CSS we’ll need to add some styling to make sure the select will show above our map and in a good location. I picked right below our zoom controls. In our main.js file we’ll add a ‘change’ event listener for that select option. If you remember when changing the basemap we had to control the order of the layers painting. We’ll need to do that again here but we will also need to remove the biodiversity layer first, since we can’t add a layer that is already there (even if you can’t see it).

//index.html
<select id="basemap-selector">
  <option value="topo">Topo</option>
  <option value="satellite">Satelitte Hybrid</option>
</select>

//style.css
#basemap-selector {
  position: absolute;
  top: 80px;
  left: 9px;
}

//main.js
const basemapSelector = document.getElementById('basemap-selector');

const update = () => {
  const selection = basemapSelector.value;
  map.removeLayer(biodiversityLayer)
  if(selection === 'satellite'){
    olms(map, 'https://api.maptiler.com/maps/hybrid/style.json?key=<Your API Key>').then(function(map){
      map.addLayer(biodiversityLayer)
    });
  } else if (selection === 'topo'){
    olms(map, 'https://api.maptiler.com/maps/topo-v2/style.json?key=<Your API Key>').then(function(map){
      map.addLayer(biodiversityLayer)
    });
  }
}
basemapSelector.addEventListener('change', update);

Let’s not forgot we also need to add attributions for the map content. We’ll need to import the default controls and add that to our map object to be true. Finally we’ll need to add attribution to the biodiversity layer, which will get automatically added to the attribution control.

import {defaults as defaultControls} from 'ol/control.js';
...
const biodiversityLayer = 
  new VectorLayer({
    source: new VectorSource({
      format: new GeoJSON(),
      url: '/Biodiversity_Hotspots_Simplify.geojson',
      attributions: 'Conservation International'
    }),
...

const map = new Map({
  target: 'map',
  overlays: [overlay],
  controls: defaultControls({attribution: true}),
  view: new View({
    center: [0, 0],
    zoom: 2
  })
});

Step Five - Publishing to the Web using Netlify

Finally we are ready to work on deploying this site. Make sure you’ve got your code committed to Github so Netlify can pull it. I would also suggest making a new API key so you can add Allowed HTTP origins to control which sites can use your key. See How to Protect Your Key for more information.

The create ol-app has already set up an npm build function for us that will create a dist directory that can be used in deployment.

At this point your code should be pretty similar to the main branch of my repo. Go to Netlify and create an account if you haven’t already. Click the Deploy to Netlify button and connect it to your Github account and your updated repository. Click Save and Deploy and wait for the build to finish. To start Netlify will assign a random production URL to your site. For example when I first deployed this it was assigned the url ‘animated-muffin-522845’. You can leave as is or you can change it in the Settings for Domain management. Be sure to add this domain to the http origins control.

Be sure to checkout the finished project at the link below.

Read More
Alicea Halsted Alicea Halsted

Mapbox GL JS Tutorial: Part Two

If you haven’t already check out part one to see how we got to this point.

Step Three - Adding interactivity to the map

In our last step we add the basic GeoJSON data to the map but users probably want some information on the trails and trailheads they are seeing on the map and not just the location of points and lines. We will start with the trailheads first by changing the marker for them to a more official looking icon and adding a popup with the name of the trailhead.

First let’s go find a good icon for us to use. I headed over to The Noun Project and downloaded a customized SVG from their wide array of options. You can also download a free uncustomized SVGs and PNGs from them if you want to pick something different. After picking an icon you can add it to your ‘src’ folder under a new assets folder.

If you look back at the webpack config file you’ll see we already were set up for this using the output assetModuleFilename choice and the rules in the modules. We are going to add a marker class to our CSS that will reference this image and control its size.

However, the original way we added the trailheads doesn't allow for us to make edits to the markers to be custom. So we need to use fetch instead of Mapbox’s ‘addSource’.  Once we fetch the data we send the response to a function to create the marker and we will need to add some small CSS changes to cover it.

NOTE FROM MAPBOX

If you have a large GeoJSON file, you may want to load it as an external source rather than adding it inline. You can do so by linking to its URL, if it's hosted remotely, or by using fetch to load locally or from a third-party API.

We will use a for loop to create a div with the class name of ‘marker’ for each of the features on the list (i.e. the GeoJSON of the trailheads). This will be used by the custom CSS we are adding that is going to show a hiker SVG. Then we pass each of those divs into Mapbox’s marker feature and add a popup. We’ve looked at the GeoJSON properties for this file and the ‘LOC_NAME’ is a good option for a quick display of trailhead name in the popup. We will also add some small CSS for the popup.

//index.js after the load function
const createMarker = (featuresList) => {
  for (const feature of featuresList.features) {
    // create a HTML element for each feature
    const el = document.createElement('div');
    el.className = 'marker';
     
    // make a marker for each feature with a popup and add it to the map
    new mapboxgl.Marker(el)
        .setLngLat(feature.geometry.coordinates)
        .setPopup(
        new mapboxgl.Popup({offset: 25})
            .setHTML(
            `<h3>${feature.properties.LOC_NAME}</h3>`
            )
        )
        .addTo(map);
    }
}

fetch('data/GRSM_TRAILHEADS.geojson')
    .then(response => response.json())
    .then(data => createMarker(data))
    .catch(error => console.log(error));

//main.css
.marker {
    background-image: url('/src/assets/hiker.svg');
    background-size: cover;
    width: 25px;
    height: 25px;
    cursor: pointer;
}
.mapboxgl-popup {
    max-width: 200px;
}
  
.mapboxgl-popup-content {
    text-align: center;
    font-family: 'Open Sans', sans-serif;
}

You’ll need to reload/restart webpack since we added a new image file but then you should be able to see your new custom icons on the page. If you click on one a popup will appear.

Next we should make it so users can see the name of the trail for each line.

First let’s add a popup that will say the name of the trail. We can keep our existing data source and layer but will need to add a listener for the click event.  Mapbox has its own listener for the map and we can specify the layer to listen to. We can pass the specific event (e) into the function and then pull any details out of the GeoJSON properties in order to help our user. After checking to make sure the map is zoomed out enough for the popup we can set it on the coordinates of the trail.

//Event listener for clicks on trails layer
map.on('click', 'trails', (e)=>{
        // Get details of the clicked feature
        const coordinates = e.features[0].geometry.coordinates[0];
        const description = e.features[0].properties.TRAILNAME;
        const objectIdentifier = e.features[0].properties.OBJECTID;
        
        // Ensure that if the map is zoomed out such that multiple
        // copies of the feature are visible, the popup appears
        // over the copy being pointed to.
        while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
            coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
        }
        
        //Add a popup
        new mapboxgl.Popup()
        .setLngLat(coordinates)
        .setHTML(description)
        .addTo(map);
})

So that works alright but if you’ll notice the cursor doesn’t change to a click pointer to give the user any indication they might want to click on the trail. Our trailhead icons do not have this issue since we used a CSS attribute to account for that (“cursor: pointer”). Similar to the click event option we have a ‘mouseenter’ and ‘mouseleave’ event we can reference and specify the trails layer again. Using the API’s style cursor option we can easily add code to correct this.

// Listen for the mouse move over trails layer
// Change the cursor to a pointer
map.on('mousemove', 'trails', (e) => {
    map.getCanvas().style.cursor = 'pointer';
    });
     
// Change it back when it leaves
map.on('mouseleave', 'trails', () => {
    map.getCanvas().style.cursor = '';
});

It would also be good if we could give the user some indication of exact what line they clicked so they know for sure where the popup is referencing. To do this we will add a second layer of the trails that is filtered to not show until a click happens. Once it does we will filter that layer to show the entire length of the clicked trail (i.e. all trails with the same name) in a contrasting color.

Note: Most of the trails on this are just one line feature but a few, like the Appalachian Trail, are multi-feature. This ensures those are covered nicely by our new code.

First we add the layer to our map.on(‘load’) function but give it a different ID from the previous one. This layer should have different line-color as well and we add a filter attribute. The filter attribute translates to getting trailnames that equal a value. In this case we put empty quotes to indicate that we aren’t matching anything right now.

Then in the click event listener we add a line to update the filter for that layer using the clicked lines trailname.

map.on('load'), () => {
  ...
map.addLayer({
        id: 'trails_click',
        type: 'line',
        source: 'trails',
        layout: {
            'line-join': 'round',
            'line-cap': 'round'
            },
        paint: {
            'line-color': '#264a2a',
            'line-width': 4
            },
        filter: ['==', ['get','TRAILNAME'],''] //Only show matching filtered objects, '' means display none
    })
});

//click event listener add this line
map.setFilter('trails_click',['==', ['get','TRAILNAME'], description])

Now we can see the entire length of that trail highlighted on our map. Since we’ve got our icons and trails down we should add a legend to ensure our users understand the meaning of our symbology, even though on this simple map it is pretty obvious - it is good practice to do so.

Mapbox doesn’t have a preset legend option so we will need to add an overlay ourselves to the map. In your index.html you’ll need to add a div with two items in it.

<div class='legend'>
    <div>Trails <span class='trailKey'></span></div>
    <div>Trailheads <span class='legendIcon marker'></div>
</div>

Then we will need the corresponding CSS to put the legend in the bottom right area of our map. For our trail we have a class called trailKey which will show a small box the same color as the trails. For our trailheads we already have the marker class which has the marker svg in it but we will need to make sure that it floats left on the legend, so we add a secondary class to that. If we had multiple icons that needed to float left we could use that class over and over again.

.legend {
    position: fixed;
    bottom: 120px;
    right: 0;
    background: #fff;
    margin-right: 20px;
    font-family: 'Open Sans', sans-serif;
    overflow: auto;
    border-radius: 3px;
    padding: 10px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    line-height: 25px;
    height: 55px;
    margin-bottom: 40px;
    width: 140px;
}
.trailKey {
    border-radius: 20%;
    width: 15px;
    height: 15px;
    margin-right: 5px;
    background-color: #7e9480;
    float: right;
}

.legendIcon {
    float: right;
}

You should now have a simple legend on your map.

At this point our map is quite usable and provides good information to our user. We could stop here but there some more interactivity features we can add to really polish up this map. If you don’t want to add any more code you can skip to step five for how to publish this to the web using Netlify.

Step Four - Polishing it up

If you’ve been clicking around on the map you may have noticed that the popup for the trails is at the first coordinate pair in the GeoJSON which often is at a convergence of trails. It would look better if it was at the center of that trail segment. We could potential try to pick a random item in the array of coordinates for that line but each line will likely have a different number of coordinates and so it wouldn’t be the midpoint for all the lines. Instead we should calculate a midpoint along that line. Luckily the Turf.js API has such a feature for us to use. First we’ll need to install that package and import it into our index.js file.

If we look at the documentation for the along calculation we’ll see we need to give it a line string (our trail) plus a distance along the line to find the coordinates of. In our case this will be a calculation of half of the distance of that trail length. To get that we will use the length calculation from Turf as well. We will split this midpoint calculation into its own function for ease of readability and then call that function from our click event listener - passing in the clicked lines coordinates. You’ll noticed we send all of them and not just the first in the array. We’ll assign the result of that calculation to a variable and pass that to the popup.

//index.js
import * as turf from '@turf/turf';

//Using turf JS get the midpoint of the line
const getMidPointOfLine = (lineCoordinates) => {
    const trailLine = turf.lineString(lineCoordinates);
    const lengthTrailLine = turf.length(trailLine)
    const midpointCoord = turf.along(trailLine, lengthTrailLine/2);
    return midpointCoord.geometry.coordinates;
};

//Event listener for clicks on trails layer
map.on('click', 'trails', (e)=>{
  ...
    //Find the midpoint of the line to show the popup from
    let midpoint = getMidPointOfLine(e.features[0].geometry.coordinates);

  //Add a popup
        new mapboxgl.Popup()
        .setLngLat(midpoint) //<- different coordinates than before
        .setHTML(description)
        .addTo(map);
})

Now maybe you don’t want your users to have to click to find out the name of the trail. We could create a hover state for the mouse that will highlight the trail and provide a popup. We can use our original trail layer to adjust the line color based on a feature state of hover. (This is similar to using the layer data to drive the properties of the features. We did not touch on that here but Mapbox has a sample here). So we will replace our line color from a single hex value to an expression that is looking for a feature state. We will need to provide the boolean for the feature state based on the location of the mouse. Remember the mousemove (& mouseleave) function we made to change the cursor to a pointer? We can use that same ones to set the feature state. First we are going to create a global variable for the ID of the trail that is being highlighted. We will set this on mouse enter and the reset it on mouse leave. The using Mapbox’s set feature state we can specify the layer, the id of the feature and hover state to true.

map.on('load', () =>{
  ...
map.addLayer({
        id: 'trails',
        type: 'line',
        source: 'trails',
        layout: {
            'line-join': 'round',
            'line-cap': 'round'
            },
        paint: {
            'line-color': [
                'case',
                ['boolean', ['feature-state', 'hover'], false], //Adjust color based on hover state
                '#fbb03b','#7e9480',
            ],
            'line-width': 4
            },
        
    })
  ...
});
let hoveredStateId = null; //For use on styling hover

// Listen for the mouse move over trails layer
// Change the cursor to a pointer
// Update the feature state for hover effect
map.on('mousemove', 'trails', (e) => {
    map.getCanvas().style.cursor = 'pointer';
    if (e.features.length > 0) {
        hoveredStateId = e.features[0].id;
        map.setFeatureState(
            { source: 'trails', id: hoveredStateId },
            { hover: true }
        );
      }
});
     
//Change it back when it leaves
//Update hover state to false
map.on('mouseleave', 'trails', () => {
    map.getCanvas().style.cursor = '';
    if (hoveredStateId !== null) {
            map.setFeatureState(
            { source: 'trails', id: hoveredStateId },
            { hover: false }
            );
        }
    hoveredStateId = null;
});

Run this code and you’ll notice an occasionally funky thing happening if you keep your pointer over the line. You can get more than one line highlighted and it won’t remove itself until you go back over it. That doesn’t seem like what we want - especially if we want to add a popup as well. What’s happening is the hover state for that item isn’t getting set to false as our mouse movements aren’t triggering the mouse leave event listener.

In order to fix this we will need to check on the hover id status before setting it true. We can add a simple if statement that echos our mouseleave one to ensure only one trail remains highlighted at a time.

map.on('mousemove', 'trails', (e) => {
    map.getCanvas().style.cursor = 'pointer';
    if (e.features.length > 0) {
      //Same a mouse leave call, clear all highlights first
        if (hoveredStateId !== null) {
            map.setFeatureState(
            { source: 'trails', id: hoveredStateId },
            { hover: false }
            );
        }
        hoveredStateId = e.features[0].id;
        map.setFeatureState(
            { source: 'trails', id: hoveredStateId },
            { hover: true }
        );
      }
});

Great! Now let’s add a quick popup to this highlighted trail. I decided to reuse my midpoint code to keep the popup in the middle of the trail. Unlike our click event where we create an unnamed popup using the new mapboxgl.Popup function we’ll want to make this a name one so it can be removed on mouseleave.

Any ideas what would happen if you didn’t do this? Give it a try and see how the popups respond.

// Create a popup for hover state, but don't add it to the map yet.
const hoverpopup = new mapboxgl.Popup({
    closeButton: false,
    closeOnClick: true,
});

// Update the feature state for hover effect and add popup
map.on('mousemove', 'trails', (e) => {
    map.getCanvas().style.cursor = 'pointer';
    if (e.features.length > 0) {
        ...
        let midpoint = getMidPointOfLine(e.features[0].geometry.coordinates);
        const description = e.features[0].properties.TRAILNAME;
        hoverpopup
            .setLngLat(midpoint)
            .setHTML(description)
            .addTo(map);
        }
    });
     
//Change it back when it leaves
//Update hover state to false and remove popup
map.on('mouseleave', 'trails', () => {
    ...
    hoverpopup.remove;
});

For our final finishing touch we should add a header to our map page. In our index.html we’ll add a simple div with the class of header. We’ll add some CSS in to make the header fixed to the top of the page. I added a linear gradient to the background header so it looks like it blends right into the map. (https://cssgradient.io/ is helpful for this)

.header {
    height: 4vh;
    width: 100vw;
    text-align: center;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 2rem;
    padding: 30px 0 30px;
    background: rgb(239,233,225);
    background: linear-gradient(0deg, rgba(239,233,225,0) 0%, rgba(239,233,225,0.8715861344537815) 30%, rgba(239,233,225,1) 100%);
    z-index: 3;
    position: absolute;
    
}
@media all and (max-width: 475px) { 
    .header {
        font-size: 1rem;
        height: 1vh;
        padding: 18px;
    }
}

Step Five - Publishing To The Web

After you’ve finished your own polishing touches you should have a pretty functional map, but it’s only local right now- you want to share your creation with others. There are a number of ways to get your code out on the web (for free) and I recommend you look into a number of them but for this tutorial we are going to use Netlify as it’s a very simple process. Hopefully as you’ve been following along you’ve been committing your code and pushing it to your Github repo. This is one of the requirements for publishing to Netlify. (Check out their getting started info.)

We also need to make sure our webpack and package.json configs are in line. One of the most important items is having a build command available. By default Netlify is looking for ‘npm run build’ which we conveniently have in our package.json already. We are also going to minify our code as part of the build process to make it smaller and more efficient for production.

Note: If you are planning on running your code from a public Github repo remember that your Mapbox Access token will be accessible from there. You can, and should, also go to your Mapbox token settings and make it only accessible to a specific website URL. This means you will need to use one token for local development and one for the production code.

We are going to make some updates to our webpack but first we are going to create a dev and prod config file to keep these items separate and have them join to the main config file. (I know this might seem a little overkill for our tiny map project, but sometimes I find doing the overkill items on easy projects make its more digestible for future uses). Create two new files, webpack.dev.js and webpack.prod.js. Rename the original config file webpack.common.js. We’ll be adding a merge package and the Terser plugin to help us handle this.

npm install webpack-merge --save-dev
npm install terser-webpack-plugin --save-dev

Our common file will remain almost the same except we will need to remove the development items into the webpack.dev.js file. The production file will contain our Terser plugin that will minify the code for production.

//webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

 module.exports = merge(common, {
   mode: 'development',
   devtool: 'inline-source-map',
   devServer: {
    compress: true,
    port: 9000
  },
 });

//webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require("terser-webpack-plugin");

module.exports = merge(common, {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
});

//webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: {
    index: './src/index.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    assetModuleFilename: 'src/assets/images/[name][ext]'
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Mapbox Boilerplate',
     template: 'src/index.html',
    }),
    new WebpackManifestPlugin({
      fileName: 'site.webmanifest'
    }),
    new CopyPlugin({
      patterns: [
        { from: "src/data", to: "data" },
      ],
    }),
  ],
  module: {
    rules: [
        {
            test: /\.css$/i,
            use: ['style-loader', 'css-loader']
        }, 
        {
            test: /\.(png|svg|jpg|jpeg|gif)$/i,
            type: 'asset/resource',
          },
    ]
  }
};

Finally we will update our package.json so that the build function that Netlify is looking for will be pointing at the production config.

//package.json
"scripts": {
        "build": "webpack --config webpack.prod.js",
        "build-dev": "webpack --config webpack.dev.js",
        "start": "webpack serve --open --config webpack.dev.js",
        "watch": "webpack --watch",
        "test": "echo \"Error: no test specified\" && exit 1"
    },

At this point your code should be pretty similar to the main branch of my repo. Go to Netlify and create an account if you haven’t already. Click the Deploy to Netlify button and connect it to your Github account and your updated repository. Click Save and Deploy and wait for the build to finish. To start Netlify will assign a random production URL to your site. For example when I first deployed this it was assigned the url ‘warm-kheer-a987ad’. You can leave as is or you can change it in the Settings for Domain management.
If you do change the site name be sure to add it to the Mapbox Access Token URL list to protect your token.

Great job! Hopefully your deploy worked and you have a functioning site. You can visit the link below to see my working tutorial published via Netlify.

Read More
Alicea Halsted Alicea Halsted

Mapbox GL JS Tutorial: Part One

Follow along with this tutorial to see how to make this trail map for the Great Smokey Mountains National Park

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:

|- src
  |- 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.

Mapbox GL JS

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.

|- src
  |- 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.

Read More