Coverage Report

Created: 2026-01-09 06:51

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/h3/src/h3lib/lib/area.c
Line
Count
Source
1
/*
2
 * Copyright 2025 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 area.c
17
 * @brief   Algorithms for computing areas of regions on a sphere (GeoLoop,
18
 * cells, polygons, multipolygons, etc.)
19
 */
20
21
#include "area.h"
22
23
#include <math.h>
24
25
#include "adder.h"
26
#include "constants.h"
27
#include "h3Assert.h"
28
#include "h3api.h"
29
30
// Cagnoli contribution for edge arc x to y, following d3-geo’s
31
// area implementation:
32
// https://github.com/d3/d3-geo/blob/8c53a90ae70c94bace73ecb02f2c792c649c86ba/src/area.js#L51-L70
33
4.96k
static inline double cagnoli(LatLng x, LatLng y) {
34
4.96k
    x.lat = x.lat / 2.0 + M_PI / 4.0;
35
4.96k
    y.lat = y.lat / 2.0 + M_PI / 4.0;
36
37
4.96k
    double sa = sin(x.lat) * sin(y.lat);
38
4.96k
    double ca = cos(x.lat) * cos(y.lat);
39
40
4.96k
    double d = y.lng - x.lng;
41
4.96k
    double sd = sin(d);
42
4.96k
    double cd = cos(d);
43
44
4.96k
    return -2.0 * atan2(sa * sd, sa * cd + ca);
45
4.96k
}
46
47
/**
48
 * Area in radians^2 enclosed by vertices in GeoLoop.
49
 *
50
 * The GeoLoop should represent a simple curve with no self-intersections.
51
 * Vertices should be ordered according to the "right hand rule".
52
 * That is, if you are looking from outer space at a spherical polygon loop
53
 * on the surface of the earth whose interior is contained within a hemisphere,
54
 * then the vertices should be ordered counter-clockwise. The interior of the
55
 * loop is to the left of a person walking along the boundary of the polygon
56
 * in the counter-clockwise direction.
57
 *
58
 * Note that GeoLoops do not need to repeat the first vertex at the end of the
59
 * array to close the loop; this is done automatically.
60
 *
61
 * The edge arcs between adjacent vertices are assumed to be the shortest
62
 * geodesic path between them; that is, all arcs are interpreted to be less
63
 * than 180 degrees or pi radians.
64
 * Avoid arcs that are exactly pi (i.e, two antipodal vertices).
65
 * "Large" polygon loops (e.g., that cannot be contained in a hemisphere) can
66
 * still be constructed by using intermediate vertices with arcs less than
67
 * 180 degrees, and the loop area will still be computed correctly.
68
 *
69
 * The area of the entire globe is 4*pi radians^2. If, for example, you have a
70
 * small GeoLoop with area `a << 4*pi` and then reverse the order of the
71
 * vertices, you produce GeoLoop with area `4*pi - a`, since, by the right hand
72
 * rule, the new loop's interior is the majority of the globe,
73
 * or "everything except the original polygon".
74
 * Note that the area enclosed by the loop is determined by the vertex order;
75
 * this function does **not** return `min(a, 4*pi - a)`.
76
 *
77
 * @param   loop  GeoLoop of boundary vertices in counter-clockwise order
78
 * @param    out  loop area in radians^2, in interval [0, 4*pi]
79
 * @return        E_SUCCESS on success, or an error code otherwise
80
 */
81
801
H3Error geoLoopAreaRads2(GeoLoop loop, double *out) {
82
    // Use `Adder` to improve numerical accuracy of the sum of many Cagnoli
83
    // terms
84
801
    Adder adder = {};
85
86
5.76k
    for (int i = 0; i < loop.numVerts; i++) {
87
4.96k
        int j = (i + 1) % loop.numVerts;
88
4.96k
        kadd(&adder, cagnoli(loop.verts[i], loop.verts[j]));
89
4.96k
    }
90
91
    // The Cagnoli sum above yields a signed area, with the sign switching
92
    // with the orientation of the vertices. Since we want our area to always be
93
    // positive, we normalize into [0, 4*pi] by adding 4*pi when the signed
94
    // area is negative.
95
801
    if (adder.sum < 0.0) {
96
24
        kadd(&adder, 4.0 * M_PI);
97
24
    }
98
99
801
    *out = adder.sum;
100
801
    return E_SUCCESS;
101
801
}
102
103
/**
104
 * Area of H3 cell in radians^2.
105
 *
106
 * Uses `geoLoopAreaRads2` to compute cell area.
107
 *
108
 * @param   cell  H3 cell
109
 * @param    out  cell area in radians^2
110
 * @return        E_SUCCESS on success, or an error code otherwise
111
 */
