Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add polygon area calculation #21

Merged
merged 15 commits into from
Jan 8, 2018
Merged
6 changes: 6 additions & 0 deletions Turf.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
052521D81FC47A1300DD266A /* polygon.geojson in Resources */ = {isa = PBXBuildFile; fileRef = 052521D71FC47A0300DD266A /* polygon.geojson */; };
052521D91FC47A1900DD266A /* polygon.geojson in Resources */ = {isa = PBXBuildFile; fileRef = 052521D71FC47A0300DD266A /* polygon.geojson */; };
353E9B101F3E093A007CFA23 /* Turf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 353E9B071F3E093A007CFA23 /* Turf.framework */; };
353E9B1E1F3E09D3007CFA23 /* CoreLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356D24531F17948C003BBB9D /* CoreLocation.swift */; };
353E9B1F1F3E09D8007CFA23 /* Turf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35650B0A1F150DDB00B5C158 /* Turf.swift */; };
Expand Down Expand Up @@ -41,6 +43,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
052521D71FC47A0300DD266A /* polygon.geojson */ = {isa = PBXFileReference; lastKnownFileType = text; path = polygon.geojson; sourceTree = "<group>"; };
353E9B071F3E093A007CFA23 /* Turf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Turf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
353E9B0F1F3E093A007CFA23 /* TurfMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TurfMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
35650AF01F150DC500B5C158 /* Turf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Turf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -135,6 +138,7 @@
isa = PBXGroup;
children = (
356D24581F179B72003BBB9D /* dc-line.geojson */,
052521D71FC47A0300DD266A /* polygon.geojson */,
);
path = Fixtures;
sourceTree = "<group>";
Expand Down Expand Up @@ -291,6 +295,7 @@
buildActionMask = 2147483647;
files = (
35CB7F6F1F798A51008A18C8 /* dc-line.geojson in Resources */,
052521D91FC47A1900DD266A /* polygon.geojson in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -306,6 +311,7 @@
buildActionMask = 2147483647;
files = (
356D24591F179B72003BBB9D /* dc-line.geojson in Resources */,
052521D81FC47A1300DD266A /* polygon.geojson in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
4 changes: 2 additions & 2 deletions Turf/CoreLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ extension CLLocationCoordinate2D: Equatable {

/// Returns a coordinate a certain Haversine distance away in the given direction.
public func coordinate(at distance: CLLocationDistance, facing direction: CLLocationDirection) -> CLLocationCoordinate2D {
let radianCoordinate = RadianCoordinate2D(self).coordinate(at: distance / metersPerRadian, facing: direction.toRadians())
let radianCoordinate = RadianCoordinate2D(self).coordinate(at: distance / Constants.metersPerRadian, facing: direction.toRadians())
return CLLocationCoordinate2D(radianCoordinate)
}

/**
Returns the Haversine distance between two coordinates measured in degrees.
*/
public func distance(to coordinate: CLLocationCoordinate2D) -> CLLocationDistance {
return RadianCoordinate2D(self).distance(to: RadianCoordinate2D(coordinate)) * metersPerRadian
return RadianCoordinate2D(self).distance(to: RadianCoordinate2D(coordinate)) * Constants.metersPerRadian
}
}

69 changes: 68 additions & 1 deletion Turf/Turf.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ public typealias LocationRadians = Double
public typealias RadianDistance = Double
public typealias RadianDirection = Double

let metersPerRadian = 6_373_000.0
struct Constants {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure a generic Constants struct adds much value to the file, since these constants are internal to the library.

static let metersPerRadian = 6_373_000.0
static let equatorialRadius:Double = 6378137
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify CLLocationDistance (which is always expressed in meters) as the type of both constants.

}

