<!DOCTYPE html>
<html>
<head>
<!-- This is the HEAD of the HTML file, things in here do not appear on the page. It is
used for settings, such as the language and page title, as well as loading CSS styles and
scripts that you want to load before the content of the page. -->
<meta charset="UTF-8">
<title>GIS and the Web</title>
<!-- Load the CSS and JavaScript for Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<!-- Load Turf.js -->
<script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
<!-- http://tannerjt.github.io/geotanner/javascript/color-theory/2014/10/29/classybrew-jenks-heart-colorbrewer.html -->
<!-- https://www.npmjs.com/package/classybrew -->
<!-- This is where we set the style for the different elements in the page's content -->
<style>
/* Avoid having a newline between the two divs */
div {
display:inline-block;
}
/* Style the map */
#map {
/* This is the dimensions of the map on the screen, you can set it in
px (pixels) or % of the total screen size*/
width: 800px;
height: 500px;
}
/* Style the info box so that it looks like a Leaflet control */
.info {
padding: 6px 8px;
background: white;
background: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px;
}
/* Style the legend */
.legend {
font-size: small;
padding-top: 10px;
text-align: left;
line-height: 18px;
}
/* Style the i tags within the legend - we use these to make the coloured squares */
.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.6;
}
</style>
</head>
<body onload='initMap()'>
<!-- This is where the map will go -->
<div id='map'></div>
<!-- Add the legend for the map -->
<div class="legend">
<i style="background:#FFEDA0"></i> 0 - 10<br>
<i style="background:#FED976"></i> 10 - 20<br>
<i style="background:#FEB24C"></i> 20 - 40<br>
<i style="background:#FD8D3C"></i> 40 - 60<br>
<i style="background:#FC4E2A"></i> 60 - 80<br>
<i style="background:#E31A1C"></i> 80 - 100<br>
<i style="background:#BD0026"></i> 100 - 120<br>
<i style="background:#800026"></i> 120 +
</div>
<script>
//setup global variables
let map, info, crimesLayer;
/**
* Initialise the Map
*/
function initMap(){
// this is a variable that holds the reference to the Leaflet map object
map = L.map('map').setView([53.4807593, -2.2426305], 15);
// this adds the basemap tiles to the map
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// add the info box to the map
addInfoBox();
//call to get all crimes (set to June 2019 as data availability is poor latter half of this year)
makeRequest("https://data.police.uk/api/crimes-street/all-crime?date=2019-06&lat=53.4807593&lng=-2.2426305", crimesToMap);
}
/**
* Create the info box in the top right corner of the map
*/
function addInfoBox(){
// create a Leaflet control (generic term for anything you add to the map)
info = L.control();
// create the info box to update with crime figures
info.onAdd = function (map) {
this._div = L.DomUtil.create('div', 'info');
this.update("<b>Loading...</b>");
return this._div;
};
// create a function called update() that updates the contents of the info box
info.update = function (value) {
this._div.innerHTML = value;
};
// add the info window to the map
info.addTo(map);
}
/**
* Make a request for JSON over HTTP, pass resulting text to callback when ready
*/
function makeRequest(url, callback) {
// initialise the XMLHttpRequest object
var httpRequest = new XMLHttpRequest();
//s et an event listener for when the HTTP state changes
httpRequest.onreadystatechange = function () {
// a successful HTTP request returns a state of DONE and a status of 200
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
// parse the JSON and pass to the callback
callback(JSON.parse(httpRequest.responseText));
}
}
};
//prepare and send the request
httpRequest.open('GET', url);
httpRequest.send();
}
/**
* Parse JSON crime data and load onto the map
*/
function crimesToMap(data) {
// loop through data and build an array of turf point features
let points = [];
for (let i = 0; i < data.length; i++) {
points.push(turf.point([parseFloat(data[i].location.longitude),
parseFloat(data[i].location.latitude)], {}));
}
// convert the resulting points array into a feature collection
const pointsCollection = turf.featureCollection(points);
// get the bounds of the points
const bbox = turf.bbox(pointsCollection);
// build a 100m hex grid to cover the area of interest
const hexgrid = turf.hexGrid(bbox, 100, {units: 'meters'});
// loop through each polygon feature in the hex grid
let pointsWithin;
let polygons = []
for (let i = 0; i < hexgrid.features.length; i++) {
// calculate the number of crimes within that polygon
pointsWithin = turf.pointsWithinPolygon(pointsCollection, hexgrid.features[i]);
// create a new polygon, combining the old geometry with the new 'crimes' property
polygons.push( turf.polygon(hexgrid.features[i].geometry.coordinates,
{ crimes: pointsWithin.features.length }));
}
// convert the resulting points array into a feature collection
const polygonCollection = turf.featureCollection(polygons);
// load into L.GeoJson, style and add to the map
crimesLayer = L.geoJson(polygonCollection, {
style: styleGenerator,
onEachFeature: setEvents
}).addTo(map);
//update the info box with the default message
info.update("Hover over a hex bin");
}
/**
* This function styles the data (a single country)
*/
function styleGenerator(feature) {
//return a style
return {
weight: 1,
color: 'white',
fillOpacity: 0.6,
fillColor: getColour(feature.properties.crimes) //the colour is set using a function
};
}
/**
* Return a colour based upon the given population value
*/
function getColour(crimes) {
// directly return a colour value based upon the value of crimes
if (crimes > 120){
return '#800026';
} else if (crimes > 100) {
return '#BD0026';
} else if (crimes > 80) {
return '#E31A1C';
} else if (crimes > 60) {
return '#FC4E2A';
} else if (crimes > 40) {
return '#FD8D3C';
} else if (crimes > 20) {
return '#FEB24C';
} else if (crimes > 10) {
return '#FED976';
} else if (crimes > 0) {
return '#FFEDA0';
} else {
return "#DDDDDD";
}
}
/**
* Create a function to tie the mouseover and mouseout events to re-styling the layer
*/
function setEvents(feature, layer) {
//add event listeners for mouseover and mouseout
layer.on({
mouseover: highlightFeature,
mouseout: resetFeature,
});
}
/**
* This function re-styles the data to yellow (for when you hover over it)
*/
function highlightFeature(e) { // e refers to the event object
// e.target is the hexbin that was hovered over
const feature = e.target;
// set the style to yellow
feature.setStyle({
fillColor: 'yellow',
});
//update the info box
info.update("<b>" + feature.feature.properties.crimes + "</b> crimes");
}
/**
* Reset the style after the hover is over
*/
function resetFeature(e) { // e refers to the event object
// e.target is the hexbin that was hovered over
const feature = e.target;
//reset the style of the country that was turned yellow
crimesLayer.resetStyle(feature); // e.target is the hexbin that was hovered over
//update the info box
info.update("Hover over a hex bin");
}
</script>
</body>
</html>
This version of the course is several years out of date and parts of it will not work - it is here just for the benefit of my 2022 dissertation students who want some grounding in Leaflet and associated technologies.
This course has not yet begun.
Course material will appear here week by week as the course progresses.