LogoLogo
OS Docs HomeOS NGDOS APIsOS Download ProductsMore than MapsContact Us
  • More than Maps
  • Geographic Data Visualisation
    • Guide to cartography
      • Introduction to cartography
      • Types of maps
      • Symbology
      • Colour
      • Text on maps
      • Generalisation
      • Coordinate reference systems
      • Projections
      • Scale
      • Map legends
      • Map layout
      • Relief representation
      • North arrows
    • Guide to data visualisation
      • Introduction to data visualisation
      • GeoDataViz design principles
      • Types of visualisation
      • Thematic mapping techniques
      • Data visualisation critique
      • Accessible data visualisation
      • Ethical data visualisation
      • Software
      • Data
    • GeoDataViz assets
      • GeoDataViz basemaps
      • Stylesheets
      • GeoDataViz virtual gallery
      • Equal area cartograms
      • How did I make that?
        • Apollo 11 Landing
        • North York Moors National Park, 70 years
        • Snowdonia National Park, 70 years
        • Great Britain's National Parks
        • Great Britain's Islands
        • Great Britain's AONB's and National Scenic Areas
        • Famous shipwrecks of Pembrokeshire
        • Trig pillars today
        • Britain's most complex motorway junctions
      • #30DayMapChallenge
  • Data in Action
    • Examples
  • Demonstrators
    • 🆕Product Viewer
    • Addressing & location demonstrators
      • Address Portfolio overview
      • Which address product should you use?
      • AddressBase
      • AddressBase Core
      • AddressBase Plus
      • AddressBase Premium
      • Address Classifications
      • Addressing Lifecycle
      • OS Emergency Services Gazetteer
      • What are Vertical Streets?
      • Why are there differences in boundaries?
    • Contextual demonstrators
    • Customer best practice
      • Channel Shift
      • Data Management and OS Data Hub
      • End User Licence vs Contractor Licence
      • 🆕 IDs vs Spatial Relationships
      • Why we should capture good quality addresses at source
      • Why we Snap and Trace
    • Network Demonstrators
      • OS Detailed Path Network
      • OS Multi Modal Routing Network
        • OS Multi Modal Routing Network
      • Water Networks overview
      • OS MasterMap Highways Network and OS NGD Speeds
      • OS MasterMap® Highways Network and OS Open Roadsâ„¢
    • OS MasterMap Generation APIs
      • Using the OS Features API
      • Using the OS Features API Archive
      • Using the OS Downloads API
      • Using OS APIs in ESRI Software
    • 🆕OS NGD (National Geographic Database)
      • OS NGD Address
      • OS NGD Boundaries
      • 🆕OS NGD Buildings
        • 🆕Building and Building Access Feature Types
        • Building Part and Building Line Feature Types
      • 🆕OS NGD Geographical Names
      • OS NGD Land
      • OS NGD Land Cover enhancements
      • 🆕OS NGD Land Use
      • OS NGD Land Use enhancements
      • 🆕OS NGD Structures
        • 🆕OS NGD Structures
        • Field Boundaries
      • 🆕OS NGD Transport Features
      • 🆕OS NGD Transport Network
      • OS NGD Transport RAMI
      • OS NGD Water Features
      • OS NGD Water Network
      • OS NGD API - Features
      • Ordering OS NGD data
      • Change only updates
      • OS NGD Versioning
      • Creating a topographic map from OS NGD Data
      • Analytical styling for OS NGD data
    • OS MasterMap® demonstrators
    • 🆕Product & API Comparisons
      • 🆕Comparison of Water Network Products
  • Tutorials
    • GeoDataViz
      • Thematic Mapping Techniques
      • Downloading and using data from the OS Data Hub
      • How to download and use OS stylesheets
      • How to use the OS Maps API
      • Creating a bespoke style in Maputnik
    • GIS
      • Analysing pavement widths
      • Basic routing with OS Open Data and QGIS
      • Walktime analysis using OS Multi-modal Routing Network and QGIS
      • Creating 3D Symbols for GIS Applications
      • Constructing a Single Line Address using a Geographic Address
      • Creating a Digital Terrain Model (DTM)
      • Visualising a road gradient using a Digital Terrain Model
      • Visualising a road gradient using OSMM Highways
    • 🆕APIs
      • 🆕Using OS APIs with EPC API
      • 🆕OS APIs and ArcGIS
  • Deep Dive
    • Introduction to address matching
    • Guide to routing for the Public Sector
      • Part 1: Guide to routing
      • Part 2: Routing software and data options
      • Part 3: Building a routable network
    • Unlocking the Power of Geospatial Data
    • Using Blender for Geospatial Projects
    • A Guide to Coordinate Systems in Great Britain
      • Myths about coordinate systems
      • The shape of the Earth
      • What is position?
        • Types of coordinates
        • We need a datum
        • Position summary
      • Modern GNSS coordinate systems
        • Realising WGS84 with a TRF
        • The WGS84 broadcast TRF
        • The International Terrestrial Reference Frame (ITRF)
        • The International GNSS Service (IGS)
        • European Terrestrial Reference System 1989 (ETRS89)
      • Ordnance Survey coordinate systems
        • ETRS89 realised through OS Net
        • National Grid and the OSGB36 TRF
        • Ordnance Datum Newlyn
        • The future of British mapping coordinate systems
        • The future of British mapping coordinate systems
      • From one coordinate system to another: geodetic transformations
        • What is a geodetic transformation?
        • Helmert datum transformations
        • National Grid Transformation OSTN15 (ETRS89–OSGB36)
        • National Geoid Model OSGM15 (ETRS89-Orthometric height)
        • ETRS89 to and from ITRS
        • Approximate WGS84 to OSGB36/ODN transformation
        • Transformation between OS Net v2001 and v2009 realisations
      • Transverse Mercator map projections
        • The National Grid reference convention
      • Datum, ellipsoid and projection information
      • Converting between 3D Cartesian and ellipsoidal latitude, longitude and height coordinates
      • Converting between grid eastings and northings and ellipsoidal latitude and longitude
      • Helmert transformation worked example
      • Further information
  • Code
    • Ordnance Survey APIs
    • Mapping
    • Routing with pgRouting
      • Getting started with OS MasterMap Highways and pgRouting
      • Getting started with OS MasterMap Highways Network - Paths and pgRouting
      • Getting started with OS NGD Transport Theme and pgRouting
      • Getting started with OS NGD Transport Path features and pgRouting
  • RESOURCES
    • 🆕Data Visualisation External Resources
