Mapworks What3Words Integration


Level of Experience Intermediate
Requirements Embedded map and completed feature creation and styling tutorial
Time Required 30 min
Source Code https://github.com/mapworksio/what3words-integration

Overview

In this tutorial, we'll be using what we've learnt so far to create an app that integrates the Mapworks platform with what3words.

What3words is a service that converts a latitude and longitude into 3 words and vice versa for easy recollection of any geolocation. 

This app will allow us to:

  • Click on the map and get the corresponding 3 words
  • Allow the user to type in 3 words and zoom to the location
  • Show the words immediately surrounding a chosen location
  • Define the 3 words through a URL parameter

For more information about what3words, check out their website and their developers API.

Application UI

For this example, we'll assume that you've set up the base application as in API Getting Started. We can add in additional bootstrap components by using the following HTML and CSS:

<head>
...
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
</head>
 
<body>
...
   <div id="what-3-words">
		<nav class="navbar navbar-default navbar-static-top" id="w3w-navbar">
			<form class="navbar-form navbar-left" role="search">
				<div class="form-group">
					<div class="input-group">
						<span class="input-group-addon w3w-background" id="basic-addon1">
							<img src="https://what3words.com/wp-content/uploads/2015/12/w3w_logo_final-white.png" alt="what3words" class="w3w-logo">
						</span>
						<input type="text" class="form-control w3w" id="txtWhat3Words" placeholder="What 3 Words">
						<span class="input-group-btn">
							<button class="btn btn-default" type="button" id="btnGo">Go!</button>
						</span>
					</div>
				</div>
				<button class="btn btn-default" type="button" id="btnSurround">Show Surrounding Words</button>
				<span id='spinner' style='display:none' class="glyphicon glyphicon-refresh gly-spin"></span>
			</form>
		</nav>
	</div>
</body>
 ...
 
      #what-3-words {
        max-width: 700px;
		min-width: 700px;
        height: auto;
        position: absolute;
      }
      
      #w3w-navbar {
        background: transparent;
        border: none;
        box-shadow: none;
      }
      
      .w3w-logo {
        width: auto;
        height: 30px;
        max-width: 100px;
        max-height: 88px;
      }
      
      #txtWhat3Words {
        width: 250px;
      }
      
      .w3w-background {
		  background-color: #e6323e;
		  padding-top: 0;
		  padding-bottom: 0;
      }
	.gly-spin {
		-webkit-animation: spin 2s infinite linear;
		-moz-animation: spin 2s infinite linear;
		-o-animation: spin 2s infinite linear;
		animation: spin 2s infinite linear;
	}
	@-webkit-keyframes spin {
		0% {
			-webkit-transform: rotate(0deg);
			transform: rotate(0deg);
		}
		100% {
			-webkit-transform: rotate(359deg);
			transform: rotate(359deg);
		}
	}
	@keyframes spin {
		0% {
			-webkit-transform: rotate(0deg);
			transform: rotate(0deg);
		}
		100% {
			-webkit-transform: rotate(359deg);
			transform: rotate(359deg);
		}
	}

Getting Words from Map Click 

The first thing we are going to do is pick up the map click events, draw a point, make a request to the what3words server and display the words in the text box.

Lets start by getting a hold of the Underscore and jQuery used by Mapworks Studio so that we can easily use it ourselves. We'll then call an init() function that will do some setup for us.

	// Call when the map has finished
	map.once('ready', function() {
		$ = Studio.$; //JQuery
		_ = Studio._; //Underscore
		//Initialise
		w3w.init();
	});

We are going to namespace all our what3words functions with "w3w" to help keep our code a bit cleaner. We also have a variable "w3wApiKey" which will store our API key. Replace this if you want to use this in your own applications. 

Lets start by creating a layer where our pointer will be drawn when a user clicks on the map. We'll try to be a bit fancy, overlying multiple styles to give us a pointer that looks like 

var w3wApiKey='W5UMPVKL';
w3w = {
	init: function() {
		// create the layer that shows our main w3w point
		w3w.layer = new Studio.core.entity.TreeVectorLayerEntity({
			visible: true
		}, {
			map: map
		});
		map.getTree().add(w3w.layer);
		var layerStyles = new Studio.core.entity.LayerStylesEntity({
			w3wStyle2: {
				default: {
					pointFill: '#e6323e',
					pointWidth: 8
				},
				order: 2
			},
			w3wStyle1: {
				default: {
					pointFill: '#000000',
					pointOpacity: 0,
					pointWidth: 18,
					pointLineWidth: 2,
					pointLineFill: '#e6323e'
				},
				order: 1
			}
		});
		w3w.layer.setStyles(layerStyles);
	}
}

