-
Notifications
You must be signed in to change notification settings - Fork 55
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
Conversation
Turf/Turf.swift
Outdated
var radius:Float = 6378137 | ||
var coordinates: [CLLocationCoordinate2D] | ||
|
||
var polygonArea: Float { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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] |
There was a problem hiding this comment.
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.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turf/Turf.swift
Outdated
static let equatorialRadius:Double = 6378137 | ||
} | ||
|
||
let metersPerRadian: CLLocationDistance = 6_373_000.0 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
TurfTests/TurfTests.swift
Outdated
|
||
let polygon = Polygon(polygonCoordinates: coordinates) | ||
let polygon = Polygon(outerRing: outerRing, innerRings: []) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
TurfTests/TurfTests.swift
Outdated
@@ -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]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
😉
There was a problem hiding this comment.
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. 😄
91907e3 adds an entry for |
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