Skip to content

Commit

Permalink
Merge pull request #138 from okcoker/feature/polygon-simplify
Browse files Browse the repository at this point in the history
Add Polygon simplify
  • Loading branch information
Udumft authored Mar 18, 2021
2 parents dec3ece + a345fb1 commit d07675a
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Turf.js | Turf-swift
[turf-polygon-to-line](https://github.com/Turfjs/turf/tree/master/packages/turf-polygon-to-line/) | `LineString(_:)`<br>`MultiLineString(_:)`<br>`FeatureCollection(_:)`
[turf-simplify](https://github.com/Turfjs/turf/tree/master/packages/turf-simplify) | `LineString.simplified(tolerance:highestQuality:)`
[turf-polygon-smooth](https://github.com/Turfjs/turf/tree/master/packages/turf-polygon-smooth) | `Polygon.smooth(iterations:)`
[turf-simplify](https://github.com/Turfjs/turf/tree/master/packages/turf-simplify) | `Polygon.simplified(tolerance:highestQuality:)`
— | `CLLocationDirection.difference(from:)`<br>`LocationDirection.difference(from:)` on Linux
— | `CLLocationDirection.wrap(min:max:)`<br>`LocationDirection.wrap(min:max:)` on Linux

Expand Down
157 changes: 157 additions & 0 deletions Sources/Turf/Geometries/Polygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,161 @@ extension Polygon {
tempOutput[i] = ring + [ring[0]]
})
}

/// Returns a copy of the Polygon 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 simplify(tolerance: Double = 1.0, highestQuality: Bool = false) -> Polygon {
var copy = Polygon(coordinates)
copy.simplified(tolerance: tolerance, highestQuality: highestQuality)
return copy
}

/// Mutates the Polygon 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 simplified(tolerance: Double = 1.0, highestQuality: Bool = false) {
guard coordinates.allSatisfy({ $0.count > 3 }) else { return }

coordinates = coordinates.map({ ring in
let squareTolerance = tolerance * tolerance
var tolerance = tolerance

if !highestQuality {
simplified(radialTolerance: squareTolerance)
}

var simpleRing = simplifyDouglasPeucker(ring, tolerance: tolerance);
//remove 1 percent of tolerance until enough points to make a triangle
while (!checkValidity(ring: simpleRing)) {
tolerance -= tolerance * 0.01;
simpleRing = simplifyDouglasPeucker(ring, tolerance: tolerance)
}

if (
simpleRing[simpleRing.count - 1].latitude != simpleRing[0].latitude ||
simpleRing[simpleRing.count - 1].longitude != simpleRing[0].longitude
) {
simpleRing.append(simpleRing[0]);
}

return simpleRing;
})
}

private mutating func simplified(radialTolerance: Double) {
coordinates = coordinates.map{ ring in
guard ring.count > 2 else { return ring }


var prevCoordinate = ring[0]
var newCoordinates = [prevCoordinate]
var coordinate = ring[1]

for index in 1 ..< ring.count {
coordinate = ring[index]

if squareDistance(from: coordinate, to: prevCoordinate) > radialTolerance {
newCoordinates.append(coordinate)
prevCoordinate = coordinate
}
}

if prevCoordinate != coordinate {
newCoordinates.append(coordinate)
}

return newCoordinates
}
}

private func squareDistance(from origin: LocationCoordinate2D, to destination: LocationCoordinate2D) -> Double {
let dx = origin.longitude - destination.longitude
let dy = origin.latitude - destination.latitude
return dx * dx + dy * dy
}

private func squareSegmentDistance(_ coordinate: LocationCoordinate2D, segmentStart: LocationCoordinate2D, segmentEnd: LocationCoordinate2D) -> LocationDistance {
var x = segmentStart.latitude
var y = segmentStart.longitude
var dx = segmentEnd.latitude - x
var dy = segmentEnd.longitude - y

if dx != 0 || dy != 0 {
let t = ((segmentStart.latitude - x) * dx + (coordinate.longitude - y) * dy) / (dx * dx + dy * dy)
if t > 1 {
x = segmentEnd.latitude
y = segmentEnd.longitude
} else if t > 0 {
x += dx * t
y += dy * t
}
}

dx = coordinate.latitude - x
dy = coordinate.longitude - y

return dx * dx + dy * dy
}

private func simplifyDouglasPeuckerStep(_ coordinates: [LocationCoordinate2D], first: Int, last: Int, tolerance: Double, simplified: inout [LocationCoordinate2D]) {
var maxSquareDistance = tolerance
var index = 0

for i in first + 1 ..< last {
let squareDistance = squareSegmentDistance(coordinates[i], segmentStart: coordinates[first], segmentEnd: coordinates[last])

if squareDistance > maxSquareDistance {
index = i
maxSquareDistance = squareDistance
}
}

if maxSquareDistance > tolerance {
if index - first > 1 {
simplifyDouglasPeuckerStep(coordinates, first: first, last: index, tolerance: tolerance, simplified: &simplified)
}
simplified.append(coordinates[index])
if last - index > 1 {
simplifyDouglasPeuckerStep(coordinates, first: index, last: last, tolerance: tolerance, simplified: &simplified)
}
}
}

