From 872bc44fa38d038e5d8c9d77068b28a342b7f208 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 11 Jan 2024 13:07:56 -0800 Subject: [PATCH] Fix OffsetCurve to handle end seg issue (#1029) --- .../function/OffsetCurveFunctions.java | 22 +++++++ .../jts/operation/buffer/OffsetCurve.java | 66 ++++++++++++++----- .../jts/operation/buffer/OffsetCurveTest.java | 40 ++++++++++- 3 files changed, 108 insertions(+), 20 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java index 008784e6cc..1302a5e6dc 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java @@ -16,6 +16,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.util.GeometryCombiner; +import org.locationtech.jts.operation.buffer.BufferParameters; import org.locationtech.jts.operation.buffer.OffsetCurve; import org.locationtech.jtstest.geomfunction.Metadata; @@ -75,4 +76,25 @@ public static Geometry rawCurve(Geometry geom, double distance) return curve; } + public static Geometry rawCurveWithParams(Geometry geom, + Double distance, + @Metadata(title="Quadrant Segs") + Integer quadrantSegments, + @Metadata(title="NOT USED") + Integer capStyle, + @Metadata(title="Join style") + Integer joinStyle, + @Metadata(title="Mitre limit") + Double mitreLimit) + { + BufferParameters bufferParams = new BufferParameters(); + if (quadrantSegments >= 0) bufferParams.setQuadrantSegments(quadrantSegments); + if (joinStyle >= 0) bufferParams.setJoinStyle(joinStyle); + if (mitreLimit >= 0) bufferParams.setMitreLimit(mitreLimit); + Coordinate[] pts = OffsetCurve.rawOffset((LineString) geom, distance, bufferParams); + Geometry curve = geom.getFactory().createLineString(pts); + return curve; + } + + } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java index 79c598af06..25d044cccd 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java @@ -24,6 +24,7 @@ import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.util.GeometryMapper; @@ -421,8 +422,9 @@ private int matchSegments(Coordinate raw0, Coordinate raw1, int rawCurveIndex, private static class MatchCurveSegmentAction extends MonotoneChainSelectAction { - private Coordinate p0; - private Coordinate p1; + private Coordinate raw0; + private Coordinate raw1; + private double rawLen; private int rawCurveIndex; private Coordinate[] bufferRingPts; private double matchDistance; @@ -430,11 +432,12 @@ private static class MatchCurveSegmentAction private double minRawLocation = -1; private int bufferRingMinIndex = -1; - public MatchCurveSegmentAction(Coordinate p0, Coordinate p1, + public MatchCurveSegmentAction(Coordinate raw0, Coordinate raw1, int rawCurveIndex, double matchDistance, Coordinate[] bufferRingPts, double[] rawCurveLoc) { - this.p0 = p0; - this.p1 = p1; + this.raw0 = raw0; + this.raw1 = raw1; + rawLen = raw0.distance(raw1); this.rawCurveIndex = rawCurveIndex; this.bufferRingPts = bufferRingPts; this.matchDistance = matchDistance; @@ -448,34 +451,61 @@ public int getBufferMinIndex() { public void select(MonotoneChain mc, int segIndex) { /** - * A curveRingPt segment may match all or only a portion of a single raw segment. - * There may be multiple curve ring segs that match along the raw segment. + * Generally buffer segments are no longer than raw curve segments, + * since the final buffer line likely has node points added. + * So a buffer segment may match all or only a portion of a single raw segment. + * There may be multiple buffer ring segs that match along the raw segment. + * + * HOWEVER, in some cases the buffer construction may contain + * a matching buffer segment which is slightly longer than a raw curve segment. + * Specifically, at the endpoint of a closed line with nearly parallel end segments + * - the closing fillet line is very short so is heuristically removed in the buffer. + * In this case, the buffer segment must still be matched. + * This produces closed offset curves, which is technically + * an anomaly, but only happens in rare cases. */ double frac = segmentMatchFrac(bufferRingPts[segIndex], bufferRingPts[segIndex+1], - p0, p1, matchDistance); + raw0, raw1, matchDistance); //-- no match if (frac < 0) return; //-- location is used to sort segments along raw curve double location = rawCurveIndex + frac; rawCurveLoc[segIndex] = location; - //-- record lowest index + //-- buffer seg index at lowest raw location is the curve start if (minRawLocation < 0 || location < minRawLocation) { minRawLocation = location; bufferRingMinIndex = segIndex; } } - } - private static double segmentMatchFrac(Coordinate p0, Coordinate p1, - Coordinate seg0, Coordinate seg1, double matchDistance) { - if (matchDistance < Distance.pointToSegment(p0, seg0, seg1)) + private double segmentMatchFrac(Coordinate buf0, Coordinate buf1, + Coordinate raw0, Coordinate raw1, double matchDistance) { + if (! isMatch(buf0, buf1, raw0, raw1, matchDistance)) return -1; - if (matchDistance < Distance.pointToSegment(p1, seg0, seg1)) - return -1; - //-- matched - determine position as fraction along segment - LineSegment seg = new LineSegment(seg0, seg1); - return seg.segmentFraction(p0); + + //-- matched - determine location as fraction along raw segment + LineSegment seg = new LineSegment(raw0, raw1); + return seg.segmentFraction(buf0); + } + + private boolean isMatch(Coordinate buf0, Coordinate buf1, Coordinate raw0, Coordinate raw1, double matchDistance) { + double bufSegLen = buf0.distance(buf1); + if (rawLen <= bufSegLen) { + if (matchDistance < Distance.pointToSegment(raw0, buf0, buf1)) + return false; + if (matchDistance < Distance.pointToSegment(raw1, buf0, buf1)) + return false; + } + else { + //TODO: only match longer buf segs at raw curve end segs? + if (matchDistance < Distance.pointToSegment(buf0, raw0, raw1)) + return false; + if (matchDistance < Distance.pointToSegment(buf1, raw0, raw1)) + return false; + } + return true; + } } /** diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java index f4cc9bb177..366737150e 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java @@ -215,14 +215,14 @@ public void testClosedCurve() { public void testOverlapTriangleInside() { checkOffsetCurve( - "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80))", 10, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10, "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)" ); } public void testOverlapTriangleOutside() { checkOffsetCurve( - "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80))", -10, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", -10, "LINESTRING (70 90, 40 90, 10 90, 8.11 89.82, 6.29 89.29, 4.6 88.42, 3.11 87.25, 1.87 85.82, 0.91 84.18, 0.29 82.39, 0.01 80.51, 0.1 78.61, 0.54 76.77, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.46 76.77, 99.9 78.61, 99.99 80.51, 99.71 82.39, 99.09 84.18, 98.13 85.82, 96.89 87.25, 95.4 88.42, 93.71 89.29, 91.89 89.82, 90 90, 70 90)" ); } @@ -293,6 +293,15 @@ public void testInfiniteLoop() { ); } + // see https://github.com/shapely/shapely/issues/820 + public void testOffsetError() { + checkOffsetCurve( + "LINESTRING (12 20, 60 68, 111 114, 151 159, 210 218)", + 3, + "LINESTRING (9.878679656440358 22.121320343559642, 57.878679656440355 70.12132034355965, 57.99069368916718 70.22770917070595, 108.86775926900314 116.11682714467565, 148.75777204394902 160.99309151648976, 148.87867965644037 161.12132034355963, 207.87867965644037 220.12132034355963)" + ); + } + //--------------------------------------- public void testQuadSegs() { @@ -337,6 +346,33 @@ public void testMinQuadrantSegments_QGIS() { ); } + // See https://trac.osgeo.org/postgis/ticket/4072 + public void testMitreJoinError() { + checkOffsetCurve( + "LINESTRING(362194.505 5649993.044,362197.451 5649994.125,362194.624 5650001.876,362189.684 5650000.114,362192.542 5649992.324,362194.505 5649993.044)", + -0.045, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (362194.52050157124 5649993.001754275, 362197.5086649931 5649994.098225646, 362194.65096611937 5650001.933395073, 362189.626113625 5650000.141129872, 362192.51525161567 5649992.266257602, 362194.5204958858 5649993.001752188)" + ); + } + + // See https://trac.osgeo.org/postgis/ticket/4072 + public void testMitreJoinErrorSimple() { + checkOffsetCurve( + "LINESTRING (4.821 0.72, 7.767 1.801, 4.94 9.552, 0 7.79, 2.858 0, 4.821 0.72)", + -0.045, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (4.83650157122754 0.6777542748970088, 7.824664993161384 1.7742256459460533, 4.966966119329371 9.6093950732796, -0.057886375241824 7.817129871774653, 2.8312516154153906 -0.0577423980712891, 4.836495885800319 0.6777521891305186)" + ); + } + + // See https://trac.osgeo.org/postgis/ticket/3279 + public void testMitreJoinSingleLine() { + checkOffsetCurve( + "LINESTRING (0.39 -0.02, 0.4650008997915482 -0.02, 0.4667128891457749 -0.0202500016082272, 0.4683515425280024 -0.0210000000000019, 0.4699159706879993 -0.0222499999999996, 0.4714061701120011 -0.0240000000000018, 0.4929087886040002 -0.0535958153351002, 0.4968358395870001 -0.0507426457862002, 0.4774061701119963 -0.0239999999999952, 0.476353470688 -0.0222500000000011, 0.4761015425280001 -0.0210000000000007, 0.4766503813740676 -0.0202500058185111, 0.4779990890331232 -0.02, 0.6189999999999996 -0.02, 0.619 -0.0700000000000002, 0.634 -0.0700000000000002, 0.6339999999999998 -0.02, 0.65 -0.02)", + -0.002, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (0.39 -0.022, 0.4648556402268155 -0.022, 0.4661407414895839 -0.0221876631893964, 0.4672953866748729 -0.022716134946407, 0.4685176359449585 -0.0236927292232623, 0.4698334593862525 -0.0252379526243584, 0.4924663251198579 -0.0563894198284619, 0.499629444080312 -0.0511851092703384, 0.479075235654203 -0.022894668402962, 0.4785370545613636 -0.022, 0.6169999999999995 -0.022, 0.617 -0.0720000000000002, 0.636 -0.0720000000000002, 0.6359999999999998 -0.022, 0.65 -0.022)" + ); + } + //======================================= private static final double EQUALS_TOL = 0.05;