From d679d077c24ed24902821315c1fd49c57a0069e7 Mon Sep 17 00:00:00 2001 From: Sandy Chapman Date: Fri, 14 Sep 2018 14:38:59 -0300 Subject: [PATCH] Ported the turf-boolean-point-in-polygon function to Turf Swift. Uses the identical algorithm as used in Turf.js. https://github.com/Turfjs/turf/blob/e53677b0931da9e38bb947da448ee7404adc369d/packages/turf-boolean-point-in-polygon/index.ts --- README.md | 1 + Sources/Turf/BoundingBox.swift | 32 +++++++++++++++++++++ Sources/Turf/Polygon.swift | 29 +++++++++++++++++++ Sources/Turf/Ring.swift | 41 +++++++++++++++++++++++++++ Tests/TurfTests/PolygonTests.swift | 45 ++++++++++++++++++++++++++++++ Turf.xcodeproj/project.pbxproj | 10 +++++++ 6 files changed, 158 insertions(+) create mode 100644 Sources/Turf/BoundingBox.swift diff --git a/README.md b/README.md index b81989a8..2651e5a3 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Turf.js | Turf-swift ----|---- [turf-along](https://github.com/Turfjs/turf/tree/master/packages/turf-along/) | `LineString.coordinateFromStart(distance:)` [turf-area](https://github.com/Turfjs/turf/blob/master/packages/turf-area/) | `Polygon.area` +[turf-boolean-point-in-polygon](https://github.com/Turfjs/turf/tree/master/packages/turf-boolean-point-in-polygon) | `Polygon.contains(point:ignoreBoundary:)` [turf-destination](https://github.com/Turfjs/turf/tree/master/packages/turf-destination/) | `CLLocationCoordinate2D.coordinate(at:facing:)`
`RadianCoordinate2D.coordinate(at:facing:)` [turf-distance](https://github.com/Turfjs/turf/tree/master/packages/turf-distance/) | `CLLocationCoordinate2D.distance(to:)`
`RadianCoordinate2D.distance(to:)` [turf-helpers#polygon](https://github.com/Turfjs/turf/tree/master/packages/turf-helpers/#polygon) | `Polygon(outerRing:innerRings:)` diff --git a/Sources/Turf/BoundingBox.swift b/Sources/Turf/BoundingBox.swift new file mode 100644 index 00000000..fc837e16 --- /dev/null +++ b/Sources/Turf/BoundingBox.swift @@ -0,0 +1,32 @@ +import Foundation +#if !os(Linux) +import CoreLocation +#endif + +public class BoundingBox { + + public init?(points: [CLLocationCoordinate2D]?) { + guard points?.count ?? 0 > 0 else { + return nil + } + (minLat, maxLat, minLon, maxLon) = points! + .reduce((minLat: 0, maxLat: 0, minLon: 0, maxLon: 0)) { (result, coordinate) -> (minLat: Double, maxLat: Double, minLon: Double, maxLon: Double) in + let minLat = min(coordinate.latitude, result.0) + let maxLat = max(coordinate.latitude, result.1) + let minLon = min(coordinate.longitude, result.2) + let maxLon = max(coordinate.longitude, result.3) + return (minLat: minLat, maxLat: maxLat, minLon: minLon, maxLon: maxLon) + } + } + + public func contains(point: CLLocationCoordinate2D) -> Bool { + return minLat < point.latitude && maxLat > point.latitude && minLon < point.longitude && maxLon > point.longitude + } + + // MARK: - Private + + let minLat: Double + let maxLat: Double + let minLon: Double + let maxLon: Double +} diff --git a/Sources/Turf/Polygon.swift b/Sources/Turf/Polygon.swift index 3e587312..886b98cf 100644 --- a/Sources/Turf/Polygon.swift +++ b/Sources/Turf/Polygon.swift @@ -59,3 +59,32 @@ extension Polygon { .reduce(0, +) } } + +extension Polygon { + + /** + * Determines if the given point falls within the polygon and outside of its interior rings. + * The optional parameter `ignoreBoundary` will result in the method returning true if the given point + * lies on the boundary line of the polygon or its interior rings. + * + * Ported from: https://github.com/Turfjs/turf/blob/e53677b0931da9e38bb947da448ee7404adc369d/packages/turf-boolean-point-in-polygon/index.ts#L31-L75 + */ + public func contains(point: CLLocationCoordinate2D, ignoreBoundary: Bool = false) -> Bool { + let bbox = BoundingBox(points: self.coordinates.first) + guard bbox?.contains(point: point) ?? false else { + return false + } + guard self.outerRing.contains(point: point, ignoreBoundary: ignoreBoundary) else { + return false + } + if let innerRings = innerRings { + for ring in innerRings { + if ring.contains(point: point, ignoreBoundary: ignoreBoundary) { + return false + } + } + } + return true + } +} + diff --git a/Sources/Turf/Ring.swift b/Sources/Turf/Ring.swift index 5cb0ecb5..f7c29227 100644 --- a/Sources/Turf/Ring.swift +++ b/Sources/Turf/Ring.swift @@ -53,3 +53,44 @@ public struct Ring { return area } } + +extension Ring { + /** + * Determines if the given point falls within the ring. + * The optional parameter `ignoreBoundary` will result in the method returning true if the given point + * lies on the boundary line of the ring. + * + * Ported from: https://github.com/Turfjs/turf/blob/e53677b0931da9e38bb947da448ee7404adc369d/packages/turf-boolean-point-in-polygon/index.ts#L77-L108 + */ + public func contains(point: CLLocationCoordinate2D, ignoreBoundary: Bool = false) -> Bool { + var ring: ArraySlice! + var isInside = false + if coordinates.first == coordinates.last { + ring = coordinates.prefix(coordinates.count - 1) + } + else { + ring = coordinates.prefix(coordinates.count) + } + var i = 0 + var j = ring.count - 1 + while i < ring.count { + let xi = ring[i].longitude + let yi = ring[i].latitude + let xj = ring[j].longitude + let yj = ring[j].latitude + let onBoundary = (point.latitude * (xi - xj) + yi * (xj - point.longitude) + yj * (point.longitude - xi) == 0) && + ((xi - point.longitude) * (xj - point.longitude) <= 0) && ((yi - point.latitude) * (yj - point.latitude) <= 0) + if onBoundary { + return !ignoreBoundary + } + let intersect = ((yi > point.latitude) != (yj > point.latitude)) && + (point.longitude < (xj - xi) * (point.latitude - yi) / (yj - yi) + xi); + if (intersect) { + isInside = !isInside; + } + j = i + i = i + 1 + } + return isInside + } +} diff --git a/Tests/TurfTests/PolygonTests.swift b/Tests/TurfTests/PolygonTests.swift index bf9621ab..cb4c34c1 100644 --- a/Tests/TurfTests/PolygonTests.swift +++ b/Tests/TurfTests/PolygonTests.swift @@ -28,4 +28,49 @@ class PolygonTests: XCTestCase { XCTAssert(decoded.geometry.outerRing.coordinates.count == 5) XCTAssert(decoded.geometry.innerRings!.first?.coordinates.count == 5) } + + func testPolygonContains() { + let coordinate = CLLocationCoordinate2D(latitude: 44, longitude: -77) + let polygon = Polygon([[ + CLLocationCoordinate2D(latitude: 41, longitude: -81), + CLLocationCoordinate2D(latitude: 47, longitude: -81), + CLLocationCoordinate2D(latitude: 47, longitude: -72), + CLLocationCoordinate2D(latitude: 41, longitude: -72), + CLLocationCoordinate2D(latitude: 41, longitude: -81), + ]]) + XCTAssertTrue(polygon.contains(point: coordinate)) + } + + func testPolygonDoesNotContain() { + let coordinate = CLLocationCoordinate2D(latitude: 44, longitude: -77) + let polygon = Polygon([[ + CLLocationCoordinate2D(latitude: 41, longitude: -51), + CLLocationCoordinate2D(latitude: 47, longitude: -51), + CLLocationCoordinate2D(latitude: 47, longitude: -42), + CLLocationCoordinate2D(latitude: 41, longitude: -42), + CLLocationCoordinate2D(latitude: 41, longitude: -51), + ]]) + XCTAssertFalse(polygon.contains(point: coordinate)) + } + + func testPolygonDoesNotContainWithHole() { + let coordinate = CLLocationCoordinate2D(latitude: 44, longitude: -77) + let polygon = Polygon([ + [ + CLLocationCoordinate2D(latitude: 41, longitude: -81), + CLLocationCoordinate2D(latitude: 47, longitude: -81), + CLLocationCoordinate2D(latitude: 47, longitude: -72), + CLLocationCoordinate2D(latitude: 41, longitude: -72), + CLLocationCoordinate2D(latitude: 41, longitude: -81), + ], + [ + CLLocationCoordinate2D(latitude: 43, longitude: -76), + CLLocationCoordinate2D(latitude: 43, longitude: -78), + CLLocationCoordinate2D(latitude: 45, longitude: -78), + CLLocationCoordinate2D(latitude: 45, longitude: -76), + CLLocationCoordinate2D(latitude: 43, longitude: -76), + ], + ]) + XCTAssertFalse(polygon.contains(point: coordinate)) + } } diff --git a/Turf.xcodeproj/project.pbxproj b/Turf.xcodeproj/project.pbxproj index 292ca969..ebf7574f 100644 --- a/Turf.xcodeproj/project.pbxproj +++ b/Turf.xcodeproj/project.pbxproj @@ -118,6 +118,10 @@ C46809D721241B5100BAD5E1 /* featurecollection-no-properties.geojson in Resources */ = {isa = PBXBuildFile; fileRef = C46809D52124199500BAD5E1 /* featurecollection-no-properties.geojson */; }; C46809D821249E8F00BAD5E1 /* featurecollection-no-properties.geojson in Resources */ = {isa = PBXBuildFile; fileRef = C46809D52124199500BAD5E1 /* featurecollection-no-properties.geojson */; }; C46809D921249E9700BAD5E1 /* featurecollection-no-properties.geojson in Resources */ = {isa = PBXBuildFile; fileRef = C46809D52124199500BAD5E1 /* featurecollection-no-properties.geojson */; }; + CE2EB998214C246A00915A30 /* BoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2EB997214C246A00915A30 /* BoundingBox.swift */; }; + CE2EB999214C247100915A30 /* BoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2EB997214C246A00915A30 /* BoundingBox.swift */; }; + CE2EB99A214C247100915A30 /* BoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2EB997214C246A00915A30 /* BoundingBox.swift */; }; + CE2EB99B214C247200915A30 /* BoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2EB997214C246A00915A30 /* BoundingBox.swift */; }; DA39EB6E20101F99004D87F7 /* Turf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA39EB6520101F98004D87F7 /* Turf.framework */; }; DA39EB7C20101FB9004D87F7 /* Turf.h in Headers */ = {isa = PBXBuildFile; fileRef = 3547ECEF200C3C78009DA062 /* Turf.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA39EB7D20101FCD004D87F7 /* CoreLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3547ECF0200C3C78009DA062 /* CoreLocation.swift */; }; @@ -197,6 +201,7 @@ 35ECAF2E20974A1800DC3BC3 /* Geometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Geometry.swift; sourceTree = ""; }; 35ECAF352099EC0700DC3BC3 /* FeatureIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIdentifier.swift; sourceTree = ""; }; C46809D52124199500BAD5E1 /* featurecollection-no-properties.geojson */ = {isa = PBXFileReference; lastKnownFileType = text; path = "featurecollection-no-properties.geojson"; sourceTree = ""; }; + CE2EB997214C246A00915A30 /* BoundingBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundingBox.swift; sourceTree = ""; }; DA39EB6520101F98004D87F7 /* Turf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Turf.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DA39EB6D20101F99004D87F7 /* TurfTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TurfTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DA94249B2010283900CDB4E6 /* Turf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Turf.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -263,6 +268,7 @@ children = ( 3547ECEF200C3C78009DA062 /* Turf.h */, 3547ECF2200C3C78009DA062 /* Info.plist */, + CE2EB997214C246A00915A30 /* BoundingBox.swift */, 3502F795201F53FC00399EFE /* Codable.swift */, 3547ECF0200C3C78009DA062 /* CoreLocation.swift */, 35B56E9120DAF66A00C4923D /* FeatureCollection.swift */, @@ -679,6 +685,7 @@ 35B56E7520DAF47D00C4923D /* Polygon.swift in Sources */, 35B56E7F20DAF4E700C4923D /* LineString.swift in Sources */, 35B56E7A20DAF4BF00C4923D /* Point.swift in Sources */, + CE2EB999214C247100915A30 /* BoundingBox.swift in Sources */, 35B56E7020DAF41F00C4923D /* Ring.swift in Sources */, 3502F797201F53FC00399EFE /* Codable.swift in Sources */, 35B56E8E20DAF59700C4923D /* MultiPolygon.swift in Sources */, @@ -717,6 +724,7 @@ 35B56E7420DAF47D00C4923D /* Polygon.swift in Sources */, 35B56E7E20DAF4E700C4923D /* LineString.swift in Sources */, 35B56E7920DAF4BF00C4923D /* Point.swift in Sources */, + CE2EB998214C246A00915A30 /* BoundingBox.swift in Sources */, 35B56E6F20DAF41F00C4923D /* Ring.swift in Sources */, 3502F796201F53FC00399EFE /* Codable.swift in Sources */, 35B56E8D20DAF59700C4923D /* MultiPolygon.swift in Sources */, @@ -755,6 +763,7 @@ 35B56E7620DAF47D00C4923D /* Polygon.swift in Sources */, 35B56E8020DAF4E700C4923D /* LineString.swift in Sources */, 35B56E7B20DAF4BF00C4923D /* Point.swift in Sources */, + CE2EB99A214C247100915A30 /* BoundingBox.swift in Sources */, 35B56E7120DAF41F00C4923D /* Ring.swift in Sources */, 3502F798201F53FC00399EFE /* Codable.swift in Sources */, 35B56E8F20DAF59700C4923D /* MultiPolygon.swift in Sources */, @@ -793,6 +802,7 @@ 35B56E7720DAF47D00C4923D /* Polygon.swift in Sources */, 35B56E8120DAF4E700C4923D /* LineString.swift in Sources */, 35B56E7C20DAF4BF00C4923D /* Point.swift in Sources */, + CE2EB99B214C247200915A30 /* BoundingBox.swift in Sources */, 35B56E7220DAF41F00C4923D /* Ring.swift in Sources */, 3502F799201F53FC00399EFE /* Codable.swift in Sources */, 35B56E9020DAF59700C4923D /* MultiPolygon.swift in Sources */,