Coverage Report

Created: 2026-09-14 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/alg/marching_squares/polygon_ring_appender.h
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  Marching square algorithm
4
 * Purpose:  Core algorithm implementation for contour line generation.
5
 * Author:   Oslandia <infos at oslandia dot com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2018, Oslandia <infos at oslandia dot com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
#ifndef MARCHING_SQUARE_POLYGON_RING_APPENDER_H
13
#define MARCHING_SQUARE_POLYGON_RING_APPENDER_H
14
15
#include <vector>
16
#include <list>
17
#include <map>
18
#include <deque>
19
#include <cassert>
20
#include <iterator>
21
#include <memory>
22
#include <algorithm>
23
24
#include "cpl_quad_tree.h"
25
26
#include "point.h"
27
#include "ogr_api.h"
28
#include "ogr_geometry.h"
29
30
namespace marching_squares
31
{
32
33
// Receive rings of different levels and organize them
34
// into multi-polygons with possible interior rings when requested.
35
template <typename PolygonWriter> class PolygonRingAppender
36
{
37
  private:
38
    struct Ring
39
    {
40
0
        Ring() : points(), bbox(), interiorRings()
41
0
        {
42
0
        }
43
44
0
        Ring(const Ring &other) = default;
45
        Ring &operator=(const Ring &other) = default;
46
        // Declaring the copy operations above suppresses the
47
        // implicit move operations, so vector reshuffles and reallocations
48
        // deep-copied entire ring subtrees. Restore them.
49
0
        Ring(Ring &&other) = default;
50
0
        Ring &operator=(Ring &&other) = default;
51
52
        LineString points;
53
54
        // Bounding box, computed once when the ring is complete;
55
        // gives isIn() an O(1) reject so parent search stops being
56
        // O(rings * vertices) per insertion.
57
        OGREnvelope bbox;
58
59
        void computeBBox()
60
0
        {
61
0
            bbox = OGREnvelope();
62
0
            for (const auto &pt : points)
63
0
                bbox.Merge(pt.x, pt.y);
64
0
        }
65
66
        mutable std::vector<Ring> interiorRings;
67
68
        const Ring *closestExterior = nullptr;
69
70
        bool isIn(const Ring &other) const
71
0
        {
72
            // Check if this is inside other using the winding number algorithm
73
0
            auto checkPoint = this->points.front();
74
            // A point outside the candidate ring's bounding box
75
            // cannot be inside the ring.
76
0
            if (checkPoint.x < other.bbox.MinX ||
77
0
                checkPoint.x > other.bbox.MaxX ||
78
0
                checkPoint.y < other.bbox.MinY ||
79
0
                checkPoint.y > other.bbox.MaxY)
80
0
            {
81
0
                return false;
82
0
            }
83
0
            int windingNum = 0;
84
0
            auto otherIter = other.points.begin();
85
            // p1 and p2 define each segment of the ring other that will be
86
            // tested
87
0
            auto p1 = *otherIter;
88
0
            while (true)
89
0
            {
90
0
                otherIter++;
91
0
                if (otherIter == other.points.end())
92
0
                {
93
0
                    break;
94
0
                }
95
0
                auto p2 = *otherIter;
96
0
                if (p1.y <= checkPoint.y)
97
0
                {
98
0
                    if (p2.y > checkPoint.y)
99
0
                    {
100
0
                        if (isLeft(p1, p2, checkPoint))
101
0
                        {
102
0
                            ++windingNum;
103
0
                        }
104
0
                    }
105
0
                }
106
0
                else
107
0
                {
108
0
                    if (p2.y <= checkPoint.y)
109
0
                    {
110
0
                        if (!isLeft(p1, p2, checkPoint))
111
0
                        {
112
0
                            --windingNum;
113
0
                        }
114
0
                    }
115
0
                }
116
0
                p1 = p2;
117
0
            }
118
0
            return windingNum != 0;
119
0
        }
120
121
#ifdef DEBUG
122
        size_t id() const
123
        {
124
            return size_t(static_cast<const void *>(this)) & 0xffff;
125
        }
126
127
        void print(std::ostream &ostr) const
128
        {
129
            ostr << id() << ":";
130
            for (const auto &pt : points)
131
            {
132
                ostr << pt.x << "," << pt.y << " ";
133
            }
134
        }
135
#endif
136
    };
137
138
    void processTree(const std::vector<Ring> &tree, int level)
139
0
    {
140
0
        if (level % 2 == 0)
141
0
        {
142
0
            for (auto &r : tree)
143
0
            {
144
0
                writer_.addPart(r.points);
145
0
                for (auto &innerRing : r.interiorRings)
146
0
                {
147
0
                    writer_.addInteriorRing(innerRing.points);
148
0
                }
149
0
            }
150
0
        }
151
0
        for (auto &r : tree)
152
0
        {
153
0
            processTree(r.interiorRings, level + 1);
154
0
        }
155
0
    }
156
157
    // level -> rings
158
    std::map<double, std::vector<Ring>> rings_;
159
160
    // Point-in-polygon accelerator for one target ring: an
161
    // OGRPreparedGeometry (GEOS indexed point-in-area locator) over the
162
    // ring, built lazily when a ring turns out to capture many candidates.
163
    // The pathological case is a domain-spanning ring with millions of
164
    // vertices capturing tens of thousands of earlier rings; testing each
165
    // candidate against the raw ring is O(candidates * vertices). In builds
166
    // without GEOS support the capture step falls back to the linear
167
    // winding test.
168
    struct PreparedRing
169
    {
170
0
        PreparedRing() : poly(), prep()
171
0
        {
172
0
        }
173
174
        OGRPolygon poly;
175
        OGRPreparedGeometryUniquePtr prep;
176
177
        bool build(const Ring &r)
178
0
        {
179
0
            poly.empty();
180
0
            prep.reset();
181
0
            auto ring = std::make_unique<OGRLinearRing>();
182
0
            ring->setNumPoints(static_cast<int>(r.points.size()));
183
0
            int i = 0;
184
0
            for (const auto &pt : r.points)
185
0
                ring->setPoint(i++, pt.x, pt.y);
186
0
            poly.addRingDirectly(ring.release());
187
0
            poly.closeRings();
188
0
            prep.reset(OGRCreatePreparedGeometry(OGRGeometry::ToHandle(&poly)));
189
0
            return prep != nullptr;
190
0
        }
191
192
        bool contains(const Point &p) const
193
0
        {
194
0
            OGRPoint pt(p.x, p.y);
195
0
            return CPL_TO_BOOL(OGRPreparedGeometryContains(
196
0
                prep.get(), OGRGeometry::ToHandle(&pt)));
197
0
        }
198
    };
199
200
    // Per-level spatial index over TOP-LEVEL rings: a CPLQuadTree over ring
201
    // bounding boxes. Each stored feature points at the ring's slot index in
202
    // the level's ring vector, held in a std::deque so the pointer survives
203
    // growth; vector reallocation of the rings themselves is harmless. Rings
204
    // captured as interior rings of a later ring are removed from the tree
205
    // and their slot tombstoned (points cleared) rather than erased, keeping
206
    // the remaining indices stable.
207
    struct QuadTreeDestroyer
208
    {
209
        void operator()(CPLQuadTree *t) const
210
0
        {
211
0
            CPLQuadTreeDestroy(t);
212
0
        }
213
    };
214
215
    std::map<double, std::unique_ptr<CPLQuadTree, QuadTreeDestroyer>> index_;
216
    std::map<double, std::deque<std::size_t>> slots_;
217
    CPLRectObj domain_;
218
219
    static std::size_t featureSlot(const void *f)
220
0
    {
221
0
        return *static_cast<const std::size_t *>(f);
222
0
    }
223
224
    static CPLRectObj ringRect(const Ring &r)
225
0
    {
226
0
        return CPLRectObj{r.bbox.MinX, r.bbox.MinY, r.bbox.MaxX, r.bbox.MaxY};
227
0
    }
228
229
    PolygonWriter &writer_;
230
231
  public:
232
    const bool polygonize = true;
233
234
    PolygonRingAppender(PolygonWriter &writer, double minX, double minY,
235
                        double maxX, double maxY)
236
0
        : rings_(), index_(), slots_(), domain_{minX, minY, maxX, maxY},
237
0
          writer_(writer)
238
0
    {
239
0
    }
240
241
    void addLine(double level, LineString &ls, bool)
242
0
    {
243
0
        auto &levelRings = rings_[level];
244
0
        auto &levelTree = index_[level];
245
0
        auto &levelSlots = slots_[level];
246
0
        if (!levelTree)
247
0
            levelTree.reset(CPLQuadTreeCreate(&domain_, nullptr));
248
0
        if (ls.empty())
249
0
        {
250
0
            return;
251
0
        }
252
        // Create a new ring from the LineString
253
0
        Ring newRing;
254
0
        newRing.points.swap(ls);
255
0
        newRing.computeBBox();
256
        // Find the top-level parent (if any) through the index instead of
257
        // scanning every top-level ring, then descend the (short) nested
258
        // sibling lists exactly as before.
259
0
        Ring *parentRing = nullptr;
260
0
        {
261
0
            Ring *top = nullptr;
262
0
            const auto &fp0 = newRing.points.front();
263
0
            CPLRectObj aoi{fp0.x, fp0.y, fp0.x, fp0.y};
264
0
            int nHits = 0;
265
0
            void **hits = CPLQuadTreeSearch(levelTree.get(), &aoi, &nHits);
266
0
            for (int h = 0; h < nHits && top == nullptr; h++)
267
0
            {
268
0
                Ring &cand = levelRings[featureSlot(hits[h])];
269
0
                if (!cand.points.empty() && newRing.isIn(cand))
270
0
                    top = &cand;
271
0
            }
272
0
            CPLFree(hits);
273
0
            if (top != nullptr)
274
0
            {
275
0
                parentRing = top;
276
                // This queue holds the rings to be checked
277
0
                std::deque<Ring *> queue;
278
0
                std::transform(
279
0
                    top->interiorRings.begin(), top->interiorRings.end(),
280
0
                    std::back_inserter(queue), [](Ring &r) { return &r; });
281
0
                while (!queue.empty())
282
0
                {
283
0
                    Ring *curRing = queue.front();
284
0
                    queue.pop_front();
285
0
                    if (newRing.isIn(*curRing))
286
0
                    {
287
                        // We know that there should only be one ring per
288
                        // level that we should fit in, so we can discard the
289
                        // rest of the queue and try again with the children
290
                        // of this ring
291
0
                        parentRing = curRing;
292
0
                        queue.clear();
293
0
                        std::transform(curRing->interiorRings.begin(),
294
0
                                       curRing->interiorRings.end(),
295
0
                                       std::back_inserter(queue),
296
0
                                       [](Ring &r) { return &r; });
297
0
                    }
298
0
                }
299
0
            }
300
0
        }
301
0
        if (parentRing == nullptr)
302
0
        {
303
            // Top-level insertion: capture existing top-level rings that lie
304
            // inside the new ring, via the index. Build a per-target PIP
305
            // index lazily so a huge ring capturing many candidates costs
306
            // O(V + R * V/B), not O(R * V).
307
0
            std::vector<std::size_t> captured;
308
0
            PreparedRing pip;
309
0
            bool pipTried = false;
310
0
            bool pipBuilt = false;
311
0
            std::size_t nCandidates = 0;
312
0
            {
313
0
                CPLRectObj aoi = ringRect(newRing);
314
0
                int nHits = 0;
315
0
                void **hits = CPLQuadTreeSearch(levelTree.get(), &aoi, &nHits);
316
0
                for (int h = 0; h < nHits; h++)
317
0
                {
318
0
                    const std::size_t idx = featureSlot(hits[h]);
319
0
                    Ring &cand = levelRings[idx];
320
0
                    if (cand.points.empty())
321
0
                        continue;
322
0
                    const auto &fp = cand.points.front();
323
0
                    if (fp.x < newRing.bbox.MinX || fp.x > newRing.bbox.MaxX ||
324
0
                        fp.y < newRing.bbox.MinY || fp.y > newRing.bbox.MaxY)
325
0
                        continue;
326
0
                    if (!pipTried && ++nCandidates > 16 &&
327
0
                        newRing.points.size() > 512)
328
0
                    {
329
0
                        pipTried = true;
330
0
                        pipBuilt = pip.build(newRing);
331
0
                    }
332
0
                    const bool inside =
333
0
                        pipBuilt ? pip.contains(fp) : cand.isIn(newRing);
334
0
                    if (inside)
335
0
                        captured.push_back(idx);
336
0
                }
337
0
                CPLFree(hits);
338
0
            }
339
            // Sorting by slot restores insertion order, so captured rings
340
            // nest in the same order the original linear scan produced.
341
0
            std::sort(captured.begin(), captured.end());
342
0
            captured.erase(std::unique(captured.begin(), captured.end()),
343
0
                           captured.end());
344
0
            for (std::size_t idx : captured)
345
0
            {
346
0
                CPLRectObj rb = ringRect(levelRings[idx]);
347
0
                CPLQuadTreeRemove(levelTree.get(), &levelSlots[idx], &rb);
348
0
                newRing.interiorRings.push_back(std::move(levelRings[idx]));
349
0
                levelRings[idx].points.clear();  // tombstone the slot
350
0
            }
351
0
            levelRings.push_back(std::move(newRing));
352
0
            levelSlots.push_back(levelRings.size() - 1);
353
0
            CPLRectObj nb = ringRect(levelRings.back());
354
0
            CPLQuadTreeInsertWithBounds(levelTree.get(), &levelSlots.back(),
355
0
                                        &nb);
356
0
        }
357
0
        else
358
0
        {
359
            // Get a pointer to the list we need to check for rings to include
360
            // in this ring
361
0
            std::vector<Ring> *parentRingList = &(parentRing->interiorRings);
362
            // We found a valid parent, so we need to:
363
            // 1. Find all the inner rings of the parent that are inside the new
364
            // ring
365
0
            auto trueGroupIt = std::partition(
366
0
                parentRingList->begin(), parentRingList->end(),
367
0
                [&newRing](Ring &pRing) { return !pRing.isIn(newRing); });
368
            // 2. Move those rings out of the parent and into the new ring's
369
            // interior rings
370
0
            std::move(trueGroupIt, parentRingList->end(),
371
0
                      std::back_inserter(newRing.interiorRings));
372
            // 3. Get rid of the moved-from elements in the parent's interior
373
            // rings
374
0
            parentRingList->erase(trueGroupIt, parentRingList->end());
375
            // 4. Add the new ring to the parent's interior rings
376
0
            parentRingList->push_back(std::move(newRing));
377
0
        }
378
0
    }
379
380
    ~PolygonRingAppender()
381
0
    {
382
        // If there's no rings, nothing to do here
383
0
        if (rings_.size() == 0)
384
0
            return;
385
386
        // Traverse tree of rings
387
0
        for (auto &r : rings_)
388
0
        {
389
            // Drop tombstoned slots (rings captured as interior
390
            // rings of later-arriving parents) before traversal.
391
0
            std::vector<Ring> live;
392
0
            live.reserve(r.second.size());
393
0
            for (auto &ring : r.second)
394
0
                if (!ring.points.empty())
395
0
                    live.push_back(std::move(ring));
396
            // For each level, create a multipolygon by traversing the tree of
397
            // rings and adding a part for every other level
398
0
            writer_.startPolygon(r.first);
399
0
            processTree(live, 0);
400
0
            writer_.endPolygon();
401
0
        }
402
0
    }
403
};
404
405
}  // namespace marching_squares
406
407
#endif