Lets write some helper methods that will help us send a request to the what3words server, draw our point and toggle our progress spinner.

w3w = {
	...
 
	/**
	 * Helper function to call What 3 Words API
	 * @param String type Can be on of reverse, forward, or grid.
	 * @param Object data The data required for the call. @see https://docs.what3words.com/api/v2/#description
	 * @param Function callback A callback function.
	 */
	request: function(type, data, callback) {
		if (!type || !data) {
			return "No parms!";
		}
		def = {
			display: 'full',
			format: 'json',
			key: w3wApiKey
		}
		return $.ajax({
			url: 'https://api.what3words.com/v2/' + type,
			data: _.extend(def, data),
			success: callback || function(data, status) {
				console.log(status, data);
			}
		})
	},
 
	// Draw the pointer on the map at the given coordinates [x,y]
	pointer: function(coords) {
		if (w3w.pt == null) {
			w3w.pt = map.createPoint(coords[0], coords[1], w3w.layer);
		} else {
			w3w.pt.setCoordinates(coords[0], coords[1]);
		}
		w3w.layer.redraw();
	},
 
	// Helper method to set the busy spinner
	inProgress: function(busy) {
		if (busy)
			$('#spinner').show();
		else
			$('#spinner').hide();
	},

Now we'll draw the pointer object when ever the user clicks on the map and set the data in our text box to be the returned words.

w3w = {
	init: function() {
		...

		// listen to the mouse click event to find the w3w value
		map.listenTo(map, 'feature:mouseclick', function(ev) {
			w3w.inProgress(true);
			var coords = map.getCoordinates(ev.getX(), ev.getY());
			w3w.request('reverse', {
					coords: coords[1] + "," + coords[0]
				},
				function(data, status) {
					$('#txtWhat3Words')[0].value = data.words;
					w3w.inProgress(false);
				});
			w3w.pointer(coords);
		});
		
		...
	}
}

Zoom to Location Given 3 Words

Lets now do the reverse operation and zoom to a map location given 3 words. 

We'll start by defining what scale to zoom to. 

w3w = {
	zoomScale: 5000,

When the user clicks the "Go!" button, we need to:

  • Verify that the 3 words are in the correct format
  • Get the corresponding coordinates
  • Zoom to that location
  • Draw our point there
  • Handle any errors
w3w = {
	init: function() {
		...
 
		// Get the coordinates from 3 words, checking to make sure the word pattern is correct	
		$('#btnGo').click(function(ev) {
			var word = $('#txtWhat3Words')[0].value;
			var pattern = /\w.\w.\w/g;
			
			// check pattern
			if (pattern.test(word)) {
				w3w.inProgress(true);
				w3w.request('forward', {
						addr: word
					},
					function(data, status) {
						// check for valid response
						if(data.status.status == 200 && data.status.code == null){
							var geom = data.geometry;
							var bounds = data.bounds;
							map.setViewCenter(geom.lng, geom.lat, w3w.zoomScale); //Zoom to the point.
							w3w.pointer([geom.lng, geom.lat]);
						}
						// handle errors
						else{
							alert("Error retrieving what3words location. "+data.status.message);
						}
						w3w.inProgress(false);
					}
				);
			} else {
				alert('No words specified. Must be in the format of word1.word2.word3');
			}
		});
 
		...
	}
}

Lets also account for the annoying tendency for the page to reload when a user presses the "Enter" key in the text box

 w3w = {
	init: function() {
		...

		// cancel the textbox submission on enter and trigger the w3w retrieval instead
		$(document).on("keypress", "input", function(event) {
			if (event.keyCode == 13) {
				event.preventDefault();
				$("#btnGo").click();
				return false;
			}
		});
 
		...
	}
}

Also, lets stop the map from stealing the focus whenever you mouse over it. This makes it a bit easier for people to type in their 3 words. See Studio.core.Map.setTakeFocus()

  w3w = {
	init: function() {
		map.setTakeFocus(false);
		...
	}
}

Show Surrounding Words

A useful feature is to be able to display the words immediately surrounding a point. That allows the user to find a nice set of words for the location they are trying to reference. 

To do this, we will use what3words' grid API to get an indication of where the point distribution and make individual requests for each point. 

To avoid clutter, we will first need to reduce the minimum scale of the map so that we can zoom in closer to the map.

w3w = {
	minScale: 70,
	...
 
	init: function() {
		map.setMinScale(w3w.minScale);
		...
	}
}

Lets create our layer which will show the surrounding points. It will have single "title" field which we will use as the point label. We will create the layer using a combination of JSON attributes object and TreeVectorLayerEntity.setFields()

 w3w = {
	init: function() {
		...
 
		// create the layer that will show the surrounding words
		w3w.surroundingLayer = new Studio.core.entity.TreeVectorLayerEntity({
			visible: true,
			labelled: true,
			styles: {
				"#": {
					"default": {
						pointFill: 'red',
						pointWidth: 8,
						labelFont: "Sans-Serif",
						labelSize: [12],
						labelTemplate: '|title|'
					}
				}
			},
		}, {
			map: map
		});
		w3w.surroundingLayer.setFields([{
			name: "title",
			title: "Title", 
			type: Studio.core.entity.LayerFieldEntity.TypeMap.VARCHAR
		}]);
		map.getTree().add(w3w.surroundingLayer);
 
		...
	}
}

When the user clicks the "Show Surrounding Words" button, we'll zoom into the last clicked location and draw the surrounding points. If they havent clicked anywhere yet, then we'll zoom into the middle of the screen.

  w3w = {
	init: function() {
		...
		//Trigger request to draw the surrounding points
		$('#btnSurround').click(function(ev) {
			if (w3w.pt != null) {
				// zoom on last clicked position 
				map.setViewCenter(w3w.pt.getX(), w3w.pt.getY(), w3w.minScale);
			} else {
				// zoom to the middle of the screen
				var viewCenter = map.getViewCenter();
				w3w.pointer(viewCenter);
				map.setViewCenter(viewCenter[0], viewCenter[1], w3w.minScale);
			}
			
			// use our current view as the input for the what3words grid query
			var bbox = map.getViewExtent().getBounds().getBoundsAsArray();
			w3w.inProgress(true);
			w3w.request('grid', {
				bbox: bbox.reverse().toString()
			}, function(data, status) {
				if (data.lines) {
					w3w.drawSurrounding(data.lines);
				} else {
					alert(data.status.message);
				}
			});
		});

		...
	}
}

OK now lets actually go about drawing these points. 

First up we need to clear the layer. Then we have to get a grid of latitudes and longitudes to iterate through

w3w = {
	...
 
 	// Draw the surrounding points given the w3w grid 
	drawSurrounding: function(grid) {
		//Remove existing points
		w3w.surroundingLayer.reload();

		// create array of longitude and latitude points based on the w3w grid
		var longs = [];
		var lats = [];

		for (i = 0; i < grid.length - 1; i++) {
			// check if this is a latitude line or longitude line and push to the appropriate array
			if (grid[i].start.lat == grid[i].end.lat) {
				lats.push(grid[i].start.lat);
			} else if (grid[i].start.lng == grid[i].end.lng) {
				longs.push(grid[i].start.lng);
			}

			// draw lines for debugging
			//map.createPolyline([grid[i].start.lng, grid[i].end.lng], [grid[i].start.lat, grid[i].end.lat], 2, w3w.surroundingLayer);
		}
 
	...
	}
}

Next we iterate through each section of the grid, adding a slight offset so that we dont end up with boundary errors

 w3w = {
	...

	drawSurrounding: function(grid) {
		...
		// Create Points with a slight offset so that we dont run into boundary problems
		var points = []
		var offset = 0.000001;
		for (i = 0; i < longs.length; i++) {
			for (j = 0; j < lats.length; j++) {
				points.push([lats[j] - offset, longs[i] + offset]);
			}
		}
		...
	}
}

Finally we'll get the words for each of the points and draw it on the map, removing the progress spinner at the end

w3w = {
	...

	drawSurrounding: function(grid) {
		...
 
		// retrieve the words for each point
		var promises = points.map(function(point) {
			return w3w.request('reverse', {
					coords: point.toString()
				},
				function(data, status) {
					// draw each of them on the map 
					map.createPoint(data.geometry.lng, data.geometry.lat, w3w.surroundingLayer)
						.setFields({
							Title: data.words
						});
					w3w.surroundingLayer.redraw();
				});
		});
		//End spinner when all drawn
		$.when.apply($, promises).then(function() {
			w3w.inProgress(false);
		});
	}
}

URL Parameter 

The last thing we'll do is have the ability to define the 3 words in your URL parameter. 

We'll start by defining a function to get the parameter by its name.

 function getParameterByName(name) {
    var url = window.location.href;
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

Now after all the w3w functions have initialised, check for the parameter and trigger the search.

 	// Call when the map has finished
	map.once('ready', function() {
		$ = Studio.$; //JQuery
		_ = Studio._; //Underscore
		//Initialise
		w3w.init();
		// check if there is a 'w3w' parameter in the URL
		var urlParam = getParameterByName('w3w');
		if(urlParam != null){
			$('#txtWhat3Words')[0].value = urlParam;
			$("#btnGo").click();
		}
	});

And here is our final product!

See the Pen Mapworks What3Words Integration by Mapworks (@mapworks) on CodePen.