private func simplifyDouglasPeucker(_ coordinates: [LocationCoordinate2D], tolerance: Double) -> [LocationCoordinate2D] {
if coordinates.count <= 2 {
return coordinates
}

let lastPoint = coordinates.count - 1
var result = [coordinates[0]]
simplifyDouglasPeuckerStep(coordinates, first: 0, last: lastPoint, tolerance: tolerance, simplified: &result)
result.append(coordinates[lastPoint])
return result
}

/// Checks if a ring has at least 3 coordinates. Will return false for a 3 coordinate ring
/// where the first and last coordinates are the same
///
/// - Parameter ring: Array of coordinates to be checked
/// - Returns: true if valid
private func checkValidity(ring: [LocationCoordinate2D]) -> Bool {
guard ring.count >= 3 else { return false }
// if the last point is the same as the first, it's not a triangle
return !(
ring.count == 3 &&
ring[2].latitude == ring[0].latitude &&
ring[2].longitude == ring[0].longitude
)
}
}
99 changes: 99 additions & 0 deletions Tests/TurfTests/PolygonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,103 @@ class PolygonTests: XCTestCase {

XCTAssertEqual(smoothed.coordinates, expected)
}

func testSimplifySimplePolygon() {
let original = [
[
LocationCoordinate2D(latitude: 26.148429528000065, longitude: -28.29755210099995),
LocationCoordinate2D(latitude: 26.148582685000065, longitude: -28.29778390599995),
LocationCoordinate2D(latitude: 26.149207731000047, longitude: -28.29773837299996),
LocationCoordinate2D(latitude: 26.14925541100007, longitude: -28.297771688999944),
LocationCoordinate2D(latitude: 26.149255844000038, longitude: -28.297773261999964),
LocationCoordinate2D(latitude: 26.149276505000046, longitude: -28.29784835099997),
LocationCoordinate2D(latitude: 26.14928482700003, longitude: -28.29787859399994),
LocationCoordinate2D(latitude: 26.14928916200006, longitude: -28.29800647199994),
LocationCoordinate2D(latitude: 26.14931069800008, longitude: -28.298641791999955),
LocationCoordinate2D(latitude: 26.149339971000074, longitude: -28.298641232999955),
LocationCoordinate2D(latitude: 26.151298488000066, longitude: -28.29860385099994),
LocationCoordinate2D(latitude: 26.151290002000053, longitude: -28.298628995999934),
LocationCoordinate2D(latitude: 26.151417002000073, longitude: -28.299308003999954),
LocationCoordinate2D(latitude: 26.15159000400007, longitude: -28.299739003999946),
LocationCoordinate2D(latitude: 26.151951998000072, longitude: -28.30051100299994),
LocationCoordinate2D(latitude: 26.15206407200003, longitude: -28.30076885099993),
LocationCoordinate2D(latitude: 26.152066543000046, longitude: -28.30077453499996),
LocationCoordinate2D(latitude: 26.151987021000025, longitude: -28.300799009999935),
LocationCoordinate2D(latitude: 26.149896693000073, longitude: -28.301442350999935),
LocationCoordinate2D(latitude: 26.150354333000053, longitude: -28.30260575099993),
LocationCoordinate2D(latitude: 26.14914131000006, longitude: -28.302975170999957),
LocationCoordinate2D(latitude: 26.14836387300005, longitude: -28.302853868999932),
LocationCoordinate2D(latitude: 26.147575408000023, longitude: -28.30269948399996),
LocationCoordinate2D(latitude: 26.146257624000043, longitude: -28.302462392999928),
LocationCoordinate2D(latitude: 26.14557943400007, longitude: -28.302181192999967),
LocationCoordinate2D(latitude: 26.145492669000078, longitude: -28.302154609999945),
LocationCoordinate2D(latitude: 26.144921243000056, longitude: -28.303395982999973),
LocationCoordinate2D(latitude: 26.14482272200007, longitude: -28.30455853999996),
LocationCoordinate2D(latitude: 26.14431040900007, longitude: -28.30451913099995),
LocationCoordinate2D(latitude: 26.14429070400007, longitude: -28.304144747999942),
LocationCoordinate2D(latitude: 26.143837504000032, longitude: -28.304144747999942),
LocationCoordinate2D(latitude: 26.143613499000026, longitude: -28.304592757999956),
LocationCoordinate2D(latitude: 26.14346312200007, longitude: -28.304893512999968),
LocationCoordinate2D(latitude: 26.143260178000048, longitude: -28.304893512999968),
LocationCoordinate2D(latitude: 26.143246374000057, longitude: -28.304893512999968),
LocationCoordinate2D(latitude: 26.143147852000027, longitude: -28.304893512999968),
LocationCoordinate2D(latitude: 26.14295080900007, longitude: -28.304834399999947),
LocationCoordinate2D(latitude: 26.14200500000004, longitude: -28.30449942699994),
LocationCoordinate2D(latitude: 26.14198529600003, longitude: -28.304420608999976),
LocationCoordinate2D(latitude: 26.141525339000054, longitude: -28.304298579999966),
LocationCoordinate2D(latitude: 26.141019783000047, longitude: -28.30416445299994),
LocationCoordinate2D(latitude: 26.141118305000077, longitude: -28.304637356999933),
LocationCoordinate2D(latitude: 26.140940966000073, longitude: -28.30512996599998),
LocationCoordinate2D(latitude: 26.140376789000072, longitude: -28.306172836999963),
LocationCoordinate2D(latitude: 26.140476282000066, longitude: -28.30621363399996),
LocationCoordinate2D(latitude: 26.14041675800007, longitude: -28.306326533999936),
LocationCoordinate2D(latitude: 26.140146555000058, longitude: -28.30640398099996),
LocationCoordinate2D(latitude: 26.140073975000064, longitude: -28.306410747999962),
LocationCoordinate2D(latitude: 26.137315367000042, longitude: -28.305189078999945),
LocationCoordinate2D(latitude: 26.136645419000047, longitude: -28.304854104999947),
LocationCoordinate2D(latitude: 26.135719315000074, longitude: -28.30451913099995),
LocationCoordinate2D(latitude: 26.135515376000058, longitude: -28.304330879999952),
LocationCoordinate2D(latitude: 26.13546315800005, longitude: -28.304282678999982),
LocationCoordinate2D(latitude: 26.13558800000004, longitude: -28.30419999999998),
LocationCoordinate2D(latitude: 26.137463000000025, longitude: -28.30242899999996),
LocationCoordinate2D(latitude: 26.13794500000006, longitude: -28.30202799999995),
LocationCoordinate2D(latitude: 26.13796479100006, longitude: -28.30201049699997),
LocationCoordinate2D(latitude: 26.13798299700005, longitude: -28.302025000999947),
LocationCoordinate2D(latitude: 26.139450004000025, longitude: -28.30074499999995),
LocationCoordinate2D(latitude: 26.141302000000053, longitude: -28.29914199999996),
LocationCoordinate2D(latitude: 26.141913997000074, longitude: -28.29862600399997),
LocationCoordinate2D(latitude: 26.14212216900006, longitude: -28.29845037299998),
LocationCoordinate2D(latitude: 26.144304360000035, longitude: -28.296499429999983),
LocationCoordinate2D(latitude: 26.144799071000023, longitude: -28.29614006399993),
LocationCoordinate2D(latitude: 26.145209090000037, longitude: -28.295759748999956),
LocationCoordinate2D(latitude: 26.145465732000048, longitude: -28.295507246999932),
LocationCoordinate2D(latitude: 26.14575028200005, longitude: -28.295352539999953),
LocationCoordinate2D(latitude: 26.14589208800004, longitude: -28.295275441999934),
LocationCoordinate2D(latitude: 26.146584820000044, longitude: -28.295135245999973),
LocationCoordinate2D(latitude: 26.146587504000024, longitude: -28.295134702999974),
LocationCoordinate2D(latitude: 26.146827588000065, longitude: -28.295606591999956),
LocationCoordinate2D(latitude: 26.14685742000006, longitude: -28.29565372899998),
LocationCoordinate2D(latitude: 26.14691261200005, longitude: -28.29574093599996),
LocationCoordinate2D(latitude: 26.147077344000024, longitude: -28.296001226999977),
LocationCoordinate2D(latitude: 26.147117344000037, longitude: -28.296041226999932),
LocationCoordinate2D(latitude: 26.147907966000048, longitude: -28.29696016899993),
LocationCoordinate2D(latitude: 26.147913396000035, longitude: -28.296966331999954),
LocationCoordinate2D(latitude: 26.148429528000065, longitude: -28.29755210099995)
]
]

let expected = [
[
LocationCoordinate2D(latitude: 26.148429528000065, longitude: -28.29755210099995),
LocationCoordinate2D(latitude: 26.135515376000058, longitude: -28.304330879999952),
LocationCoordinate2D(latitude: 26.13546315800005, longitude: -28.304282678999982),
LocationCoordinate2D(latitude: 26.148429528000065, longitude: -28.29755210099995)
]
]

let polygon = Polygon(original)
let simplified = polygon.simplify(tolerance: 100, highestQuality: false);

XCTAssertEqual(simplified.coordinates, expected)
}
}

0 comments on commit d07675a

Please sign in to comment.