Powered by GitBook

Website

  • Ordnance Survey

Data

  • OS Data Hub
On this page
  • Tools and APIs
  • Tutorial
  • The Data
  • The HTML
  • The JavaScript

Was this helpful?

  1. Tutorials
  2. Web

Web development: National Parks locator

Last updated 1 year ago

Was this helpful?

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

National Park locator

Tools and APIs

Tutorial

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.

The Data

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "id": 1,
                "name": "Lake District National Park",
                "url": "https://www.lakedistrict.gov.uk/"
            },
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [[ -2.671007, 54.47243 ],
                    [ -2.666572, 54.47255 ],
                    [ -2.647861, 54.479841 ],
                    /* ... an array of polygon vertices */
                    ]
                ]
            }
        },
        { /* National Park Feature #2 */ },
         /* ... and so on, for each national park  */
]

The HTML

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.

The JavaScript

Setting up the map

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.

// API key - required to fetch data from the OS Maps API
var config = { apikey: "YOUR_KEY_HERE" };

// Define map options including where the map loads and zoom constraints
var mapOptions = {
  minZoom: 7,
  maxZoom: 20,
  center: [54.92240688263684, -5.84949016571045],
  zoom: 7,
  attributionControl: false,
  zoomControl: false
};

// Instantiate a new L.map object
var map = new L.map("map", mapOptions);

// Add scale control to the map.
var ctrlScale = L.control.scale({ position: "bottomright" }).addTo(map);

// Load and display ZXY tile layer on the map.
const endpoints = {
  zxy: "https://api.os.uk/maps/raster/v1/zxy"
};

var basemap = L.tileLayer(
  endpoints.zxy + "/Outdoor_3857/{z}/{x}/{y}.png?key=" + config.apikey,
  {
    maxZoom: 20
  }
).addTo(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.

Event Callbacks

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:

// A helper function to select the L.geoJSON Feature layer by its properties.id value.
// For those looking to adapt the code - this version will only work with unique properties ...
function getFeatureById(dataId) {
  let filtered = Object.values(map._layers).filter((l) => {
    if ("feature" in l) {
      return l.feature.properties.id == dataId;
    }
  });

  return filtered[0];
}

// Highlights a map feature by ID.
function highlightGeojson(dataId) {
  let geojson = getFeatureById(dataId);
  geojson.setStyle({
    fillOpacity: 0.6,
    weight: 3
  });
}

// Unhighlights a map feature by ID.
function unhighlightGeojson(dataId) {
  let geojson = getFeatureById(dataId);
  geojson.setStyle({
    fillOpacity: 0.3,
    weight: 1
  });
}

// Highlights a li element by data-np-id=ID.
function highlightListElement(dataId) {
  $('[data-np-id="' + String(dataId) + '"]').addClass("highlight");
}

// Unhighlights a li element by data-np-id=ID.
function unhighlightListElement(dataId) {
  $('[data-np-id="' + String(dataId) + '"]').removeClass("highlight");
}

// Animated map.flyToBounds() with padding to account for the panel covering part of the map.
function flyToBoundsOffset(dataId, elPosition = "left") {
  let offset = os.main.viewportPaddingOptions()[elPosition];

  let geojsonLayer = getFeatureById(dataId);

  let paddingOptions;

  if (elPosition == "left") {
    paddingOptions = {
      paddingTopLeft: [offset, 50],
      paddingBottomRight: [50, 50]
    };
  } else if (elPosition == "right") {
    paddingOptions = {
      paddingTopLeft: [50, 50],
      paddingBottomRight: [offset, 50]
    };
  }

  map.flyToBounds(geojsonLayer.getBounds(), paddingOptions);
}

Now, the GeoJSON

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

// Set up the Leaflet GeoJSON object.
// We'll pass this into the omnivore.geojson() method shortly
var parksLayer = L.geoJSON(null, {
  style: {
    fillColor: os.palette.sequential.s2[3],
    color: os.palette.sequential.s2[6],
    fillOpacity: 0.3,
    weight: 1
  },

  // A function to be called for each Feature in the FeatureCollection
  onEachFeature: function (feature, layer) {
    layer.on({
      mouseover: function (e) {
        highlightGeojson(feature.properties.id);
        highlightListElement(feature.properties.id);
      },
      mouseout: function (e) {
        unhighlightGeojson(feature.properties.id);
        unhighlightListElement(feature.properties.id);
      },
      click: function (e) {
        flyToBoundsOffset(feature.properties.id);
      }
    });
  }
});

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

// Then fetch the geojson using Leaflet Omnivore, which returns a L.geoJSON object
var nationalParks = omnivore.geojson('./data/national-parks.json', null, parksLayer)
    .on('ready', function () { // <- this callback is executed once data is loaded

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.

    // This code is being executed inside the omnivore.geojson().on('ready') callback.
    // Remember that nationalParks is a L.geoJSON object - with a .getLayers() method
    nationalParks.getLayers().forEach(function (nationalParkFeature, i) {

        let nationalPark = nationalParkFeature.feature; // <- the GeoJSON Feature object

        // First create the HTML element that will represent the park
        let element = `<li class="layer" data-np-id="${nationalPark.properties.id}">
            <div class="layer-element icon" data-type="list-item" data-id="${nationalPark.properties.id}">
                <div class="label">
                    <img class='np-arrow' src='./img/np-arrow.png' />
                    <span class='np-name'>${ nationalPark.properties.name }
                        </span>
                        <a href="${ nationalPark.properties.url }" target="_blank">
                            <i class="material-icons"
                                onClick="this.href='${ nationalPark.properties.url }'"
                                aria-label="">launch</i>
                        </a>
                </div>
            </div>
        </li>`

        element = $.parseHTML(element);

        // Set up event handlers for the <li> elements and children
        $(element).find('span').on('click', function (e) {
            e.preventDefault();
            // Fly to the feature
            flyToBoundsOffset(nationalPark.properties.id)
        });

        // Highlight on mouseenter ...
        $(element).on('mouseenter', function () {
            highlightGeojson(nationalPark.properties.id)
            highlightListElement(nationalPark.properties.id)
        });

        // ... and unhighlight on mouseleave
        $(element).on('mouseleave', function () {
            unhighlightGeojson(nationalPark.properties.id)
            unhighlightListElement(nationalPark.properties.id)
        });

        // Append the <li> to the <ul>
        $('.layers').append(element);
    });
})
.on('error', function (err) {
    console.error(err);
})
.addTo(map) // <- And add the L.geoJSON object to the map

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!

We use data the and APIs , as well as , and to build this interactive web interface.

We created a lightweight JSON file, . 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 's Open Zoomstack layer. To simplify the geometries and make this GeoJSON more lightweight, we used 's awesome Simplify tool. And we used to trim unnecesary coordinate precision, further reducing file size.)

In the key logic of the locator app is defined. Here we'll walk through line by line to explain how it works.

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 to fetch the GeoJSON file we've prepared and load it into a L.geoJSON object.

OS Maps
OS Features
Leaflet
leaflet-omnivore
jQuery
/data/national-parks.json
OS Features API
mapshaper.org
geojson-precision
tutorial.js
leaflet-omnivore