@@ -27,7 +27,7 @@ import com.google.maps.android.MathUtil.sinFromHav
27
27
import com.google.maps.android.MathUtil.sinSumFromHav
28
28
import com.google.maps.android.MathUtil.wrap
29
29
import com.google.maps.android.SphericalUtil.computeDistanceBetween
30
- import java.util.Stack
30
+ import kotlin.collections.ArrayDeque
31
31
import kotlin.math.cos
32
32
import kotlin.math.max
33
33
import kotlin.math.min
@@ -300,65 +300,93 @@ object PolyUtil {
300
300
* @return a simplified poly produced by the Douglas-Peucker algorithm
301
301
*/
302
302
@JvmStatic
303
- fun simplify (poly : MutableList <LatLng >, tolerance : Double ): List <LatLng > {
304
- val n = poly.size
305
- require(n >= 1 ) { " Polyline must have at least 1 point" }
303
+ fun simplify (poly : List <LatLng >, tolerance : Double ): List <LatLng > {
304
+ require(poly.isNotEmpty()) { " Polyline must have at least 1 point" }
306
305
require(tolerance > 0 ) { " Tolerance must be greater than zero" }
307
306
308
- val closedPolygon = isClosedPolygon(poly)
309
- var lastPoint: LatLng ? = null
310
-
311
- // Check if the provided poly is a closed polygon
312
- if (closedPolygon) {
313
- // Add a small offset to the last point for Douglas-Peucker on polygons (see #201)
314
- val OFFSET = 0.00000000001
315
- lastPoint = poly.last()
316
- poly.removeAt(poly.size - 1 )
317
- poly.add(LatLng (lastPoint.latitude + OFFSET , lastPoint.longitude + OFFSET ))
307
+ // The simplification process is handled by the Douglas-Peucker algorithm,
308
+ // which is implemented in a separate private function for clarity.
309
+ // Before we can apply the algorithm, we need to handle a special case for closed polygons.
310
+ val workingPoly = if (isClosedPolygon(poly)) {
311
+ // For closed polygons, the Douglas-Peucker algorithm needs to "see" the connection
312
+ // between the last and first points. A common trick to achieve this is to temporarily
313
+ // open the polygon and add a point that is very close to the last point. This ensures
314
+ // that the simplification process takes the closing segment into account.
315
+ val lastPoint = poly.last()
316
+ val offset = 0.00000000001
317
+ poly.toMutableList().apply {
318
+ removeAt(size - 1 )
319
+ add(LatLng (lastPoint.latitude + offset, lastPoint.longitude + offset))
320
+ }
321
+ } else {
322
+ poly
318
323
}
319
324
320
- var maxIdx = 0
321
- val stack = Stack <IntArray >()
322
- val dists = DoubleArray (n)
323
- dists[0 ] = 1.0
324
- dists[n - 1 ] = 1.0
325
- var maxDist: Double
326
- var dist: Double
327
- var current: IntArray
325
+ // The douglasPeucker function returns a boolean array indicating which points to keep.
326
+ val pointsToKeep = douglasPeucker(workingPoly, tolerance)
327
+
328
+ // We then filter the original, unmodified polyline based on the results of the
329
+ // simplification algorithm. This ensures that the original points are preserved in the
330
+ // final output.
331
+ return poly.filterIndexed { index, _ -> pointsToKeep[index] }
332
+ }
333
+
334
+ /* *
335
+ * Implements the Douglas-Peucker algorithm for simplifying a polyline.
336
+ *
337
+ * The algorithm works by recursively dividing the polyline into smaller segments and finding
338
+ * the point that is farthest from the line segment connecting the start and end points.
339
+ * If this point is farther than the specified tolerance, it is kept, and the algorithm is
340
+ * applied recursively to the two new segments.
341
+ *
342
+ * @param poly The polyline to be simplified.
343
+ * @param tolerance The tolerance in meters.
344
+ * @return A boolean array where `true` indicates that the point at the corresponding index
345
+ * should be kept in the simplified polyline.
346
+ */
347
+ private fun douglasPeucker (poly : List <LatLng >, tolerance : Double ): BooleanArray {
348
+ val n = poly.size
349
+ // We start with a boolean array that will mark the points to keep.
350
+ // Initially, only the first and last points are marked for keeping.
351
+ val keepPoint = BooleanArray (n) { false }
352
+ keepPoint[0 ] = true
353
+ keepPoint[n - 1 ] = true
328
354
355
+ // The algorithm is only needed if the polyline has more than 2 points.
329
356
if (n > 2 ) {
330
- val stackVal = intArrayOf(0 , n - 1 )
331
- stack.push(stackVal)
357
+ // We use a stack (implemented with ArrayDeque for efficiency) to manage the
358
+ // segments that we need to process. Initially, this contains the entire polyline.
359
+ val stack = ArrayDeque <Pair <Int , Int >>()
360
+ stack.addLast(0 to n - 1 )
361
+
362
+ // We process segments from the stack until it's empty.
332
363
while (stack.isNotEmpty()) {
333
- current = stack.pop()
334
- maxDist = 0.0
335
- for (idx in current[0 ] + 1 until current[1 ]) {
336
- dist = distanceToLine(poly[idx], poly[current[0 ]], poly[current[1 ]])
364
+ val (start, end) = stack.removeLast()
365
+ var maxDist = 0.0
366
+ var maxIdx = 0
367
+
368
+ // For the current segment, we find the point that is farthest from the line
369
+ // connecting the start and end points.
370
+ for (idx in start + 1 until end) {
371
+ val dist = distanceToLine(poly[idx], poly[start], poly[end])
337
372
if (dist > maxDist) {
338
373
maxDist = dist
339
374
maxIdx = idx
340
375
}
341
376
}
377
+
378
+ // If the farthest point is farther than the tolerance, we mark it to be kept.
379
+ // We then push two new segments onto the stack to be processed recursively:
380
+ // one from the start to the farthest point, and one from the farthest point to the end.
342
381
if (maxDist > tolerance) {
343
- dists[maxIdx] = maxDist
344
- val stackValCurMax = intArrayOf(current[0 ], maxIdx)
345
- stack.push(stackValCurMax)
346
- val stackValMaxCur = intArrayOf(maxIdx, current[1 ])
347
- stack.push(stackValMaxCur)
382
+ keepPoint[maxIdx] = true
383
+ stack.addLast(start to maxIdx)
384
+ stack.addLast(maxIdx to end)
348
385
}
349
386
}
350
387
}
351
388
352
- if (closedPolygon) {
353
- // Replace last point w/ offset with the original last point to re-close the polygon
354
- poly.removeAt(poly.size - 1 )
355
- if (lastPoint != null ) {
356
- poly.add(lastPoint)
357
- }
358
- }
359
-
360
- // Generate the simplified line
361
- return poly.filterIndexed { idx, _ -> dists[idx] != 0.0 }
389
+ return keepPoint
362
390
}
363
391
364
392
/* *
0 commit comments