NB

Porting a Turf.js function to Swift

I really love Turf.js. Last year, I started diving into iOS development. I wanted to use methods from Turf.js in iOS applications, so I was excited to see my colleague start the turf-swift project.

After expressing interest in bringing new methods over to turf-swift, I began weekly pair programming sessions to learn how to port over Turf’s polygon area method. Here’s how we did it.

How polygon areas are calculated

If you have a geometry background, its not too hard to calculate the area of a polygon on a flat plane. But because maps are hard, calculating the area of a polygon that lies on a sphere is way harder. So how is it done? Well luckily, very smart people at NASA already did this for us.

When Turf.js was created, this algorithm and many others were translated from pure math into JavaScript. Our part of this project would be to translate the JavaScript implementation of this algorithm to Swift code.

One of the challenging features of working with polygons is that they can have holes in them. For example, if you have a polygon in the shape of a donut, you’ll need to subtract the area of the donut hole from the area of the donut to calculate the correct area of the donut shape. This would also have to be taken into consideration while building out the Swift version of this method.

The translation

Polygon features are composed of one outer ring with one or more inner rings. For our Swift implementation, we began defining a new Polygon struct that could be created with an outer ring and an array of one or more inner rings.

public struct Polygon {
    var outerRing: Ring
    var innerRings: [Ring]
}

Another struct called Ring can be created using an array of CLLocationCoordinates. Rings must have three or more coordinates. Within the Ring struct, we port over the JavaScript code necessary from Turf.js turf-area package to calculate the ring area.

Once we can create a new Ring struct, we can calculate the area of that ring like so:

let ringCoordinates = [
            CLLocationCoordinate2DMake(40.17, -83.58),
            CLLocationCoordinate2DMake(30.67, -69.34),
            CLLocationCoordinate2DMake(24.52, -88.94),
            CLLocationCoordinate2DMake(40.17, -83.58)
]

let ring = Ring(coordinates: ringCoordinates)

print(ring.area)

In this case ring.area came out to 1419766110812.166 square meters. To make sure this was correct, I ran the same function with the above coordinates with Turf.js, which resulted in the same value.

Now, to account for the donuts. Back in the Polygon struct, we needed to calculate the entire area given the outer ring and any inner rings, if they existed. The Swift implementation of this JavaScript code looks like this:

public struct Polygon {
    var outerRing: Ring
    var innerRings: [Ring]

    public var area: Double {
        return abs(outerRing.area) - innerRings
            .map { abs($0.area) }
            .reduce(0, +)
    }
}

I think this looks a lot nicer than JavaScript!

In the Polygon’s area property, there are a couple of things happening:

Result

Below is the full turf-swift implementation of the polygon ring area calculation for the polygon illustrated above:

let outerRing = [
[
    CLLocationCoordinate2DMake(37.00255267215955, -109.05029296875),
    CLLocationCoordinate2DMake(37.020098201368114, -102.0849609375),
    CLLocationCoordinate2DMake(41.0130657870063, -102.041015625),
    CLLocationCoordinate2DMake(40.97989806962013, -109.072265625),
    CLLocationCoordinate2DMake(37.00255267215955, -109.05029296875)
]

let innerRing = [
    CLLocationCoordinate2DMake(40.6306300839918, -108.56689453125),
    CLLocationCoordinate2DMake(37.43997405227057, -108.61083984375),
    CLLocationCoordinate2DMake(37.405073750176925, -102.50244140624999),
    CLLocationCoordinate2DMake(40.66397287638688, -102.4365234375),
    CLLocationCoordinate2DMake(40.6306300839918, -108.56689453125)
]

let polygon = Polygon(outerRing: outerRing, innerRings: [innerRing])

print(polygon.area)

The result? 78588446934.433655 square meters. The same calculation with Turf.js also results in the same value. We added some tests to confirm this and a new turf-swift method was born.