-
Notifications
You must be signed in to change notification settings - Fork 60
/
Copy pathh3_bbox.c
311 lines (288 loc) · 11.5 KB
/
h3_bbox.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/*
* Copyright 2016-2021 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file bbox.c
* @brief Geographic bounding box functions
*/
#include "h3_bbox.h"
#include <float.h>
#include <math.h>
#include <stdbool.h>
#include "h3_constants.h"
#include "h3_h3Index.h"
#include "h3_latLng.h"
/**
* Width of the bounding box, in rads
* @param bbox Bounding box to inspect
* @return width, in rads
*/
double bboxWidthRads(const BBox *bbox) {
return bboxIsTransmeridian(bbox) ? bbox->east - bbox->west + M_2PI
: bbox->east - bbox->west;
}
/**
* Height of the bounding box
* @param bbox Bounding box to inspect
* @return height, in rads
*/
double bboxHeightRads(const BBox *bbox) { return bbox->north - bbox->south; }
/**
* Whether the given bounding box crosses the antimeridian
* @param bbox Bounding box to inspect
* @return is transmeridian
*/
bool bboxIsTransmeridian(const BBox *bbox) { return bbox->east < bbox->west; }
/**
* Get the center of a bounding box
* @param bbox Input bounding box
* @param center Output center coordinate
*/
void bboxCenter(const BBox *bbox, LatLng *center) {
center->lat = (bbox->north + bbox->south) * 0.5;
// If the bbox crosses the antimeridian, shift east 360 degrees
double east = bboxIsTransmeridian(bbox) ? bbox->east + M_2PI : bbox->east;
center->lng = constrainLng((east + bbox->west) * 0.5);
}
/**
* Whether the bounding box contains a given point
* @param bbox Bounding box
* @param point Point to test
* @return Whether the point is contained
*/
bool bboxContains(const BBox *bbox, const LatLng *point) {
return point->lat >= bbox->south && point->lat <= bbox->north &&
(bboxIsTransmeridian(bbox) ?
// transmeridian case
(point->lng >= bbox->west || point->lng <= bbox->east)
:
// standard case
(point->lng >= bbox->west && point->lng <= bbox->east));
}
/**
* Whether two bounding boxes overlap
* @param a First bounding box
* @param b Second bounding box
* @return Whether the bounding boxes overlap
*/
bool bboxOverlapsBBox(const BBox *a, const BBox *b) {
// Check whether latitude coords overlap
if (a->north < b->south || a->south > b->north) {
return false;
}
// Check whether longitude coords overlap, accounting for transmeridian
// bboxes
LongitudeNormalization aNormalization;
LongitudeNormalization bNormalization;
bboxNormalization(a, b, &aNormalization, &bNormalization);
if (normalizeLng(a->east, aNormalization) <
normalizeLng(b->west, bNormalization) ||
normalizeLng(a->west, aNormalization) >
normalizeLng(b->east, bNormalization)) {
return false;
}
return true;
}
/**
* Whether one bounding box contains another
* @param a First bounding box
* @param b Second bounding box
* @return Whether a contains b
*/
bool bboxContainsBBox(const BBox *a, const BBox *b) {
// Check whether latitude coords are contained
if (a->north < b->north || a->south > b->south) {
return false;
}
// Check whether longitude coords are contained
// Account for transmeridian bboxes
LongitudeNormalization aNormalization;
LongitudeNormalization bNormalization;
bboxNormalization(a, b, &aNormalization, &bNormalization);
return normalizeLng(a->west, aNormalization) <=
normalizeLng(b->west, bNormalization) &&
normalizeLng(a->east, aNormalization) >=
normalizeLng(b->east, bNormalization);
}
/**
* Whether two bounding boxes are strictly equal
* @param b1 Bounding box 1
* @param b2 Bounding box 2
* @return Whether the boxes are equal
*/
bool bboxEquals(const BBox *b1, const BBox *b2) {
return b1->north == b2->north && b1->south == b2->south &&
b1->east == b2->east && b1->west == b2->west;
}
CellBoundary bboxToCellBoundary(const BBox *bbox) {
// Convert bbox to cell boundary, CCW vertex order
CellBoundary bboxBoundary = {.numVerts = 4,
.verts = {{bbox->north, bbox->east},
{bbox->north, bbox->west},
{bbox->south, bbox->west},
{bbox->south, bbox->east}}};
return bboxBoundary;
}
/**
* _hexRadiusKm returns the radius of a given hexagon in Km
*
* @param h3Index the index of the hexagon
* @return the radius of the hexagon in Km
*/
double _hexRadiusKm(H3Index h3Index) {
// There is probably a cheaper way to determine the radius of a
// hexagon, but this way is conceptually simple
LatLng h3Center;
CellBoundary h3Boundary;
H3_EXPORT(cellToLatLng)(h3Index, &h3Center);
H3_EXPORT(cellToBoundary)(h3Index, &h3Boundary);
return H3_EXPORT(greatCircleDistanceKm)(&h3Center, h3Boundary.verts);
}
/**
* bboxHexEstimate returns an estimated number of hexagons that fit
* within the cartesian-projected bounding box
*
* @param bbox the bounding box to estimate the hexagon fill level
* @param res the resolution of the H3 hexagons to fill the bounding box
* @param out the estimated number of hexagons to fill the bounding box
* @return E_SUCCESS (0) on success, or another value otherwise.
*/
H3Error bboxHexEstimate(const BBox *bbox, int res, int64_t *out) {
// Get the area of the pentagon as the maximally-distorted area possible
H3Index pentagons[12] = {0};
H3Error pentagonsErr = H3_EXPORT(getPentagons)(res, pentagons);
if (pentagonsErr) {
return pentagonsErr;
}
double pentagonRadiusKm = _hexRadiusKm(pentagons[0]);
// Area of a regular hexagon is 3/2*sqrt(3) * r * r
// The pentagon has the most distortion (smallest edges) and shares its
// edges with hexagons, so the most-distorted hexagons have this area,
// shrunk by 20% off chance that the bounding box perfectly bounds a
// pentagon.
double pentagonAreaKm2 =
0.8 * (2.59807621135 * pentagonRadiusKm * pentagonRadiusKm);
// Then get the area of the bounding box of the geoloop in question
LatLng p1, p2;
p1.lat = bbox->north;
p1.lng = bbox->east;
p2.lat = bbox->south;
p2.lng = bbox->west;
double d = H3_EXPORT(greatCircleDistanceKm)(&p1, &p2);
double lngDiff = fabs(p1.lng - p2.lng);
double latDiff = fabs(p1.lat - p2.lat);
if (lngDiff == 0 || latDiff == 0) {
return E_FAILED;
}
double length = fmax(lngDiff, latDiff);
double width = fmin(lngDiff, latDiff);
double ratio = length / width;
// Derived constant based on: https://math.stackexchange.com/a/1921940
// Clamped to 3 as higher values tend to rapidly drag the estimate to zero.
double a = d * d / fmin(3.0, ratio);
// Divide the two to get an estimate of the number of hexagons needed
double estimateDouble = ceil(a / pentagonAreaKm2);
if (!isfinite(estimateDouble)) {
return E_FAILED;
}
int64_t estimate = (int64_t)estimateDouble;
if (estimate == 0) estimate = 1;
*out = estimate;
return E_SUCCESS;
}
/**
* lineHexEstimate returns an estimated number of hexagons that trace
* the cartesian-projected line
*
* @param origin the origin coordinates
* @param destination the destination coordinates
* @param res the resolution of the H3 hexagons to trace the line
* @param out Out parameter for the estimated number of hexagons required to
* trace the line
* @return E_SUCCESS (0) on success or another value otherwise.
*/
H3Error lineHexEstimate(const LatLng *origin, const LatLng *destination,
int res, int64_t *out) {
// Get the area of the pentagon as the maximally-distorted area possible
H3Index pentagons[12] = {0};
H3Error pentagonsErr = H3_EXPORT(getPentagons)(res, pentagons);
if (pentagonsErr) {
return pentagonsErr;
}
double pentagonRadiusKm = _hexRadiusKm(pentagons[0]);
double dist = H3_EXPORT(greatCircleDistanceKm)(origin, destination);
double distCeil = ceil(dist / (2 * pentagonRadiusKm));
if (!isfinite(distCeil)) {
return E_FAILED;
}
int64_t estimate = (int64_t)distCeil;
if (estimate == 0) estimate = 1;
*out = estimate;
return E_SUCCESS;
}
/**
* Scale a given bounding box by some factor. Scales both width and height
* by the factor, rather than scaling area, which will scale at scale^2.
* Note that this function is meant to handle bounding boxes and scales,
* within a reasonable domain, and does not guarantee reasonable results for
* extreme values.
* @param bbox Bounding box to scale, in-place
* @param scale Scale factor
*/
void scaleBBox(BBox *bbox, double scale) {
double width = bboxWidthRads(bbox);
double height = bboxHeightRads(bbox);
double widthBuffer = (width * scale - width) * 0.5;
double heightBuffer = (height * scale - height) * 0.5;
// Scale north and south, clamping to latitude domain
bbox->north += heightBuffer;
if (bbox->north > M_PI_2) bbox->north = M_PI_2;
bbox->south -= heightBuffer;
if (bbox->south < -M_PI_2) bbox->south = -M_PI_2;
// Scale east and west, clamping to longitude domain
bbox->east += widthBuffer;
if (bbox->east > M_PI) bbox->east -= M_2PI;
if (bbox->east < -M_PI) bbox->east += M_2PI;
bbox->west -= widthBuffer;
if (bbox->west > M_PI) bbox->west -= M_2PI;
if (bbox->west < -M_PI) bbox->west += M_2PI;
}
/**
* Determine the longitude normalization scheme for two bounding boxes, either
* or both of which might cross the antimeridian. The goal is to transform
* latitudes in one or both boxes so that they are in the same frame of
* reference and can be operated on with standard Cartesian functions.
* @param a First bounding box
* @param b Second bounding box
* @param aNormalization Output: Normalization for longitudes in the first box
* @param bNormalization Output: Normalization for longitudes in the second box
*/
void bboxNormalization(const BBox *a, const BBox *b,
LongitudeNormalization *aNormalization,
LongitudeNormalization *bNormalization) {
bool aIsTransmeridian = bboxIsTransmeridian(a);
bool bIsTransmeridian = bboxIsTransmeridian(b);
bool aToBTrendsEast = a->west - b->east < b->west - a->east;
// If neither is transmeridian, no normalization.
// If both are transmeridian, normalize east by convention.
// If one is transmeridian and one is not, normalize toward the other.
*aNormalization = !aIsTransmeridian ? NORMALIZE_NONE
: bIsTransmeridian ? NORMALIZE_EAST
: aToBTrendsEast ? NORMALIZE_EAST
: NORMALIZE_WEST;
*bNormalization = !bIsTransmeridian ? NORMALIZE_NONE
: aIsTransmeridian ? NORMALIZE_EAST
: aToBTrendsEast ? NORMALIZE_WEST
: NORMALIZE_EAST;
}