GEOS 3.15.0dev
Grid.h
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#pragma once
16
17#include <numeric>
18#include <stdexcept>
19#include <vector>
20
21#include <geos/geom/Envelope.h>
22
23constexpr double DEFAULT_GRID_COMPAT_TOL = 1e-6;
24
25namespace geos::operation::grid {
26struct infinite_extent
27{
28 static constexpr size_t padding = 1;
29};
30
31struct bounded_extent
32{
33 static constexpr size_t padding = 0;
34};
35
36template<typename extent_tag>
37class Grid
38{
39
40 public:
41 Grid(const geom::Envelope& extent, double dx, double dy)
42 : m_extent{ extent }
43 , m_domain{}
44 , m_dx{ dx }
45 , m_dy{ dy }
46 , m_num_rows{ 2 * extent_tag::padding + (extent.getMaxY() > extent.getMinY() ? static_cast<size_t>(std::round(extent.getHeight() / dy)) : 0) }
47 , m_num_cols{ 2 * extent_tag::padding + (extent.getMaxX() > extent.getMinX() ? static_cast<size_t>(std::round(extent.getWidth() / dx)) : 0) } {
48 static_assert(std::is_same_v<extent_tag, bounded_extent>);
49 }
50
51 Grid(const geom::Envelope& extent, double dx, double dy, const geom::Envelope& domain)
52 : m_extent{ extent }
53 , m_domain{ domain }
54 , m_dx{ dx }
55 , m_dy{ dy }
56 , m_num_rows{ 2 * extent_tag::padding + (extent.getMaxY() > extent.getMinY() ? static_cast<size_t>(std::round(extent.getHeight() / dy)) : 0) }
57 , m_num_cols{ 2 * extent_tag::padding + (extent.getMaxX() > extent.getMinX() ? static_cast<size_t>(std::round(extent.getWidth() / dx)) : 0) }
58 {
59 static_assert(std::is_same_v<extent_tag, infinite_extent>);
60 }
61
62
63 static Grid makeEmpty()
64 {
65 if constexpr (std::is_same_v<extent_tag, bounded_extent>) {
66 return Grid({ 0, 0, 0, 0 }, 0, 0);
67 } else {
68 return Grid({ 0, 0, 0, 0 }, 0, 0, geom::Envelope());
69 }
70 }
71
72 size_t getColumn(double x) const
73 {
74 if (extent_tag::padding) {
75 if (x < m_extent.getMinX())
76 return 0;
77 if (x > m_extent.getMaxX())
78 return m_num_cols - 1;
79 if (x == m_extent.getMaxX()) // special case, returning the cell for which xmax is the right
80 return m_num_cols - 2;
81 } else {
82 if (x < m_extent.getMinX() || x > m_extent.getMaxX())
83 throw std::out_of_range("x");
84
85 if (x == m_extent.getMaxX())
86 return m_num_cols - 1;
87 }
88
89 // Since we've already range-checked our x value, make sure that
90 // the computed column index is no greater than the column index
91 // associated with xmax.
92 return std::min(
93 extent_tag::padding + static_cast<size_t>(std::floor((x - m_extent.getMinX()) / m_dx)),
94 getColumn(m_extent.getMaxX()));
95 }
96
97 size_t getRow(double y) const
98 {
99 if (extent_tag::padding) {
100 if (y > m_extent.getMaxY())
101 return 0;
102 if (y < m_extent.getMinY())
103 return m_num_rows - 1;
104 if (y == m_extent.getMinY()) // special case, returning the cell for which ymin is the bottom
105 return m_num_rows - 2;
106 } else {
107 if (y < m_extent.getMinY() || y > m_extent.getMaxY())
108 throw std::out_of_range("y");
109
110 if (y == m_extent.getMinY())
111 return m_num_rows - 1;
112 }
113
114 // Since we've already range-checked our y value, make sure that
115 // the computed row index is no greater than the column index
116 // associated with ymin.
117 return std::min(
118 extent_tag::padding + static_cast<size_t>(std::floor((m_extent.getMaxY() - y) / m_dy)),
119 getRow(m_extent.getMinY()));
120 }
121
122 std::size_t getCell(double x, double y) const
123 {
124 return getRow(y) * getNumCols() + getColumn(x);
125 }
126
127 bool isEmpty() const { return m_num_rows <= 2 * extent_tag::padding && m_num_cols <= 2 * extent_tag::padding; }
128
129 size_t getNumRows() const { return m_num_rows; }
130
131 size_t getNumCols() const { return m_num_cols; }
132
133 size_t getSize() const { return getNumRows() * getNumCols(); }
134
135 double xmin() const { return m_extent.getMinX(); }
136
137 double xmax() const { return m_extent.getMaxX(); }
138
139 double ymin() const { return m_extent.getMinY(); }
140
141 double ymax() const { return m_extent.getMaxY(); }
142
143 double dx() const { return m_dx; }
144
145 double dy() const { return m_dy; }
146
147 const geom::Envelope& getExtent() const { return m_extent; }
148
149 size_t getRowOffset(const Grid& other) const { return static_cast<size_t>(std::round(std::abs(other.m_extent.getMaxY() - m_extent.getMaxY()) / m_dy)); }
150
151 size_t getColOffset(const Grid& other) const { return static_cast<size_t>(std::round(std::abs(m_extent.getMinX() - other.m_extent.getMinX()) / m_dx)); }
152
153 double getColX(size_t col) const { return m_extent.getMinX() + (static_cast<double>(col - extent_tag::padding) + 0.5) * m_dx; }
154
155 double getRowY(size_t row) const { return m_extent.getMaxY() - (static_cast<double>(row - extent_tag::padding) + 0.5) * m_dy; }
156
157 Grid<extent_tag> shrinkToFit(const geom::Envelope& b) const
158 {
159 if (b.getArea() == 0) {
160 return makeEmpty();
161 }
162
163 if (b.getMinX() < m_extent.getMinX() || b.getMinY() < m_extent.getMinY() || b.getMaxX() > m_extent.getMaxX() || b.getMaxY() > m_extent.getMaxY()) {
164 throw std::range_error("Cannot shrink extent to bounds larger than original.");
165 }
166
167 size_t col0 = getColumn(b.getMinX());
168 size_t row1 = getRow(b.getMaxY());
169
170 // Shrink xmin and ymax to fit the upper-left corner of the supplied extent
171 double snapped_xmin = m_extent.getMinX() + static_cast<double>(col0 - extent_tag::padding) * m_dx;
172 double snapped_ymax = m_extent.getMaxY() - static_cast<double>(row1 - extent_tag::padding) * m_dy;
173
174 // Make sure x0 and y1 are within the reduced extent. Because of
175 // floating point round-off errors, this is not always the case.
176 if (b.getMinX() < snapped_xmin) {
177 snapped_xmin -= m_dx;
178 col0--;
179 }
180
181 if (b.getMaxY() > snapped_ymax) {
182 snapped_ymax += m_dy;
183 row1--;
184 }
185
186 size_t col1 = getColumn(b.getMaxX());
187 size_t row0 = getRow(b.getMinY());
188
189 size_t num_rows = 1 + (row0 - row1);
190 size_t num_cols = 1 + (col1 - col0);
191
192 // If xmax or ymin falls cleanly on a cell boundary, we don't
193 // need as many rows or columns as we otherwise would, because
194 // we assume that the rightmost cell of the grid is a closed
195 // interval in x, and the lowermost cell of the grid is a
196 // closed interval in y.
197 if (num_rows > 2 && (snapped_ymax - static_cast<double>(num_rows - 1) * m_dy <= b.getMinY())) {
198 num_rows--;
199 }
200 if (num_cols > 2 && (snapped_xmin + static_cast<double>(num_cols - 1) * m_dx >= b.getMaxX())) {
201 num_cols--;
202 }
203
204 // Perform offsets relative to the new xmin, ymax origin
205 // points. If this is not done, then floating point roundoff
206 // error can cause progressive shrink() calls with the same
207 // inputs to produce different results.
208 geom::Envelope reduced_box = {
209 snapped_xmin,
210 std::max(snapped_xmin + static_cast<double>(num_cols) * m_dx, b.getMaxX()),
211 std::min(snapped_ymax - static_cast<double>(num_rows) * m_dy, b.getMinY()),
212 snapped_ymax
213 };
214
215 // Fudge computed xmax and ymin, if needed, to prevent extent
216 // from growing during a shrink operation.
217 if (reduced_box.getMaxX() > m_extent.getMaxX()) {
218 if (std::round(reduced_box.getWidth() / m_dx) ==
219 std::round((m_extent.getMaxX() - reduced_box.getMinX()) / m_dx)) {
220 reduced_box = geom::Envelope(reduced_box.getMinX(), m_extent.getMaxX(), reduced_box.getMinY(), reduced_box.getMaxY());
221 } else {
222 throw std::runtime_error("Shrink operation failed.");
223 }
224 }
225 if (reduced_box.getMinY() < m_extent.getMinY()) {
226 if (std::round(reduced_box.getHeight() / m_dy) ==
227 std::round((reduced_box.getMaxY() - m_extent.getMinY()) / m_dy)) {
228 reduced_box = geom::Envelope(reduced_box.getMinX(), reduced_box.getMaxX(), m_extent.getMinY(), reduced_box.getMaxY());
229 } else {
230 throw std::runtime_error("Shrink operation failed.");
231 }
232 }
233
234 if constexpr (std::is_same_v<extent_tag, bounded_extent>) {
235 Grid<extent_tag> reduced{ reduced_box, m_dx, m_dy };
236
237 if (b.getMinX() < reduced.xmin() || b.getMinY() < reduced.ymin() || b.getMaxX() > reduced.xmax() || b.getMaxY() > reduced.ymax()) {
238 throw std::runtime_error("Shrink operation failed.");
239 }
240
241 return reduced;
242 } else {
243 Grid<extent_tag> reduced{ reduced_box, m_dx, m_dy , m_domain };
244
245 if (b.getMinX() < reduced.xmin() || b.getMinY() < reduced.ymin() || b.getMaxX() > reduced.xmax() || b.getMaxY() > reduced.ymax()) {
246 throw std::runtime_error("Shrink operation failed.");
247 }
248
249 return reduced;
250
251 }
252 }
253
254 bool operator==(const Grid<extent_tag>& b) const
255 {
256 return m_extent == b.m_extent &&
257 m_dx == b.m_dx &&
258 m_dy == b.m_dy;
259 }
260
261 bool operator!=(const Grid<extent_tag>& b) const
262 {
263 return !(*this == b);
264 }
265
266 geom::Envelope getCellEnvelope(size_t row, size_t col) const;
267
268 private:
269 geom::Envelope m_extent;
270 geom::Envelope m_domain;
271
272 double m_dx;
273 double m_dy;
274
275 size_t m_num_rows;
276 size_t m_num_cols;
277};
278
279Grid<infinite_extent>
280make_infinite(const Grid<bounded_extent>& grid, const geom::Envelope& domain);
281Grid<bounded_extent>
282make_finite(const Grid<infinite_extent>& grid);
283
284}
double round(double val)
Definition math.h:36