Skip to content

Commit 9cc4e94

Browse files
committed
Merge master
Signed-off-by: Mike Barry <msb5014@gmail.com>
1 parent 220bbdd commit 9cc4e94

File tree

11 files changed

+593
-299
lines changed

11 files changed

+593
-299
lines changed

doc/JTS_Version_History.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Distributions for older JTS versions can be obtained at the
3636
* Improve `OffsetCurve` to return more linework for some input situations (#956)
3737
* Reduce buffer curve short fillet segments (#960)
3838
* Added ability to specify boundary for `LargestEmptyCircle` (#973)
39+
* Improve `DouglaPeuckerSimplifier` and `TopologyPreservingSimplifier` to handle ring endpoints (#1013)
3940

4041
### Bug Fixes
4142
* Fix `PreparedGeometry` handling of EMPTY elements (#904)

modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package org.locationtech.jts.simplify;
1414

1515
import org.locationtech.jts.geom.Coordinate;
16+
import org.locationtech.jts.geom.CoordinateArrays;
1617
import org.locationtech.jts.geom.CoordinateList;
1718
import org.locationtech.jts.geom.LineSegment;
1819

@@ -24,16 +25,18 @@
2425
*/
2526
class DouglasPeuckerLineSimplifier
2627
{
27-
public static Coordinate[] simplify(Coordinate[] pts, double distanceTolerance)
28+
public static Coordinate[] simplify(Coordinate[] pts, double distanceTolerance, boolean isPreserveEndpoint)
2829
{
2930
DouglasPeuckerLineSimplifier simp = new DouglasPeuckerLineSimplifier(pts);
3031
simp.setDistanceTolerance(distanceTolerance);
32+
simp.setPreserveEndpoint(isPreserveEndpoint);
3133
return simp.simplify();
3234
}
3335

3436
private Coordinate[] pts;
3537
private boolean[] usePt;
3638
private double distanceTolerance;
39+
private boolean isPreserveEndpoint = false;
3740

3841
public DouglasPeuckerLineSimplifier(Coordinate[] pts)
3942
{
@@ -50,19 +53,44 @@ public void setDistanceTolerance(double distanceTolerance) {
5053
this.distanceTolerance = distanceTolerance;
5154
}
5255

56+
private void setPreserveEndpoint(boolean isPreserveEndpoint) {
57+
this.isPreserveEndpoint = isPreserveEndpoint;
58+
}
59+
5360
public Coordinate[] simplify()
5461
{
5562
usePt = new boolean[pts.length];
5663
for (int i = 0; i < pts.length; i++) {
5764
usePt[i] = true;
5865
}
5966
simplifySection(0, pts.length - 1);
67+
6068
CoordinateList coordList = new CoordinateList();
6169
for (int i = 0; i < pts.length; i++) {
6270
if (usePt[i])
6371
coordList.add(new Coordinate(pts[i]));
6472
}
65-
return coordList.toCoordinateArray();
73+
74+
if (! isPreserveEndpoint && CoordinateArrays.isRing(pts)) {
75+
simplifyRingEndpoint(coordList);
76+
}
77+
78+
return coordList.toCoordinateArray();
79+
}
80+
81+
private void simplifyRingEndpoint(CoordinateList pts) {
82+
//-- avoid collapsing triangles
83+
if (pts.size() < 4)
84+
return;
85+
//-- base segment for endpoint
86+
seg.p0 = pts.get(1);
87+
seg.p1 = pts.get(pts.size() - 2);
88+
double distance = seg.distance(pts.get(0));
89+
if (distance <= distanceTolerance) {
90+
pts.remove(0);
91+
pts.remove(pts.size() - 1);
92+
pts.closeRing();
93+
}
6694
}
6795

6896
private LineSegment seg = new LineSegment();

modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,14 @@ public DPTransformer(boolean isEnsureValidTopology, double distanceTolerance)
136136
@Override
137137
protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent)
138138
{
139+
boolean isPreserveEndpoint = ! (parent instanceof LinearRing);
139140
Coordinate[] inputPts = coords.toCoordinateArray();
140141
Coordinate[] newPts = null;
141142
if (inputPts.length == 0) {
142143
newPts = new Coordinate[0];
143144
}
144145
else {
145-
newPts = DouglasPeuckerLineSimplifier.simplify(inputPts, distanceTolerance);
146+
newPts = DouglasPeuckerLineSimplifier.simplify(inputPts, distanceTolerance, isPreserveEndpoint);
146147
}
147148
return factory.getCoordinateSequenceFactory().create(newPts);
148149
}

modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,23 @@ class TaggedLineString
3434
private TaggedLineSegment[] segs;
3535
private List resultSegs = new ArrayList();
3636
private int minimumSize;
37+
private boolean isPreserveEndpoint = true;
3738

3839
public TaggedLineString(LineString parentLine) {
39-
this(parentLine, 2);
40+
this(parentLine, 2, true);
4041
}
4142

42-
public TaggedLineString(LineString parentLine, int minimumSize) {
43+
public TaggedLineString(LineString parentLine, int minimumSize, boolean isPreserveEndpoint) {
4344
this.parentLine = parentLine;
4445
this.minimumSize = minimumSize;
46+
this.isPreserveEndpoint = isPreserveEndpoint;
4547
init();
4648
}
4749

50+
public boolean isPreserveEndpoint() {
51+
return isPreserveEndpoint;
52+
}
53+
4854
public int getMinimumSize() { return minimumSize; }
4955
public LineString getParent() { return parentLine; }
5056
public Coordinate[] getParentCoordinates() { return parentLine.getCoordinates(); }
@@ -58,6 +64,20 @@ public int getResultSize()
5864

5965
public TaggedLineSegment getSegment(int i) { return segs[i]; }
6066

67+
/**
68+
* Gets a segment of the result list.
69+
* Negative indexes can be used to retrieve from the end of the list.
70+
* @param i the segment index to retrieve
71+
* @return the result segment
72+
*/
73+
public LineSegment getResultSegment(int i) {
74+
int index = i;
75+
if (i < 0) {
76+
index = resultSegs.size() + i;
77+
}
78+
return (LineSegment) resultSegs.get(index);
79+
}
80+
6181
private void init()
6282
{
6383
Coordinate[] pts = parentLine.getCoordinates();
@@ -98,5 +118,12 @@ private static Coordinate[] extractCoordinates(List segs)
98118
return pts;
99119
}
100120

121+
void removeRingEndpoint()
122+
{
123+
LineSegment firstSeg = (LineSegment) resultSegs.get(0);
124+
LineSegment lastSeg = (LineSegment) resultSegs.get(resultSegs.size() - 1);
101125

126+
firstSeg.p0 = lastSeg.p0;
127+
resultSegs.remove(resultSegs.size() - 1);
128+
}
102129
}

modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.locationtech.jts.algorithm.LineIntersector;
1919
import org.locationtech.jts.algorithm.RobustLineIntersector;
2020
import org.locationtech.jts.geom.Coordinate;
21+
import org.locationtech.jts.geom.CoordinateArrays;
2122
import org.locationtech.jts.geom.LineSegment;
2223

2324
/**
@@ -66,13 +67,16 @@ void simplify(TaggedLineString line)
6667
this.line = line;
6768
linePts = line.getParentCoordinates();
6869
simplifySection(0, linePts.length - 1, 0);
70+
71+
if (! line.isPreserveEndpoint() && CoordinateArrays.isRing(linePts)) {
72+
simplifyRingEndpoint();
73+
}
6974
}
7075

7176
private void simplifySection(int i, int j, int depth)
7277
{
7378
depth += 1;
74-
int[] sectionIndex = new int[2];
75-
if((i+1) == j) {
79+
if ((i+1) == j) {
7680
LineSegment newSeg = line.getSegment(i);
7781
line.addToResult(newSeg);
7882
// leave this segment in the input index, for efficiency
@@ -101,9 +105,7 @@ private void simplifySection(int i, int j, int depth)
101105
LineSegment candidateSeg = new LineSegment();
102106
candidateSeg.p0 = linePts[i];
103107
candidateSeg.p1 = linePts[j];
104-
sectionIndex[0] = i;
105-
sectionIndex[1] = j;
106-
if (hasBadIntersection(line, sectionIndex, candidateSeg)) {
108+
if (hasBadIntersection(line, i, j, candidateSeg)) {
107109
isValidToSimplify = false;
108110
}
109111

@@ -116,6 +118,21 @@ private void simplifySection(int i, int j, int depth)
116118
simplifySection(furthestPtIndex, j, depth);
117119
}
118120

121+
private void simplifyRingEndpoint()
122+
{
123+
if (line.getResultSize() > line.getMinimumSize()) {
124+
LineSegment firstSeg = line.getResultSegment(0);
125+
LineSegment lastSeg = line.getResultSegment(-1);
126+
127+
LineSegment simpSeg = new LineSegment(lastSeg.p0, firstSeg.p1);
128+
//-- the excluded segments are the ones containing the endpoint
129+
if (simpSeg.distance(firstSeg.p0) <= distanceTolerance
130+
&& ! hasBadIntersection(line, line.getSegments().length - 2, 0, simpSeg)) {
131+
line.removeRingEndpoint();
132+
}
133+
}
134+
}
135+
119136
private int findFurthestPoint(Coordinate[] pts, int i, int j, double[] maxDistance)
120137
{
121138
LineSegment seg = new LineSegment();
@@ -158,12 +175,25 @@ private LineSegment flatten(int start, int end)
158175
return newSeg;
159176
}
160177

161-
private boolean hasBadIntersection(TaggedLineString parentLine,
162-
int[] sectionIndex,
178+
/**
179+
* Tests if a flattening segment intersects a line
180+
* (excluding a given section of segments).
181+
* The excluded section is being replaced by the flattening segment,
182+
* so there is no need to test it
183+
* (and it may well intersect the segment).
184+
*
185+
* @param line
186+
* @param excludeStart
187+
* @param excludeEnd
188+
* @param candidateSeg
189+
* @return
190+
*/
191+
private boolean hasBadIntersection(TaggedLineString line,
192+
int excludeStart, int excludeEnd,
163193
LineSegment candidateSeg)
164194
{
165195
if (hasBadOutputIntersection(candidateSeg)) return true;
166-
if (hasBadInputIntersection(parentLine, sectionIndex, candidateSeg)) return true;
196+
if (hasBadInputIntersection(line, excludeStart, excludeEnd, candidateSeg)) return true;
167197
return false;
168198
}
169199

@@ -179,16 +209,19 @@ private boolean hasBadOutputIntersection(LineSegment candidateSeg)
179209
return false;
180210
}
181211

182-
private boolean hasBadInputIntersection(TaggedLineString parentLine,
183-
int[] sectionIndex,
212+
private boolean hasBadInputIntersection(TaggedLineString line,
213+
int excludeStart, int excludeEnd,
184214
LineSegment candidateSeg)
185215
{
186216
List querySegs = inputIndex.query(candidateSeg);
187217
for (Iterator i = querySegs.iterator(); i.hasNext(); ) {
188218
TaggedLineSegment querySeg = (TaggedLineSegment) i.next();
189219
if (hasInvalidIntersection(querySeg, candidateSeg)) {
190-
//-- don't fail if the segment is part of parent line
191-
if (isInLineSection(parentLine, sectionIndex, querySeg))
220+
/**
221+
* Ignore the intersection if the intersecting segment is part of the section being collapsed
222+
* to the candidate segment
223+
*/
224+
if (isInLineSection(line, excludeStart, excludeEnd, querySeg))
192225
continue;
193226
return true;
194227
}
@@ -197,23 +230,36 @@ private boolean hasBadInputIntersection(TaggedLineString parentLine,
197230
}
198231

199232
/**
200-
* Tests whether a segment is in a section of a TaggedLineString
201-
* @param line
202-
* @param sectionIndex
203-
* @param seg
204-
* @return
233+
* Tests whether a segment is in a section of a TaggedLineString.
234+
* Sections may wrap around the endpoint of the line,
235+
* to support ring endpoint simplification.
236+
* This is indicated by excludedStart > excludedEnd
237+
*
238+
* @param line the TaggedLineString containing the section segments
239+
* @param excludeStart the index of the first segment in the excluded section
240+
* @param excludeEnd the index of the last segment in the excluded section
241+
* @param seg the segment to test
242+
* @return true if the test segment intersects some segment in the line not in the excluded section
205243
*/
206244
private static boolean isInLineSection(
207245
TaggedLineString line,
208-
int[] sectionIndex,
246+
int excludeStart, int excludeEnd,
209247
TaggedLineSegment seg)
210248
{
211-
// not in this line
249+
//-- test segment is not in this line
212250
if (seg.getParent() != line.getParent())
213251
return false;
214252
int segIndex = seg.getIndex();
215-
if (segIndex >= sectionIndex[0] && segIndex < sectionIndex[1])
253+
if (excludeStart <= excludeEnd) {
254+
//-- section is contiguous
255+
if (segIndex >= excludeStart && segIndex < excludeEnd)
256+
return true;
257+
}
258+
else {
259+
//-- section wraps around the end of a ring
260+
if (segIndex >= excludeStart || segIndex <= excludeEnd)
216261
return true;
262+
}
217263
return false;
218264
}
219265

modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
* any intersecting line segments, this property
4646
* will be preserved in the output.
4747
* <p>
48+
* For polygonal geometries and LinearRings the endpoint will participate
49+
* in simplification. For LineStrings the endpoints will not be unchanged.
50+
* <p>
4851
* For all geometry types, the result will contain
4952
* enough vertices to ensure validity. For polygons
5053
* and closed linear geometries, the result will have at
@@ -177,7 +180,8 @@ public void filter(Geometry geom)
177180
if (line.isEmpty()) return;
178181

179182
int minSize = ((LineString) line).isClosed() ? 4 : 2;
180-
TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize);
183+
boolean isPreserveEndpoint = (line instanceof LinearRing) ? false : true;
184+
TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize, isPreserveEndpoint);
181185
tps.linestringMap.put(line, taggedLine);
182186
}
183187
}

0 commit comments

Comments
 (0)