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

Add polygon area calculation #21

merged 15 commits into from
Jan 8, 2018

Conversation

captainbarbosa
Copy link
Contributor

Closes #20 by adding the ability to calculate the area of a polygon.

Turf.js reference: https://github.com/Turfjs/turf/blob/master/packages/turf-area/index.js

@captainbarbosa captainbarbosa self-assigned this Nov 10, 2017
@captainbarbosa captainbarbosa added the improvement Improvement for an existing feature. label Nov 10, 2017
Turf/Turf.swift Outdated
var radius:Float = 6378137
var coordinates: [CLLocationCoordinate2D]

var polygonArea: Float {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's change all Float to Double so we don't have to cast when converting to radians.

Turf/Turf.swift Outdated
p3 = coordinates[upperIndex]
area += (p3.longitude.toRadians() - p1.longitude.toRadians()) * sin(p2.latitude.toRadians())
}
var RADIUS = 6378137
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: radius is already defined in the Polygon struct.

Turf/Turf.swift Outdated
@@ -285,3 +285,65 @@ public struct Polyline {
return closestCoordinate
}
}

public struct Polygon {
var radius:Float = 6378137
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's rename this to equatorialRadius for clarity and move it to the top where metersPerRadian is.

@frederoni
Copy link
Contributor

We should probably migrate this project to an Xcode 9 stack on Bitrise.

Turf/Turf.swift Outdated
@@ -285,3 +286,61 @@ public struct Polyline {
return closestCoordinate
}
}

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

@frederoni frederoni Nov 22, 2017

Choose a reason for hiding this comment

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

Capturing from chat: The coordinates property on a GeoJSON Polygon should be represented as a [[CLLocationCoordate2D]]

Even better now encapsulated in a Ring.

@captainbarbosa captainbarbosa requested a review from 1ec5 December 8, 2017 14:54
Turf/Turf.swift Outdated
@@ -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.

Turf/Turf.swift Outdated
let metersPerRadian = 6_373_000.0
struct Constants {
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.

Turf/Turf.swift Outdated
var polygonArea: Double {
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.

Turf/Turf.swift Outdated
var polygonArea: Double {
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.

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.

Turf/Turf.swift Outdated
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

Turf/Turf.swift Outdated
let coordinatesCount: Int = coordinates.count

if (coordinatesCount > 2) {
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.

Turf/Turf.swift Outdated
public struct Polygon {
var polygonCoordinates: [[CLLocationCoordinate2D]]

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.

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.

Turf/Turf.swift Outdated
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.

Turf/Turf.swift Outdated
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.

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])
}
// …

Turf/Turf.swift Outdated
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.

Turf/Turf.swift Outdated
var lowerIndex: Int
var middleIndex: Int
var upperIndex: Int
internal func area() -> Double {
Copy link
Contributor

Choose a reason for hiding this comment

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

Turn this method into a computed property to reduce the friction of using it.

Turf/Turf.swift Outdated
}

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

Choose a reason for hiding this comment

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

Nit: similar to in metersPerRadian, use underscores to group the digits.

Turf/Turf.swift Outdated
}

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

Choose a reason for hiding this comment

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

equatorialRadius is currently set to the equatorial radius according to WGS84 and the IUGG. This value is used in OSRM and mbgl for Web Mercator calculations.

Since different radii are used in different formulas and the errors can sometimes build up, let’s document which is which.

Turf/Turf.swift Outdated
static let equatorialRadius:Double = 6378137
}

let metersPerRadian: CLLocationDistance = 6_373_000.0
Copy link
Contributor

Choose a reason for hiding this comment

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

Apparently this value of 6 373 000 m came from an old version of turf-distance that I originally ported to an internal Swift application, before it got copied into another internal Swift application, then copied into the navigation SDK, then moved here. It’s close to the value of 6 372 797.560 856 that Osmium describes as “Earth’s quadratic mean radius for WGS84”.

These days, Turf.js uses a spherical approximation of 6 671 008.8 m for its radian-to-meter conversion and Haversine formula. The Haversine formula was always meant to be used with a spherical meters-per-radian value. We should probably change this value to match Turf.js. 🙀

/ref Turfjs/turf#978 Turfjs/turf#1012 Turfjs/turf#1176
/cc @frederoni @bsudekum

Copy link
Contributor

Choose a reason for hiding this comment

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

Since the polygon area calculation doesn’t depend on metersPerRadian, we can treat this change as tail work: #26.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we're going to change metersPerRadian to 6 671 008.8 m, should this happen in a different PR since it would globally impact this library? Or are you ok with me making the change here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, let’s do it in a separate PR for #26, so that any fallout is easier to track down.

In the meantime, can you add some comments explaining why these two constants differ?


let polygon = Polygon(polygonCoordinates: coordinates)
let polygon = Polygon(outerRing: outerRing, innerRings: [])
Copy link
Contributor

Choose a reason for hiding this comment

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

What about the inner rings in allRings?

Turf/Turf.swift Outdated
static let equatorialRadius:Double = 6378137
}

let metersPerRadian: CLLocationDistance = 6_373_000.0
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, let’s do it in a separate PR for #26, so that any fallout is easier to track down.

In the meantime, can you add some comments explaining why these two constants differ?

@@ -295,9 +295,10 @@ class TurfTests: XCTestCase {
$0.map { CLLocationCoordinate2D(latitude: $0[1], longitude: $0[0]) }
}
let outerRing = Ring(coordinates: allRings.first!)
let innerRing = Ring(coordinates: allRings[1])
Copy link
Contributor

Choose a reason for hiding this comment

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

innerRings can be expressed as allRings.prefix(from: 1) – handy for dealing with a two-hole polygon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks like prefix(from:) isn't available in Swift 4.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooooo I think you meant suffix(from: 1) 😉

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that gets me every time. 😄

@captainbarbosa captainbarbosa merged commit 15fe874 into master Jan 8, 2018
@captainbarbosa captainbarbosa deleted the polygon-area branch January 8, 2018 17:59
@1ec5
Copy link
Contributor

1ec5 commented Jan 12, 2018

91907e3 adds an entry for Polygon.area to the readme.

@captainbarbosa captainbarbosa changed the title [WIP] Add polygon area calculation Add polygon area calculation Jan 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
improvement Improvement for an existing feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants