From fb9b8cc49c26d20e05b1be4702d7fbbfca11aa42 Mon Sep 17 00:00:00 2001 From: udumft Date: Fri, 29 Oct 2021 15:31:04 +0300 Subject: [PATCH 1/4] vk-141-linestring-intersection: added Linestring.intersection method --- Sources/Turf/Geometries/LineString.swift | 23 ++++++++++++++++++++++- Sources/Turf/Turf.swift | 2 ++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Sources/Turf/Geometries/LineString.swift b/Sources/Turf/Geometries/LineString.swift index 09afb02b..deda0e6a 100644 --- a/Sources/Turf/Geometries/LineString.swift +++ b/Sources/Turf/Geometries/LineString.swift @@ -308,7 +308,7 @@ extension LineString { let direction = segment.0.direction(to: segment.1) let perpendicularPoint1 = coordinate.coordinate(at: maxDistance, facing: direction + 90) let perpendicularPoint2 = coordinate.coordinate(at: maxDistance, facing: direction - 90) - let intersectionPoint = intersection((perpendicularPoint1, perpendicularPoint2), segment) + let intersectionPoint = Turf.intersection((perpendicularPoint1, perpendicularPoint2), segment) let intersectionDistance: LocationDistance? = intersectionPoint != nil ? coordinate.distance(to: intersectionPoint!) : nil if distances.0 < closestDistance ?? .greatestFiniteMagnitude { @@ -364,4 +364,25 @@ extension LineString { // Ported from https://github.com/Turfjs/turf/blob/4e8342acb1dbd099f5e91c8ee27f05fb2647ee1b/packages/turf-simplify/lib/simplify.js coordinates = Simplifier.simplify(coordinates, tolerance: tolerance, highestQuality: highestQuality) } + + /** + Returns all intersections with another `LineString`. + + This function is roughly equivalent to the [turf-line-intersect](https://turfjs.org/docs/#lineIntersect) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/)). + */ + public func intersection(with line: LineString) -> [LocationCoordinate2D]? { + var intersections: [String : LocationCoordinate2D] = [:] + zip(coordinates.dropLast(), coordinates.dropFirst()).forEach { segment1 in + zip(line.coordinates.dropLast(), line.coordinates.dropFirst()).forEach { segment2 in + if let intersection = Turf.intersection(LineSegment(segment1.0, segment1.1), + LineSegment(segment2.0, segment2.1)) { + let key = "\(intersection.latitude),\(intersection.longitude)" + if intersections[key] == nil { + intersections[key] = intersection + } + } + } + } + return intersections.isEmpty ? ([LocationCoordinate2D]?).none : Array(intersections.values) + } } diff --git a/Sources/Turf/Turf.swift b/Sources/Turf/Turf.swift index 8129d7a1..4139b2b9 100644 --- a/Sources/Turf/Turf.swift +++ b/Sources/Turf/Turf.swift @@ -14,6 +14,8 @@ public typealias LineSegment = (LocationCoordinate2D, LocationCoordinate2D) Returns the intersection of two line segments. This function is roughly equivalent to the [turf-line-intersect](https://turfjs.org/docs/#lineIntersect) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/)), except that it only accepts individual line segments instead of whole line strings. + + - seealso: `LineString.intersection(with:)` */ public func intersection(_ line1: LineSegment, _ line2: LineSegment) -> LocationCoordinate2D? { // Ported from https://github.com/Turfjs/turf/blob/142e137ce0c758e2825a260ab32b24db0aa19439/packages/turf-point-on-line/index.js, in turn adapted from http://jsfiddle.net/justin_c_rounds/Gd2S2/light/ From 911149c44085776b77c33e1ee033e5f863431b2d Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 8 Nov 2021 12:17:39 +0300 Subject: [PATCH 2/4] vk-141-linesting-intersection: updated code comments; updated README reference; refactored `LineString.intersections` method to explicify line segments. --- README.md | 2 +- Sources/Turf/Geometries/LineString.swift | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ac08df76..ec18b569 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Turf.js | Turf for Swift [turf-helpers#radiansToDegrees](https://turfjs.org/docs/#radiansToDegrees) | `CLLocationDegrees.toDegrees()`
`LocationDegrees.toDegrees()` on Linux [turf-helpers#convertLength](https://turfjs.org/docs/#convertLength)
[turf-helpers#convertArea](https://turfjs.org/docs/#convertArea) | `Measurement.converted(to:)` [turf-length#length](https://turfjs.org/docs/#length) | `LineString.distance(from:to:)` -[turf-line-intersect#lineIntersect](https://turfjs.org/docs/#lineIntersect) | `intersection(_:_:)` +[turf-line-intersect#lineIntersect](https://turfjs.org/docs/#lineIntersect) | `LineString.intersections(with:)` [turf-line-slice#lineSlice](https://turfjs.org/docs/#lineSlice) | `LineString.sliced(from:to:)` [turf-line-slice-along#lineSliceAlong](https://turfjs.org/docs/#lineSliceAlong) | `LineString.trimmed(from:to:)` [turf-midpoint#midpoint](https://turfjs.org/docs/#midpoint) | `mid(_:_:)` diff --git a/Sources/Turf/Geometries/LineString.swift b/Sources/Turf/Geometries/LineString.swift index deda0e6a..e1ddfa93 100644 --- a/Sources/Turf/Geometries/LineString.swift +++ b/Sources/Turf/Geometries/LineString.swift @@ -31,6 +31,13 @@ public struct LineString: Equatable { public init(_ ring: Ring) { self.coordinates = ring.coordinates } + + /** + Representation of current `LineString` as an array of `LineSegment`s. + */ + var segments: [LineSegment] { + return zip(coordinates.dropLast(), coordinates.dropFirst()).map { LineSegment($0.0, $0.1) } + } } extension LineString: Codable { @@ -368,12 +375,17 @@ extension LineString { /** Returns all intersections with another `LineString`. - This function is roughly equivalent to the [turf-line-intersect](https://turfjs.org/docs/#lineIntersect) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/)). + This function is roughly equivalent to the [turf-line-intersect](https://turfjs.org/docs/#lineIntersect) package of Turf.js ([source code](https://github.com/Turfjs/turf/tree/master/packages/turf-line-intersect/)). Order of found intersections is not determined. + + You can also use `Turf.intersection(_:, _:)` if you need to find intersection of individual `LineSegment`s. + + - seealso: `Turf.intersection(_:, _:)` */ - public func intersection(with line: LineString) -> [LocationCoordinate2D]? { + public func intersections(with line: LineString) -> [LocationCoordinate2D] { var intersections: [String : LocationCoordinate2D] = [:] - zip(coordinates.dropLast(), coordinates.dropFirst()).forEach { segment1 in - zip(line.coordinates.dropLast(), line.coordinates.dropFirst()).forEach { segment2 in + + for segment1 in segments { + for segment2 in line.segments { if let intersection = Turf.intersection(LineSegment(segment1.0, segment1.1), LineSegment(segment2.0, segment2.1)) { let key = "\(intersection.latitude),\(intersection.longitude)" @@ -383,6 +395,6 @@ extension LineString { } } } - return intersections.isEmpty ? ([LocationCoordinate2D]?).none : Array(intersections.values) + return Array(intersections.values) } } From 0a4cd0c05717fa73537b9843bf438aa129f82ef7 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 9 Nov 2021 11:42:37 +0300 Subject: [PATCH 3/4] vk-141-linestring-intersection: refactored LineString.Intersection to use hashable struct; added Unit tests; fixed(?) a bug where intersections were not found on end points. --- Sources/Turf/Geometries/LineString.swift | 25 +++++++++++++++++------- Sources/Turf/Turf.swift | 4 ++-- Tests/TurfTests/LineStringTests.swift | 17 ++++++++++++++++ Tests/TurfTests/TurfTests.swift | 7 +++++++ 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Sources/Turf/Geometries/LineString.swift b/Sources/Turf/Geometries/LineString.swift index e1ddfa93..69bdc37f 100644 --- a/Sources/Turf/Geometries/LineString.swift +++ b/Sources/Turf/Geometries/LineString.swift @@ -382,19 +382,30 @@ extension LineString { - seealso: `Turf.intersection(_:, _:)` */ public func intersections(with line: LineString) -> [LocationCoordinate2D] { - var intersections: [String : LocationCoordinate2D] = [:] - + var intersections = Set() for segment1 in segments { for segment2 in line.segments { if let intersection = Turf.intersection(LineSegment(segment1.0, segment1.1), LineSegment(segment2.0, segment2.1)) { - let key = "\(intersection.latitude),\(intersection.longitude)" - if intersections[key] == nil { - intersections[key] = intersection - } + intersections.insert(.init(intersection)) } } } - return Array(intersections.values) + return intersections.map(\.locationCoordinate) + } + + private struct HashableCoordinate: Hashable { + let latitude: Double + let longitude: Double + + var locationCoordinate: LocationCoordinate2D { + return LocationCoordinate2D(latitude: latitude, + longitude: longitude) + } + + init(_ coordinate: LocationCoordinate2D) { + self.latitude = coordinate.latitude + self.longitude = coordinate.longitude + } } } diff --git a/Sources/Turf/Turf.swift b/Sources/Turf/Turf.swift index 4139b2b9..10e79892 100644 --- a/Sources/Turf/Turf.swift +++ b/Sources/Turf/Turf.swift @@ -37,9 +37,9 @@ public func intersection(_ line1: LineSegment, _ line2: LineSegment) -> Location longitude: line1.0.longitude + a * (line1.1.longitude - line1.0.longitude)) /// True if line 1 is finite and line 2 is infinite. - let intersectsWithLine1 = a > 0 && a < 1 + let intersectsWithLine1 = a >= 0 && a <= 1 /// True if line 2 is finite and line 1 is infinite. - let intersectsWithLine2 = b > 0 && b < 1 + let intersectsWithLine2 = b >= 0 && b <= 1 return intersectsWithLine1 && intersectsWithLine2 ? intersection : nil } diff --git a/Tests/TurfTests/LineStringTests.swift b/Tests/TurfTests/LineStringTests.swift index 8446925b..d5fe4563 100644 --- a/Tests/TurfTests/LineStringTests.swift +++ b/Tests/TurfTests/LineStringTests.swift @@ -410,4 +410,21 @@ class LineStringTests: XCTestCase { sliced = line1.trimmed(from: startDistance, to: stopDistance) XCTAssertNil(sliced, "should return nil") } + + func testIntersections() { + let lineString = LineString([.init(latitude: 2, longitude: 1), + .init(latitude: 2, longitude: 5), + .init(latitude: 2, longitude: 9)]) + + let intersectingLineString = LineString([.init(latitude: 4, longitude: 1), + .init(latitude: 0, longitude: 5), + .init(latitude: 2, longitude: 9)]) + + let intersections = lineString.intersections(with: intersectingLineString) + + XCTAssertEqual(intersections.sorted(by: { $0.longitude < $1.longitude }), + [.init(latitude: 2, longitude: 3), + .init(latitude: 2, longitude: 9)], + accuracy: 0, "LineString intersections are not correct.") + } } diff --git a/Tests/TurfTests/TurfTests.swift b/Tests/TurfTests/TurfTests.swift index 8e3681a0..5344870d 100644 --- a/Tests/TurfTests/TurfTests.swift +++ b/Tests/TurfTests/TurfTests.swift @@ -40,6 +40,13 @@ class TurfTests: XCTestCase { XCTAssertEqual(a, coord1) } + func testIntersectionOnEnd() { + let coord1 = LocationCoordinate2D(latitude: 20, longitude: 20) + let a = intersection((LocationCoordinate2D(latitude: 20, longitude: 20), LocationCoordinate2D(latitude: 40, longitude: 40)), + (LocationCoordinate2D(latitude: 20, longitude: 20), LocationCoordinate2D(latitude: 40, longitude: 20))) + XCTAssertEqual(a, coord1) + } + func testCLLocationDegrees() { let degree: CLLocationDegrees = 100 let a = degree.toRadians() From 16049b256ba7d759ef822e300574d6df80d578f8 Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 10 Nov 2021 12:08:42 +0300 Subject: [PATCH 4/4] vk-141-linestring-intersection: linux build fix; unit tests extended --- Sources/Turf/Geometries/LineString.swift | 2 +- Tests/TurfTests/LineStringTests.swift | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/Turf/Geometries/LineString.swift b/Sources/Turf/Geometries/LineString.swift index 69bdc37f..614c2c33 100644 --- a/Sources/Turf/Geometries/LineString.swift +++ b/Sources/Turf/Geometries/LineString.swift @@ -391,7 +391,7 @@ extension LineString { } } } - return intersections.map(\.locationCoordinate) + return intersections.map { $0.locationCoordinate } } private struct HashableCoordinate: Hashable { diff --git a/Tests/TurfTests/LineStringTests.swift b/Tests/TurfTests/LineStringTests.swift index d5fe4563..c0d047c1 100644 --- a/Tests/TurfTests/LineStringTests.swift +++ b/Tests/TurfTests/LineStringTests.swift @@ -420,11 +420,25 @@ class LineStringTests: XCTestCase { .init(latitude: 0, longitude: 5), .init(latitude: 2, longitude: 9)]) - let intersections = lineString.intersections(with: intersectingLineString) + var intersections = lineString.intersections(with: intersectingLineString) XCTAssertEqual(intersections.sorted(by: { $0.longitude < $1.longitude }), [.init(latitude: 2, longitude: 3), .init(latitude: 2, longitude: 9)], accuracy: 0, "LineString intersections are not correct.") + + let notIntersectingLineString = LineString([.init(latitude: 1, longitude: 1), + .init(latitude: 1, longitude: 5), + .init(latitude: 1, longitude: 9)]) + + intersections = lineString.intersections(with: notIntersectingLineString) + + XCTAssertTrue(intersections.isEmpty, "Found impossible LineString intersection(s).") + + let emptyLineString = LineString([]) + + intersections = lineString.intersections(with: emptyLineString) + + XCTAssertTrue(intersections.isEmpty, "Found impossible LineString intersection(s).") } }