Skip to content

Commit

Permalink
Merge pull request #148 from nighthawk/feature/polygon-center
Browse files Browse the repository at this point in the history
Add Polygon centroid and center of mass
  • Loading branch information
1ec5 authored Jul 27, 2021
2 parents a5d6a07 + 682201c commit 8815a2b
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 3 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ Turf.js | Turf-swift
[turf-bearing](https://turfjs.org/docs/#bearing) | `CLLocationCoordinate2D.direction(to:)`<br>`LocationCoordinate2D.direction(to:)` on Linux<br>`RadianCoordinate2D.direction(to:)`
[turf-bezier-spline](https://github.com/Turfjs/turf/tree/master/packages/turf-bezier-spline/) | `LineString.bezier(resolution:sharpness:)`
[turf-boolean-point-in-polygon](https://github.com/Turfjs/turf/tree/master/packages/turf-boolean-point-in-polygon) | `Polygon.contains(_:ignoreBoundary:)`
[turf-center](http://turfjs.org/docs/#center) | `Polygon.center`
[turf-center-of-mass](http://turfjs.org/docs/#centerOfMass) | `Polygon.centerOfMass`
[turf-centroid](http://turfjs.org/docs/#centroid) | `Polygon.centroid`
[turf-circle](https://turfjs.org/docs/#circle) | `Polygon(center:radius:vertices:)` |
[turf-destination](https://github.com/Turfjs/turf/tree/master/packages/turf-destination/) | `CLLocationCoordinate2D.coordinate(at:facing:)`<br>`LocationCoordinate2D.coordinate(at:facing:)` on Linux<br>`RadianCoordinate2D.coordinate(at:facing:)`
[turf-distance](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/) | `CLLocationCoordinate2D.distance(to:)`<br>`LocationCoordinate2D.distance(to:)` on Linux<br>`RadianCoordinate2D.distance(to:)`
Expand Down
16 changes: 14 additions & 2 deletions Sources/Turf/CoreLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ public struct LocationCoordinate2D {
/**
The latitude in degrees.
*/
public let latitude: LocationDegrees
public var latitude: LocationDegrees

/**
The longitude in degrees.
*/
public let longitude: LocationDegrees
public var longitude: LocationDegrees

/**
Creates a degree-based geographic coordinate.
Expand All @@ -71,6 +71,18 @@ public struct LocationCoordinate2D {
}
#endif

extension LocationCoordinate2D {
/**
Returns a normalized coordinate, wrapped to -180 and 180 degrees latitude
*/
var normalized: LocationCoordinate2D {
return .init(
latitude: latitude,
longitude: longitude.wrap(min: -180, max: 180)
)
}
}

extension LocationDirection {
/**
Returns a normalized number given min and max bounds.
Expand Down
68 changes: 67 additions & 1 deletion Sources/Turf/Geometries/Polygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,70 @@ extension Polygon {
ring[2].longitude == ring[0].longitude
)
}
}

/// Calculates the absolute centre (of the bounding box).
public var center: LocationCoordinate2D? {
// This implementation is a port of: https://github.com/Turfjs/turf/blob/master/packages/turf-center/index.ts
return BoundingBox(from: outerRing.coordinates)
.map { .init(
latitude: ($0.southWest.latitude + $0.northEast.latitude) / 2,
longitude: ($0.southWest.longitude + $0.northEast.longitude) / 2
) }
}

/// Calculates the centroid using the mean of all vertices.
/// This lessens the effect of small islands and artifacts when calculating the centroid of a set of polygons.
public var centroid: LocationCoordinate2D? {
// This implementation is a port of: https://github.com/Turfjs/turf/blob/master/packages/turf-centroid/index.ts

let coordinates = outerRing.coordinates.dropLast()
guard coordinates.count > 0 else { return nil }

let summed = coordinates
.reduce(into: LocationCoordinate2D(latitude: 0, longitude: 0)) { acc, next in
acc.latitude += next.latitude
acc.longitude += next.longitude
}
return LocationCoordinate2D(
latitude: summed.latitude / Double(coordinates.count),
longitude: summed.longitude / Double(coordinates.count)
).normalized
}

/// Calculates the [center of mass](https://en.wikipedia.org/wiki/Center_of_mass) using this formula: [Centroid of Polygon](https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon).
public var centerOfMass: LocationCoordinate2D? {
// This implementation is a port of: https://github.com/Turfjs/turf/blob/master/packages/turf-center-of-mass/index.ts

// First, we neutralize the feature (set it around coordinates [0,0]) to prevent rounding errors
// We take any point to translate all the points around 0
guard let center = centroid else { return nil }
let coordinates = outerRing.coordinates
let neutralized = coordinates.map {
LocationCoordinate2D(latitude: $0.latitude - center.latitude, longitude: $0.longitude - center.longitude)
}

var signedArea: Double = 0
var sum = LocationCoordinate2D(latitude: 0, longitude: 0)
let zipped = zip(neutralized.prefix(upTo: neutralized.count - 1), neutralized.suffix(from: 1))
for (pi, pj) in zipped {
let (xi, yi) = (pi.longitude, pi.latitude)
let (xj, yj) = (pj.longitude, pj.latitude)

// common factor to compute the signed area and the final coordinates
let a = xi * yj - xj * yi
signedArea += a
sum.longitude += (xi + xj) * a
sum.latitude += (yi + yj) * a
}
guard signedArea != 0 else { return center }

// compute signed area, and factorise 1/6A
let area = signedArea / 2
let areaFactor = 1 / (6 * area)

// final coordinates, adding back values that have been neutralized
return LocationCoordinate2D(
latitude: center.latitude + areaFactor * sum.latitude,
longitude: center.longitude + areaFactor * sum.longitude
).normalized
}}
147 changes: 147 additions & 0 deletions Tests/TurfTests/PolygonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,153 @@ class PolygonTests: XCTestCase {

XCTAssertEqual(expectedDiameter, diameter, accuracy: 0.25)
}

func testPolygonCentre() {
// Adopted from https://github.com/Turfjs/turf/blob/3b20c568e5638f680cde39c26b56fbcf034133f2/packages/turf-center/test.js
let coordinate = LocationCoordinate2D(latitude: 45.7536760235992, longitude: 4.841880798339844)
let polygon = Polygon([
[
LocationCoordinate2D(latitude: 45.79398056386735, longitude: 4.8250579833984375),
LocationCoordinate2D(latitude: 45.79254427435898, longitude: 4.882392883300781),
LocationCoordinate2D(latitude: 45.76081677972451, longitude: 4.910373687744141),
LocationCoordinate2D(latitude: 45.7271539426975, longitude: 4.894924163818359),
LocationCoordinate2D(latitude: 45.71337148333104, longitude: 4.824199676513671),
LocationCoordinate2D(latitude: 45.74021417890731, longitude: 4.773387908935547),
LocationCoordinate2D(latitude: 45.778418789239055, longitude: 4.778022766113281),
LocationCoordinate2D(latitude: 45.79398056386735, longitude: 4.8250579833984375),
],
])
let center = polygon.center!
XCTAssertLessThan(center.distance(to: coordinate), 1)
}

func testPolygonImbalancedCentre() {
// Adopted from https://github.com/Turfjs/turf/blob/3b20c568e5638f680cde39c26b56fbcf034133f2/packages/turf-center/test.js
let coordinate = LocationCoordinate2D(latitude: 45.778762648296855, longitude: 4.851944446563721)
let polygon = Polygon([
[
LocationCoordinate2D(latitude: 45.77258200374433, longitude: 4.854240417480469),
LocationCoordinate2D(latitude: 45.777431068484894, longitude: 4.8445844650268555),
LocationCoordinate2D(latitude: 45.778658234059755, longitude: 4.845442771911621),
LocationCoordinate2D(latitude: 45.779376562352425, longitude: 4.845914840698242),
LocationCoordinate2D(latitude: 45.78021460033108, longitude: 4.846644401550292),
LocationCoordinate2D(latitude: 45.78078326178593, longitude: 4.847245216369629),
LocationCoordinate2D(latitude: 45.78138184652523, longitude: 4.848060607910156),
LocationCoordinate2D(latitude: 45.78186070968964, longitude: 4.8487043380737305),
LocationCoordinate2D(latitude: 45.78248921135124, longitude: 4.849562644958495),
LocationCoordinate2D(latitude: 45.78302792142197, longitude: 4.850893020629883),
LocationCoordinate2D(latitude: 45.78374619341895, longitude: 4.852008819580077),
LocationCoordinate2D(latitude: 45.784075398324866, longitude: 4.852995872497559),
LocationCoordinate2D(latitude: 45.78443452873236, longitude: 4.853854179382324),
LocationCoordinate2D(latitude: 45.78470387501975, longitude: 4.8549699783325195),
LocationCoordinate2D(latitude: 45.784793656826345, longitude: 4.85569953918457),
LocationCoordinate2D(latitude: 45.784853511283764, longitude: 4.857330322265624),
LocationCoordinate2D(latitude: 45.78494329284938, longitude: 4.858231544494629),
LocationCoordinate2D(latitude: 45.784883438488365, longitude: 4.859304428100585),
LocationCoordinate2D(latitude: 45.77294120818474, longitude: 4.858360290527344),
LocationCoordinate2D(latitude: 45.77258200374433, longitude: 4.854240417480469)
],
])
let center = polygon.center!
XCTAssertLessThan(center.distance(to: coordinate), 1)
}

func testPolygonCentroid() {
// Adopted from https://github.com/Turfjs/turf/blob/3b20c568e5638f680cde39c26b56fbcf034133f2/packages/turf-centroid/test.js
let coordinate = LocationCoordinate2D(latitude: 45.75807143030368, longitude: 4.841194152832031)
let polygon = Polygon([
[
LocationCoordinate2D(latitude: 45.79398056386735, longitude: 4.8250579833984375),
LocationCoordinate2D(latitude: 45.79254427435898, longitude: 4.882392883300781),
LocationCoordinate2D(latitude: 45.76081677972451, longitude: 4.910373687744141),
LocationCoordinate2D(latitude: 45.7271539426975, longitude: 4.894924163818359),
LocationCoordinate2D(latitude: 45.71337148333104, longitude: 4.824199676513671),
LocationCoordinate2D(latitude: 45.74021417890731, longitude: 4.773387908935547),
LocationCoordinate2D(latitude: 45.778418789239055, longitude: 4.778022766113281),
LocationCoordinate2D(latitude: 45.79398056386735, longitude: 4.8250579833984375),
],
])
XCTAssertLessThan(polygon.centroid!.distance(to: coordinate), 1)
}

func testPolygonImbalancedCentroid() {
// Adopted from https://github.com/Turfjs/turf/blob/3b20c568e5638f680cde39c26b56fbcf034133f2/packages/turf-centroid/test.js
let coordinate = LocationCoordinate2D(latitude: 45.78143055383553, longitude: 4.851791984156558)
let polygon = Polygon([
[
LocationCoordinate2D(latitude: 45.77258200374433, longitude: 4.854240417480469),
LocationCoordinate2D(latitude: 45.777431068484894, longitude: 4.8445844650268555),
LocationCoordinate2D(latitude: 45.778658234059755, longitude: 4.845442771911621),
LocationCoordinate2D(latitude: 45.779376562352425, longitude: 4.845914840698242),
LocationCoordinate2D(latitude: 45.78021460033108, longitude: 4.846644401550292),
LocationCoordinate2D(latitude: 45.78078326178593, longitude: 4.847245216369629),
LocationCoordinate2D(latitude: 45.78138184652523, longitude: 4.848060607910156),
LocationCoordinate2D(latitude: 45.78186070968964, longitude: 4.8487043380737305),
LocationCoordinate2D(latitude: 45.78248921135124, longitude: 4.849562644958495),
LocationCoordinate2D(latitude: 45.78302792142197, longitude: 4.850893020629883),
LocationCoordinate2D(latitude: 45.78374619341895, longitude: 4.852008819580077),
LocationCoordinate2D(latitude: 45.784075398324866, longitude: 4.852995872497559),
LocationCoordinate2D(latitude: 45.78443452873236, longitude: 4.853854179382324),
LocationCoordinate2D(latitude: 45.78470387501975, longitude: 4.8549699783325195),
LocationCoordinate2D(latitude: 45.784793656826345, longitude: 4.85569953918457),
LocationCoordinate2D(latitude: 45.784853511283764, longitude: 4.857330322265624),
LocationCoordinate2D(latitude: 45.78494329284938, longitude: 4.858231544494629),
LocationCoordinate2D(latitude: 45.784883438488365, longitude: 4.859304428100585),
LocationCoordinate2D(latitude: 45.77294120818474, longitude: 4.858360290527344),
LocationCoordinate2D(latitude: 45.77258200374433, longitude: 4.854240417480469)
],
])
XCTAssertLessThan(polygon.centroid!.distance(to: coordinate), 1)
}

func testPolygonCentreOfMass() {
// Adopted from https://github.com/Turfjs/turf/blob/3b20c568e5638f680cde39c26b56fbcf034133f2/packages/turf-center-of-mass/test.js
let coordinate = LocationCoordinate2D(latitude: 45.75581209996416, longitude: 4.840728965137111)
let polygon = Polygon([
[
LocationCoordinate2D(latitude: 45.79398056386735, longitude: 4.8250579833984375),
LocationCoordinate2D(latitude: 45.79254427435898, longitude: 4.882392883300781),
LocationCoordinate2D(latitude: 45.76081677972451, longitude: 4.910373687744141),
LocationCoordinate2D(latitude: 45.7271539426975, longitude: 4.894924163818359),
LocationCoordinate2D(latitude: 45.71337148333104, longitude: 4.824199676513671),
LocationCoordinate2D(latitude: 45.74021417890731, longitude: 4.773387908935547),
LocationCoordinate2D(latitude: 45.778418789239055, longitude: 4.778022766113281),
LocationCoordinate2D(latitude: 45.79398056386735, longitude: 4.8250579833984375),
],
])
XCTAssertLessThan(polygon.centerOfMass!.distance(to: coordinate), 1)
}

func testPolygonImbalancedCentreOfMass() {
// Adopted from https://github.com/Turfjs/turf/blob/3b20c568e5638f680cde39c26b56fbcf034133f2/packages/turf-center-of-mass/test.js
let coordinate = LocationCoordinate2D(latitude: 45.77877742486245, longitude: 4.853372894819807)
let polygon = Polygon([
[
LocationCoordinate2D(latitude: 45.77258200374433, longitude: 4.854240417480469),
LocationCoordinate2D(latitude: 45.777431068484894, longitude: 4.8445844650268555),
LocationCoordinate2D(latitude: 45.778658234059755, longitude: 4.845442771911621),
LocationCoordinate2D(latitude: 45.779376562352425, longitude: 4.845914840698242),
LocationCoordinate2D(latitude: 45.78021460033108, longitude: 4.846644401550292),
LocationCoordinate2D(latitude: 45.78078326178593, longitude: 4.847245216369629),
LocationCoordinate2D(latitude: 45.78138184652523, longitude: 4.848060607910156),
LocationCoordinate2D(latitude: 45.78186070968964, longitude: 4.8487043380737305),
LocationCoordinate2D(latitude: 45.78248921135124, longitude: 4.849562644958495),
LocationCoordinate2D(latitude: 45.78302792142197, longitude: 4.850893020629883),
LocationCoordinate2D(latitude: 45.78374619341895, longitude: 4.852008819580077),
LocationCoordinate2D(latitude: 45.784075398324866, longitude: 4.852995872497559),
LocationCoordinate2D(latitude: 45.78443452873236, longitude: 4.853854179382324),
LocationCoordinate2D(latitude: 45.78470387501975, longitude: 4.8549699783325195),
LocationCoordinate2D(latitude: 45.784793656826345, longitude: 4.85569953918457),
LocationCoordinate2D(latitude: 45.784853511283764, longitude: 4.857330322265624),
LocationCoordinate2D(latitude: 45.78494329284938, longitude: 4.858231544494629),
LocationCoordinate2D(latitude: 45.784883438488365, longitude: 4.859304428100585),
LocationCoordinate2D(latitude: 45.77294120818474, longitude: 4.858360290527344),
LocationCoordinate2D(latitude: 45.77258200374433, longitude: 4.854240417480469)
],
])
let center = polygon.centerOfMass!
XCTAssertLessThan(center.distance(to: coordinate), 1)
}

func testSmoothClose() {
let original = [
Expand Down

0 comments on commit 8815a2b

Please sign in to comment.