/src/geos/src/operation/grid/TraversalAreas.cpp
Line | Count | Source |
1 | | /********************************************************************** |
2 | | * |
3 | | * GEOS - Geometry Engine Open Source |
4 | | * http://geos.osgeo.org |
5 | | * |
6 | | * Copyright (C) 2018-2025 ISciences, LLC |
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 | | #include <limits> |
16 | | #include <vector> |
17 | | |
18 | | #include <geos/algorithm/Area.h> |
19 | | #include <geos/algorithm/Orientation.h> |
20 | | #include <geos/geom/Coordinate.h> |
21 | | #include <geos/geom/CoordinateSequence.h> |
22 | | #include <geos/geom/Geometry.h> |
23 | | #include <geos/geom/GeometryFactory.h> |
24 | | #include <geos/operation/grid/PerimeterDistance.h> |
25 | | |
26 | | #include <geos/operation/polygonize/Polygonizer.h> |
27 | | #include <geos/operation/grid/TraversalAreas.h> |
28 | | |
29 | | using geos::geom::CoordinateSequence; |
30 | | using geos::geom::CoordinateXY; |
31 | | using geos::geom::Geometry; |
32 | | using geos::geom::GeometryFactory; |
33 | | using geos::geom::Envelope; |
34 | | |
35 | | namespace geos::operation::grid { |
36 | | |
37 | | struct CoordinateChain |
38 | | { |
39 | | double start; // perimeter distance value of the first coordinate |
40 | | double stop; // perimeter distance value of the last coordinate |
41 | | const std::vector<CoordinateXY>* coordinates; |
42 | | bool visited; |
43 | | |
44 | | CoordinateChain(double p_start, double p_stop, const std::vector<CoordinateXY>* p_coords) |
45 | 0 | : start{ p_start } |
46 | 0 | , stop{ p_stop } |
47 | 0 | , coordinates{ p_coords } |
48 | 0 | , visited{ false } |
49 | 0 | { |
50 | 0 | } |
51 | | }; |
52 | | |
53 | | static double |
54 | | exit_to_entry_perimeter_distance_ccw(const CoordinateChain& c1, const CoordinateChain& c2, double perimeter) |
55 | 0 | { |
56 | 0 | return PerimeterDistance::getPerimeterDistanceCCW(c1.stop, c2.start, perimeter); |
57 | 0 | } |
58 | | |
59 | | static CoordinateChain* |
60 | | getNextChain(std::vector<CoordinateChain>& chains, |
61 | | const CoordinateChain* chain, |
62 | | const CoordinateChain* kill, |
63 | | double perimeter) |
64 | 0 | { |
65 | |
|
66 | 0 | CoordinateChain* min = nullptr; |
67 | 0 | double min_distance = std::numeric_limits<double>::max(); |
68 | |
|
69 | 0 | for (CoordinateChain& candidate : chains) { |
70 | 0 | if (candidate.visited && std::addressof(candidate) != kill) { |
71 | 0 | continue; |
72 | 0 | } |
73 | | |
74 | 0 | double distance = exit_to_entry_perimeter_distance_ccw(*chain, candidate, perimeter); |
75 | 0 | if (distance < min_distance) { |
76 | 0 | min_distance = distance; |
77 | 0 | min = std::addressof(candidate); |
78 | 0 | } |
79 | 0 | } |
80 | |
|
81 | 0 | if (min == nullptr) { |
82 | 0 | throw std::runtime_error("Failed to find next CoordinateChain"); |
83 | 0 | } |
84 | | |
85 | 0 | return min; |
86 | 0 | } |
87 | | |
88 | | template<typename T> |
89 | | bool |
90 | | hasMultipleUniqueCoordinates(const T& vec) |
91 | 0 | { |
92 | 0 | for (std::size_t i = 1; i < vec.size(); i++) { |
93 | 0 | if (vec[i] != vec[0]) { |
94 | 0 | return true; |
95 | 0 | } |
96 | 0 | } |
97 | | |
98 | 0 | return false; |
99 | 0 | } |
100 | | |
101 | | /** |
102 | | * @brief Identify counter-clockwise rings formed by the supplied coordinate vectors and the boundary of this box. |
103 | | * |
104 | | * @param box Box to be included in rings |
105 | | * @param coord_lists A list of coordinate vectors, with each vector representing a traversal of `box` or a closed ring. |
106 | | * @param visitor Function be applied to each ring identified. Because `coord_lists` may include both clockwise and |
107 | | * counter-clockwise closed rings, `visitor` will be provided with the orientation of each ring as an |
108 | | * argument. |
109 | | */ |
110 | | template<typename F> |
111 | | void |
112 | | visitRings(const Envelope& box, const std::vector<const std::vector<CoordinateXY>*>& coord_lists, F&& visitor) |
113 | 0 | { |
114 | 0 | std::vector<CoordinateChain> chains; |
115 | 0 | chains.reserve(coord_lists.size() + 4); |
116 | |
|
117 | 0 | for (const auto& coords : coord_lists) { |
118 | 0 | if (!hasMultipleUniqueCoordinates(*coords)) { |
119 | 0 | continue; |
120 | 0 | } |
121 | | |
122 | 0 | if (coords->front() == coords->back() && hasMultipleUniqueCoordinates(*coords)) { |
123 | | // Closed ring. Check orientation. |
124 | | |
125 | | // TODO: Remove copy |
126 | 0 | CoordinateSequence seq(0, false, false); |
127 | 0 | seq.setPoints(*coords); |
128 | 0 | bool is_ccw = algorithm::Orientation::isCCW(&seq); |
129 | 0 | visitor(*coords, is_ccw); |
130 | 0 | } else { |
131 | 0 | double start = PerimeterDistance::getPerimeterDistance(box, coords->front()); |
132 | 0 | double stop = PerimeterDistance::getPerimeterDistance(box, coords->back()); |
133 | |
|
134 | 0 | chains.emplace_back(start, stop, coords); |
135 | 0 | } |
136 | 0 | } |
137 | |
|
138 | 0 | double height{ box.getHeight() }; |
139 | 0 | double width{ box.getWidth() }; |
140 | 0 | double perimeter{ box.getPerimeter() }; |
141 | | |
142 | | // create coordinate lists for corners |
143 | 0 | std::vector<CoordinateXY> bottom_left = { CoordinateXY(box.getMinX(), box.getMinY()) }; |
144 | 0 | std::vector<CoordinateXY> top_left = { CoordinateXY(box.getMinX(), box.getMaxY()) }; |
145 | 0 | std::vector<CoordinateXY> top_right = { CoordinateXY(box.getMaxX(), box.getMaxY()) }; |
146 | 0 | std::vector<CoordinateXY> bottom_right = { CoordinateXY(box.getMaxX(), box.getMinY()) }; |
147 | | |
148 | | // Add chains for corners |
149 | 0 | chains.emplace_back(0.0, 0.0, &bottom_left); |
150 | 0 | chains.emplace_back(height, height, &top_left); |
151 | 0 | chains.emplace_back(height + width, height + width, &top_right); |
152 | 0 | chains.emplace_back(2 * height + width, 2 * height + width, &bottom_right); |
153 | |
|
154 | 0 | std::vector<CoordinateXY> coords; |
155 | 0 | for (auto& chain_ref : chains) { |
156 | 0 | if (chain_ref.visited || chain_ref.coordinates->size() == 1) { |
157 | 0 | continue; |
158 | 0 | } |
159 | | |
160 | 0 | coords.clear(); |
161 | |
|
162 | 0 | CoordinateChain* chain = std::addressof(chain_ref); |
163 | 0 | CoordinateChain* first_chain = chain; |
164 | 0 | do { |
165 | 0 | chain->visited = true; |
166 | 0 | coords.insert(coords.end(), chain->coordinates->cbegin(), chain->coordinates->cend()); |
167 | 0 | chain = getNextChain(chains, chain, first_chain, perimeter); |
168 | 0 | } while (chain != first_chain); |
169 | |
|
170 | 0 | coords.push_back(coords[0]); |
171 | |
|
172 | 0 | if (hasMultipleUniqueCoordinates(coords)) { |
173 | 0 | visitor(coords, true); |
174 | 0 | } |
175 | 0 | } |
176 | 0 | } Unexecuted instantiation: TraversalAreas.cpp:void geos::operation::grid::visitRings<geos::operation::grid::TraversalAreas::getLeftHandArea(geos::geom::Envelope const&, std::__1::vector<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*, std::__1::allocator<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*> > const&)::$_0>(geos::geom::Envelope const&, std::__1::vector<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*, std::__1::allocator<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*> > const&, geos::operation::grid::TraversalAreas::getLeftHandArea(geos::geom::Envelope const&, std::__1::vector<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*, std::__1::allocator<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*> > const&)::$_0&&) Unexecuted instantiation: TraversalAreas.cpp:void geos::operation::grid::visitRings<geos::operation::grid::TraversalAreas::getLeftHandRings(geos::geom::GeometryFactory const&, geos::geom::Envelope const&, std::__1::vector<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*, std::__1::allocator<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*> > const&)::$_0>(geos::geom::Envelope const&, std::__1::vector<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*, std::__1::allocator<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*> > const&, geos::operation::grid::TraversalAreas::getLeftHandRings(geos::geom::GeometryFactory const&, geos::geom::Envelope const&, std::__1::vector<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*, std::__1::allocator<std::__1::vector<geos::geom::CoordinateXY, std::__1::allocator<geos::geom::CoordinateXY> > const*> > const&)::$_0&&) |
177 | | |
178 | | double |
179 | | TraversalAreas::getLeftHandArea(const Envelope& box, const std::vector<const std::vector<CoordinateXY>*>& coord_lists) |
180 | 0 | { |
181 | 0 | double ccw_sum = 0; |
182 | 0 | double cw_sum = 0; |
183 | 0 | bool found_a_ring = false; |
184 | |
|
185 | 0 | visitRings(box, coord_lists, [&cw_sum, &ccw_sum, &found_a_ring](const std::vector<CoordinateXY>& coords, bool is_ccw) { |
186 | 0 | found_a_ring = true; |
187 | |
|
188 | 0 | if (is_ccw) { |
189 | 0 | ccw_sum += algorithm::Area::ofRing(coords); |
190 | 0 | } else { |
191 | 0 | cw_sum += algorithm::Area::ofRing(coords); |
192 | 0 | } |
193 | 0 | }); |
194 | |
|
195 | 0 | if (!found_a_ring) { |
196 | 0 | throw std::runtime_error("Cannot determine coverage fraction (it is either 0 or 100%)"); |
197 | 0 | } |
198 | | |
199 | | // If this box has only clockwise rings (holes) then the area |
200 | | // of those holes should be subtracted from the complete box area. |
201 | 0 | if (ccw_sum < cw_sum) { |
202 | 0 | ccw_sum += box.getArea(); |
203 | 0 | } |
204 | |
|
205 | 0 | return ccw_sum - cw_sum; |
206 | 0 | } |
207 | | |
208 | | std::unique_ptr<Geometry> |
209 | | TraversalAreas::getLeftHandRings(const GeometryFactory& gfact, const Envelope& box, const std::vector<const std::vector<CoordinateXY>*>& coord_lists) |
210 | 0 | { |
211 | 0 | using geom::LinearRing; |
212 | |
|
213 | 0 | std::vector<std::unique_ptr<LinearRing>> shells; |
214 | 0 | std::vector<std::unique_ptr<LinearRing>> holes; |
215 | |
|
216 | 0 | bool found_a_ring = false; |
217 | |
|
218 | 0 | visitRings(box, coord_lists, [&gfact, &shells, &holes, &found_a_ring](const std::vector<CoordinateXY>& coords, bool is_ccw) { |
219 | 0 | found_a_ring = true; |
220 | | |
221 | | // finding a collapsed ring is sufficient to determine whether the cell interior is inside our outside, |
222 | | // but we don't want to actually construct the ring. |
223 | 0 | if (algorithm::Area::ofRing(coords) == 0) { |
224 | 0 | return; |
225 | 0 | } |
226 | | |
227 | 0 | CoordinateSequence seq(0, false, false); |
228 | 0 | seq.setPoints(coords); |
229 | 0 | auto ring = gfact.createLinearRing(std::move(seq)); |
230 | |
|
231 | 0 | if (is_ccw) { |
232 | 0 | shells.push_back(std::move(ring)); |
233 | 0 | } else { |
234 | 0 | holes.push_back(std::move(ring)); |
235 | 0 | } |
236 | 0 | }); |
237 | |
|
238 | 0 | if (!found_a_ring) { |
239 | 0 | throw std::runtime_error("Cannot determine coverage fraction (it is either 0 or 100%)"); |
240 | 0 | } |
241 | | |
242 | 0 | if (shells.empty() && holes.empty()) { |
243 | 0 | return gfact.createPolygon(); |
244 | 0 | } |
245 | | |
246 | 0 | if (shells.empty() && !holes.empty()) { |
247 | 0 | CoordinateSequence seq(5, false, false); |
248 | 0 | seq.setAt(CoordinateXY{box.getMinX(), box.getMinY()}, 0); |
249 | 0 | seq.setAt(CoordinateXY{box.getMaxX(), box.getMinY()}, 1); |
250 | 0 | seq.setAt(CoordinateXY{box.getMaxX(), box.getMaxY()}, 2); |
251 | 0 | seq.setAt(CoordinateXY{box.getMinX(), box.getMaxY()}, 3); |
252 | 0 | seq.setAt(CoordinateXY{box.getMinX(), box.getMinY()}, 4); |
253 | |
|
254 | 0 | shells.push_back(gfact.createLinearRing(std::move(seq))); |
255 | 0 | } |
256 | |
|
257 | 0 | if (holes.empty()) { |
258 | 0 | std::vector<std::unique_ptr<Geometry>> polygons; |
259 | 0 | for (auto& shell : shells) { |
260 | 0 | polygons.push_back(gfact.createPolygon(std::move(shell))); |
261 | 0 | } |
262 | |
|
263 | 0 | if (polygons.size() == 1) { |
264 | 0 | return std::move(polygons.front()); |
265 | 0 | } else { |
266 | 0 | return gfact.createMultiPolygon(std::move(polygons)); |
267 | 0 | } |
268 | 0 | } |
269 | | |
270 | 0 | if (shells.size() == 1) { |
271 | 0 | return gfact.createPolygon(std::move(shells.front()), std::move(holes)); |
272 | 0 | } |
273 | | |
274 | 0 | polygonize::Polygonizer polygonizer(true); |
275 | |
|
276 | 0 | for (const auto& shell : shells) { |
277 | 0 | polygonizer.add(static_cast<const Geometry*>(shell.get())); |
278 | 0 | } |
279 | 0 | for (const auto& hole : holes) { |
280 | 0 | polygonizer.add(static_cast<const Geometry*>(hole.get())); |
281 | 0 | } |
282 | |
|
283 | 0 | auto polygonized = polygonizer.getPolygons(); |
284 | 0 | return gfact.createMultiPolygon(std::move(polygonized)); |
285 | 0 | } |
286 | | |
287 | | } |