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: Percent Built On
  • The HTML
  • The API Key
  • MapboxDraw
  • "draw." Event Listeners
  • Preparing the map sources
  • Fetch and Calculate!
  • Wrapping Up

Was this helpful?

  1. Tutorials
  2. Web

Web development: Percent built on

Last updated 1 year ago

Was this helpful?

In this tutorial we'll develop a web app that lets users draw a polygon, then calculates what percentage of the geometry is built on. This technique is useful for determining land cover of any number of layers - green space, buildings, road surface and so on.

Percent built on

Tools and APIs

Tutorial: Percent Built On

Spatial analysis is a powerful way to understand the world. The insights derived from analysis of points, lines, polygons and raster data can serve as the evidence to inform intelligent decision-making. Let's look at how to build a web interface capable of sophisticated spatial analytics.

<iframe style="width:100%;height:400px;max-width:1200px;border:1px solid #f5f5f5;" src="/public/os-data-hub-tutorials/dist/web-development/percent-built-on/"></iframe>

The HTML

Most of this tutorial will focus on the JavaScript required to create this app, but we do need to set up our HTML page so we have the right libraries loaded, and elements with the appropriate classes and IDs.

Have a skim through our index.html: you'll notice we load mapbox-gl.css, mapbox-gl-draw.css, turf.jsand local stylesheets in the head, and jquery.js, mapbox-gl.js, mapbox-gl-draw.js and local scripts before the closing </body> tag.

Other than loading these assets, index.html includes elements for the code in tutorial.js to interact with, most notably a #map div element, a #percent-built p element to hold the computed result and a #fetch-and-calculate button element.

Alright - on to the JavaScript.

The API Key

const apiKey = "YOUR_API_KEY";
const endpoints = {
  zxy: "https://api.os.uk/maps/raster/v1/zxy",
  wfs: "https://api.os.uk/maps/raster/v1/wfs"
};

MapboxDraw

// Create a new MapboxDraw instance with styles, controls and interaction modes
var draw = new MapboxDraw({
  styles: mbDrawConfig.styles, // custom OS styles, defined in ./config.js
  displayControlsDefault: false,
  controls: {
    polygon: true,
    trash: true
  },
  modes: {
    ...MapboxDraw.modes,
    simple_select: NewSimpleSelect, // Interaction modes, also defined in ./config.js
    direct_select: NewDirectSelect
  }
});

// Add to map and add event listeners
map.addControl(draw);

"draw." Event Listeners

map.on("draw.create", activateFetch);
map.on("draw.delete", disactivateFetch);

// ...

// Defined below 👇 and hoisted 👆
function activateFetch() {
  $("#draw-prompt").text("What is the percent built on?");
  $("#percent-built").css("display", "block");
  $("#fetch-and-calculate").attr("disabled", false);

  map.fitBounds(turf.bbox(draw.getAll()), {
    padding: {
      left: $(".osel-sliding-side-panel").width() + 50,
      right: 50,
      bottom: 50,
      top: 50
    }
  });
}

function disactivateFetch() {
  $("#draw-prompt").text("Draw a polygon to analyse.");
  $("#percent-built").css("display", "none");
  $("#fetch-and-calculate").attr("disabled", true);

  // Clear the building and outlines layers so we can display another query if the user draws a new polygon
  if (map.getLayer("buildings")) {
    map.removeLayer("buildings");
    map.removeLayer("intersection-outline");
  }

  $("#percent-built span").text(". . .");
}

Preparing the map sources

Mapbox GL JS works by visualising styled layers, which reference a data source.

We'll be adding two sources and layers to visualise in addition to the query polygon the user draws on the map: buildings and buildings-intersection. Our users might draw a polygon that cuts through a building footprint, and we only want to count the section of the footprint that intersects the query polygon. The buildings source / layer will hold and visualise polygons from the OS Features API, and buildings-intersection will hold polygon intersections computed in the browser.

Until we have a query to analyse we will just add the sources to the map:

map.on("style.load", function () {
  map.addSource("buildings", {
    type: "geojson",
    data: null
  });

  map.addSource("buildings-intersection", {
    type: "geojson",
    data: null
  });
});

Fetch and Calculate!