/**
A `RadianCoordinate2D` is a coordinate represented in radians as opposed to
Expand Down Expand Up @@ -285,3 +288,67 @@ public struct Polyline {
return closestCoordinate
}
}

public struct Polygon {
var polygonCoordinates: [[CLLocationCoordinate2D]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GeoJSON represents a polygon as a collection of rings (one outer ring and multiple inner rings). Similarly, the Mapbox Maps SDKs for iOS and macOS represent a polygon as an outer polygon containing multiple inner polygons. Either way, it’s nice to be able to work with a formal structure instead of a nested array. Consider replacing this property with:

var rings: [Ring]

or:

var outerRing: Ring
var innerRings: [Ring]

where Ring is either a struct or a type alias for [CLLocationCoordinate2D]. As a struct, it would be a good home for the ringArea(_:) method.


var polygonArea: Double {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property already lives inside a struct named Polygon, so the word “polygon” here is redundant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment with a permalink to the specific commit of the Turf package you’re porting this code from, in case we need to track down a discrepancy in the future.

var area:Double = 0

if (!polygonCoordinates.isEmpty && polygonCoordinates.count > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: drop the parentheses.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more idiomatic way to express this in Swift is if let outerRing = polygonCoordinates.first, which allows you to avoid the [0] on the next line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use a non-optional outerRing properties as in #21 (comment), then we can assume there’s an outer ring and won’t need the if statement at all.


area += abs(ringArea(polygonCoordinates[0]))

for coordinate in polygonCoordinates.suffix(from: 1) {
area -= abs(ringArea(coordinate))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that each item in polygonCoordinates represents a ring’s coordinates, not an individual CLLocationCoordinate2D.

A more functional approach would be:

let area = abs(ringArea(polygonCoordinates.first ?? []))
    - polygonCoordinates.suffix(from: 1)
        .map { abs(ringArea($0)) } // convert the inner rings to their areas
        .reduce(0, +) // sum the inner areas

Copy link
Contributor

@1ec5 1ec5 Dec 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use separate outerRing and innerRings properties as in #21 (comment), then this simplifies further to:

let area = abs(outerRing.area)
    - innerRings
        .map { abs($0.area) } // convert the inner rings to their areas
        .reduce(0, +) // sum the inner areas

}
}
return area
}

/**
* Calculate the approximate area of the polygon were it projected onto the earth.
* Note that this area will be positive if ring is oriented clockwise, otherwise it will be negative.
*
* Reference:
* Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion
* Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409
*
*/
private func ringArea(_ coordinates: [CLLocationCoordinate2D]) -> Double {
var p1: CLLocationCoordinate2D
var p2: CLLocationCoordinate2D
var p3: CLLocationCoordinate2D
var lowerIndex: Int
var middleIndex: Int
var upperIndex: Int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These six variables are always written inside each iteration of the for loop and read immediately thereafter, and never read outside the loop. Move these variables to inside the loop. The original turf-area implementation declares these variables upfront to avoid hoisting, but it isn’t even necessary to do so in modern, strict-mode JavaScript.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a tuple consisting of three coordinates to ensure that all three are set simultaneously:

let controlPoints: (CLLocationCoordinate2D, CLLocationCoordinate2D, CLLocationCoordinate2D)
if index == coordinatesCount - 2 {
    controlPoints = (coordinates[coordinatesCount - 2], coordinates[coordinatesCount - 1], coordinates[0])
}
// …

var area: Double = 0
let coordinatesCount: Int = coordinates.count

if (coordinatesCount > 2) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: drop the parentheses (×3).

for index in 0...coordinatesCount - 1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0..<coordinatesCount would be a little clearer.

if (index == coordinatesCount - 2) {
lowerIndex = coordinatesCount - 2
middleIndex = coordinatesCount - 1
upperIndex = 0
} else if(index == coordinatesCount - 1) {
lowerIndex = coordinatesCount - 1
middleIndex = 0
upperIndex = 1
} else {
lowerIndex = index
middleIndex = index + 1
upperIndex = index + 2
}

p1 = coordinates[lowerIndex]
p2 = coordinates[middleIndex]
p3 = coordinates[upperIndex]
area += (p3.longitude.toRadians() - p1.longitude.toRadians()) * sin(p2.latitude.toRadians())
}

area = area * Constants.equatorialRadius * Constants.equatorialRadius / 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: use the *= operator.

}
return area
}
}
19 changes: 19 additions & 0 deletions TurfTests/Fixtures/polygon.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[125, -15],
[113, -22],
[117, -37],
[130, -33],
[148, -39],
[154, -27],
[144, -15],
[125, -15]
]
]
}
}
13 changes: 13 additions & 0 deletions TurfTests/TurfTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,17 @@ class TurfTests: XCTestCase {
let b = radian.toDegrees()
XCTAssertEqual(b, 229, accuracy: 1)
}

func testPolygonArea() {
let json = Fixture.JSONFromFileNamed(name: "polygon")
let geometry = json["geometry"] as! [String: Any]
let geoJSONCoordinates = geometry["coordinates"] as! [[[Double]]]
let coordinates = geoJSONCoordinates.map {
$0.map { CLLocationCoordinate2D(latitude: $0[1], longitude: $0[0]) }
}

let polygon = Polygon(polygonCoordinates: coordinates)

XCTAssertEqual(polygon.polygonArea, 7766240997209, accuracy: 0.1)
}
}