@@ -101,4 +101,155 @@ extension GeoJson {
101
101
return result
102
102
}
103
103
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
+
104
255
}
0 commit comments