Published on

Using the data.police.uk API with Leaflet

Authors

Following on from my previous post on setting up a Leaflet map in a Jekyll blog, I thought it would be useful to show off some of Leaflet's functionality using real world data.

The first part of this post is a walkthrough covering the steps taken to get a 'Neighbourhoods' boundary visible on a Leaflet map, via a few dropdowns and the data.police.uk site.


The site makes police recorded crime data available through its API (If you're unfamiliar with APIs then check out this post by Benjy Weinberger), which powers quite a few web and mobile apps (such as the UK Crime statistics site). It's a standard JSON web service, using HTTP GET and POST requests, and is relatively well documented here.

There are three high level methods in the data.police.uk API docs, broken down into Force related, Crime related and Neighbourhood related. As there are more than a few neighbourhoods in England & Wales, I'm going to make users choose a Police Force first, and then a neighbourhood within that force.

To get a list of forces, we can use https://data.police.uk/api/forces which returns a JSON object.

To select a specific force, we can add a dropdown to our map by extending the Leaflet Control class with the following code:

var ForceControl = L.Control.extend({
  initialize: function (name, options) {
    L.Util.setOptions(this, options)
  },

  //function to be called when the control is added to the map
  onAdd: function (map) {
    //create the control container with a
    //class name
    var container = L.DomUtil.create('div', 'force-control')

    //create a list of available police force names and ids
    //using the data.police.uk api Forces method inside the
    //'force' global variable.
    forces = []

    //jquery method to retrieve JSON object
    $.getJSON('https://data.police.uk/api/forces', function (data) {
      //create the htmlString that will form the
      //innerHTML of the forces dropdown
      var htmlString =
        '<select id=forcesList ' +
        'onchange="updateNeighbourhoods()"><option>' +
        'Select a Force</option>'

      //create individual force <option> tags within the
      //<select> tag
      $.each(data, function (i, item) {
        forces[i] = item
        htmlString = htmlString + '<option>' + forces[i].name + '</option>'
      })

      //close the select tag
      htmlString += '</select>'

      //update the dropdown list innerHTML
      //with the list of forces
      container.innerHTML = htmlString

      //allow a user to select a single option
      container.firstChild.onmousedown = container.firstChild.ondblclick =
        L.DomEvent.stopPropagation
    })

    return container
  },
})

To update the list of neighbourhoods in the selected force we name a function in the innerHTML, updateNeighbourhoods() that listens for an onChange event. This means that whenever a new force is selected, the function is called. Before we cover that though, lets add a new dropdown to the map, with a standard placeholder text instead of a neighbourhood name:

//extend the L.Control class to create a custom drop down box
// initially with simple placeholder text
var NeighbourhoodControl = L.Control.extend({
  initialize: function (name, options, placeholder) {
    L.Util.setOptions(this, options)
  },

  //once added to the map div, carry out the following
  onAdd: function (map) {
    //create the control container with a particular
    //class name
    var container = L.DomUtil.create('iv', 'neighbourhoods_control')

    //add the following to the innerHTML
    var htmlString =
      '<select id="neighbourhoodsList"' +
      'onchange="neighbourhoodChanged()">' +
      '<option>Select a police force</option></select>'

    //make this the div's innerHTML
    container.innerHTML = htmlString

    //allow a user to select a single option
    container.firstChild.onmousedown = container.firstChild.ondblclick = L.DomEvent.stopPropagation

    return container
  },
})

Both these controls can then be added:

map.addControl(new ForceControl('forcesList', { position: 'topright' }))

map.addControl(new NeighbourhoodControl('neighbourhoodList', { position: 'topright' }))

To update the innerHTML of this div dynamically, we can use the updateNeighbourhoods() function that we call whenever a new police force is selected:

//update the current list of neighbourhoods using the selected
//force id
var updateNeighbourhoods = function (name) {
  //get the force name
  var force = $('#forcesList').val()

  //empty the neighbourhoods array
  neighbourhoods = []

  //find the force id for the api call
  for (var i in forces) {
    if (forces[i].name === force) {
      var id = forces[i].id
    }
  }

  //if the id is matched successfully then get a list of
  //neighbourhoods in that force area.
  if (id) {
    //url to retrieve a list of neighbourhoods
    var url = 'https://data.police.uk/api/' + id + '/neighbourhoods'

    //jquery function to get the JSON object
    $.getJSON(url, function (data) {
      //create the html string to be used as the div's
      //inner HTML
      var htmlString = '<select id="neighbourhoodsList">' + '<option>Select a police force</option>'

      //loop through the JSON object
      $.each(data, function (i, item) {
        //add each to an array
        neighbourhoods[i] = item

        //create an <option> tag for each element in
        //the JSON object
        htmlString = htmlString + '<option>' + neighbourhoods[i].name + '</option>'
      })

      //add a closing </select> tag
      htmlString = htmlString + '</select>'

      //use the div id to update the innerHTML
      $('#neighbourhoodsList').html(htmlString)

      //reorder the options
      $('#neighbourhoodsList').html(
        $('#neighbourhoodsList option').sort(function (a, b) {
          return a.text == b.text ? 0 : a.text < b.text ? -1 : 1
        })
      )

      //Prepend a placeholder so that onchange
      //is fired when user selects a neighbourhood
      $('#neighbourhoodsList').prepend($('<option>Select a neighbourhood</option>'))
    })
  } else {
    //retain the original placeholder text, or reset it
    $('#neighbourhoodsList').html(
      '<select id=' + '"neighbourhoodsList"><option>Select a ' + 'neighbourhood</option></select>'
    )
  }
}

The final thing to add is a function that is called when a neighbourhood is selected. This queries the API for the selected neighbourhood boundary (which is returned as standard JSON rather than GeoJSON), gets the bounding box of the boundary, automatically recenters and zooms to the specified position then adds the layer.

//function called when the selected neighbourhood is changed
var neighbourhoodChanged = function () {
  //get the selected 'hood name
  var hood = $('#neighbourhoodsList').val()

  //compare the name of the 'hood to get the id
  for (i in neighbourhoods) {
    if (neighbourhoods[i].name === hood) {
      var id = []
      id[0] = neighbourhoods[i].id
    }
  }

  //compare the name of the force to get the id
  var force = $('#forcesList').val()
  for (var i in forces) {
    if (forces[i].name === force) {
      id[1] = forces[i].id
    }
  }

  //if we match both the force and 'hood then
  //carry on, else break
  if (id[0] && id[1]) {
    var latlng = []
    var url = 'https://data.police.uk/api/' + id[1] + '/' + id[0] + '/boundary'

    //jquery to get the JSON
    $.getJSON(url, function (data) {
      //create an array of boundary lat lon pairs
      $.each(data, function (i, item) {
        latlng.push(new L.LatLng(data[i].latitude, data[i].longitude))
      })

      //if a layer is already present, remove it
      if (areaLayer) {
        map.removeLayer(areaLayer)
      }

      //create a new polygon object using the latlng array
      areaLayer = new L.Polygon(latlng, {
        clickable: true,
        weight: 3,
        opacity: 0.4,
        fillOpacity: 0.1,
      })

      //redraw the map to the bounds of the new polygon
      map.fitBounds(areaLayer.getBounds())
      //add the polygon to the map
      areaLayer.addTo(map)
    })
  }
}

Here is the final map, with the two drop down selections available:

That's a pretty long post, but I hope it gives you an idea of how we can tap into the police API, which we've only really scratched the surface of. I'll follow this post up shortly, where I'll add a geocoder to the map div that takes postcodes and placenames and focuses the map on the relevant area.