Skip to content

Commit 6201957

Browse files
authored
Added overlappingSegments(tolerance:) and estimatedOverlap(tolerance:) for self-overlapping GeoJSONs (#71)
1 parent 5ce6e84 commit 6201957

File tree

4 files changed

+182
-0
lines changed

4 files changed

+182
-0
lines changed

Sources/GISTools/Algorithms/LineOverlap.swift

+151
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,155 @@ extension GeoJson {
101101
return result
102102
}
103103

104+
/// Returns the overlapping segments with the receiver itself.
105+
///
106+
/// This implementation is streamlined for finding self-overlaps.
107+
///
108+
/// - Note: Altitude values will be ignored.
109+
///
110+
/// - Parameters:
111+
/// - tolerance: The tolerance, in meters. Choosing this too small might lead to memory explosion.
112+
/// Using `0.0` will only return segments that *exactly* overlap.
113+
///
114+
/// - Returns: All segments that at least overlap with one other segment. Each segment will
115+
/// appear in the result only once.
116+
public func overlappingSegments(
117+
tolerance: CLLocationDistance
118+
) -> MultiLineString? {
119+
let tolerance = abs(tolerance)
120+
let distanceFunction = FrechetDistanceFunction.haversine
121+
122+
guard let line = if tolerance > 0.0 {
123+
LineString(lineSegments)?.evenlyDivided(segmentLength: tolerance)
124+
}
125+
else {
126+
LineString(lineSegments)
127+
}
128+
else {
129+
return nil
130+
}
131+
132+
let p = line.allCoordinates
133+
var ca: [OrderedIndexPair: Double] = [:]
134+
135+
func index(_ pI: Int, _ qI: Int) -> OrderedIndexPair {
136+
.init(pI, qI)
137+
}
138+
139+
// Distances between each coordinate pair
140+
for i in 0 ..< p.count {
141+
for j in i + 1 ..< p.count {
142+
let distance = distanceFunction.distance(between: p[i], and: p[j])
143+
if distance > tolerance { continue }
144+
ca[index(i, j)] = distance
145+
}
146+
}
147+
148+
// Find coordinate pairs within the tolerance
149+
var pairs: Set<OrderedIndexPair> = []
150+
151+
var i = 0
152+
outer: while i < p.count - 1 {
153+
defer { i += 1 }
154+
155+
var j = i + 2
156+
while ca[index(i, j), default: Double.greatestFiniteMagnitude] <= tolerance {
157+
j += 1
158+
if j == p.count { break outer }
159+
}
160+
161+
while j < p.count {
162+
defer { j += 1 }
163+
164+
if ca[index(i, j), default: Double.greatestFiniteMagnitude] <= tolerance {
165+
pairs.insert(index(i, j))
166+
}
167+
}
168+
}
169+
170+
// Find overlapping segments
171+
var scratchList = pairs.sorted()
172+
var result: Set<OrderedIndexPair> = []
173+
while scratchList.isNotEmpty {
174+
let candidate = scratchList.removeFirst()
175+
176+
if candidate.first > 0,
177+
candidate.second > 0,
178+
pairs.contains(index(candidate.first - 1, candidate.second - 1))
179+
{
180+
result.insert(index(candidate.first, candidate.first - 1))
181+
result.insert(index(candidate.second, candidate.second - 1))
182+
continue
183+
}
184+
185+
if candidate.first > 0,
186+
candidate.second < p.count - 1,
187+
pairs.contains(index(candidate.first - 1, candidate.second + 1))
188+
{
189+
result.insert(index(candidate.first, candidate.first - 1))
190+
result.insert(index(candidate.second, candidate.second + 1))
191+
continue
192+
}
193+
194+
if candidate.first < p.count - 1,
195+
candidate.second > 0,
196+
pairs.contains(index(candidate.first + 1, candidate.second - 1))
197+
{
198+
result.insert(index(candidate.first, candidate.first + 1))
199+
result.insert(index(candidate.second, candidate.second - 1))
200+
continue
201+
}
202+
203+
if candidate.first < p.count - 1,
204+
candidate.second < p.count - 1,
205+
pairs.contains(index(candidate.first + 1, candidate.second + 1))
206+
{
207+
result.insert(index(candidate.first, candidate.first + 1))
208+
result.insert(index(candidate.second, candidate.second + 1))
209+
continue
210+
}
211+
}
212+
213+
return MultiLineString(result.map({ LineString(unchecked: [p[$0.first], p[$0.second]]) }))
214+
}
215+
216+
/// An estimate of how much the receiver overlaps with itself.
217+
///
218+
/// - Parameters:
219+
/// - tolerance: The tolerance, in meters. Choosing this too small might lead to memory explosion.
220+
/// Using `0.0` will only use segments that *exactly* overlap.
221+
///
222+
/// - Returns: The length of all segments that overlap within `tolerance`.
223+
public func estimatedOverlap(
224+
tolerance: CLLocationDistance
225+
) -> Double {
226+
guard let result = overlappingSegments(tolerance: tolerance) else { return 0.0 }
227+
228+
return result.length
229+
}
230+
231+
}
232+
233+
// MARK: - Private
234+
235+
private struct OrderedIndexPair: Hashable, Comparable, CustomStringConvertible {
236+
237+
let first: Int
238+
let second: Int
239+
240+
init(_ first: Int, _ second: Int) {
241+
self.first = min(first, second)
242+
self.second = max(first, second)
243+
}
244+
245+
var description: String {
246+
"(\(first)-\(second))"
247+
}
248+
249+
static func < (lhs: OrderedIndexPair, rhs: OrderedIndexPair) -> Bool {
250+
if lhs.first < rhs.first { return true }
251+
if lhs.first > rhs.first { return false }
252+
return lhs.second < rhs.second
253+
}
254+
104255
}

Sources/GISTools/Extensions/ArrayExtensions.swift

+7
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ extension Array {
157157
}
158158
}
159159

160+
/// Split the array into equal sized chunks.
161+
func chunked(into chunkSize: Int) -> [[Element]] {
162+
stride(from: 0, to: count, by: chunkSize).map { chunk in
163+
Array(self[chunk ..< Swift.min(chunk + chunkSize, count)])
164+
}
165+
}
166+
160167
/// Fetches an element from the array, or returns nil if the index is out of bounds.
161168
///
162169
/// - parameter index: The index in the array. May be negative. In this case, -1 will be the last element, -2 the second-to-last, and so on.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// Created by Thomas Rasch on 21.04.23.
3+
//
4+
5+
import Foundation
6+
7+
// MARK: Private
8+
9+
extension Equatable {
10+
11+
func isIn(_ c: [Self]) -> Bool {
12+
return c.contains(self)
13+
}
14+
15+
func isNotIn(_ c: [Self]) -> Bool {
16+
return !c.contains(self)
17+
}
18+
19+
}

Sources/GISTools/Extensions/SetExtensions.swift

+5
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ extension Set {
99
Array(self)
1010
}
1111

12+
/// A Boolean value indicating whether the collection is not empty.
13+
var isNotEmpty: Bool {
14+
!isEmpty
15+
}
16+
1217
}

0 commit comments

Comments
 (0)