Proximity detection and the Voronoi geom

A Voronoi geom (found in the d3-voronoi package) chops a geographic shape into discrete regions around points, such that no section overlaps and the entirety of the area is covered. Anything within a particular point's section is closer to that point than any other point. It's effectively another layout, but it's used more for utility than for display -- for instance, a grid of Voronoi regions is often used to increase the mouseover area of data in a chart so that your cursor will always be highlighting the nearest point. We will use the Voronoi geom to figure out what the closest major airport is to your current location, which we'll supply via the HTML5 Location API.

Replace the last line we wrote with the following:

async function renderView(ctx, next) {
if (ctx.method === 'GET') {
ctx.body =
&grave;<!doctype html>
<html>
<head>
<title>Find your nearest airport!</title>
</head>
<body>
<form method="POST" action="#">
<h1>Enter your latitude and longitude, or allow your browser to check./h1>

<input type="text" name="location" /> <br />
<input type="submit" value="Check" />
</form>
<script type="text/javascript">
navigator.geolocation.getCurrentPosition(function(pos) {
var latlng = pos.coords.latitude + ',' + pos.coords.longitude;
document.querySelector('[name="location"]').value = latlng;
});
</script>
</body>
</html>&grave;;
} else if (ctx.method === 'POST'){
await next(); // This ensures all the other middleware runs first!

const airport = ctx.state.airport.data;
ctx.body = &grave;<!doctype html>
<html>
<head>
<title>Your nearest airport is: ${airport.name}</title>
</head>
<body style="text-align: center;">
<h1>
The airport closest to your location is: ${airport.name}
</h1>
<table style="margin: 0 auto;">
<tr>
${Object.keys(airport).map(v => &grave;<th>${v}</th>&grave;).join('')}
</tr>
<tr>
${Object.keys(airport)
.map(v => &grave;<td>${airport[v]}</td>&grave;).join('')}
</tr>
</table>
</body>
</html>&grave;;
}
app.use(renderView);

Holy moly, that's a lot of code. What's all going on here?

We first create a function and then assign it as a piece of middleware to our Koa app. All middleware functions receive a context (ctx) and next() argument; the former is used to pass instructions around the application, whereas the latter is used to control flow. In our renderView() function, first we create an HTML page template for when we make a GET request to our web server -- in other words, when we first load the site. We then make another page template for when we POST to our web server. This is where things get interesting. You'll notice in our else statement that before we do anything else, we call next() and await it to return. What this means is, "wait until the downstream middleware runs, then continue onward in this function." This means our function can return our GET request without invoking the other middleware, or in the case of a POST request, invoke our other middleware, have those add data to our context's state object, then continue with the rest of the function.

You'll notice that we put view code inside of our web server logic, which isn't great, but it works for our purposes. If you're creating multipage applications with multiple routes, you might want to use something like Koa-Router and Koa-Views. The former allows you to define URL routes for requests, and the latter allows you to use template languages, such as Handlebars, to define HTML output.

If you haven't recently, restart the server by press Ctrl+C and then run the following command:

$ npm run server

It's worth noting that, unlike in the frontend, we need to restart the server every time there's a change. If something isn't working as expected, try restarting it.

This sends the web browser a basic HTML document that asks the user to fill in latitude and longitude as a comma-separated value. Alternatively, if the user accepts the browser's request to use the HTML5 location API, it will auto-populate.

Next, we need to create a new function for the Voronoi calculations. Add the following at the top of import section of chapter8/index.js:

import { readFileSync } from 'fs'; 
import * as d3 from 'd3';
import * as boundaries from '../../data/cultural.json';
import * as land from '../../data/land.json';

This imports D3 and Node's built-in synchronous filesystem loading function. We also import our TopoJSON files from Chapter 4, Making Data Useful, and because we have access to the filesystem in Node, we can happily just import them like any old JavaScript file instead of doing an XHR request.

After that, add this:

const airportData = readFileSync('data/airports.dat', 
{ encoding: 'utf8' });
const points = d3.csvParseRows(airportData)
.filter(airport => !airport[5].match(/N/) && airport[4] !== '')
.map(airport => ({
name: airport[1],
location: airport[2],
country: airport[3],
code: airport[4],
latitude: airport[6],
longitude: airport[7],
timezone: airport[11],
}));

This will parse all of our airport data into an object that we can use throughout our application. Next, we create a function that creates a voronoi layout used to calculate which of the airports is closest to our location:

export function nearestLocation(location, points) { 
const coords = location.split(/,s?/);
const voronoi = d3.voronoi()
.x(d => d.latitude)
.y(d => d.longitude);

return voronoi(points).find(coords[0], coords[1]);
}

We assume our location is in a comma-separated string, which we convert into an array. We then instantiate our d3.voronoi layout generator, telling it to get each datum's latitude and longitude properties to calculate the x and y-values, respectively. We then pass our object containing all airports as points, then use the new .find() method to calculate which point is the closest, which we then return.

The Voronoi geom has changed a fair bit in D3 v4, most notably with the addition of layout.find(). Earlier, one had to do a lot of fiddly math stuff, and this chapter was quite a bit more complex. That is no more! Thanks, Mike!

This is useful, but in order to tie it into our Koa app, we need to write a new piece of middleware. Add this function to lib/chapter8/index.js:

function nearestAirport(ctx, next) { 
if (ctx.request.body.location) {
const { location } = ctx.request.body;
ctx.state.airport = nearestLocation(location, points);
next();
}
}

This is pretty straightforward. We get the location value from the request body, use our nearestLocation() function to determine the nearest airport, then assign that to the context state object. We then call next() to invoke the rest of the downstream middleware and pass our updated state object to it. Our renderView middleware will await nearestAirport to call next() and then use the updated state object to render a table of information about the nearest airport.

Find the lines where we instantiate our middleware using app.use(). Add our middleware to the stack, after bodyParser and renderView, so it looks like the following:

app.use(bodyParser()) 
.use(renderView)
.use(nearestAirport);

Click on save, press Ctrl+C in your terminal to kill the server and then type this command:

    npm run server

This will restart it. Go to localhost:5555, and it should ask you for permission to geolocate:

This is actually from Firefox, but Chrome should look similar:

Click on the check button, and you should have a screen like the following:

Hey, that's actually kind of cool. Also, we've figured something out using D3 that didn't even involve drawing anything.

By now, you should be seeing D3 less as a bunch of magical tools that turn data into pretty visual things, and more just as a collection of functions that output mathematics in a particular way. While D3 is certainly the most interesting when it's drawing things in a web browser, it's such a powerful library that you can use it for tasks far removed from its original use case of rendering SVG in the DOM.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset