From 4967e4140428c5b2ab852108a4c66d691a5158ec Mon Sep 17 00:00:00 2001 From: Jeroen Bloemscheer Date: Fri, 17 Jul 2026 07:34:04 +0200 Subject: [PATCH] Use explicit stack in DouglasPeuckerLineSimplifier (#1127) Recursive simplifySection can throw StackOverflowError on long, highly oscillatory linework where each split peels one vertex (depth O(n)). Rewrite as an iterative pair-stack, preserving left-then-right split order and results. Add a zig-zag regression test that forces deep unbalanced splits. --- .../DouglasPeuckerLineSimplifier.java | 70 +++++++++++++------ .../DouglasPeuckerSimplifierTest.java | 19 +++++ 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java index e1121fa5a9..6fe693af72 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java @@ -95,30 +95,58 @@ private void simplifyRingEndpoint(CoordinateList pts) { private LineSegment seg = new LineSegment(); - private void simplifySection(int i, int j) + /** + * Douglas-Peucker section simplification using an explicit stack. + *

+ * The classic recursive form can throw {@link StackOverflowError} on long, + * highly oscillatory linework where the furthest vertex is always adjacent + * to one endpoint (depth O(n)). An explicit stack keeps the same left-then-right + * split order while bounding memory by heap rather than the JVM call stack + * (see JTS #1127). + */ + private void simplifySection(int i0, int j0) { - if((i+1) == j) { - return; - } - seg.p0 = pts[i]; - seg.p1 = pts[j]; - double maxDistance = -1.0; - int maxIndex = i; - for (int k = i + 1; k < j; k++) { - double distance = seg.distance(pts[k]); - if (distance > maxDistance) { - maxDistance = distance; - maxIndex = k; + // Pair stack: [i, j, i, j, ...]. Worst-case depth is O(n). + int[] stack = new int[Math.max(4, pts.length * 2)]; + int top = 0; + stack[top++] = i0; + stack[top++] = j0; + + while (top > 0) { + int j = stack[--top]; + int i = stack[--top]; + if ((i + 1) == j) { + continue; } - } - if (maxDistance <= distanceTolerance) { - for(int k = i + 1; k < j; k++) { - usePt[k] = false; + seg.p0 = pts[i]; + seg.p1 = pts[j]; + double maxDistance = -1.0; + int maxIndex = i; + for (int k = i + 1; k < j; k++) { + double distance = seg.distance(pts[k]); + if (distance > maxDistance) { + maxDistance = distance; + maxIndex = k; + } + } + if (maxDistance <= distanceTolerance) { + for (int k = i + 1; k < j; k++) { + usePt[k] = false; + } + } + else { + // Grow if needed (should not for size 2*n, but stay safe). + if (top + 4 > stack.length) { + int[] grown = new int[stack.length * 2]; + System.arraycopy(stack, 0, grown, 0, top); + stack = grown; + } + // Push right then left so left is processed first (same as recursion). + stack[top++] = maxIndex; + stack[top++] = j; + stack[top++] = i; + stack[top++] = maxIndex; } - } - else { - simplifySection(i, maxIndex); - simplifySection(maxIndex, j); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java index 7fa33786b7..c3d6e68a5e 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java @@ -165,6 +165,25 @@ public void testPolygonEndpointCollapse() { "POLYGON EMPTY" ); } + + /** + * High-frequency zig-zag forces O(n) recursion depth in classic recursive + * Douglas-Peucker (each split peels one vertex). Matches the failure mode + * in JTS #1127 (noisy GPS tracks). Explicit-stack implementation must + * complete without StackOverflowError and keep every vertex at tolerance 0. + */ + public void testLongUnbalancedLineDoesNotStackOverflow() { + int n = 5000; + StringBuilder sb = new StringBuilder("LINESTRING (0 0"); + for (int i = 1; i < n; i++) { + // alternate height so the furthest vertex is always adjacent to an endpoint + sb.append(", ").append(i).append(i % 2 == 0 ? " 0" : " 1"); + } + sb.append(")"); + Geometry geom = read(sb.toString()); + Geometry result = DouglasPeuckerSimplifier.simplify(geom, 0.0); + assertEquals(n, result.getNumPoints()); + } private void checkDP(String wkt, double tolerance, String wktExpected) { Geometry geom = read(wkt);