Week 4 Solution

<!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>
			
		<!-- This is loading the stylesheet for the Leaflet map from their website. 
		 Make sure you put this BEFORE Leaflet's JavaScript -->
		<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"/>

		<!-- This is loading the Leaflet JavaScript library from their website
		 Make sure you put this AFTER Leaflet's CSS -->
		<script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"></script>

		<!-- Load Turf.js -->
		<script src="https://cdn.jsdelivr.net/npm/@turf/turf@5/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>
	
	<!-- The onload event of the body tag is being used to call the function that creates the map.
	 This means that nothing will happen until the body is completely loaded, giving us a little more
	 control over the order in which the web page loads. -->
	<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>
		
		<!-- This is where we put our JavaScript, note that it is below the div, as the div needs
		 to exist before we can put a map in it!-->
		<script>		
			
			//setup global variables
			var map, info, geojson;
			
			/**
			 * Initialise the Map
			 */
			function initMap(){
			
				// this is a variable that holds the reference to the Leaflet map object
				// creates a world map (centre where the equator crosses the GPM)
				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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
				}).addTo(map);
				
				// add the info box to the map
				addInfoBox();
				
				// set the info box to 'loading'
				info._div.innerHTML = '<h2>Loading...</h2>';
				
				//call to get all crimes
				makeRequest("https://data.police.uk/api/crimes-street/all-crime?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 population figures on hover
				info.onAdd = function (map) {
					this._div = L.DomUtil.create('div', 'info');
					this.update();
					return this._div;
				};

				// create a function called update() that updates the contents
				info.update = function (props) {
					
					// if properties have been passed, then use them to fill in the info box
					if (props){
						this._div.innerHTML = '<b>' + props.crimes + '</b> crimes';
					
					// otherwise, just set to a default message
					} else {
						this._div.innerHTML = 'Hover over a hex bin';
					}
				};

				// 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();

				//set an event listener for when the HTTP state changes
				httpRequest.onreadystatechange = function () {

					//if it works, parse the JSON and pass to the callback	
					//a successful HTTP request returns a state of DONE and a status of 200
					if (httpRequest.readyState === XMLHttpRequest.DONE) {
						if (httpRequest.status === 200) {
							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
				var points = [];
				for (var 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 
				var pointsCollection = turf.featureCollection(points);
				
				// build a 100m hex grid to cover the area of interest
				var hexgrid = turf.hexGrid(turf.bbox(pointsCollection), 100, {units: 'meters'});
				
				// loop through each polygon feature in the hex grid			
				var pointsWithin;
				var polygons = []			
				for (var 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 
				var polygonCollection = turf.featureCollection(polygons);
				
				// load into L.GeoJson, style and add to the map
				geojson = L.geoJson(polygonCollection, {
					style: style,
					onEachFeature: setEvents
				}).addTo(map);
				
				//update the info box with the default message
				info.update();
            }


			/**
			 * This function styles the data (a single country)
			 */
			function style(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) {
			
				// create a variable to hold the colour
				var colour;
			
				// assign a colour based upon the value of crimes
				if (crimes > 120){
					colour = '#800026';
				} else if (crimes > 100) {
					colour = '#BD0026';
				} else if (crimes > 80) {
					colour = '#E31A1C';
				} else if (crimes > 60) {
					colour = '#FC4E2A';
				} else if (crimes > 40) {
					colour = '#FD8D3C';
				} else if (crimes > 20) {
					colour = '#FEB24C';
				} else if (crimes > 10) {
					colour = '#FED976';
				} else if (crimes > 0) {
					colour = '#FFEDA0';
				} else {
					colour = "#DDDDDD";
				}
				
				// return the resulting colour
				return colour;
			}
			
			
			/**
			 * 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: resetHighlight,
				});
			}

			
			/**
			 * 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
				var layer = e.target;

				// set the style to yellow
				layer.setStyle({
					fillColor: 'yellow',
				});

				//update the info box
				info.update(layer.feature.properties);
			}
			

			/**
			 * Reset the style after the hover is over
			 */
			function resetHighlight(e) {	// e refers to the event object
				
				//reset the style of the country that was turned yellow
				geojson.resetStyle(e.target);	// e.target is the hexbin that was hovered over
				
				//update the info box
				info.update();
			}
		</script>
	</body>
</html>