Using the data.police.uk API with Leaflet

Posted by James Gardiner on May 5, 2015

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(
			'div', '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:

``` javascript //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.