/src/geos/src/operation/buffer/BufferCurveSetBuilder.cpp
Line | Count | Source |
1 | | /********************************************************************** |
2 | | * |
3 | | * GEOS - Geometry Engine Open Source |
4 | | * http://geos.osgeo.org |
5 | | * |
6 | | * Copyright (C) 2011 Sandro Santilli <strk@kbt.io> |
7 | | * Copyright (C) 2005 Refractions Research Inc. |
8 | | * Copyright (C) 2001-2002 Vivid Solutions Inc. |
9 | | * |
10 | | * This is free software; you can redistribute and/or modify it under |
11 | | * the terms of the GNU Lesser General Public Licence as published |
12 | | * by the Free Software Foundation. |
13 | | * See the COPYING file for more information. |
14 | | * |
15 | | ********************************************************************** |
16 | | * |
17 | | * Last port: operation/buffer/BufferCurveSetBuilder.java 4c343e79f (JTS-1.19) |
18 | | * |
19 | | **********************************************************************/ |
20 | | |
21 | | #include <geos/constants.h> |
22 | | #include <geos/algorithm/Distance.h> |
23 | | #include <geos/algorithm/Orientation.h> |
24 | | #include <geos/util/IllegalArgumentException.h> |
25 | | #include <geos/util/UnsupportedOperationException.h> |
26 | | #include <geos/operation/buffer/BufferCurveSetBuilder.h> |
27 | | #include <geos/operation/valid/RepeatedPointRemover.h> |
28 | | #include <geos/geom/CoordinateSequence.h> |
29 | | #include <geos/geom/Geometry.h> |
30 | | #include <geos/geom/GeometryCollection.h> |
31 | | #include <geos/geom/Point.h> |
32 | | #include <geos/geom/LinearRing.h> |
33 | | #include <geos/geom/LineString.h> |
34 | | #include <geos/geom/Polygon.h> |
35 | | #include <geos/geom/Location.h> |
36 | | #include <geos/geom/Triangle.h> |
37 | | #include <geos/geom/Position.h> |
38 | | #include <geos/geomgraph/Label.h> |
39 | | #include <geos/noding/NodedSegmentString.h> |
40 | | #include <geos/util.h> |
41 | | #include <geos/io/WKTWriter.h> |
42 | | |
43 | | #include <algorithm> // for min |
44 | | #include <cmath> |
45 | | #include <cassert> |
46 | | #include <iomanip> |
47 | | #include <memory> |
48 | | #include <vector> |
49 | | #include <typeinfo> |
50 | | |
51 | | #ifndef GEOS_DEBUG |
52 | | #define GEOS_DEBUG 0 |
53 | | #endif |
54 | | |
55 | | using namespace geos::geom; |
56 | | using geos::noding::NodedSegmentString; |
57 | | using geos::noding::SegmentString; |
58 | | using geos::geomgraph::Label; |
59 | | using geos::algorithm::Distance; |
60 | | using geos::algorithm::Orientation; |
61 | | |
62 | | namespace geos { |
63 | | namespace operation { // geos.operation |
64 | | namespace buffer { // geos.operation.buffer |
65 | | |
66 | | |
67 | | BufferCurveSetBuilder::~BufferCurveSetBuilder() |
68 | 0 | { |
69 | 0 | for(std::size_t i = 0, n = curveList.size(); i < n; ++i) { |
70 | 0 | SegmentString* ss = curveList[i]; |
71 | 0 | delete ss; |
72 | 0 | } |
73 | 0 | for(std::size_t i = 0, n = newLabels.size(); i < n; ++i) { |
74 | 0 | delete newLabels[i]; |
75 | 0 | } |
76 | 0 | } |
77 | | |
78 | | /* public */ |
79 | | std::vector<SegmentString*>& |
80 | | BufferCurveSetBuilder::getCurves() |
81 | 0 | { |
82 | 0 | add(inputGeom); |
83 | 0 | return curveList; |
84 | 0 | } |
85 | | |
86 | | /*public*/ |
87 | | void |
88 | | BufferCurveSetBuilder::addCurves(std::vector<std::unique_ptr<CoordinateSequence>>& lineList, |
89 | | geom::Location leftLoc, geom::Location rightLoc) |
90 | 0 | { |
91 | 0 | for(std::size_t i = 0, n = lineList.size(); i < n; ++i) { |
92 | 0 | addCurve(std::move(lineList[i]), leftLoc, rightLoc); |
93 | 0 | } |
94 | 0 | } |
95 | | |
96 | | /*private*/ |
97 | | void |
98 | | BufferCurveSetBuilder::addCurve(std::unique_ptr<CoordinateSequence> coord, |
99 | | geom::Location leftLoc, geom::Location rightLoc) |
100 | 0 | { |
101 | | #if GEOS_DEBUG |
102 | | std::cerr << __FUNCTION__ << ": coords=" << coord->toString() << std::endl; |
103 | | #endif |
104 | | // don't add null curves! |
105 | 0 | if(coord->getSize() < 2) { |
106 | | #if GEOS_DEBUG |
107 | | std::cerr << " skipped (size<2)" << std::endl; |
108 | | #endif |
109 | 0 | return; |
110 | 0 | } |
111 | | |
112 | | // add the edge for a coordinate list which is a raw offset curve |
113 | 0 | Label* newlabel = new Label(0, Location::BOUNDARY, leftLoc, rightLoc); |
114 | |
|
115 | 0 | const bool hasZ = coord->hasZ(); |
116 | 0 | const bool hasM = coord->hasM(); |
117 | | // coord ownership transferred to SegmentString |
118 | 0 | SegmentString* e = new NodedSegmentString(std::move(coord), hasZ, hasM, newlabel); |
119 | | |
120 | | // SegmentString doesn't own the sequence, so we need to delete in |
121 | | // the destructor |
122 | 0 | newLabels.push_back(newlabel); |
123 | 0 | curveList.push_back(e); |
124 | 0 | } |
125 | | |
126 | | |
127 | | /*private*/ |
128 | | void |
129 | | BufferCurveSetBuilder::add(const Geometry& g) |
130 | 0 | { |
131 | 0 | if(g.isEmpty()) { |
132 | | #if GEOS_DEBUG |
133 | | std::cerr << __FUNCTION__ << ": skip empty geometry" << std::endl; |
134 | | #endif |
135 | 0 | return; |
136 | 0 | } |
137 | | |
138 | 0 | const Polygon* poly = dynamic_cast<const Polygon*>(&g); |
139 | 0 | if(poly) { |
140 | 0 | addPolygon(poly); |
141 | 0 | return; |
142 | 0 | } |
143 | | |
144 | 0 | const LineString* line = dynamic_cast<const LineString*>(&g); |
145 | 0 | if(line) { |
146 | 0 | addLineString(line); |
147 | 0 | return; |
148 | 0 | } |
149 | | |
150 | 0 | const Point* point = dynamic_cast<const Point*>(&g); |
151 | 0 | if(point) { |
152 | 0 | addPoint(point); |
153 | 0 | return; |
154 | 0 | } |
155 | | |
156 | 0 | const GeometryCollection* collection = dynamic_cast<const GeometryCollection*>(&g); |
157 | 0 | if(collection) { |
158 | 0 | addCollection(collection); |
159 | 0 | return; |
160 | 0 | } |
161 | | |
162 | 0 | std::string out = typeid(g).name(); |
163 | 0 | throw util::UnsupportedOperationException("GeometryGraph::add(Geometry &): unknown geometry type: " + out); |
164 | 0 | } |
165 | | |
166 | | /*private*/ |
167 | | void |
168 | | BufferCurveSetBuilder::addCollection(const GeometryCollection* gc) |
169 | 0 | { |
170 | 0 | for(std::size_t i = 0, n = gc->getNumGeometries(); i < n; i++) { |
171 | 0 | const Geometry* g = gc->getGeometryN(i); |
172 | 0 | add(*g); |
173 | 0 | } |
174 | 0 | } |
175 | | |
176 | | /*private*/ |
177 | | void |
178 | | BufferCurveSetBuilder::addPoint(const Point* p) |
179 | 0 | { |
180 | | // a zero or negative width buffer of a point is empty |
181 | 0 | if(distance <= 0.0) { |
182 | 0 | return; |
183 | 0 | } |
184 | 0 | const CoordinateSequence* coord = p->getCoordinatesRO(); |
185 | 0 | if (coord->size() >= 1 && ! coord->getAt(0).isValid()) { |
186 | 0 | return; |
187 | 0 | } |
188 | 0 | std::vector<std::unique_ptr<CoordinateSequence>> lineList; |
189 | 0 | curveBuilder.getLineCurve(coord, distance, lineList); |
190 | |
|
191 | 0 | addCurves(lineList, Location::EXTERIOR, Location::INTERIOR); |
192 | 0 | } |
193 | | |
194 | | /*private*/ |
195 | | void |
196 | | BufferCurveSetBuilder::addLineString(const LineString* line) |
197 | 0 | { |
198 | 0 | if (curveBuilder.isLineOffsetEmpty(distance)) { |
199 | 0 | return; |
200 | 0 | } |
201 | | |
202 | 0 | auto coord = operation::valid::RepeatedPointRemover::removeRepeatedAndInvalidPoints(line->getCoordinatesRO()); |
203 | |
|
204 | 0 | if (coord->size() == 0) { |
205 | 0 | return; |
206 | 0 | } |
207 | | |
208 | | /** |
209 | | * Rings (closed lines) are generated with a continuous curve, |
210 | | * with no end arcs. This produces better quality linework, |
211 | | * and avoids noding issues with arcs around almost-parallel end segments. |
212 | | * See JTS #523 and #518. |
213 | | * |
214 | | * Singled-sided buffers currently treat rings as if they are lines. |
215 | | */ |
216 | 0 | if (coord->isRing() && ! curveBuilder.getBufferParameters().isSingleSided()) { |
217 | 0 | addLinearRingSides(coord.get(), distance); |
218 | 0 | } |
219 | 0 | else { |
220 | 0 | std::vector<std::unique_ptr<CoordinateSequence>> lineList; |
221 | 0 | curveBuilder.getLineCurve(coord.get(), distance, lineList); |
222 | 0 | addCurves(lineList, Location::EXTERIOR, Location::INTERIOR); |
223 | 0 | } |
224 | 0 | } |
225 | | |
226 | | /* private */ |
227 | | void |
228 | | BufferCurveSetBuilder::addLinearRingSides(const CoordinateSequence* coord, double p_distance) |
229 | 0 | { |
230 | | /* |
231 | | * (f "hole" side will be eroded completely, avoid generating it. |
232 | | * This prevents hole artifacts (e.g. https://github.com/libgeos/geos/issues/1223) |
233 | | */ |
234 | | //-- distance is assumed positive, due to previous checks |
235 | 0 | Envelope env; |
236 | 0 | coord->expandEnvelope(env); |
237 | 0 | bool isHoleComputed = ! isRingFullyEroded(coord, &env, true, distance); |
238 | |
|
239 | 0 | bool isCCW = isRingCCW(coord); |
240 | |
|
241 | 0 | bool isShellLeft = ! isCCW; |
242 | 0 | if (isShellLeft || isHoleComputed) { |
243 | 0 | addRingSide(coord, p_distance, |
244 | 0 | Position::LEFT, |
245 | 0 | Location::EXTERIOR, Location::INTERIOR); |
246 | 0 | } |
247 | |
|
248 | 0 | bool isShellRight = isCCW; |
249 | 0 | if (isShellRight || isHoleComputed) { |
250 | 0 | addRingSide(coord, p_distance, |
251 | 0 | Position::RIGHT, |
252 | 0 | Location::INTERIOR, Location::EXTERIOR); |
253 | 0 | } |
254 | 0 | } |
255 | | |
256 | | /*private*/ |
257 | | void |
258 | | BufferCurveSetBuilder::addPolygon(const Polygon* p) |
259 | 0 | { |
260 | 0 | double offsetDistance = distance; |
261 | |
|
262 | 0 | int offsetSide = Position::LEFT; |
263 | 0 | if(distance < 0.0) { |
264 | 0 | offsetDistance = -distance; |
265 | 0 | offsetSide = Position::RIGHT; |
266 | 0 | } |
267 | |
|
268 | 0 | const LinearRing* shell = p->getExteriorRing(); |
269 | | |
270 | | // optimization - don't bother computing buffer |
271 | | // if the polygon would be completely eroded |
272 | 0 | if(distance < 0.0 && isRingFullyEroded(shell, false, distance)) { |
273 | | #if GEOS_DEBUG |
274 | | std::cerr << __FUNCTION__ << ": polygon is eroded completely " << std::endl; |
275 | | #endif |
276 | 0 | return; |
277 | 0 | } |
278 | | |
279 | 0 | auto shellCoords = |
280 | 0 | operation::valid::RepeatedPointRemover::removeRepeatedAndInvalidPoints(shell->getCoordinatesRO()); |
281 | |
|
282 | 0 | if (shellCoords->isEmpty()) { |
283 | 0 | return; |
284 | | //throw util::GEOSException("Shell empty after removing invalid points"); |
285 | 0 | } |
286 | | |
287 | | // don't attempt to buffer a polygon |
288 | | // with too few distinct vertices |
289 | 0 | if(distance <= 0.0 && shellCoords->size() < 3) { |
290 | 0 | return; |
291 | 0 | } |
292 | | |
293 | 0 | addPolygonRingSide( |
294 | 0 | shellCoords.get(), |
295 | 0 | offsetDistance, |
296 | 0 | offsetSide, |
297 | 0 | Location::EXTERIOR, |
298 | 0 | Location::INTERIOR); |
299 | |
|
300 | 0 | for(std::size_t i = 0, n = p->getNumInteriorRing(); i < n; ++i) { |
301 | 0 | const LineString* hls = p->getInteriorRingN(i); |
302 | 0 | const LinearRing* hole = detail::down_cast<const LinearRing*>(hls); |
303 | | |
304 | | // optimization - don't bother computing buffer for this hole |
305 | | // if the hole would be completely covered |
306 | 0 | if(distance > 0.0 && isRingFullyEroded(hole, true, distance)) { |
307 | 0 | continue; |
308 | 0 | } |
309 | | |
310 | 0 | auto holeCoords = valid::RepeatedPointRemover::removeRepeatedAndInvalidPoints(hole->getCoordinatesRO()); |
311 | | |
312 | | //-- skip if no valid coordinates |
313 | 0 | if (holeCoords->isEmpty()) |
314 | 0 | continue; |
315 | | |
316 | | // Holes are topologically labelled opposite to the shell, |
317 | | // since the interior of the polygon lies on their opposite |
318 | | // side (on the left, if the hole is oriented CCW) |
319 | 0 | addPolygonRingSide( |
320 | 0 | holeCoords.get(), |
321 | 0 | offsetDistance, |
322 | 0 | Position::opposite(offsetSide), |
323 | 0 | Location::INTERIOR, |
324 | 0 | Location::EXTERIOR); |
325 | 0 | } |
326 | 0 | } |
327 | | |
328 | | /* private */ |
329 | | void |
330 | | BufferCurveSetBuilder::addPolygonRingSide(const CoordinateSequence* coord, |
331 | | double offsetDistance, int side, geom::Location cwLeftLoc, geom::Location cwRightLoc) |
332 | 0 | { |
333 | | |
334 | | // don't bother adding ring if it is "flat" and |
335 | | // will disappear in the output |
336 | 0 | if(offsetDistance == 0.0 && coord->size() < LinearRing::MINIMUM_VALID_SIZE) { |
337 | 0 | return; |
338 | 0 | } |
339 | | |
340 | 0 | Location leftLoc = cwLeftLoc; |
341 | 0 | Location rightLoc = cwRightLoc; |
342 | | #if GEOS_DEBUG |
343 | | std::cerr << "BufferCurveSetBuilder::addPolygonRing: "; |
344 | | try { |
345 | | bool isCcw = Orientation::isCCW(coord); |
346 | | std::cerr << (isCcw ? "CCW" : "CW"); |
347 | | } catch (const util::IllegalArgumentException& ex) { |
348 | | std::cerr << "failed to determine orientation: " << ex.what(); |
349 | | } |
350 | | std::cerr << std::endl; |
351 | | #endif |
352 | 0 | bool isCCW = isRingCCW(coord); |
353 | 0 | if (coord->size() >= LinearRing::MINIMUM_VALID_SIZE && isCCW) |
354 | 0 | { |
355 | 0 | leftLoc = cwRightLoc; |
356 | 0 | rightLoc = cwLeftLoc; |
357 | | #if GEOS_DEBUG |
358 | | std::cerr << " side " << side << " becomes " << Position::opposite(side) << std::endl; |
359 | | #endif |
360 | 0 | side = Position::opposite(side); |
361 | 0 | } |
362 | 0 | addRingSide(coord, offsetDistance, side, leftLoc, rightLoc); |
363 | 0 | } |
364 | | |
365 | | /* private */ |
366 | | void |
367 | | BufferCurveSetBuilder::addRingSide(const CoordinateSequence* coord, |
368 | | double offsetDistance, int side, geom::Location leftLoc, geom::Location rightLoc) |
369 | 0 | { |
370 | 0 | std::vector<std::unique_ptr<CoordinateSequence>> lineList; |
371 | 0 | curveBuilder.getRingCurve(coord, side, offsetDistance, lineList); |
372 | | // ASSERT: lineList contains exactly 1 curve (this is the JTS semantics) |
373 | 0 | if (lineList.size() > 0) { |
374 | 0 | const CoordinateSequence* curve = lineList[0].get(); |
375 | | /** |
376 | | * If the offset curve has inverted completely it will produce |
377 | | * an unwanted artifact in the result, so skip it. |
378 | | */ |
379 | 0 | if (isRingCurveInverted(coord, offsetDistance, curve)) { |
380 | 0 | for(auto& line: lineList ) { |
381 | 0 | line.reset(); |
382 | 0 | } |
383 | 0 | return; |
384 | 0 | } |
385 | 0 | } |
386 | 0 | addCurves(lineList, leftLoc, rightLoc); |
387 | 0 | } |
388 | | |
389 | | /* private static*/ |
390 | | bool |
391 | | BufferCurveSetBuilder::isRingCurveInverted( |
392 | | const CoordinateSequence* inputRing, double dist, |
393 | | const CoordinateSequence* curveRing) |
394 | 0 | { |
395 | 0 | if (dist == 0.0) return false; |
396 | | /** |
397 | | * Only proper rings can invert. |
398 | | */ |
399 | 0 | if (inputRing->size() <= 3) return false; |
400 | | /** |
401 | | * Heuristic based on low chance that a ring with many vertices will invert. |
402 | | * This low limit ensures this test is fairly efficient. |
403 | | */ |
404 | 0 | if (inputRing->size() >= MAX_INVERTED_RING_SIZE) return false; |
405 | | |
406 | | /** |
407 | | * An inverted curve has no more points than the input ring. |
408 | | * This also eliminates concave inputs (which will produce fillet arcs) |
409 | | */ |
410 | 0 | if (curveRing->size() > INVERTED_CURVE_VERTEX_FACTOR * inputRing->size()) return false; |
411 | | |
412 | | /** |
413 | | * If curve contains points which are on the buffer, |
414 | | * it is not inverted and can be included in the raw curves. |
415 | | */ |
416 | 0 | if (hasPointOnBuffer(inputRing, dist, curveRing)) |
417 | 0 | return false; |
418 | | |
419 | | //-- curve is inverted, so discard it |
420 | 0 | return true; |
421 | | //std::cout << std::setprecision(10) << io::WKTWriter::toLineString(*curveRing) << std::endl; |
422 | | //std::cout << "isRingCurveInverted: " << isCurveTooClose << " maxDist = " << maxDist << std::endl; |
423 | 0 | } |
424 | | |
425 | | /* private static*/ |
426 | | bool |
427 | | BufferCurveSetBuilder::hasPointOnBuffer( |
428 | | const CoordinateSequence* inputRing, double dist, |
429 | | const CoordinateSequence* curveRing) |
430 | 0 | { |
431 | 0 | double distTol = NEARNESS_FACTOR * fabs(dist); |
432 | |
|
433 | 0 | for (std::size_t i = 0; i < curveRing->size(); i++) { |
434 | 0 | const CoordinateXY& v = curveRing->getAt(i); |
435 | | |
436 | | //-- check curve vertices |
437 | 0 | double distVertex = Distance::pointToSegmentString(v, inputRing); |
438 | 0 | if (distVertex > distTol) { |
439 | 0 | return true; |
440 | 0 | } |
441 | | |
442 | | //-- check curve segment midpoints |
443 | 0 | std::size_t iNext = (i < curveRing->size() - 1) ? i + 1 : 0; |
444 | 0 | const CoordinateXY& vnext = curveRing->getAt(iNext); |
445 | 0 | CoordinateXY midPt = LineSegment::midPoint(v, vnext); |
446 | |
|
447 | 0 | double distMid = Distance::pointToSegmentString(midPt, inputRing); |
448 | 0 | if (distMid > distTol) { |
449 | 0 | return true; |
450 | 0 | } |
451 | 0 | } |
452 | 0 | return false; |
453 | 0 | } |
454 | | |
455 | | /*private*/ |
456 | | bool |
457 | | BufferCurveSetBuilder::isRingFullyEroded(const LinearRing* ring, bool isHole, |
458 | | double bufferDistance) |
459 | 0 | { |
460 | 0 | const CoordinateSequence* ringCoord = ring->getCoordinatesRO(); |
461 | 0 | const Envelope* env = ring->getEnvelopeInternal(); |
462 | 0 | return isRingFullyEroded(ringCoord, env, isHole, bufferDistance); |
463 | 0 | } |
464 | | |
465 | | /*private*/ |
466 | | bool |
467 | | BufferCurveSetBuilder::isRingFullyEroded(const CoordinateSequence* ringCoord, const Envelope* env, bool isHole, |
468 | | double bufferDistance) |
469 | 0 | { |
470 | | // degenerate ring has no area |
471 | 0 | if(ringCoord->getSize() < 4) { |
472 | 0 | return true; |
473 | 0 | } |
474 | | |
475 | | // important test to eliminate inverted triangle bug |
476 | | // also optimizes erosion test for triangles |
477 | 0 | if(ringCoord->getSize() == 4) { |
478 | 0 | return isTriangleErodedCompletely(ringCoord, bufferDistance); |
479 | 0 | } |
480 | | |
481 | 0 | bool isErodable = |
482 | 0 | ( isHole && bufferDistance > 0) || |
483 | 0 | (! isHole && bufferDistance < 0); |
484 | |
|
485 | 0 | if (isErodable) { |
486 | | //-- if envelope is narrower than twice the buffer distance, ring is eroded |
487 | 0 | double envMinDimension = std::min(env->getHeight(), env->getWidth()); |
488 | 0 | if (2 * std::abs(bufferDistance) > envMinDimension) { |
489 | 0 | return true; |
490 | 0 | } |
491 | 0 | } |
492 | 0 | return false; |
493 | 0 | } |
494 | | |
495 | | /*private*/ |
496 | | bool |
497 | | BufferCurveSetBuilder::isTriangleErodedCompletely( |
498 | | const CoordinateSequence* triangleCoord, double bufferDistance) |
499 | 0 | { |
500 | 0 | Triangle tri(triangleCoord->getAt(0), triangleCoord->getAt(1), triangleCoord->getAt(2)); |
501 | |
|
502 | 0 | CoordinateXY inCentre; |
503 | 0 | tri.inCentre(inCentre); |
504 | 0 | double distToCentre = Distance::pointToSegment(inCentre, tri.p0, tri.p1); |
505 | 0 | bool ret = distToCentre < std::fabs(bufferDistance); |
506 | 0 | return ret; |
507 | 0 | } |
508 | | |
509 | | |
510 | | /*private*/ |
511 | | bool |
512 | | BufferCurveSetBuilder::isRingCCW(const CoordinateSequence* coords) const |
513 | 0 | { |
514 | 0 | bool isCCW = algorithm::Orientation::isCCWArea(coords); |
515 | | //--- invert orientation if required |
516 | 0 | if (isInvertOrientation) return ! isCCW; |
517 | 0 | return isCCW; |
518 | 0 | } |
519 | | |
520 | | } // namespace geos.operation.buffer |
521 | | } // namespace geos.operation |
522 | | } // namespace geos |