Most of the logic in this app will be executed once the user has drawn a polygon and clicks Fetch and Calculate.

The things that need to happen:

  1. Generate a Features API query to request building features intersecting the drawn geometry - and send the request.

  2. Loop through the array of returned building features, creating another array of the building footprints intersecting the query polygon.

  3. Calculate the percent built on by dividing the area of the building intersection features by the total area of the query polygon.

  4. Add buildings and intersection features to the map, and update the HTML to show the percent built on.

We'll walk through this step by step.

But, of course, all this code only needs to run when the user clicks "Fetch and Run", so we'll start by adding a "click" event listener to that button element. The callback function we define will be executed when the button is clicked - we'll be writing an async function to help us write clean code handling asynchronous API calls.

document
  .getElementById("fetch-and-calculate")
  .addEventListener("click", async function () {
    addSpinner();
    // 👇 The callback body code will go here!
  });

1. The OS Features API call

First step: fetch buildings that intersect the query polygon the user drew.

The OS Features API is a Web Features Service. Users can query the API with spatial parameters, including making requests for features that intersect a polygon. Within the callback, we take the geometry of the drawn polygon, geom, and call getIntersectingFeatures(geom), which builds the request with the spatial filter and fetches features from the API.

let geom = draw.getAll();

// For this demo we will cap query geometry size to limit the number of API calls
let area = turf.area(geom.features[0].geometry),
  rounded_area = Math.round(area * 100) / 100;

if (rounded_area > 100000) {
  notification.show(
    "warning",
    "Drawn polygon exceeds maximum size limit of 0.1 square km. Please try again."
  );
  $("#loader").css({ visibility: "hidden" });
  draw.deleteAll();
  return; // <- break out of the callback
}

let buildings = await getIntersectingFeatures(geom);

Let's look closely at the getIntersectingFeatures function:

async function getIntersectingFeatures(polygon) {
  // Get the circle geometry coordinates and return a new space-delimited string.
  var coords = turf.flip(polygon.features[0]).geometry.coordinates[0].join(" ");

  // Create an OGC XML filter parameter value which will select the Greenspace
  // features intersecting the circle polygon coordinates.
  // *** ADD Functionality to filter by Type attribute based on dropdown input!
  var xml = `<Filter>
                <And>
                <ogc:Intersects>
                    <ogc:PropertyName>SHAPE</ogc:PropertyName>
                    <gml:Polygon srsName="EPSG:4326">
                    <gml:outerBoundaryIs>
                        <gml:LinearRing>
                        <gml:coordinates>${coords}</gml:coordinates>
                        </gml:LinearRing>
                    </gml:outerBoundaryIs>
                    </gml:Polygon>
                </ogc:Intersects>
                <ogc:PropertyIsEqualTo>
                    <ogc:PropertyName>DescriptiveGroup</ogc:PropertyName>
                    <ogc:Literal>Building</ogc:Literal>
                </ogc:PropertyIsEqualTo>
                </And>
                </Filter>`;

  // Define parameters object.
  let wfsParams = {
    key: apiKey,
    service: "WFS",
    request: "GetFeature",
    version: "2.0.0",
    typeNames: "Topography_TopographicArea",
    outputFormat: "GEOJSON",
    filter: xml,
    count: 100,
    startIndex: 0
  };

  // Create an empty GeoJSON FeatureCollection.
  let geojson = {
    type: "FeatureCollection",
    features: []
  };

  geojson.features.length = 0;

  var resultsRemain = true;

  while (resultsRemain) {
    let response = await fetch(getUrl(wfsParams));
    let data = await response.json();

    wfsParams.startIndex += wfsParams.count;
    geojson.features.push.apply(geojson.features, data.features);
    resultsRemain = data.features.length < wfsParams.count ? false : true;

    if (geojson.features.length > 499) {
      console.log("Cutting off queries for demo.");
      resultsRemain = false;
    }
  }

  return geojson;
}

So, a call to getIntersectingFeatures() with a query geometry will return a GeoJSON FeatureCollection with an array of features representing building polygons from the OS Features API's Topography_TopographicArea feature type. With this information we can proceed to the in-client spatial analysis step.

2. Looping through intersections

