Web development: National Parks locator
Last updated
Last updated
Website
Ordnance SurveyData
OS Data HubMaps help people work out where they are. In this tutorial, we'll build an interactive locator, to help users find features on a map. At Ordnance Survey we love helping people explore the natural beauty of Great Britain, so this project will help users location national parks - but it could just as easily be built to locate stores, offices, railway stations, hospitals and so on.
We use data the OS Maps and OS Features APIs , as well as Leaflet, leaflet-omnivore and jQuery to build this interactive web interface.
Our locator app will have two main areas of the interface: a list of features to locate in a panel, and a map with those features displayed.
To make things interesting we are going to use JavaScript to interactively connect the map and list representations of the national parks - when the user hovers on a national park on the list, the corresponding geometry should highlight, and vice versa. We will also implement event handlers so when a user clicks on either the <li>
or the polygon, the map zooms to that national park.
We created a lightweight JSON file, /data/national-parks.json
. The file contains a GeoJSON FeatureCollection. Each Feature in the collection represents a national park. Each Feature's property
attribute contains the name
and a url
for the park, plus an id
.
(As a quick aside - to create this file we fetched national park geometries from the OS Features API's Open Zoomstack layer. To simplify the geometries and make this GeoJSON more lightweight, we used mapshaper.org's awesome Simplify tool. And we used geojson-precision
to trim unnecesary coordinate precision, further reducing file size.)
The index.html
document contains elements ready to accept content dynamically generated by looping through the national-parks.json
file.
Crucially, the HTML includes a <ul class="layers">
element, as well as a <div id="map">
. These two elements are containers for content.
In tutorial.js
the key logic of the locator app is defined. Here we'll walk through line by line to explain how it works.
First, a new Leaflet map object is instantiated and a basemap is added using the OS Maps API. (We'll connect to the ZXY version of the API in this project.) This populates the #map
div with an OS map.
With that our basemap is set up. Next we need to fetch and parse the GeoJSON in national-parks.json
. We'll add the FeatureCollection as a layer to our map and attach event listeners to each Feature. We'll also loop through the array of Features and add a <li>
customised for each national park.
Before we get to the fetch and parse logic though, let's define our event listeners. We'll write functions to highlight and unhighlight both of the visual elements we use to represent each national park: a <li>
element and a GeoJSON Feature on the map.
We included the id
as one of each Feature's properties so we could have a unique reference to each national park. With this id we'll be able to select the right feature from the GeoJSON layer we add to the map, and select the right <li>
(by a data-np-id
attribute) with jQuery. We'll write all of our highlight/unhighlight functions based on this ID.
We also wrote a function - flyToBoundsOffset()
- to fly to a feature accounting for the lefthand panel that is covering part of the map div.
Looking at the code:
Because we're loading data from an external resource, we need to make sure that the data loads before we move on to subsequent lines. We will use a handy library called leaflet-omnivore to fetch the GeoJSON file we've prepared and load it into a L.geoJSON
object.
When the omnivore.geojson()
method has loaded the data, it fires a 'ready'
event. We'll place the code that relies on the GeoJSON inside this event handler so we can be sure that it only is executed once the data has loaded.
Creating a custom L.geoJSON object
Omnivore fetches GeoJSON and instantiates a L.geoJSON object. We want to customize ours, so we're going to define a custom L.geoJSON
object before we actually load the GeoJSON.
This pattern lets us bind event listeners to each feature, using Leaflet's onEachFeature
option. (Note: here with Leaflet layer
refers to each Feature in the FeatureCollection.)
Load and Process the GeoJSON FeatureCollection
Next we use Omnivore to fetch the external resource, parse the GeoJSON and add it to the custom L.geoJSON
object we've defined (using the L.geoJSON().addData()
method under the hood). Note that because this is an asynchronous operation, code reliant on the layer being loaded is placed inside the .on('ready', function () { ... })
event handler callback.
Inside the callback we loop through the Features in the GeoJSON FeatureCollection, adding a <li>
to our lefthand panel <ul>
element with park-specific data.
After everything is said and done we take the layer we've created and .addTo(map)
.
Alright, our geographic features are added to the L.geoJSON
object with event handlers bound. Now let's set up the lefthand panel with <li>
elements.
Making a <li>
To make our list we loop through the array of features in the L.geoJSON
object. In the loop, we will be creating a <li>
element with park-specific data to place in the left panel. Then we'll attach event listeners and append it to the <ul class="layers">
defined in index.html
.
And that's it! Our app fetches the national parks geometries, loads them, adds them to an unordered list on the left panel, and attaches event handlers to highlight and fly to the appropriate park on click. We included an external link icon so users could visit the national park's official website. The perfect launchpad for exploring Great Britain's amazing national parks.
Feel free to adapt this code to suit your needs. If you make anything cool with OS data - let us know!