polygonAlgos.h 6.9 KB

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