Coverage Report

Created: 2025-10-28 07:10

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