The Wonder of Turf.js
Originally posted at Digital Mapmaking, Victor Temprano's blog.
I absolutely love TurfJS. It’s a fantastic JS library that contains a lot of utility functions related to geography. Things like measuring length, getting points on features, comparing distances, and more — way more.
It’s served our company very well in dozens of different applications. In the years before TurfJS was widely available (or before I knew about it), I used to do a lot of geographic calculations by writing out the functions by hand, or finding answers in complicated math forums.
Let’s look at some of my favourite, most-used functions in Turf, and do a quick example of how we might use it.
turf.center()
This quickly finds the geographical center of any line or polygon feature. This is awesome! However, centers are actually pretty tricky when it comes to irregular polygons. The TurfJS method finds the absolute center — for something like finding a center that’s always inside the polygon (for instance, imagine a crescent-moon shaped polygon — the center is outside the polygon), you need to use a different formula.
turf.bbox()
Quickly and easily find a bounding box! A great method for a single feature or many features at once.
turf.distance()
Easily find distance between two points. This takes into account the curvature of the earth, which is crucial in long-distance geographic calculations.
turf.intersect()
Find any place that two polygons overlap (there are other similar functions that can do this, too).
turf.buffer()
This creates a certain amount of padding around a feature — for instance, expanding a polygon by a few meters or kilometers.
One Geographic Problem
There’s a ton more we could discuss, but let’s go to an example of how we might use Turf to tackle a given problem from a client.
In this example, we are using the Mapbox Directions API and building a navigation interface for users on their mobile device. A key part of this is showing written directions to users. In order to do that, we need to know which step of a particular set of directions the user is on, and how far along that step they are, so that we can show the next instruction at a good time.
We’ll try to leave out the heavy details of how we’re actually dealing with the Directions API itself, but you can basically imagine we have a couple inputs:
- The user location (longitude, latitude)
- The destination location (longitude, latitude)
And when we fire off to the Directions API, we get back a lot, but let’s keep it simple to what will be helpful here:
- A LineString representing the user route along the road from origin to designation
- A series of “steps” which contain instructions and the total distance of each individual section of the journey
Now, assuming this is pretty much all we have — how are we going to be able to figure out which “step” of the journey the user is on, so we can show the appropriate instructions? There’s lots of ways to do this, but here’s how we’ll approach it: we want to get the user’s distance from the origin any time their location updates, and use that to know which step they are on, and then show the appropriate instructions. So how do we figure that out using Turf?
Figuring it out with TurfJS
Now, let’s break down the steps of what we’ll do here:
- Use the user location as it updates (geolocation) to know where the user is
- Find a way to find a corresponding point on the big directions line
- Find the distance from the start to the corresponding point
- Measure that against the distance to that step
Turf is so helpful here. Here’s how we break down these steps.
We use turf.nearestPointOnLine() to find the nearest point that exists exactly on the directions line when compared to the user location. We have to do this because geolocation isn’t perfectly accurate, and because the line might not have the user’s exact location on it.
We then check the distance between these points with turf.distance(). This is a bit of an extra step — because, if the user’s more than 25 meters off the line, we’re going to do a total recalculation of the directions (in this case, probably the user took a different turn or something). If not, we continue along to find the right instructions.
We use turf.lineSlice() to create a new LineString consisting of the existing large directions linestring, but cut off at the “nearest point on line” that we found before. This gives us a line that pretty accurately represents the user’s progress so far from the start.
Finally, we use turf.length() to find the total length of our sliced line. Ultimately, we can then compare this against the distances given from the Directions API.
This is really cool! Through a combination of different TurfJS methods, we can end up doing some pretty custom analysis that otherwise could be quite complex. There are obviously other ways to approach this problem, but we find this one to be pretty computationally straightforward and avoid any looping or repeated measurements.
Hopefully this helps you understand and appreciate Turf too!
Code Example
Check out the complete code example here: https://jsbin.com/powupucuma/edit?html,js,console
const sampleFeatureCollection = { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [ [ -8.404541015625, 43.3616313239934 ], [ -8.407974243164062, 43.352644787504495 ], [ -8.408660888671875, 43.345154990451135 ], [ -8.405914306640623, 43.337164854911094 ], [ -8.397674560546875, 43.33167102675838 ], [ -8.388748168945312, 43.33067209551502 ], [ -8.375701904296873, 43.32967314784175 ], [ -8.36334228515625, 43.32667620624288 ], [ -8.354415893554688, 43.32118142926661 ], [ -8.343429565429688, 43.321680974983344 ], [ -8.320770263671875, 43.32517767999296 ], [ -8.308410644531248, 43.31968276747214 ], [ -8.3001708984375, 43.320182325511006 ], [ -8.30360412597656, 43.33266994157184 ], [ -8.298797607421875, 43.34165943554058 ], [ -8.308410644531248, 43.3496489795891 ], [ -8.309097290039062, 43.35963443090036 ], [ -8.306350708007812, 43.369119087738554 ], [ -8.307037353515625, 43.37860226166394 ], [ -8.299484252929688, 43.38558891458754 ], [ -8.298797607421875, 43.38958092619001 ] ] } } ] };
const sampleUserLocations = { "type" : "FeatureCollection", "features" : [{ "type": "Feature", "properties": {}, "geometry": { "type": "Point", "coordinates": [ -8.398189544677734, 43.33204562173882 ] } }, { "type": "Feature", "properties": {}, "geometry": { "type": "Point", "coordinates": [ -8.360080718994139, 43.32480304265438 ] } }, { "type": "Feature", "properties": {}, "geometry": { "type": "Point", "coordinates": [ -8.316478729248047, 43.32055699134471 ] } }]};
// Looping over user locations (could be done in a geolocation call instead)
const sampleLine = sampleFeatureCollection.features[0]
sampleUserLocations.features.forEach(userLocation => {
const nearestPoint = turf.nearestPointOnLine(sampleLine, userLocation);
const distanceToLine = turf.distance(nearestPoint, userLocation);
// normally here we'd check if the distance is close enough, but here we'll just allow it
const slicedLine = turf.lineSlice(sampleLine.geometry.coordinates[0], nearestPoint, sampleLine);
const length = turf.length(slicedLine);
console.log(length); // Shows us the length in KM for each point we checked
})
very well explained, thanks for sharing.
https://theminimilitia.net/
Very interesting and well explained, thank you!
https://www.minimilitia.mobi/ https://www.applock.ooo/
Good article–do you plan on sharing any sample code in regards to the example? Thank you!
Added some sample code! Hope that helps! Thanks Charles.