From 9f02cdead71807b86fd6cdbc86f73886967a3e8a Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 14 Mar 2024 20:54:40 -0700 Subject: [PATCH] Improve generation of var buffer caps --- .../jts/operation/buffer/VariableBuffer.java | 126 ++++++++++++------ .../operation/buffer/VariableBufferTest.java | 40 ++++-- 2 files changed, 118 insertions(+), 48 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java index 3624295600..6874f1d2c8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java @@ -15,6 +15,7 @@ import java.util.List; import org.locationtech.jts.algorithm.Angle; +import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.Geometry; @@ -38,6 +39,8 @@ */ public class VariableBuffer { + private static final int MIN_CAP_SEG_LEN_FACTOR = 4; + /** * Creates a buffer polygon along a line with the buffer distance interpolated * between a start distance and an end distance. @@ -270,22 +273,28 @@ public Geometry getResult() { private Polygon segmentBuffer(Coordinate p0, Coordinate p1, double dist0, double dist1) { /** - * Skip polygon if both distances are zero + * Skip buffer polygon if both distances are zero */ if (dist0 <= 0 && dist1 <= 0) return null; /** - * Compute for increasing distance only, so flip if needed + * Generation algorithm requires increasing distance, so flip if needed */ if (dist0 > dist1) { - return segmentBuffer(p1, p0, dist1, dist0); + return segmentBufferOriented(p1, p0, dist1, dist0); } - - // forward tangent line + return segmentBufferOriented(p0, p1, dist0, dist1); + } + + private Polygon segmentBufferOriented(Coordinate p0, Coordinate p1, + double dist0, double dist1) { + //-- Assert: dist0 <= dist1 + + //-- forward tangent line LineSegment tangent = outerTangent(p0, dist0, p1, dist1); - // if tangent is null then compute a buffer for largest circle + //-- if tangent is null then compute a buffer for largest circle if (tangent == null) { Coordinate center = p0; double dist = dist0; @@ -296,37 +305,32 @@ private Polygon segmentBuffer(Coordinate p0, Coordinate p1, return circle(center, dist); } - Coordinate t0 = tangent.getCoordinate(0); - Coordinate t1 = tangent.getCoordinate(1); - - // reverse tangent line on other side of segment - LineSegment seg = new LineSegment(p0, p1); - Coordinate tr0 = seg.reflect(t0); - Coordinate tr1 = seg.reflect(t1); - //-- avoid numeric jitter if first distance is zero - if (dist0 == 0) - tr0 = p0.copy(); + //-- reverse tangent line on other side of segment + LineSegment tangentReflect = reflect(tangent, p0, p1, dist0); CoordinateList coords = new CoordinateList(); - coords.add(t0, false); - coords.add(t1, false); - - // end cap - addCap(p1, dist1, t1, tr1, coords); - - coords.add(tr1, false); - coords.add(tr0, false); + //-- end cap + addCap(p1, dist1, tangent.p1, tangentReflect.p1, coords); + //-- start cap + addCap(p0, dist0, tangentReflect.p0, tangent.p0, coords); - // start cap - addCap(p0, dist0, tr0, t0, coords); - - // close - coords.add(t0, false); + coords.closeRing(); Coordinate[] pts = coords.toCoordinateArray(); Polygon polygon = geomFactory.createPolygon(pts); +//System.out.println(polygon); return polygon; } + + private LineSegment reflect(LineSegment seg, Coordinate p0, Coordinate p1, double dist0) { + LineSegment line = new LineSegment(p0, p1); + Coordinate r0 = line.reflect(seg.p0); + Coordinate r1 = line.reflect(seg.p1); + //-- avoid numeric jitter if first distance is zero (second dist must be > 0) + if (dist0 == 0) + r0 = p0.copy(); + return new LineSegment(r0, r1); + } /** * Returns a circular polygon. @@ -350,6 +354,10 @@ private Polygon circle(Coordinate center, double radius) { /** * Adds a semi-circular cap CCW around the point p. + * <>p> + * The vertices in caps are generated at fixed angles around a point. + * This allows caps at the same point to share vertices, + * which reduces artifacts when the segment buffers are merged. * * @param p the centre point of the cap * @param r the cap radius @@ -358,12 +366,14 @@ private Polygon circle(Coordinate center, double radius) { * @param coords the coordinate list to add to */ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, CoordinateList coords) { - //-- handle zero-width at vertex + //-- if radius is zero just copy the vertex if (r == 0) { coords.add(p.copy(), false); return; } + coords.add(t1, false); + double angStart = Angle.angle(p, t1); double angEnd = Angle.angle(p, t2); if (angStart < angEnd) @@ -372,18 +382,55 @@ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, Coordi int indexStart = capAngleIndex(angStart); int indexEnd = capAngleIndex(angEnd); - for (int i = indexStart; i > indexEnd; i--) { - // use negative increment to create points CW + double capSegLen = r * 2 * Math.sin(Math.PI / 4 / quadrantSegs); + double minSegLen = capSegLen / MIN_CAP_SEG_LEN_FACTOR; + + for (int i = indexStart; i >= indexEnd; i--) { + //-- use negative increment to create points CW double ang = capAngle(i); - coords.add( projectPolar(p, r, ang), false ); + Coordinate capPt = projectPolar(p, r, ang); + + boolean isCapPointHighQuality = true; + /** + * Due to the fixed locations of the cap points, + * a start or end cap point might create + * a "reversed" segment to the next tangent point. + * This causes an unwanted narrow spike in the buffer curve, + * which can cause holes in the final buffer polygon. + * These checks remove these points. + */ + if (i == indexStart + && Orientation.CLOCKWISE != Orientation.index(p, t1, capPt)) { + isCapPointHighQuality = false; + } + else if (i == indexEnd + && Orientation.COUNTERCLOCKWISE != Orientation.index(p, t2, capPt)) { + isCapPointHighQuality = false; + } + + /** + * Remove short segments between the cap and the tangent segments. + */ + if (capPt.distance(t1) < minSegLen) { + isCapPointHighQuality = false; + } + else if (capPt.distance(t2) < minSegLen) { + isCapPointHighQuality = false; + } + + if (isCapPointHighQuality) { + coords.add(capPt, false ); + } } + + coords.add(t2, false); } /** - * Computes the angle for the given cap point index. + * Computes the actual angle for a cap angle index. * - * @param index the fillet angle index - * @return + * @param index the cap angle index + * @return the angle */ private double capAngle(int index) { double capSegAng = Math.PI / 2 / quadrantSegs; @@ -392,15 +439,13 @@ private double capAngle(int index) { /** * Computes the canonical cap point index for a given angle. - * The angle is rounded down to the next lower - * index. + * The angle is rounded down to the next lower index. *

* In order to reduce the number of points created by overlapping end caps, * cap points are generated at the same locations around a circle. * The index is the index of the points around the circle, * with 0 being the point at (1,0). - * The total number of points around the circle is - * 4 * quadrantSegs. + * The total number of points around the circle is 4 * quadrantSegs. * * @param ang the angle * @return the index for the angle. @@ -414,6 +459,7 @@ private int capAngleIndex(double ang) { /** * Computes the two circumference points defining the outer tangent line * between two circles. + * The tangent line may be null if one circle mostly overlaps the other. *

* For the algorithm see Wikipedia. * diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java index b5e65a94e8..c0370b2f1a 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java @@ -43,52 +43,76 @@ public void testZeroLength() { public void testSegmentInverseDist() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 1, - "POLYGON ((200.09 99.00405823463417, 100.9 90.04058234634172, 100 90, 98.04909677983872 90.19214719596769, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98.04909677983872 109.80785280403231, 100 110, 100.9 109.95941765365829, 200.09 100.99594176536583, 200.19509032201614 100.98078528040323, 200.3826834323651 100.9238795325113, 200.5555702330196 100.83146961230254, 200.70710678118655 100.70710678118655, 200.83146961230256 100.55557023301961, 200.92387953251128 100.38268343236508, 200.98078528040324 100.19509032201613, 201 100, 200.98078528040324 99.80490967798387, 200.92387953251128 99.61731656763492, 200.83146961230256 99.44442976698039, 200.70710678118655 99.29289321881345, 200.5555702330196 99.16853038769746, 200.3826834323651 99.0761204674887, 200.09 99.00405823463417))" + "POLYGON ((100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 100.9 109.96, 200.09 101, 200.2 100.98, 200.38 100.92, 200.56 100.83, 200.71 100.71, 200.83 100.56, 200.92 100.38, 200.98 100.2, 201 100, 200.98 99.8, 200.92 99.62, 200.83 99.44, 200.71 99.29, 200.56 99.17, 200.38 99.08, 200.2 99.02, 200.09 99, 100.9 90.04, 100 90))" ); } public void testSegmentSameDist() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 10, - "POLYGON ((90.192 101.951, 90.761 103.827, 91.685 105.556, 92.929 107.071, 94.444 108.315, 96.173 109.239, 98.049 109.808, 100 110, 200 110, 201.951 109.808, 203.827 109.239, 205.556 108.315, 207.071 107.071, 208.315 105.556, 209.239 103.827, 209.808 101.951, 210 100, 209.808 98.049, 209.239 96.173, 208.315 94.444, 207.071 92.929, 205.556 91.685, 203.827 90.761, 201.951 90.192, 200 90, 100 90, 98.049 90.192, 96.173 90.761, 94.444 91.685, 92.929 92.929, 91.685 94.444, 90.761 96.173, 90.192 98.049, 90 100, 90.192 101.951))" + "POLYGON ((201.95 109.81, 203.83 109.24, 205.56 108.31, 207.07 107.07, 208.31 105.56, 209.24 103.83, 209.81 101.95, 210 100, 209.81 98.05, 209.24 96.17, 208.31 94.44, 207.07 92.93, 205.56 91.69, 203.83 90.76, 201.95 90.19, 200 90, 100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 200 110, 201.95 109.81))" ); } public void testOneSegment() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 30, -"POLYGON ((98 109.79795897113272, 194 129.39387691339815, 194.14729033951616 129.42355841209692, 200 130, 205.85270966048384 129.42355841209692, 211.4805029709527 127.7163859753386, 216.66710699058808 124.94408836907635, 221.21320343559643 121.21320343559643, 224.94408836907635 116.66710699058807, 227.7163859753386 111.4805029709527, 229.42355841209692 105.85270966048385, 230 100, 229.42355841209692 94.14729033951615, 227.7163859753386 88.5194970290473, 224.94408836907635 83.33289300941193, 221.21320343559643 78.78679656440357, 216.66710699058808 75.05591163092365, 211.4805029709527 72.2836140246614, 205.85270966048384 70.57644158790309, 200 70, 194 70.60612308660184, 98 90.20204102886728, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98 109.79795897113272))" +"POLYGON ((200 130, 205.85 129.42, 211.48 127.72, 216.67 124.94, 221.21 121.21, 224.94 116.67, 227.72 111.48, 229.42 105.85, 230 100, 229.42 94.15, 227.72 88.52, 224.94 83.33, 221.21 78.79, 216.67 75.06, 211.48 72.28, 205.85 70.58, 200 70, 194 70.61, 98 90.2, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98 109.8, 194 129.39, 200 130))" ); } public void testSegments2() { checkBuffer("LINESTRING( 0 0, 40 40, 60 -20)", 10, 20, - "POLYGON ((53.52863576494982 45.80469132164433, 78.37960104024995 -12.113919503248614, 78.47759065022574 -12.346331352698204, 79.61570560806462 -16.098193559677433, 80 -20, 79.61570560806462 -23.901806440322567, 78.47759065022574 -27.653668647301796, 76.62939224605091 -31.111404660392044, 74.14213562373095 -34.14213562373095, 71.11140466039204 -36.629392246050905, 67.6536686473018 -38.477590650225736, 63.90180644032257 -39.61570560806461, 60 -40, 56.09819355967743 -39.61570560806461, 52.34633135269821 -38.477590650225736, 48.88859533960796 -36.629392246050905, 45.85786437626905 -34.14213562373095, 43.370607753949095 -31.111404660392044, 40.56467086974921 -24.718896226748868, 31.314401806419635 13.379424895487343, 6.456226258387812 -7.636566145886759, 5.555702330196018 -8.314696123025454, 3.8268343236509 -9.238795325112866, 1.950903220161283 -9.807852804032304, 0 -10, -1.9509032201612866 -9.807852804032303, -3.8268343236509033 -9.238795325112864, -5.555702330196022 -8.314696123025453, -7.071067811865477 -7.071067811865475, -8.314696123025454 -5.55570233019602, -9.238795325112868 -3.8268343236508966, -9.807852804032304 -1.9509032201612837, -10 0, -9.807852804032304 1.9509032201612861, -9.238795325112868 3.826834323650899, -8.314696123025453 5.555702330196022, -7.636566145886759 6.456226258387811, 28.75793640390754 49.5044428085851, 29.59042683391263 50.40957316608737, 31.821250844443494 52.240363117601376, 34.366379598327015 53.60076277898068, 37.12800522487612 54.4384927541594, 40 54.721359549995796, 42.87199477512389 54.4384927541594, 45.633620401672985 53.60076277898068, 48.17874915555651 52.240363117601376, 50.40957316608737 50.40957316608737, 52.240363117601376 48.17874915555651, 53.52863576494982 45.80469132164433))" + "POLYGON ((79.62 -16.1, 80 -20, 79.62 -23.9, 78.48 -27.65, 76.63 -31.11, 74.14 -34.14, 71.11 -36.63, 67.65 -38.48, 63.9 -39.62, 60 -40, 56.1 -39.62, 52.35 -38.48, 48.89 -36.63, 45.86 -34.14, 43.37 -31.11, 41.52 -27.65, 40.56 -24.72, 31.31 13.38, 6.46 -7.64, 5.56 -8.31, 3.83 -9.24, 1.95 -9.81, 0 -10, -1.95 -9.81, -3.83 -9.24, -5.56 -8.31, -7.07 -7.07, -8.31 -5.56, -9.24 -3.83, -9.81 -1.95, -10 0, -9.81 1.95, -9.24 3.83, -8.31 5.56, -7.64 6.46, 28.76 49.5, 29.59 50.41, 31.82 52.24, 34.37 53.6, 37.13 54.44, 40 54.72, 42.87 54.44, 45.63 53.6, 48.18 52.24, 50.41 50.41, 52.24 48.18, 53.53 45.8, 78.38 -12.11, 79.62 -16.1))" ); } public void testLargeDistance() { checkBuffer("LINESTRING( 0 0, 10 10)", 1, 200, - "POLYGON ((-190 10, -186.1570560806461 49.01806440322572, -174.77590650225736 86.53668647301798, -156.29392246050907 121.11404660392043, -131.42135623730948 151.4213562373095, -101.11404660392039 176.29392246050907, -66.53668647301795 194.77590650225736, -29.018064403225637 206.1570560806461, 10 210, 49.018064403225665 206.1570560806461, 86.53668647301797 194.77590650225736, 121.11404660392046 176.29392246050904, 151.4213562373095 151.42135623730948, 176.29392246050904 121.11404660392043, 194.77590650225736 86.53668647301795, 206.1570560806461 49.01806440322565, 210 10, 206.15705608064607 -29.018064403225743, 194.7759065022573 -66.53668647301808, 176.29392246050904 -101.11404660392043, 151.42135623730948 -131.42135623730954, 121.11404660392037 -156.2939224605091, 86.536686473018 -174.77590650225733, 49.01806440322566 -186.1570560806461, 10 -190, -29.018064403225736 -186.15705608064607, -66.53668647301807 -174.7759065022573, -101.11404660392043 -156.29392246050904, -131.42135623730954 -131.42135623730948, -156.2939224605091 -101.11404660392039, -174.77590650225736 -66.53668647301794, -186.1570560806461 -29.018064403225672, -190 10))" + "POLYGON ((206.16 -29.02, 194.78 -66.54, 176.29 -101.11, 151.42 -131.42, 121.11 -156.29, 86.54 -174.78, 49.02 -186.16, 10 -190, -29.02 -186.16, -66.54 -174.78, -101.11 -156.29, -131.42 -131.42, -156.29 -101.11, -174.78 -66.54, -186.16 -29.02, -190 10, -186.16 49.02, -174.78 86.54, -156.29 121.11, -131.42 151.42, -101.11 176.29, -66.54 194.78, -29.02 206.16, 10 210, 49.02 206.16, 86.54 194.78, 121.11 176.29, 151.42 151.42, 176.29 121.11, 194.78 86.54, 206.16 49.02, 210 10, 206.16 -29.02))" ); } public void testZeroDistanceAtVertex() { checkBuffer("LINESTRING( 10 10, 20 20, 30 30)", new double[] { 5, 0, 5 }, - "MULTIPOLYGON (((5.096 10.975, 5.381 11.913, 5.843 12.778, 6.464 13.536, 7.222 14.157, 7.943 14.557, 20 20, 14.557 7.943, 14.157 7.222, 13.536 6.464, 12.778 5.843, 11.913 5.381, 10.975 5.096, 10 5, 9.025 5.096, 8.087 5.381, 7.222 5.843, 6.464 6.464, 5.843 7.222, 5.381 8.087, 5.096 9.025, 5 10, 5.096 10.975)), ((25.443 32.057, 25.843 32.778, 26.464 33.536, 27.222 34.157, 28.087 34.619, 29.025 34.904, 30 35, 30.975 34.904, 31.913 34.619, 32.778 34.157, 33.536 33.536, 34.157 32.778, 34.619 31.913, 34.904 30.975, 35 30, 34.904 29.025, 34.619 28.087, 34.157 27.222, 33.536 26.464, 32.057 25.443, 20 20, 25.443 32.057)))" + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((25.44 32.06, 25.84 32.78, 26.46 33.54, 27.22 34.16, 28.09 34.62, 29.02 34.9, 30 35, 30.98 34.9, 31.91 34.62, 32.78 34.16, 33.54 33.54, 34.16 32.78, 34.62 31.91, 34.9 30.98, 35 30, 34.9 29.02, 34.62 28.09, 34.16 27.22, 33.54 26.46, 32.78 25.84, 32.06 25.44, 20 20, 25.44 32.06)))" ); } public void testZeroDistancesForSegment() { checkBuffer("LINESTRING( 10 10, 20 20, 30 30, 40 40)", new double[] { 5, 0, 0, 5 }, - "MULTIPOLYGON (((5.096 10.975, 5.381 11.913, 5.843 12.778, 6.464 13.536, 7.222 14.157, 7.943 14.557, 20 20, 14.557 7.943, 14.157 7.222, 13.536 6.464, 12.778 5.843, 11.913 5.381, 10.975 5.096, 10 5, 9.025 5.096, 8.087 5.381, 7.222 5.843, 6.464 6.464, 5.843 7.222, 5.381 8.087, 5.096 9.025, 5 10, 5.096 10.975)), ((35.443 42.057, 35.843 42.778, 36.464 43.536, 37.222 44.157, 38.087 44.619, 39.025 44.904, 40 45, 40.975 44.904, 41.913 44.619, 42.778 44.157, 43.536 43.536, 44.157 42.778, 44.619 41.913, 44.904 40.975, 45 40, 44.904 39.025, 44.619 38.087, 44.157 37.222, 43.536 36.464, 42.057 35.443, 30 30, 35.443 42.057)))" + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((35.44 42.06, 35.84 42.78, 36.46 43.54, 37.22 44.16, 38.09 44.62, 39.02 44.9, 40 45, 40.98 44.9, 41.91 44.62, 42.78 44.16, 43.54 43.54, 44.16 42.78, 44.62 41.91, 44.9 40.98, 45 40, 44.9 39.02, 44.62 38.09, 44.16 37.22, 43.54 36.46, 42.78 35.84, 42.06 35.44, 30 30, 35.44 42.06)))" ); } - + + // see https://github.com/locationtech/jts/issues/998 + public void testIssue998_Spike() { + checkBuffer("LINESTRING (0.024520295 69.50077743, 0.000508719 74.50086084, 0 76.39546845)", + new double[] { 6.47, 6.9, 7 }, + "POLYGON ((-6.87 77.76, -6.47 79.07, -5.82 80.28, -4.95 81.35, -3.89 82.22, -2.68 82.86, -1.37 83.26, 0 83.4, 1.37 83.26, 2.68 82.86, 3.89 82.22, 4.95 81.35, 5.82 80.28, 6.47 79.07, 6.87 77.76, 7 76.4, 6.99 76.03, 6.89 74.14, 6.88 74.08, 6.88 73.94, 6.47 68.98, 6.37 68.24, 6 67.02, 5.4 65.91, 4.6 64.93, 3.62 64.12, 2.5 63.52, 1.29 63.16, 0.02 63.03, -1.24 63.16, -2.45 63.52, -3.57 64.12, -4.55 64.93, -5.36 65.91, -5.95 67.02, -6.32 68.24, -6.42 68.91, -6.87 73.87, -6.88 74.05, -6.89 74.13, -6.99 76.02, -7 76.4, -6.87 77.76))" + ); + } + + public void testNoReverseSpike() { + checkBuffer("LINESTRING (0 70, 0 80)", + new double[] { 4, 7 }, + "POLYGON ((-6.87 78.63, -7 80, -6.87 81.37, -6.47 82.68, -5.82 83.89, -4.95 84.95, -3.89 85.82, -2.68 86.47, -1.37 86.87, 0 87, 1.37 86.87, 2.68 86.47, 3.89 85.82, 4.95 84.95, 5.82 83.89, 6.47 82.68, 6.87 81.37, 7 80, 6.87 78.63, 6.68 77.9, 3.82 68.8, 3.7 68.47, 3.33 67.78, 2.83 67.17, 2.22 66.67, 1.53 66.3, 0.78 66.08, 0 66, -0.78 66.08, -1.53 66.3, -2.22 66.67, -2.83 67.17, -3.33 67.78, -3.7 68.47, -3.82 68.8, -6.68 77.9, -6.87 78.63))" + ); + } + + public void testNoShortCapSegments() { + checkBuffer("LINESTRING (6.85 78.25, 18 87)", + new double[] { 5, 9 }, + "POLYGON ((11.64 93.36, 13 94.48, 14.56 95.31, 16.24 95.83, 18 96, 19.76 95.83, 21.44 95.31, 23 94.48, 24.36 93.36, 25.48 92, 26.31 90.44, 26.83 88.76, 27 87, 26.83 85.24, 26.31 83.56, 25.48 82, 24.36 80.64, 23 79.52, 21.33 78.64, 8.7 73.61, 7.83 73.35, 6.85 73.25, 5.87 73.35, 4.94 73.63, 4.07 74.09, 3.31 74.71, 2.69 75.47, 2.23 76.34, 1.95 77.27, 1.85 78.25, 1.95 79.23, 2.23 80.16, 2.78 81.15, 10.67 92.22, 11.64 93.36))" + ); + } + + //================================================================ + private void checkBuffer(String wkt, double startDist, double endDist, String wktExpected) { Geometry geom = read(wkt);