Creating viewsheds with Mapbox GL and Turf.js

I was super impressed when I saw cartographer John Nelson's tweet about an ESRI project called the Campus Blue Lights App. The ArcGIS Online application enables a user to add, remove and position Blue Lights (emergency telephones) in the context of a campus and evaluate the visibility of each beacon. The detailed write-up inspired me to try and recreate the same functionality using Mapbox GL and TurfJS. I have attempted to explain the process below, but the entire code is also available here.

The first step in creating the viewsheds was to figure out a way to create an array of points that completed a full ring around a centre point. The first attempt looked something like this:

for (i = 0; i < 360; i++) {
   var x = centerX + radius * Math.cos(2 * Math.PI * i / 360);
   var y = centerY + radius * Math.sin(2 * Math.PI * i / 360);
}

This successfully created 360 points, but as you moved away from the equator the resulting buffer became more skewed due to the Web Mercator projection. To account for this, I needed to create geodesic lines. Luckily, I found the perfect solution on Turf's Github page by Mathieu Albrespy. His code accounts for the curvature of the Earth.

I then used the following Turf functions to convert the 360 points into LinesStrings and detect whether any of them intersected any of the building polygons.

var matchLine = turf.lineString([[centerX, centerY], [x, y]]);

for (n = 0; n < buildings.features.length; n++) {

        var polyInfo = [buildings.features[n].geometry.coordinates[0]];
        var matchPoly = turf.polygon(polyInfo);

        intersection = turf.lineIntersect(matchLine, matchPoly);
}

Turf's 'intersection' function returns a list of all intersecting points, which in this case, provided all of the points where one of the LineStrings crossed a building polygon. If an intersection was detected I then pushed the returned coordinate data to an array.

intersectionArray.push(intersection.features);

If there were multiple intersections I then used Turf's 'nearest' function to determine which intersection point was closest to the centre (targetPoint).

var targetPoint = turf.point([centerX, centerY]);
var points = turf.featureCollection(pointsArray);
nearest = turf.nearestPoint(targetPoint, points);

The nearest coordinate information was then pushed to the geojson containing the data for the viewshed polygon. If there was no intersection, the original x and y coordinates were pushed.

if (conflictCheck == true) {
      var targetPoint = turf.point([centerX, centerY]);
      var points = turf.featureCollection(pointsArray);
      nearest = turf.nearestPoint(targetPoint, points);
      geojsonPolyArray.push([intersectCon[0], intersectCon[1]]);
} else {
      geojsonPolyArray.push([x, y]);
}

The resulting geojsonPolyArray was then placed inside of the data source of the viewshed layer. Once the geojsonPolyArray was built I updated the source of the viewshed layer.

function updatePoly() {
    geojsonPolyArray = [];
    geojsonPolyCoords = [];
    geojsonPoly = {
    "type": "Feature",
    "geometry": {
      "type": "Polygon",
      "coordinates": geojsonPolyCoords
     }
    };

    evalPoly(); //creates linestrings, detects intersections, and creates geojsonPolyArray

    map.getSource('polygon').setData(geojsonPoly);
}

Below is the finished product. I added a bunch of cosmetic things, but I hope the code can help someone else create their own viewsheds. Eventually I'm hoping to integrate a z factor into this using Mapbox's fill-extrusion capabilities. If anyone has questions or wants to help develop a better version of this hit me up on twitter.

Tour the pyramids
Bare bones

Drag and drop the marker