About the author
Fulcrum is a field inspection management platform built to streamline safety and quality processes, field operations, and asset inspections, especially for mobile teams.
On October 8th, I had the privilege of presenting at the annual JS.Geo conference. The event was held in Philadelphia this year and Fulcrum was proud to join the impressive list of sponsors. This inexpensive, one day conference on all things Geo and JavaScript provides a refreshing alternative to the traditional conference format. You can review the #jsgeo hashtag to follow attendee reactions on Twitter and check out the speaker slides here.
I gave a brief talk and short demo on how we’ve been experimenting with adding incredibly powerful geospatial functionality to Fulcrum by integrating the excellent open source Turf JavaScript library into calculation fields. At one point I mentioned that this technique was “kind of a hack”, and showed how we simply stuff the Turf library into our form schema, to which this group of JavaScript hackers broke out into spontaneous applause. Poking around and pushing the limits of technology is what we’re all about and if you can make your audience laugh, you’ve done your job as a presenter (despite struggling through some technical difficulties).
Turf provides a simple, modern set of modular geospatial components, written in JavaScript. It allows you to add GIS functionality to browser and Node.js apps, but can also be incorporated into your Fulcrum apps. Turf supports a wide range of spatial functions, including:
Having access to advanced geospatial functions from within Fulcrum enables users to wire up some truly amazing workflows! Let’s imagine you are a Code Enforcement Officer and you are using Fulcrum to document and manage code violations in your town. It would be super handy to know what the zoning restrictions were where the violation occurred.
If you had access to your town’s GIS zoning layer, you could load that into the background of your Fulcrum Code Enforcement app and use Turf to spatially calculate which zone your record was in. Similarly, you could determine which school district you were in, along with calculating the closest school and its distance.
While these may be fairly basic GIS tasks, which could certainly be accomplished via post-processing with standard GIS software, having this functionality in the field, in a disconnected environment cannot be understated. Fulcrum allows app designers to balance data integrity with field efficiency by providing advanced rule-based logic for determining field visibility and requirement based on the values of other fields. In the case of the Code Enforcement app, we’ve designed it so that the “Business Name” field is only visible if the zoning is commercial or industrial. If the violation occurs on a residential property, there’s no need to present the Enforcement Officer with fields that are not pertinent to that particular entry. Being able to dynamically determine the zoning while onsite, allows this workflow to be seamless.
The first thing we need to do is download the pertinent JavaScript libraries. For this exercise we will be using Turf and TopoJSON. Getting Turf to work properly in Fulcrum also requires using a little helper library.
We will copy and paste the code for these libraries into their own calculation fields in our app. Designating this.turf = module.exports; allows us to reference Turf as a variable in our other calculation fields using var turf = this.turf;. I group these fields in a hidden section called “JS Libraries” to keep my app nice and tidy.
Note: I’ve put together custom builds of both fulcrum-turf.js and fulcrum-topojson.js to help users get up and running quickly.
Next, we need to include our reference layers. Turf expects GeoJSON, so you’ll have to convert any shapefiles or other GIS formats to GeoJSON. You can convert your GIS data to GeoJSON with ogr2ogr, QGIS, or even geojson.io. If you are working with a complex polygon dataset, you’ll probably want to convert it to TopoJSON to encode the topology and dramatically reduce the file size.
For our demonstration we are working with a county-wide TopoJSON zoning polygon dataset along with standard GeoJSON school district polygons and school location points. Since these are simply JSON text files, we can open them in a text editor, copy the code, and paste it directly into a calculation field expression. This gives us a variable such as this.zoning to reference in our subsequent spatial calculations. Again, we can group our reference files in a hidden section called “GeoJSON Data”.
Now comes the fun part, actually defining our spatial calculations using Turf. In order to grab the zoning code and description from the attributes in our polygon layer, we want to perform a spatial join using the turf-tag module. This function takes a set of points (FeatureCollection) and a set of polygons (FeatureCollection) and performs a spatial join.
Our reference point is the Latitude and Longitude of our Fulcrum record, but since Turf expects a FeatureCollection, that’s what we need to provide. The expression to calculate the “Zoning Code” field looks like this:
var turf = this.turf;
var zoning = this.zoning;var points = {
“type”: “FeatureCollection”,
“features”: [turf.point([LONGITUDE(), LATITUDE()])]
};if (LONGITUDE() && LATITUDE()) {
var result = turf.tag(points, zoning, “code”, “code”);
SETRESULT(result.features[0].properties.code);
} else {
SETRESULT(“No location set”);
}
We use the same concept for calculating the “Zoning Description” and “School District” fields, referencing the zoning and school polygon datasets respectively. In order to determine the closest school and its distance from our point, we need to write an expression using both turf-nearest and turf-distance. The expression for our “Closest School” field looks like this:
var turf = this.turf;
var schools = this.schools;var point = turf.point([LONGITUDE(), LATITUDE()]);
if (LONGITUDE() && LATITUDE()) {
var result = turf.nearest(point, schools);
var name = result.properties.NAME;
var distance = turf.distance(point, result, “miles”);
SETRESULT(CONCATENATE(name, ” (“, distance.toFixed(2), ” mi)”));
} else {
SETRESULT(“No location set”);
}
4. Test It Out
Before hitting the field, it’s always a good idea to test out your app and make sure your calculations are performing as expected. Be sure to test out both the web interface, as well as the mobile client you intend to use for your data collection fieldwork.
There is a built-in JavaScript file runtime that is executed once to create the basic environment for Fulcrum expressions to be evaluated on each platform (web, iOS, Android). On Android, expressions are evaluated using an embedded version of the V8 JavaScript Engine and on iOS Fulcrum uses JavaScriptCore. These are the same JavaScript engines Chrome and Safari use, so most pure JavaScript libraries will work, but might require some tweaks depending on how the library is implemented.
We’ve open sourced the Fulcrum expressions engine so technical users can poke around and even submit their own expressions if they are so inclined!
Stuffing the entire Turf library in a calculation field is certainly pushing the boundaries in Fulcrum. This whole concept of referencing external JavaScript libraries was first concocted by Zac when he began experimenting with using proj4js to support custom projections in Fulcrum.
These late night “hacks” often plant the seeds for future feature development and we will continue experimenting with innovative solutions to the challenges of field data collection.