Coverage Report

Created: 2026-08-13 07:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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