Skip to content

Commit

Permalink
Adds Collection<GeoJSON.Position>.convexHull() (#7)
Browse files Browse the repository at this point in the history
Adds convexHull()

Also:

- SPM update to get closed polygon rings
- Refactoring
  • Loading branch information
nighthawk authored Jun 14, 2022
1 parent 98ccbcd commit 1edb6c0
Show file tree
Hide file tree
Showing 21 changed files with 1,386 additions and 63 deletions.
8 changes: 4 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
"repositoryURL": "https://github.com/maparoni/geojsonkit.git",
"state": {
"branch": null,
"revision": "a37addb9dcfe634dedece856e5f4d098d5290d1b",
"version": "0.4.8"
"revision": "8f1e5094a8ab8cbb020074cd4848a5804c17bab0",
"version": "0.5.2"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "e394bf350e38cb100b6bc4172834770ede1b7232",
"version": "1.0.3"
"revision": "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb",
"version": "1.1.2"
}
}
]
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ let package = Package(
targets: ["GeoKitten"]),
],
dependencies: [
.package(name: "GeoJSONKit", url: "https://github.com/maparoni/geojsonkit.git", from: "0.4.2"),
.package(name: "GeoJSONKit", url: "https://github.com/maparoni/geojsonkit.git", from: "0.5.2"),
// .package(name: "GeoJSONKit", path: "../GeoJSONKit"),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.0.0")),
],
targets: [
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ Turf.js | Turf-swift
[turf-center-of-mass](http://turfjs.org/docs/#centerOfMass) | `GeoJSON.Geometry.centerOfMass()` |
[turf-centroid](http://turfjs.org/docs/#centroid) | `GeoJSON.Geometry.centroid()` |
[turf-circle](https://turfjs.org/docs/#circle) | `GeoJSON.Polygon(center:radius:vertices:)` |
[turf-convex](https://turfjs.org/docs/#convex) | `GeoJSON.convexHull()`<br/>`Collection<GeoJSON.Position>.convexHull()` |
[turf-destination](https://github.com/Turfjs/turf/tree/master/packages/turf-destination/) | `GeoJSON.Position.coordinate(at:facing:)`<br/> `RadianCoordinate2D.coordinate(at:facing:)`
[turf-distance](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/) | `GeoJSON.Position.distance(to:)`<br>`RadianCoordinate2D.distance(to:)`
[turf-distance](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/) | `GeoJSON.Position.distance(to:)`<br/>`RadianCoordinate2D.distance(to:)`
[turf-helpers#degreesToRadians](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/#degreesToRadians) | `GeoJSON.Degrees.toRadians()`
[turf-helpers#radiansToDegrees](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/#radiansToDegrees) | `GeoJSON.DegreesRadians.toDegrees()`
[turf-helpers#convertLength](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertlength)<br>[turf-helpers#convertArea](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertarea) | `Measurement.converted(to:)`
[turf-helpers#convertLength](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertlength)<br/>[turf-helpers#convertArea](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers#convertarea) | `Measurement.converted(to:)`
[turf-length](https://github.com/Turfjs/turf/tree/master/packages/turf-length/) | `GeoJSON.LineString.distance(from:to:)`
[turf-line-intersect](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/) | `GeoJSON.LineString.intersection(with:)`
[turf-line-slice](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice/) | `GeoJSON.LineString.sliced(from:to:)`
[turf-line-slice-along](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/) | `GeoJSON.LineString.trimmed(from:distance:)`, `GeoJSON.LineString.trimmed(from:to:)`
[turf-line-slice-along](https://github.com/Turfjs/turf/tree/master/packages/turf-line-slice-along/) | `GeoJSON.LineString.trimmed(from:distance:)`<br/>`GeoJSON.LineString.trimmed(from:to:)`
[turf-midpoint](https://github.com/Turfjs/turf/blob/master/packages/turf-midpoint/index.js) | `mid(_:_:)`
[turf-nearest-point-on-line](https://github.com/Turfjs/turf/tree/master/packages/turf-nearest-point-on-line/) | `GeoJSON.LineString.closestCoordinate(to:)`
[turf-polygon-smooth](https://github.com/Turfjs/turf/tree/master/packages/turf-polygon-smooth) | `GeoJSON.Polygon.smooth(iterations:)`
Expand Down
40 changes: 40 additions & 0 deletions Sources/GeoJSONKitTurf/ConvexHull.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Turf+ConvexHull.swift
//
//
// Created by Adrian Schönig on 14/6/2022.
//

import Foundation

import GeoJSONKit

extension Collection where Element == GeoJSON.Position {
/// Calculates the convex hull of a given sequence of positions.
///
/// - Complexity: O(*n* log *n*), where *n* is the count of `points`.
///
/// - Returns: The convex hull of this sequence as a polygon
public func convexHull() -> GeoJSON.Polygon {
let positions = AndrewsMonotoneChain.convexHull(self).map { $0.removingAltitude }
return .init(exterior: .init(positions: positions))
}
}

extension GeoJSON {
/// Calculates the convex hull of all the elements of this GeoJSON
///
/// - Returns: The convex hull of this GeoJSON as a polygon
public func convexHull() -> GeoJSON.Polygon {
return positions.convexHull()
}
}

extension GeoJSON.Position {
fileprivate var removingAltitude: GeoJSON.Position {
guard altitude != nil else { return self }
var updated = self
updated.altitude = nil
return updated
}
}
52 changes: 52 additions & 0 deletions Sources/GeoJSONKitTurf/GeoJSON+Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// GeoJSON+Helpers.swift
//
//
// Created by Adrian Schönig on 14/6/2022.
//

import GeoJSONKit

extension GeoJSON.GeometryObject {
public var geometries: [GeoJSON.Geometry] {
switch self {
case .single(let geo): return [geo]
case .multi(let geos): return geos
case .collection(let geoObjects): return geoObjects.flatMap(\.geometries)
}
}

public var positions: [GeoJSON.Position] {
geometries.flatMap(\.positions)
}
}

extension GeoJSON.Geometry {
public var positions: [GeoJSON.Position] {
switch self {
case .point(let position): return [position]
case .lineString(let line): return line.positions
case .polygon(let polygon):
// Ignore the interior positions as the purpose of this is getting
// bounding boxes, convex hull or alike
return polygon.exterior.positions
}
}
}

extension GeoJSON {
public var geometries: [GeoJSON.Geometry] {
switch object {
case .feature(let feature):
return feature.geometry.geometries
case .featureCollection(let features):
return features.flatMap(\.geometry.geometries)
case .geometry(let geometry):
return geometry.geometries
}
}

public var positions: [GeoJSON.Position] {
geometries.flatMap(\.positions)
}
}
70 changes: 61 additions & 9 deletions Sources/GeoJSONKitTurf/Simplify.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,13 @@ extension GeoJSON.Geometry {
}
}

extension GeoJSON.LineString {

private static func simplifiedRadialDistance(_ positions: [GeoJSON.Position], squaredTolerance: Double) -> [GeoJSON.Position] {
guard positions.count > 2, let first = positions.first, let last = positions.last else { return positions }
extension Array where Element == GeoJSON.Position {
fileprivate func simplifiedRadialDistance(squaredTolerance: Double) -> [GeoJSON.Position] {
guard count > 2, let first = first, let last = last else { return self }

var newPositions = [first]

for position in positions[1...] {
for position in self[1...] {
if let lastNew = newPositions.last, position.squaredDistance(from: lastNew) > squaredTolerance {
newPositions.append(position)
}
Expand All @@ -127,7 +126,9 @@ extension GeoJSON.LineString {
}
return newPositions
}

}

extension GeoJSON.LineString {

/// Returns a copy of the LineString with the Ramer–Douglas–Peucker algorithm applied to it.
///
Expand Down Expand Up @@ -167,7 +168,60 @@ extension GeoJSON.LineString {
let squared = tolerance * tolerance
let input = options.highestQuality
? positions
: Self.simplifiedRadialDistance(positions, squaredTolerance: squared)
: positions.simplifiedRadialDistance(squaredTolerance: squared)

simplified = DouglasPeucker.simplify(input, sqTolerance: squared)

//remove 1 percent of tolerance if not verified
tolerance -= tolerance * 0.01
} while !verifier(simplified)
positions = simplified
}
}

}

extension GeoJSON.Polygon.LinearRing {

/// Returns a copy of the LinearRing with the Ramer–Douglas–Peucker algorithm applied to it.
///
/// tolerance: Controls the level of simplification by specifying the maximum allowed distance between the original line point
/// and the simplified point. Higher tolerance values results in higher simplification.
///
/// highestQuality: Excludes distance-based preprocessing step which leads to highest quality simplification. High quality simplification runs considerably slower so consider how much precision is needed in your application.
///
/// Ported from https://github.com/Turfjs/turf/blob/master/packages/turf-simplify/lib/simplify.js
public func simplified(options: SimplifyOptions = .init()) -> GeoJSON.Polygon.LinearRing {
guard positions.count > 2 else { return GeoJSON.Polygon.LinearRing(positions: positions) }

var copy = self
copy.simplify(options: options)
return copy
}

/// Mutates the LinearRing into a simplified version using the Ramer–Douglas–Peucker algorithm.
///
/// tolerance: Controls the level of simplification by specifying the maximum allowed distance between the original line point
/// and the simplified point. Higher tolerance values results in higher simplification.
///
/// highestQuality: Excludes distance-based preprocessing step which leads to highest quality simplification. High quality simplification runs considerably slower so consider how much precision is needed in your application.
///
/// Ported from https://github.com/Turfjs/turf/blob/master/packages/turf-simplify/lib/simplify.js
public mutating func simplify(options: SimplifyOptions = .init()) {
simplify(options: options, verifier: { _ in true })
}

mutating func simplify(options: SimplifyOptions, verifier: ([GeoJSON.Position]) -> Bool) {
guard positions.count > 2 else { return }

switch options.algorithm {
case .RamerDouglasPeucker(var tolerance):
var simplified: [GeoJSON.Position]
repeat {
let squared = tolerance * tolerance
let input = options.highestQuality
? positions
: positions.simplifiedRadialDistance(squaredTolerance: squared)

simplified = DouglasPeucker.simplify(input, sqTolerance: squared)

Expand Down Expand Up @@ -199,8 +253,6 @@ extension GeoJSON.Polygon {
updated.simplify(options: options, verifier: Self.checkValidity(ring:))
return updated
}

// TODO: Close exterior + interiors
}

/// Checks if a ring has at least 3 coordinates. Will return false for a 3 coordinate ring
Expand Down
27 changes: 0 additions & 27 deletions Sources/GeoJSONKitTurf/Turf+BoundingBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,3 @@ extension GeoJSON.BoundingBox {
}

}

extension GeoJSON.GeometryObject {
public var geometries: [GeoJSON.Geometry] {
switch self {
case .single(let geo): return [geo]
case .multi(let geos): return geos
case .collection(let geoObjects): return geoObjects.flatMap(\.geometries)
}
}

public var positions: [GeoJSON.Position] {
geometries.flatMap(\.positions)
}
}

extension GeoJSON.Geometry {
public var positions: [GeoJSON.Position] {
switch self {
case .point(let position): return [position]
case .lineString(let line): return line.positions
case .polygon(let polygon):
// Ignore the interior positions as the purpose of this is getting
// bounding boxes or alike
return polygon.exterior.positions
}
}
}
83 changes: 83 additions & 0 deletions Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// MonotoneChain.swift
//
//
// Created by Adrian Schönig on 14/6/2022.
//

import Foundation

import GeoJSONKit

// Adopted from https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
struct AndrewsMonotoneChain {
private static func cross(_ o: GeoJSON.Position, _ a: GeoJSON.Position, _ b: GeoJSON.Position) -> Double {
let lhs = (a.x - o.x) * (b.y - o.y)
let rhs = (a.y - o.y) * (b.x - o.x)
return lhs - rhs
}

/// Calculate and return the convex hull of a given sequence of points.
///
/// - Remark: Implements Andrew’s monotone chain convex hull algorithm.
///
/// - Complexity: O(*n* log *n*), where *n* is the count of `points`.
///
/// - Parameter points: A sequence of `GeoJSON.Position` elements.
///
/// - Returns: An array containing the convex hull of `points`, ordered
/// lexicographically from the smallest coordinates to the largest,
/// turning counterclockwise.
///
static func convexHull<Source>(_ points: Source) -> [GeoJSON.Position]
where Source : Collection,
Source.Element == GeoJSON.Position
{
// Exit early if there aren’t enough points to work with.
guard points.count > 1 else { return Array(points) }

// Create storage for the lower and upper hulls.
var lower = [GeoJSON.Position]()
var upper = [GeoJSON.Position]()

// Sort points in lexicographical order.
let points = points.sorted { a, b in
a.x < b.x || a.x == b.x && a.y < b.y
}

// Construct the lower hull.
for point in points {
while lower.count >= 2 {
let a = lower[lower.count - 2]
let b = lower[lower.count - 1]
if cross(a, b, point) > 0 { break }
lower.removeLast()
}
lower.append(point)
}

// Construct the upper hull.
for point in points.lazy.reversed() {
while upper.count >= 2 {
let a = upper[upper.count - 2]
let b = upper[upper.count - 1]
if cross(a, b, point) > 0 { break }
upper.removeLast()
}
upper.append(point)
}

// Remove each array’s last point, as it’s the same as the first point
// in the opposite array, respectively.
lower.removeLast()
upper.removeLast()

// Join the arrays to form the convex hull.
return lower + upper
}
}

fileprivate extension GeoJSON.Position {
var x: Double { longitude }
var y: Double { latitude }
}
Loading

0 comments on commit 1edb6c0

Please sign in to comment.