1 | /* |
2 | * Copyright 2018 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 Geofence 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 | #include "bbox.h" |
29 | #include "constants.h" |
30 | #include "geoCoord.h" |
31 | #include "h3api.h" |
32 | #include "linkedGeo.h" |
33 | #include "polygon.h" |
34 | |
35 | #ifndef TYPE |
36 | #error "TYPE must be defined before including this header" |
37 | #endif |
38 | |
39 | #ifndef IS_EMPTY |
40 | #error "IS_EMPTY must be defined before including this header" |
41 | #endif |
42 | |
43 | #ifndef INIT_ITERATION |
44 | #error "INIT_ITERATION must be defined before including this header" |
45 | #endif |
46 | |
47 | #ifndef ITERATE |
48 | #error "ITERATE must be defined before including this header" |
49 | #endif |
50 | |
51 | #define LOOP_ALGO_XTJOIN(a, b) a##b |
52 | #define LOOP_ALGO_TJOIN(a, b) LOOP_ALGO_XTJOIN(a, b) |
53 | #define GENERIC_LOOP_ALGO(func) LOOP_ALGO_TJOIN(func, TYPE) |
54 | |
55 | /** Macro: Normalize longitude, dealing with transmeridian arcs */ |
56 | #define NORMALIZE_LON(lon, isTransmeridian) \ |
57 | (isTransmeridian && lon < 0 ? lon + (double)M_2PI : lon) |
58 | |
59 | /** |
60 | * pointInside is the core loop of the point-in-poly algorithm |
61 | * @param loop The loop to check |
62 | * @param bbox The bbox for the loop being tested |
63 | * @param coord The coordinate to check |
64 | * @return Whether the point is contained |
65 | */ |
66 | bool GENERIC_LOOP_ALGO(pointInside)(const TYPE* loop, const BBox* bbox, |
67 | const GeoCoord* coord) { |
68 | // fail fast if we're outside the bounding box |
69 | if (!bboxContains(bbox, coord)) { |
70 | return false; |
71 | } |
72 | bool isTransmeridian = bboxIsTransmeridian(bbox); |
73 | bool contains = false; |
74 | |
75 | double lat = coord->lat; |
76 | double lng = NORMALIZE_LON(coord->lon, isTransmeridian); |
77 | |
78 | GeoCoord a; |
79 | GeoCoord b; |
80 | |
81 | INIT_ITERATION; |
82 | |
83 | while (true) { |
84 | ITERATE(loop, a, b); |
85 | |
86 | // Ray casting algo requires the second point to always be higher |
87 | // than the first, so swap if needed |
88 | if (a.lat > b.lat) { |
89 | GeoCoord tmp = a; |
90 | a = b; |
91 | b = tmp; |
92 | } |
93 | |
94 | // If we're totally above or below the latitude ranges, the test |
95 | // ray cannot intersect the line segment, so let's move on |
96 | if (lat < a.lat || lat > b.lat) { |
97 | continue; |
98 | } |
99 | |
100 | double aLng = NORMALIZE_LON(a.lon, isTransmeridian); |
101 | double bLng = NORMALIZE_LON(b.lon, isTransmeridian); |
102 | |
103 | // Rays are cast in the longitudinal direction, in case a point |
104 | // exactly matches, to decide tiebreakers, bias westerly |
105 | if (aLng == lng || bLng == lng) { |
106 | lng -= DBL_EPSILON; |
107 | } |
108 | |
109 | // For the latitude of the point, compute the longitude of the |
110 | // point that lies on the line segment defined by a and b |
111 | // This is done by computing the percent above a the lat is, |
112 | // and traversing the same percent in the longitudinal direction |
113 | // of a to b |
114 | double ratio = (lat - a.lat) / (b.lat - a.lat); |
115 | double testLng = |
116 | NORMALIZE_LON(aLng + (bLng - aLng) * ratio, isTransmeridian); |
117 | |
118 | // Intersection of the ray |
119 | if (testLng > lng) { |
120 | contains = !contains; |
121 | } |
122 | } |
123 | |
124 | return contains; |
125 | } |
126 | |
127 | /** |
128 | * Create a bounding box from a simple polygon loop. |
129 | * Known limitations: |
130 | * - Does not support polygons with two adjacent points > 180 degrees of |
131 | * longitude apart. These will be interpreted as crossing the antimeridian. |
132 | * - Does not currently support polygons containing a pole. |
133 | * @param loop Loop of coordinates |
134 | * @param bbox Output bbox |
135 | */ |
136 | void GENERIC_LOOP_ALGO(bboxFrom)(const TYPE* loop, BBox* bbox) { |
137 | // Early exit if there are no vertices |
138 | if (IS_EMPTY(loop)) { |
139 | *bbox = (BBox){0}; |
140 | return; |
141 | } |
142 | |
143 | bbox->south = DBL_MAX; |
144 | bbox->west = DBL_MAX; |
145 | bbox->north = -DBL_MAX; |
146 | bbox->east = -DBL_MAX; |
147 | double minPosLon = DBL_MAX; |
148 | double maxNegLon = -DBL_MAX; |
149 | bool isTransmeridian = false; |
150 | |
151 | double lat; |
152 | double lon; |
153 | GeoCoord coord; |
154 | GeoCoord next; |
155 | |
156 | INIT_ITERATION; |
157 | |
158 | while (true) { |
159 | ITERATE(loop, coord, next); |
160 | |
161 | lat = coord.lat; |
162 | lon = coord.lon; |
163 | if (lat < bbox->south) bbox->south = lat; |
164 | if (lon < bbox->west) bbox->west = lon; |
165 | if (lat > bbox->north) bbox->north = lat; |
166 | if (lon > bbox->east) bbox->east = lon; |
167 | // Save the min positive and max negative longitude for |
168 | // use in the transmeridian case |
169 | if (lon > 0 && lon < minPosLon) minPosLon = lon; |
170 | if (lon < 0 && lon > maxNegLon) maxNegLon = lon; |
171 | // check for arcs > 180 degrees longitude, flagging as transmeridian |
172 | if (fabs(lon - next.lon) > M_PI) { |
173 | isTransmeridian = true; |
174 | } |
175 | } |
176 | // Swap east and west if transmeridian |
177 | if (isTransmeridian) { |
178 | bbox->east = maxNegLon; |
179 | bbox->west = minPosLon; |
180 | } |
181 | } |
182 | |
183 | /** |
184 | * Whether the winding order of a given loop is clockwise, with normalization |
185 | * for loops crossing the antimeridian. |
186 | * @param loop The loop to check |
187 | * @param isTransmeridian Whether the loop crosses the antimeridian |
188 | * @return Whether the loop is clockwise |
189 | */ |
190 | static bool GENERIC_LOOP_ALGO(isClockwiseNormalized)(const TYPE* loop, |
191 | bool isTransmeridian) { |
192 | double sum = 0; |
193 | GeoCoord a; |
194 | GeoCoord b; |
195 | |
196 | INIT_ITERATION; |
197 | while (true) { |
198 | ITERATE(loop, a, b); |
199 | // If we identify a transmeridian arc (> 180 degrees longitude), |
200 | // start over with the transmeridian flag set |
201 | if (!isTransmeridian && fabs(a.lon - b.lon) > M_PI) { |
202 | return GENERIC_LOOP_ALGO(isClockwiseNormalized)(loop, true); |
203 | } |
204 | sum += ((NORMALIZE_LON(b.lon, isTransmeridian) - |
205 | NORMALIZE_LON(a.lon, isTransmeridian)) * |
206 | (b.lat + a.lat)); |
207 | } |
208 | |
209 | return sum > 0; |
210 | } |
211 | |
212 | /** |
213 | * Whether the winding order of a given loop is clockwise. In GeoJSON, |
214 | * clockwise loops are always inner loops (holes). |
215 | * @param loop The loop to check |
216 | * @return Whether the loop is clockwise |
217 | */ |
218 | bool GENERIC_LOOP_ALGO(isClockwise)(const TYPE* loop) { |
219 | return GENERIC_LOOP_ALGO(isClockwiseNormalized)(loop, false); |
220 | } |
221 | |