Skip to content

Commit 2f2374d

Browse files
authored
fix(PolyUtil): Refactored the PolyUtil.simplify function to not mutate its inputs (#1585)
* refactor(PolyUtil): make simplify more idiomatic Refactored the `simplify` function to be more idiomatic Kotlin. - Extracted the core Douglas-Peucker algorithm into a private helper function. - Replaced `java.util.Stack` with `ArrayDeque`. - Improved variable scoping. - The function no longer modifies the input list. * refactor: Improve variable name in PolyUtil.simplify
1 parent d51930c commit 2f2374d

File tree

1 file changed

+71
-43
lines changed
  • library/src/main/java/com/google/maps/android

1 file changed

+71
-43
lines changed

library/src/main/java/com/google/maps/android/PolyUtil.kt

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import com.google.maps.android.MathUtil.sinFromHav
2727
import com.google.maps.android.MathUtil.sinSumFromHav
2828
import com.google.maps.android.MathUtil.wrap
2929
import com.google.maps.android.SphericalUtil.computeDistanceBetween
30-
import java.util.Stack
30+
import kotlin.collections.ArrayDeque
3131
import kotlin.math.cos
3232
import kotlin.math.max
3333
import kotlin.math.min
@@ -300,65 +300,93 @@ object PolyUtil {
300300
* @return a simplified poly produced by the Douglas-Peucker algorithm
301301
*/
302302
@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" }
306305
require(tolerance > 0) { "Tolerance must be greater than zero" }
307306

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
318323
}
319324

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
328354

355+
// The algorithm is only needed if the polyline has more than 2 points.
329356
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.
332363
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])
337372
if (dist > maxDist) {
338373
maxDist = dist
339374
maxIdx = idx
340375
}
341376
}
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.
342381
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)
348385
}
349386
}
350387
}
351388

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
362390
}
363391

364392
/**

0 commit comments

Comments
 (0)