112
807
H3Error H3_EXPORT(cellAreaRads2)(H3Index cell, double *out) {
113
807
    CellBoundary cb;
114
807
    H3Error err = H3_EXPORT(cellToBoundary)(cell, &cb);
115
807
    if (err) {
116
6
        return err;
117
6
    }
118
119
801
    GeoLoop loop = {.verts = cb.verts, .numVerts = cb.numVerts};
120
801
    err = geoLoopAreaRads2(loop, out);
121
801
    if (NEVER(err)) {
122
0
        return err;
123
0
    }
124
125
801
    return E_SUCCESS;
126
801
}
127
128
/**
129
 * Area of GeoPolygon in radians^2.
130
 *
131
 * Outer GeoLoop vertices should be in counter-clockwise order.
132
 * Hole GeoLoop vertices should be in clockwise order.
133
 * Returned area is the area contained by the outer loop, minus
134
 * the areas of the holes. See `geoLoopAreaRads2` for the expected
135
 * form of GeoLoops.
136
 *
137
 * No check is made to ensure holes are disjoint or are contained
138
 * within the outer GeoLoop.
139
 *
140
 * @param   poly  GeoPolygon
141
 * @param    out  GeoPolygon area in radians^2
142
 * @return  E_SUCCESS on success; error code otherwise
143
 */
144
0
H3Error geoPolygonAreaRads2(GeoPolygon poly, double *out) {
145
0
    H3Error err;
146
0
    Adder adder = {};
147
0
    double term;
148
149
0
    err = geoLoopAreaRads2(poly.geoloop, &term);
150
0
    if (err) return err;
151
0
    kadd(&adder, term);
152
153
0
    for (int i = 0; i < poly.numHoles; i++) {
154
0
        err = geoLoopAreaRads2(poly.holes[i], &term);
155
0
        if (err) return err;
156
157
        // Due to clockwise order, holes will contribute area
158
        // of "everything except the hole", so adjust with -4*pi term.
159
0
        kadd(&adder, term);
160
0
        kadd(&adder, -4.0 * M_PI);
161
0
    }
162
163
0
    *out = adder.sum;
164
165
0
    return E_SUCCESS;
166
0
}
167
168
/**
169
 * Area of GeoMultiPolygon in radians^2.
170
 *
171
 * Area is the sum of the areas of the polygons contained by `mpoly`.
172
 * See `geoPolygonAreaRads2` for expected polygon format.
173
 * No check is made to ensure polygons are disjoint.
174
 *
175
 * @param   mpoly  GeoMultiPolygon
176
 * @param     out  GeoMultiPolygon area in radians^2
177
 * @return  E_SUCCESS on success; error code otherwise
178
 */
179
0
H3Error geoMultiPolygonAreaRads2(GeoMultiPolygon mpoly, double *out) {
180
0
    H3Error err;
181
0
    Adder adder = {};
182
0
    double term;
183
184
0
    for (int i = 0; i < mpoly.numPolygons; i++) {
185
0
        err = geoPolygonAreaRads2(mpoly.polygons[i], &term);
186
0
        if (err) return err;
187
0
        kadd(&adder, term);
188
0
    }
189
190
0
    *out = adder.sum;
191
192
0
    return E_SUCCESS;
193
0
}
194
195
/**
196
 * Area of H3 cell in kilometers^2.
197
 *
198
 * @param   cell  H3 cell
199
 * @param    out  cell area in kilometers^2
200
 * @return        E_SUCCESS on success, or an error code otherwise
201
 */
202
538
H3Error H3_EXPORT(cellAreaKm2)(H3Index cell, double *out) {
203
538
    H3Error err = H3_EXPORT(cellAreaRads2)(cell, out);
204
538
    if (!err) {
205
534
        *out *= EARTH_RADIUS_KM * EARTH_RADIUS_KM;
206
534
    }
207
538
    return err;
208
538
}
209
210
/**
211
 * Area of H3 cell in meters^2.
212
 *
213
 * @param   cell  H3 cell
214
 * @param    out  cell area in meters^2
215
 * @return        E_SUCCESS on success, or an error code otherwise
216
 */
217
269
H3Error H3_EXPORT(cellAreaM2)(H3Index cell, double *out) {
218
269
    H3Error err = H3_EXPORT(cellAreaKm2)(cell, out);
219
269
    if (!err) {
220
267
        *out *= 1000 * 1000;
221
267
    }
222
269
    return err;
223
269
}