diff --git a/README.md b/README.md index 975b18d6..7acdce7b 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,9 @@ Turf.js | Turf-swift [turf-bearing](https://turfjs.org/docs/#bearing) | `CLLocationCoordinate2D.direction(to:)`
`LocationCoordinate2D.direction(to:)` on Linux
`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:)`
`LocationCoordinate2D.coordinate(at:facing:)` on Linux
`RadianCoordinate2D.coordinate(at:facing:)` [turf-distance](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/) | `CLLocationCoordinate2D.distance(to:)`
`LocationCoordinate2D.distance(to:)` on Linux
`RadianCoordinate2D.distance(to:)` diff --git a/Sources/Turf/CoreLocation.swift b/Sources/Turf/CoreLocation.swift index 601cc841..aabf50a7 100644 --- a/Sources/Turf/CoreLocation.swift +++ b/Sources/Turf/CoreLocation.swift @@ -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. @@ -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. diff --git a/Sources/Turf/Geometries/Polygon.swift b/Sources/Turf/Geometries/Polygon.swift index 66ae251a..de6e45cc 100644 --- a/Sources/Turf/Geometries/Polygon.swift +++ b/Sources/Turf/Geometries/Polygon.swift @@ -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 + }} diff --git a/Tests/TurfTests/PolygonTests.swift b/Tests/TurfTests/PolygonTests.swift index 945f6554..16281168 100644 --- a/Tests/TurfTests/PolygonTests.swift +++ b/Tests/TurfTests/PolygonTests.swift @@ -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 = [