Coverage Report

Created: 2026-03-20 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/geos/src/operation/buffer/OffsetSegmentGenerator.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
 *
8
 * This is free software; you can redistribute and/or modify it under
9
 * the terms of the GNU Lesser General Public Licence as published
10
 * by the Free Software Foundation.
11
 * See the COPYING file for more information.
12
 *
13
 **********************************************************************
14
 *
15
 * Last port: operation/buffer/OffsetSegmentGenerator.java r378 (JTS-1.12)
16
 *
17
 **********************************************************************/
18
19
#include <cassert>
20
#include <cmath>
21
#include <vector>
22
23
#include <geos/algorithm/Angle.h>
24
#include <geos/algorithm/Distance.h>
25
#include <geos/algorithm/Orientation.h>
26
#include <geos/operation/buffer/OffsetSegmentGenerator.h>
27
#include <geos/operation/buffer/OffsetSegmentString.h>
28
#include <geos/operation/buffer/BufferInputLineSimplifier.h>
29
#include <geos/operation/buffer/BufferParameters.h>
30
#include <geos/geom/Position.h>
31
#include <geos/geom/CoordinateSequence.h>
32
#include <geos/geom/Coordinate.h>
33
#include <geos/geom/PrecisionModel.h>
34
#include <geos/algorithm/NotRepresentableException.h>
35
#include <geos/algorithm/CGAlgorithmsDD.h>
36
#include <geos/util.h>
37
38
39
using namespace geos::algorithm;
40
using namespace geos::geom;
41
42
namespace geos {
43
namespace operation { // geos.operation
44
namespace buffer { // geos.operation.buffer
45
46
/*private data*/
47
const double OffsetSegmentGenerator::CURVE_VERTEX_SNAP_DISTANCE_FACTOR = 1.0E-4;
48
const double OffsetSegmentGenerator::OFFSET_SEGMENT_SEPARATION_FACTOR = 1.0E-3;
49
const double OffsetSegmentGenerator::INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR = 1.0E-3;
50
const double OffsetSegmentGenerator::SIMPLIFY_FACTOR = 100.0;
51
52
/*public*/
53
OffsetSegmentGenerator::OffsetSegmentGenerator(
54
    const PrecisionModel* newPrecisionModel,
55
    const BufferParameters& nBufParams,
56
    double dist)
57
    :
58
0
    maxCurveSegmentError(0.0),
59
0
    closingSegLengthFactor(1),
60
0
    segList(),
61
0
    distance(dist),
62
0
    precisionModel(newPrecisionModel),
63
0
    bufParams(nBufParams),
64
0
    li(),
65
0
    s0(),
66
0
    s1(),
67
0
    s2(),
68
0
    seg0(),
69
0
    seg1(),
70
0
    offset0(),
71
0
    offset1(),
72
0
    side(0),
73
0
    _hasNarrowConcaveAngle(false),
74
0
    endCapIndex(0)
75
0
{
76
    // compute intersections in full precision, to provide accuracy
77
    // the points are rounded as they are inserted into the curve line
78
0
    filletAngleQuantum = Angle::PI_OVER_2 / bufParams.getQuadrantSegments();
79
80
0
    int quadSegs = bufParams.getQuadrantSegments();
81
0
    if (quadSegs < 1) quadSegs = 1;
82
0
    filletAngleQuantum = Angle::PI_OVER_2 / quadSegs;
83
84
    /*
85
     * Non-round joins cause issues with short closing segments,
86
     * so don't use them.  In any case, non-round joins
87
     * only really make sense for relatively small buffer distances.
88
     */
89
0
    if(bufParams.getQuadrantSegments() >= 8
90
0
            && bufParams.getJoinStyle() == BufferParameters::JOIN_ROUND) {
91
0
        closingSegLengthFactor = MAX_CLOSING_SEG_LEN_FACTOR;
92
0
    }
93
94
0
    init(distance);
95
0
}
96
97
/*private*/
98
void
99
OffsetSegmentGenerator::init(double newDistance)
100
0
{
101
0
    distance = newDistance;
102
0
    maxCurveSegmentError = distance * (1 - cos(filletAngleQuantum / 2.0));
103
104
    // Point list needs to be reset
105
0
    segList.reset();
106
0
    segList.setPrecisionModel(precisionModel);
107
108
    /*
109
     * Choose the min vertex separation as a small fraction of
110
     * the offset distance.
111
     */
112
0
    segList.setMinimumVertexDistance(
113
0
        distance * CURVE_VERTEX_SNAP_DISTANCE_FACTOR);
114
0
}
115
116
/*public*/
117
void
118
OffsetSegmentGenerator::initSideSegments(const Coordinate& nS1,
119
        const Coordinate& nS2, int nSide)
120
0
{
121
0
    s1 = nS1;
122
0
    s2 = nS2;
123
0
    side = nSide;
124
0
    seg1.setCoordinates(s1, s2);
125
0
    computeOffsetSegment(seg1, side, distance, offset1);
126
0
}
127
128
/*public*/
129
void
130
OffsetSegmentGenerator::addNextSegment(const Coordinate& p, bool addStartPoint)
131
0
{
132
133
    // do nothing if points are equal
134
0
    if(s2 == p) {
135
0
        return;
136
0
    }
137
138
    // s0-s1-s2 are the coordinates of the previous segment
139
    // and the current one
140
0
    s0 = s1;
141
0
    s1 = s2;
142
0
    s2 = p;
143
0
    seg0.setCoordinates(s0, s1);
144
0
    computeOffsetSegment(seg0, side, distance, offset0);
145
0
    seg1.setCoordinates(s1, s2);
146
0
    computeOffsetSegment(seg1, side, distance, offset1);
147
148
0
    int orientation = Orientation::index(s0, s1, s2);
149
0
    bool outsideTurn =
150
0
        (orientation == Orientation::CLOCKWISE
151
0
         && side == Position::LEFT)
152
0
        ||
153
0
        (orientation == Orientation::COUNTERCLOCKWISE
154
0
         && side == Position::RIGHT);
155
156
0
    if(orientation == 0) {
157
        // lines are collinear
158
0
        addCollinear(addStartPoint);
159
0
    }
160
0
    else if(outsideTurn) {
161
0
        addOutsideTurn(orientation, addStartPoint);
162
0
    }
163
0
    else {
164
        // inside turn
165
0
        addInsideTurn(orientation, addStartPoint);
166
0
    }
167
0
}
168
169
/*private*/
170
void
171
OffsetSegmentGenerator::computeOffsetSegment(const LineSegment& seg, int p_side,
172
        double p_distance, LineSegment& offset)
173
0
{
174
0
    int sideSign = p_side == Position::LEFT ? 1 : -1;
175
0
    double dx = seg.p1.x - seg.p0.x;
176
0
    double dy = seg.p1.y - seg.p0.y;
177
0
    double len = std::sqrt(dx * dx + dy * dy);
178
    // u is the vector that is the length of the offset,
179
    // in the direction of the segment
180
0
    double ux = sideSign * p_distance * dx / len;
181
0
    double uy = sideSign * p_distance * dy / len;
182
0
    offset.p0.x = seg.p0.x - uy;
183
0
    offset.p0.y = seg.p0.y + ux;
184
0
    offset.p1.x = seg.p1.x - uy;
185
0
    offset.p1.y = seg.p1.y + ux;
186
0
}
187
188
/*public*/
189
void
190
OffsetSegmentGenerator::addLineEndCap(const Coordinate& p0, const Coordinate& p1, const Coordinate& p2)
191
0
{
192
0
    LineSegment segL(p0, p1);
193
0
    LineSegment segR(p2, p1);
194
195
0
    LineSegment offsetL;
196
0
    computeOffsetSegment(segL, Position::LEFT, distance, offsetL);
197
0
    LineSegment offsetR;
198
0
    computeOffsetSegment(segR, Position::RIGHT, distance, offsetR);
199
200
0
    switch(bufParams.getEndCapStyle()) {
201
0
    case BufferParameters::CAP_ROUND:
202
        // add offset seg points with a fillet between them
203
0
        segList.addPt(offsetL.p1);
204
0
        addDirectedFillet(p1, offsetL.p1, offsetR.p1, Orientation::CLOCKWISE, distance);
205
0
        segList.addPt(offsetR.p1);
206
0
        break;
207
0
    case BufferParameters::CAP_FLAT:
208
        // only offset segment points are added
209
0
        segList.addPt(offsetL.p1);
210
0
        segList.addPt(offsetR.p1);
211
0
        break;
212
0
    case BufferParameters::CAP_SQUARE:
213
        // add a square defined by extensions of the offset
214
        // segment endpoints
215
216
0
        Coordinate squareCapSideOffset;
217
        // take average in case angles of left and right sides differ
218
0
        double dx = p1.x - p0.x/2 - p2.x/2;
219
0
        double dy = p1.y - p0.y/2 - p2.y/2;
220
0
        double angle = atan2(dy, dx);
221
0
        double sinangle, cosangle;
222
0
        Angle::sinCosSnap(angle, sinangle, cosangle);
223
0
        squareCapSideOffset.x = fabs(distance) * cosangle;
224
0
        squareCapSideOffset.y = fabs(distance) * sinangle;
225
226
0
        Coordinate squareCapLOffset(
227
0
            offsetL.p1.x + squareCapSideOffset.x,
228
0
            offsetL.p1.y + squareCapSideOffset.y);
229
0
        Coordinate squareCapROffset(
230
0
            offsetR.p1.x + squareCapSideOffset.x,
231
0
            offsetR.p1.y + squareCapSideOffset.y);
232
0
        segList.addPt(squareCapLOffset);
233
0
        segList.addPt(squareCapROffset);
234
0
        break;
235
0
    }
236
0
}
237
238
/*private*/
239
void
240
OffsetSegmentGenerator::addDirectedFillet(const Coordinate& p, const Coordinate& p0,
241
                                  const Coordinate& p1, int direction, double radius)
242
0
{
243
0
    double dx0 = p0.x - p.x;
244
0
    double dy0 = p0.y - p.y;
245
0
    double startAngle = atan2(dy0, dx0);
246
0
    double dx1 = p1.x - p.x;
247
0
    double dy1 = p1.y - p.y;
248
0
    double endAngle = atan2(dy1, dx1);
249
250
0
    if(direction == Orientation::CLOCKWISE) {
251
0
        if(startAngle <= endAngle) {
252
0
            startAngle += Angle::PI_TIMES_2;
253
0
        }
254
0
    }
255
0
    else {    // direction==COUNTERCLOCKWISE
256
0
        if(startAngle >= endAngle) {
257
0
            startAngle -= Angle::PI_TIMES_2;
258
0
        }
259
0
    }
260
261
0
    segList.addPt(p0);
262
0
    addDirectedFillet(p, startAngle, endAngle, direction, radius);
263
0
    segList.addPt(p1);
264
0
}
265
266
/*private*/
267
void
268
OffsetSegmentGenerator::addDirectedFillet(const Coordinate& p, double startAngle,
269
                                          double endAngle, int direction, double radius)
270
0
{
271
0
    int directionFactor = direction == Orientation::CLOCKWISE ? -1 : 1;
272
273
0
    double totalAngle = fabs(startAngle - endAngle);
274
0
    int nSegs = (int)(totalAngle / filletAngleQuantum + 0.5);
275
276
    // no segments because angle is less than increment-nothing to do!
277
0
    if(nSegs < 1) return;
278
279
0
    double angleInc = totalAngle / nSegs;
280
0
    double sinangle, cosangle;
281
0
    Coordinate pt;
282
0
    for (int i = 0; i < nSegs; i++) {
283
0
        Angle::sinCosSnap(startAngle + directionFactor * i * angleInc, sinangle, cosangle);
284
0
        pt.x = p.x + radius * cosangle;
285
0
        pt.y = p.y + radius * sinangle;
286
0
        segList.addPt(pt);
287
0
    }
288
0
}
289
290
/*private*/
291
void
292
OffsetSegmentGenerator::createCircle(const Coordinate& p, double p_distance)
293
0
{
294
    // add start point
295
0
    Coordinate pt(p.x + p_distance, p.y);
296
0
    segList.addPt(pt);
297
0
    addDirectedFillet(p, 0.0, Angle::PI_TIMES_2, -1, p_distance);
298
0
    segList.closeRing();
299
0
}
300
301
/*private*/
302
void
303
OffsetSegmentGenerator::createSquare(const Coordinate& p, double p_distance)
304
0
{
305
0
    segList.addPt(Coordinate(p.x + p_distance, p.y + p_distance));
306
0
    segList.addPt(Coordinate(p.x + p_distance, p.y - p_distance));
307
0
    segList.addPt(Coordinate(p.x - p_distance, p.y - p_distance));
308
0
    segList.addPt(Coordinate(p.x - p_distance, p.y + p_distance));
309
0
    segList.closeRing();
310
0
}
311
312
/* private */
313
void
314
OffsetSegmentGenerator::addCollinear(bool addStartPoint)
315
0
{
316
    /*
317
     * This test could probably be done more efficiently,
318
     * but the situation of exact collinearity should be fairly rare.
319
     */
320
321
0
    li.computeIntersection(s0, s1, s1, s2);
322
0
    auto numInt = li.getIntersectionNum();
323
324
    /*
325
     * if numInt is<2, the lines are parallel and in the same direction.
326
     * In this case the point can be ignored, since the offset lines
327
     * will also be parallel.
328
     */
329
0
    if(numInt >= 2) {
330
        /*
331
         * Segments are collinear but reversing.
332
         * Add an "end-cap" fillet
333
         * all the way around to other direction
334
         *
335
         * This case should ONLY happen for LineStrings,
336
         * so the orientation is always CW (Polygons can never
337
         * have two consecutive segments which are parallel but
338
         * reversed, because that would be a self intersection).
339
         */
340
0
        if(bufParams.getJoinStyle() == BufferParameters::JOIN_BEVEL
341
0
                || bufParams.getJoinStyle() == BufferParameters::JOIN_MITRE) {
342
0
            if(addStartPoint) {
343
0
                segList.addPt(offset0.p1);
344
0
            }
345
0
            segList.addPt(offset1.p0);
346
0
        }
347
0
        else {
348
0
            addDirectedFillet(s1, offset0.p1, offset1.p0,
349
0
                      Orientation::CLOCKWISE, distance);
350
0
        }
351
0
    }
352
0
}
353
354
/* private */
355
void
356
OffsetSegmentGenerator::addOutsideTurn(int orientation, bool addStartPoint)
357
0
{
358
    /*
359
     * Heuristic: If offset endpoints are very close together,
360
     * just use one of them as the corner vertex.
361
     * This avoids problems with computing mitre corners in the case
362
     * where the two segments are almost parallel
363
     * (which is hard to compute a robust intersection for).
364
     */
365
366
0
    if(offset0.p1.distance(offset1.p0) <
367
0
            distance * OFFSET_SEGMENT_SEPARATION_FACTOR) {
368
0
        segList.addPt(offset0.p1);
369
0
        return;
370
0
    }
371
372
0
    if(bufParams.getJoinStyle() == BufferParameters::JOIN_MITRE) {
373
0
        addMitreJoin(s1, offset0, offset1, distance);
374
0
    }
375
0
    else if(bufParams.getJoinStyle() == BufferParameters::JOIN_BEVEL) {
376
0
        addBevelJoin(offset0, offset1);
377
0
    }
378
0
    else {
379
        // add a circular fillet connecting the endpoints
380
        // of the offset segments
381
0
        if(addStartPoint) {
382
0
            segList.addPt(offset0.p1);
383
0
        }
384
385
        // TESTING - comment out to produce beveled joins
386
0
        addDirectedFillet(s1, offset0.p1, offset1.p0, orientation, distance);
387
0
        segList.addPt(offset1.p0);
388
0
    }
389
0
}
390
391
/* private */
392
void
393
OffsetSegmentGenerator::addInsideTurn(int orientation, bool addStartPoint)
394
0
{
395
0
    ::geos::ignore_unused_variable_warning(orientation);
396
0
    ::geos::ignore_unused_variable_warning(addStartPoint);
397
398
    // add intersection point of offset segments (if any)
399
0
    li.computeIntersection(offset0.p0, offset0.p1, offset1.p0, offset1.p1);
400
0
    if(li.hasIntersection()) {
401
0
        segList.addPt(li.getIntersection(0));
402
0
        return;
403
0
    }
404
405
    // If no intersection is detected, it means the angle is so small
406
    // and/or the offset so large that the offsets segments don't
407
    // intersect. In this case we must add a "closing segment" to make
408
    // sure the buffer curve is continuous,
409
    // fairly smooth (e.g. no sharp reversals in direction)
410
    // and tracks the buffer correctly around the corner.
411
    // The curve connects the endpoints of the segment offsets to points
412
    // which lie toward the centre point of the corner.
413
    // The joining curve will not appear in the final buffer outline,
414
    // since it is completely internal to the buffer polygon.
415
    //
416
    // In complex buffer cases the closing segment may cut across many
417
    // other segments in the generated offset curve.
418
    // In order to improve the performance of the noding, the closing
419
    // segment should be kept as short as possible.
420
    // (But not too short, since that would defeat it's purpose).
421
    // This is the purpose of the closingSegLengthFactor heuristic value.
422
423
    /*
424
     * The intersection test above is vulnerable to robustness errors;
425
     * i.e. it may be that the offsets should intersect very close to
426
     * their endpoints, but aren't reported as such due to rounding.
427
     * To handle this situation appropriately, we use the following test:
428
     * If the offset points are very close, don't add closing segments
429
     * but simply use one of the offset points
430
     */
431
432
0
    if(offset0.p1.distance(offset1.p0) <
433
0
            distance * INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR) {
434
0
        segList.addPt(offset0.p1);
435
0
    }
436
0
    else {
437
        // add endpoint of this segment offset
438
0
        segList.addPt(offset0.p1);
439
440
        // Add "closing segment" of required length.
441
0
        if(closingSegLengthFactor > 0) {
442
0
            Coordinate mid0(
443
0
                (closingSegLengthFactor * offset0.p1.x + s1.x) / (closingSegLengthFactor + 1),
444
0
                (closingSegLengthFactor * offset0.p1.y + s1.y) / (closingSegLengthFactor + 1)
445
0
            );
446
0
            segList.addPt(mid0);
447
448
0
            Coordinate mid1(
449
0
                (closingSegLengthFactor * offset1.p0.x + s1.x) / (closingSegLengthFactor + 1),
450
0
                (closingSegLengthFactor * offset1.p0.y + s1.y) / (closingSegLengthFactor + 1)
451
0
            );
452
0
            segList.addPt(mid1);
453
0
        }
454
0
        else {
455
            // This branch is not expected to be used
456
            // except for testing purposes.
457
            // It is equivalent to the JTS 1.9 logic for
458
            // closing segments (which results in very poor
459
            // performance for large buffer distances)
460
0
            segList.addPt(s1);
461
0
        }
462
463
        // add start point of next segment offset
464
0
        segList.addPt(offset1.p0);
465
0
    }
466
0
}
467
468
/* private */
469
void
470
OffsetSegmentGenerator::addMitreJoin(const geom::Coordinate& cornerPt,
471
                                     const geom::LineSegment& p_offset0,
472
                                     const geom::LineSegment& p_offset1,
473
                                     double p_distance)
474
0
{
475
0
    double mitreLimitDistance = bufParams.getMitreLimit() * p_distance;
476
    /**
477
     * First try a non-beveled join.
478
     * Compute the intersection point of the lines determined by the offsets.
479
     * Parallel or collinear lines will return a null point ==> need to be beveled
480
     *
481
     * Note: This computation is unstable if the offset segments are nearly collinear.
482
     * However, this situation should have been eliminated earlier by the check
483
     * for whether the offset segment endpoints are almost coincident
484
     */
485
0
    CoordinateXY intPt = algorithm::CGAlgorithmsDD::intersection(p_offset0.p0, p_offset0.p1, p_offset1.p0, p_offset1.p1);
486
487
0
    if (!intPt.isNull() && intPt.distance(cornerPt) <= mitreLimitDistance) {
488
0
        segList.addPt(Coordinate(intPt));
489
0
        return;
490
0
    }
491
    /**
492
     * In case the mitre limit is very small, try a plain bevel.
493
     * Use it if it's further than the limit.
494
     */
495
0
    double bevelDist = algorithm::Distance::pointToSegment(cornerPt, p_offset0.p1, p_offset1.p0);
496
0
    if (bevelDist >= mitreLimitDistance) {
497
0
        addBevelJoin(p_offset0, p_offset1);
498
0
        return;
499
0
    }
500
    /**
501
     * Have to construct a limited mitre bevel.
502
     */
503
0
    addLimitedMitreJoin(p_offset0, p_offset1, p_distance, mitreLimitDistance);
504
0
}
505
506
507
/* private */
508
void
509
OffsetSegmentGenerator::addLimitedMitreJoin(
510
    const geom::LineSegment& p_offset0,
511
    const geom::LineSegment& p_offset1,
512
    double p_distance,
513
    double p_mitreLimitDistance)
514
0
{
515
0
    const Coordinate& cornerPt = seg0.p1;
516
517
     // oriented angle of the corner formed by segments
518
0
    double angInterior = Angle::angleBetweenOriented(seg0.p0, cornerPt, seg1.p1);
519
    // half of the interior angle
520
0
    double angInterior2 = angInterior/2.0;
521
522
    // direction of bisector of the interior angle between the segments
523
0
    double dir0 = Angle::angle(cornerPt, seg0.p0);
524
0
    double dirBisector = Angle::normalize(dir0 + angInterior2);
525
    // rotating by PI gives the bisector of the outside angle,
526
    // which is the direction of the bevel midpoint from the corner apex
527
0
    double dirBisectorOut = Angle::normalize(dirBisector + MATH_PI);
528
529
    // compute the midpoint of the bevel segment
530
0
    Coordinate bevelMidPt = project(cornerPt, p_mitreLimitDistance, dirBisectorOut);
531
532
    // slope angle of bevel segment
533
0
    double dirBevel = Angle::normalize(dirBisectorOut + Angle::PI_OVER_2);
534
535
    // compute the candidate bevel segment by projecting both sides of the midpoint
536
0
    Coordinate bevel0 = project(bevelMidPt, p_distance, dirBevel);
537
0
    Coordinate bevel1 = project(bevelMidPt, p_distance, dirBevel + MATH_PI);
538
539
0
    Coordinate bevelInt0(Intersection::intersectionLineSegment(p_offset0.p0, p_offset0.p1, bevel0, bevel1));
540
0
    Coordinate bevelInt1(Intersection::intersectionLineSegment(p_offset1.p0, p_offset1.p1, bevel0, bevel1));
541
542
    //-- add the limited bevel, if it intersects the offsets
543
0
    if (!bevelInt0.isNull() && !bevelInt1.isNull()) {
544
0
        segList.addPt(bevelInt0);
545
0
        segList.addPt(bevelInt1);
546
0
        return;
547
0
    }
548
    /**
549
     * If the corner is very flat or the mitre limit is very small
550
     * the limited bevel segment may not intersect the offsets.
551
     * In this case just bevel the join.
552
     */
553
0
    addBevelJoin(p_offset0, p_offset1);
554
0
}
555
556
557
/* private static */
558
Coordinate
559
OffsetSegmentGenerator::project(const Coordinate& pt, double d, double dir)
560
0
{
561
0
    double sindir, cosdir;
562
0
    Angle::sinCosSnap(dir, sindir, cosdir);
563
0
    double x = pt.x + d * cosdir;
564
0
    double y = pt.y + d * sindir;
565
0
    return Coordinate(x, y);
566
0
}
567
568
569
/* private */
570
void
571
OffsetSegmentGenerator::addBevelJoin(const geom::LineSegment& p_offset0,
572
                                     const geom::LineSegment& p_offset1)
573
0
{
574
0
    segList.addPt(p_offset0.p1);
575
0
    segList.addPt(p_offset1.p0);
576
0
}
577
578
} // namespace geos.operation.buffer
579
} // namespace geos.operation
580
} // namespace geos