/src/h3/src/h3lib/include/polygonAlgos.h
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright 2018, 2020-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 |
17 | | * @brief Include file for poylgon algorithms. This includes the core |
18 | | * logic for algorithms acting over loops of coordinates, |
19 | | * allowing them to be reused for both GeoLoop and |
20 | | * LinkegGeoLoop structures. This file is intended to be |
21 | | * included inline in a file that defines the type-specific |
22 | | * macros required for iteration. |
23 | | */ |
24 | | |
25 | | #include <float.h> |
26 | | #include <math.h> |
27 | | #include <stdbool.h> |
28 | | |
29 | | #include "bbox.h" |
30 | | #include "constants.h" |
31 | | #include "h3api.h" |
32 | | #include "latLng.h" |
33 | | #include "linkedGeo.h" |
34 | | #include "polygon.h" |
35 | | |
36 | | #ifndef TYPE |
37 | | #error "TYPE must be defined before including this header" |
38 | | #endif |
39 | | |
40 | | #ifndef IS_EMPTY |
41 | | #error "IS_EMPTY must be defined before including this header" |
42 | | #endif |
43 | | |
44 | | #ifndef INIT_ITERATION |
45 | | #error "INIT_ITERATION must be defined before including this header" |
46 | | #endif |
47 | | |
48 | | #ifndef ITERATE |
49 | | #error "ITERATE must be defined before including this header" |
50 | | #endif |
51 | | |
52 | 0 | #define LOOP_ALGO_XTJOIN(a, b) a##b |
53 | 0 | #define LOOP_ALGO_TJOIN(a, b) LOOP_ALGO_XTJOIN(a, b) |
54 | 0 | #define GENERIC_LOOP_ALGO(func) LOOP_ALGO_TJOIN(func, TYPE) |
55 | | |
56 | | /** Macro: Normalize longitude, dealing with transmeridian arcs */ |
57 | | #define NORMALIZE_LNG(lng, isTransmeridian) \ |
58 | 0 | (isTransmeridian && lng < 0 ? lng + (double)M_2PI : lng) |
59 | | |
60 | | /** |
61 | | * pointInside is the core loop of the point-in-poly algorithm |
62 | | * @param loop The loop to check |
63 | | * @param bbox The bbox for the loop being tested |
64 | | * @param coord The coordinate to check |
65 | | * @return Whether the point is contained |
66 | | */ |
67 | | bool GENERIC_LOOP_ALGO(pointInside)(const TYPE *loop, const BBox *bbox, |
68 | 0 | const LatLng *coord) { |
69 | | // fail fast if we're outside the bounding box |
70 | 0 | if (!bboxContains(bbox, coord)) { |
71 | 0 | return false; |
72 | 0 | } |
73 | 0 | bool isTransmeridian = bboxIsTransmeridian(bbox); |
74 | 0 | bool contains = false; |
75 | |
|
76 | 0 | double lat = coord->lat; |
77 | 0 | double lng = NORMALIZE_LNG(coord->lng, isTransmeridian); |
78 | |
|
79 | 0 | LatLng a; |
80 | 0 | LatLng b; |
81 | |
|
82 | 0 | INIT_ITERATION; |
83 | |
|
84 | 0 | while (true) { |
85 | 0 | ITERATE(loop, a, b); |
86 | | |
87 | | // Ray casting algo requires the second point to always be higher |
88 | | // than the first, so swap if needed |
89 | 0 | if (a.lat > b.lat) { |
90 | 0 | LatLng tmp = a; |
91 | 0 | a = b; |
92 | 0 | b = tmp; |
93 | 0 | } |
94 | | |
95 | | // If the latitude matches exactly, we'll hit an edge case where |
96 | | // the ray passes through the vertex twice on successive segment |
97 | | // checks. To avoid this, adjust the latiude northward if needed. |
98 | | // |
99 | | // NOTE: This currently means that a point at the north pole cannot |
100 | | // be contained in any polygon. This is acceptable in current usage, |
101 | | // because the point we test in this function at present is always |
102 | | // a cell center or vertex, and no cell has a center or vertex on the |
103 | | // north pole. If we need to expand this algo to more generic uses we |
104 | | // might need to handle this edge case. |
105 | 0 | if (lat == a.lat || lat == b.lat) { |
106 | 0 | lat += DBL_EPSILON; |
107 | 0 | } |
108 | | |
109 | | // If we're totally above or below the latitude ranges, the test |
110 | | // ray cannot intersect the line segment, so let's move on |
111 | 0 | if (lat < a.lat || lat > b.lat) { |
112 | 0 | continue; |
113 | 0 | } |
114 | | |
115 | 0 | double aLng = NORMALIZE_LNG(a.lng, isTransmeridian); |
116 | 0 | double bLng = NORMALIZE_LNG(b.lng, isTransmeridian); |
117 | | |
118 | | // Rays are cast in the longitudinal direction, in case a point |
119 | | // exactly matches, to decide tiebreakers, bias westerly |
120 | 0 | if (aLng == lng || bLng == lng) { |
121 | 0 | lng -= DBL_EPSILON; |
122 | 0 | } |
123 | | |
124 | | // For the latitude of the point, compute the longitude of the |
125 | | // point that lies on the line segment defined by a and b |
126 | | // This is done by computing the percent above a the lat is, |
127 | | // and traversing the same percent in the longitudinal direction |
128 | | // of a to b |
129 | 0 | double ratio = (lat - a.lat) / (b.lat - a.lat); |
130 | 0 | double testLng = |
131 | 0 | NORMALIZE_LNG(aLng + (bLng - aLng) * ratio, isTransmeridian); |
132 | | |
133 | | // Intersection of the ray |
134 | 0 | if (testLng > lng) { |
135 | 0 | contains = !contains; |
136 | 0 | } |
137 | 0 | } |
138 | |
|
139 | 0 | return contains; |
140 | 0 | } Unexecuted instantiation: pointInsideGeoLoop Unexecuted instantiation: pointInsideLinkedGeoLoop |
141 | | |
142 | | /** |
143 | | * Create a bounding box from a simple polygon loop. |
144 | | * Known limitations: |
145 | | * - Does not support polygons with two adjacent points > 180 degrees of |
146 | | * longitude apart. These will be interpreted as crossing the antimeridian. |
147 | | * - Does not currently support polygons containing a pole. |
148 | | * @param loop Loop of coordinates |
149 | | * @param bbox Output bbox |
150 | | */ |
151 | 0 | void GENERIC_LOOP_ALGO(bboxFrom)(const TYPE *loop, BBox *bbox) { |
152 | | // Early exit if there are no vertices |
153 | 0 | if (IS_EMPTY(loop)) { |
154 | 0 | *bbox = (BBox){0}; |
155 | 0 | return; |
156 | 0 | } |
157 | | |
158 | 0 | bbox->south = DBL_MAX; |
159 | 0 | bbox->west = DBL_MAX; |
160 | 0 | bbox->north = -DBL_MAX; |
161 | 0 | bbox->east = -DBL_MAX; |
162 | 0 | double minPosLng = DBL_MAX; |
163 | 0 | double maxNegLng = -DBL_MAX; |
164 | 0 | bool isTransmeridian = false; |
165 | |
|
166 | 0 | double lat; |
167 | 0 | double lng; |
168 | 0 | LatLng coord; |
169 | 0 | LatLng next; |
170 | |
|
171 | 0 | INIT_ITERATION; |
172 | |
|
173 | 0 | while (true) { |
174 | 0 | ITERATE(loop, coord, next); |
175 | |
|
176 | 0 | lat = coord.lat; |
177 | 0 | lng = coord.lng; |
178 | 0 | if (lat < bbox->south) bbox->south = lat; |
179 | 0 | if (lng < bbox->west) bbox->west = lng; |
180 | 0 | if (lat > bbox->north) bbox->north = lat; |
181 | 0 | if (lng > bbox->east) bbox->east = lng; |
182 | | // Save the min positive and max negative longitude for |
183 | | // use in the transmeridian case |
184 | 0 | if (lng > 0 && lng < minPosLng) minPosLng = lng; |
185 | 0 | if (lng < 0 && lng > maxNegLng) maxNegLng = lng; |
186 | | // check for arcs > 180 degrees longitude, flagging as transmeridian |
187 | 0 | if (fabs(lng - next.lng) > M_PI) { |
188 | 0 | isTransmeridian = true; |
189 | 0 | } |
190 | 0 | } |
191 | | // Swap east and west if transmeridian |
192 | 0 | if (isTransmeridian) { |
193 | 0 | bbox->east = maxNegLng; |
194 | 0 | bbox->west = minPosLng; |
195 | 0 | } |
196 | 0 | } Unexecuted instantiation: bboxFromGeoLoop Unexecuted instantiation: bboxFromLinkedGeoLoop |
197 | | |
198 | | /** |
199 | | * Whether the winding order of a given loop is clockwise, with normalization |
200 | | * for loops crossing the antimeridian. |
201 | | * @param loop The loop to check |
202 | | * @param isTransmeridian Whether the loop crosses the antimeridian |
203 | | * @return Whether the loop is clockwise |
204 | | */ |
205 | | static bool GENERIC_LOOP_ALGO(isClockwiseNormalized)(const TYPE *loop, |
206 | 0 | bool isTransmeridian) { |
207 | 0 | double sum = 0; |
208 | 0 | LatLng a; |
209 | 0 | LatLng b; |
210 | |
|
211 | 0 | INIT_ITERATION; |
212 | 0 | while (true) { |
213 | 0 | ITERATE(loop, a, b); |
214 | | // If we identify a transmeridian arc (> 180 degrees longitude), |
215 | | // start over with the transmeridian flag set |
216 | 0 | if (!isTransmeridian && fabs(a.lng - b.lng) > M_PI) { |
217 | 0 | return GENERIC_LOOP_ALGO(isClockwiseNormalized)(loop, true); |
218 | 0 | } |
219 | 0 | sum += ((NORMALIZE_LNG(b.lng, isTransmeridian) - |
220 | 0 | NORMALIZE_LNG(a.lng, isTransmeridian)) * |
221 | 0 | (b.lat + a.lat)); |
222 | 0 | } |
223 | | |
224 | 0 | return sum > 0; |
225 | 0 | } Unexecuted instantiation: polygon.c:isClockwiseNormalizedGeoLoop Unexecuted instantiation: linkedGeo.c:isClockwiseNormalizedLinkedGeoLoop |
226 | | |
227 | | /** |
228 | | * Whether the winding order of a given loop is clockwise. In GeoJSON, |
229 | | * clockwise loops are always inner loops (holes). |
230 | | * @param loop The loop to check |
231 | | * @return Whether the loop is clockwise |
232 | | */ |
233 | 0 | bool GENERIC_LOOP_ALGO(isClockwise)(const TYPE *loop) { |
234 | 0 | return GENERIC_LOOP_ALGO(isClockwiseNormalized)(loop, false); |
235 | 0 | } Unexecuted instantiation: isClockwiseGeoLoop Unexecuted instantiation: isClockwiseLinkedGeoLoop |