OpenLayers Tutorial
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.