Here we loop through each building feature returned, finding the part of its geometry that is intersecting the query polygon. With this we'll create another GeoJSON FeatureCollection, intersections.

We'll use a few functions in the Turf.js library for spatial analysis.

// Initialise a FeatureCollection with an empty features array
let intersections = {
  type: "FeatureCollection",
  features: []
};

// Loop through each building feature
turf.featureEach(buildings, function (currentFeature) {
  // This finds the part of the building that intersects the query geometry, `geom`.
  let intersect = turf.intersect(currentFeature, geom.features[0]);
  if (intersect != null) {
    // Add to the intersections.features array
    intersections.features.push(intersect);
  }
});

With that we have a GeoJSON FeatureCollection of all the building areas inside the query polygon. We're just ready to work out the percent built on!

3. Percent Built On

If any building intersections were detected, we calculate the percent built on and add the buildings and intersections to the map, to visualise the output.

// Declare a variable to hold the computed value
let percent;

if (intersections.features.length > 0) {
  // turf.area returns the area in square metres
  percent = turf.area(intersections) / turf.area(geom);

  // Set data to the fetched and derived FeatureCollections
  map.getSource("buildings").setData(buildings);
  map.getSource("buildings-intersection").setData(intersections);

  // Add layer with styling
  map.addLayer({
    id: "buildings",
    source: "buildings",
    type: "fill",
    layout: {},
    paint: {
      "fill-color": os.palette.qualitative.lookup["2"],
      "fill-opacity": 0.3,
      "fill-outline-color": "black"
    }
  });

  // And strong outlines in the vivid OS magenta / pink for intersecting footprints
  map.addLayer({
    id: "intersection-outline",
    source: "buildings-intersection",
    type: "line",
    layout: {},
    paint: {
      "line-color": os.palette.qualitative.lookup["1"],
      "line-width": 2
    }
  });
} else {
  // Logic for if the user drew an area with no buildings
  percent = 0;
  map.getSource("buildings").setData(null);
  map.getSource("buildings-intersection").setData(null);
}

4. Display the percentage

Last but not least, show the answer by updating the righthand panel with the float value of the percentage built on. We'll also fit map bounds to the target area.

$("#percent-built span").text((percent * 100).toFixed(2));
$(".result-label").show();

map.fitBounds(turf.bbox(geojson), {
  padding: {
    left: $(".osel-sliding-side-panel").width() + 50,
    right: 50,
    bottom: 50,
    top: 50
  }
});
removeSpinner();

Wrapping Up

That's it! We've written code that let's a user draw a polygon on an OS Maps API raster basemap, fetch features representing building polygons from the OS Features API, and calculate the percentage of the query geometry that is covered by building footprints.

We used Mapbox GL JS, MapboxDraw, Turf.js, and jQuery to help us with map visualisation, map interactivity, spatial analysis and DOM manipulation.

We'll be using the for basemaps and the to fetch vector building footprint geometries.

We'll also use as our JavaScript mapping library, the package for drawing geometries, and for spatial analysis in the browser. We'll also use to manipulate the DOM.

First, head to and copy your API key from a project with both the Maps and Features APIs added. We'll start by assigning this string to a constant variable. We'll do the same with the service endpoint urls:

We won't go over setting up the basemap in detail here - you can find code on how to connect a mapboxgl.Map instance to the OS Maps API on our . Since we'll be overlaying features, the Light cartographic style is a good choice.

The package lets users draw polygons on a Mapbox GL basemap by clicking points. We create a new instance of MapboxDraw - for this app we configured the Draw tool with and customised user interaction modes on the drawn polygon. (We set up these options in to keep code clean.)

We don't want the user to be able to fetch data from the OS Features API until we have a polygon to find intersecting features, so we only activate that button's functionality if a polygon is drawn on the map. We define these functions toward the bottom of tutorial.js, knowing they'll be and available for the 'draw.create' and 'draw.delete' event listeners:

Let us know what you think - or show us what you build! Tweet and tag .

OS Maps API
Features API
Mapbox GL JS
Mapbox Draw
Turf.js
jQuery
osdatahub.os.uk
Examples page
mapbox-gl-draw
OS colours
js/config.js
hoisted
@OrdnanceSurvey
#OSDeveloper