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.

Week 10 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>
		
		<!-- Load London Underground Dataset-->
		<script src="data/underground.js"></script>

		<!-- This is where we set the style for the different elements in the page's content -->
		<style>
				
			/* Style the map */
			#map {
				width:  800px;
				height: 500px;
			}
			
			/* Style the legend */
			.legend { 
				font-family: Arial, sans-serif;
				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; 
			}
			
			/* Style the info box so that it looks like a Leaflet control */
			.info { 
				font-family: Arial, sans-serif;
				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>
	</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>
		
		<!-- 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, interval, 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( [51.48728233102447, -0.11360004378767831], 13);

				// this adds the basemap tiles to the map
				L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', {
					attribution: 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ',
					maxZoom: 16
				}).addTo(map);
				
				// add an info box to the map for the legend
				addInfoBox();
				
				// verify that the bearing calculations are correct
// 				TEST_bearings();
				
				// call a function to add the journeys to a map
				journeysToMap();
			}
			
			
			/**
			 * 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({position: 'bottomright'});

				//create the info box to update with population figures on hover
				info.onAdd = function(map) {
					this._div = L.DomUtil.create('div', 'info');
					this._div.innerHTML = '<div class="legend">  <i style="background:#bd0026"></i> <span id="interval5"></span> journeys<br/>  <i style="background:#f03b20"></i> <span id="interval4"></span> journeys<br/>  <i style="background:#fd8d3c"></i> <span id="interval3"></span> journeys<br/>  <i style="background:#fecc5c"></i> <span id="interval2"></span> journeys<br/>  <i style="background:#ffffb2"></i> <span id="interval1"></span> journeys<br/></div>';
					return this._div;
				};

				// add the info window to the map
				info.addTo(map);
			}
			

			/**
			 * Ensure and angle is always between 0-360
			 */
			function wrapAngle(angle) {
				return (angle + 360) % 360;
			}


			/**
			 * Get a turf bearing, convert it to North and wrap to 0-360
			 */
			function getBearing(a, b) {
				return (90 - turf.bearing(a, b) + 360) % 360;
			}	
			
			
			/**
			 * Run tests
			 * Success looks like this:
			 *		North: 0   true
			 *		East:  90  true
			 *		South: 180 true
			 *		West:  270 true
			 */			
			function TEST_bearings(){
			
				// never
				var a = turf.point([53, 2]);
				var b = turf.point([53.000000000001, 2]);
				var c = getBearing(a, b);
				console.log("North: ", c, (c === 0));
				
				// eat
				a = turf.point([53, 2]);
				b = turf.point([53, 2.000000000001]);
				c = getBearing(a, b);
				console.log("East: ", c, (c === 90));
				
				// shredded
				a = turf.point([53.000000000001, 2]);
				b = turf.point([53, 2]);
				c = getBearing(a, b);
				console.log("South: ", c, (c === 180));
				
				// wheat
				a = turf.point([53, 2.000000000001]);
				b = turf.point([53, 2]);
				c = getBearing(a, b);
				console.log("West: ", c, (c === 270));
			}
			
			
			/**
			 * Load the journey lines onto the map, styling intelligently into 5 equal classes
			 */
			function journeysToMap() {

				// initialise a list to hold the curves
				var curves = [];
			
				// loop through each journey
				for (var i = 0, bezier, curve; i < journeys.length; i++) {
							
					// construct a bezier curve
					bezier = makeODCurve(
						[ parseFloat(journeys[i]['startX']), parseFloat(journeys[i]['startY']) ], 
						[ parseFloat(journeys[i]['endX']),   parseFloat(journeys[i]['endY'])   ]
					);
								
					// make the curve into a polyline including the count property
					curve = turf.lineString( bezier.geometry.coordinates, {journeyCount: parseInt(journeys[i]['cnt'])} );
					
					// load into the list of curves
					curves.push(curve);
				}
				
				// compile curves into a feature collection
				var curveCollection = turf.featureCollection(curves);
				
				// determine an interval to use in styling the curves
				interval = getStyleInterval(curveCollection, 5);
				
				// now we know the interval, fill in the legend
				for (var i = 1; i <= 5; i++) {
					document.getElementById( 'interval' + i.toString() ).innerHTML = interval * i;
				}
				
				// add to the map
				geojson = L.geoJson(curveCollection, {
					style: style
				}).addTo(map);
			}
			
			
			/**
			 * Make an OD Curve between an origin and a destination
			 */
			function makeODCurve(a, b) {

				// get the distance by which to offset point c from point b
				// this should be one sixth of the distance between a and b
				var oDist = turf.distance(a, b, {units: "meters"}) * 1/6;

				// get the direction by which to offset point c from point b
				// this should be 45 degrees less than the direction between a and b
				var oDirection = wrapAngle(getBearing(a, b) - 45);

				// calculate point c (as an offset from point b)
				var c = turf.destination(b, oDist, oDirection, {units: "meters"}).geometry.coordinates;

				// construct the line a>c>b
				var line = turf.lineString([a, c, b]);

				// calculate the bezier curve and return
				return turf.bezierSpline(line, { resolution: 5000, sharpness: 0.9 });
			}	
			
			
			/**
			 * Automatically calculate intervals for 5 classes of curve
			 */
			function getStyleInterval(curveCollection, classCount) {
						
				// init variables for the largest and smallest variables encountered
				var max = -Infinity,  min = Infinity;
			
				// get the range of the passenger count values=
				for (var i = 0, journeyCount; i < curveCollection.features.length; i++) {
				
					// get current feature (just for readability)
					journeyCount = curveCollection.features[i].properties.journeyCount;
			
					// if it's bigger than the previous max, save
					if (journeyCount > max) {
						max = journeyCount;
				
					// otherwise, if it's smaller than the previous min, save
					} else if (journeyCount < min) {
						min = journeyCount;
					}
				}
			
				// get the interval between each desired classification
				return Math.round((max - min) / classCount);
			}
			
			
			/**
			 * Set the style of the curve
			 */
			function style(feature) {

				// assign the appropriate colour based upon the journeyCount value
				if (feature.properties.journeyCount <= interval) {
					return {
						color: "#ffffb2",
						opacity: 0.3,
						weight: 1
					}
				} else if (feature.properties.journeyCount <= interval * 2) {
					return {
						color: "#fecc5c",
						opacity: 0.4,
						weight: 2
					}
				} else if (feature.properties.journeyCount <= interval * 3) {
					return {
						color: "#fd8d3c",
						opacity: 0.5,
						weight: 4
					}
				} else if  (feature.properties.journeyCount <= interval * 4) {
					return {
						color: "#f03b20",
						opacity: 0.8,
						weight: 6
					}
				} else {	// greater than interval *4
					return {
						color: "#bd0026",
						opacity: 0.9,
						weight: 7
					}
				}
			}
		</script>
	</body>
</html>

This course has not yet begun.
Course material will appear here week by week as the course progresses.