h3_polygonAlgos.h (121 lines of code) (raw):

/* * Copyright 2018, 2020-2021 Uber Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** @file * @brief Include file for poylgon algorithms. This includes the core * logic for algorithms acting over loops of coordinates, * allowing them to be reused for both GeoLoop and * LinkegGeoLoop structures. This file is intended to be * included inline in a file that defines the type-specific * macros required for iteration. */ #include <float.h> #include <math.h> #include <stdbool.h> #include "h3_bbox.h" #include "h3_constants.h" #include "h3_h3api.h" #include "h3_latLng.h" #include "h3_linkedGeo.h" #include "h3_polygon.h" #ifndef TYPE #error "TYPE must be defined before including this header" #endif #ifndef IS_EMPTY #error "IS_EMPTY must be defined before including this header" #endif #ifndef INIT_ITERATION #error "INIT_ITERATION must be defined before including this header" #endif #ifndef ITERATE #error "ITERATE must be defined before including this header" #endif #define LOOP_ALGO_XTJOIN(a, b) a##b #define LOOP_ALGO_TJOIN(a, b) LOOP_ALGO_XTJOIN(a, b) #define GENERIC_LOOP_ALGO(func) LOOP_ALGO_TJOIN(func, TYPE) /** Macro: Normalize longitude, dealing with transmeridian arcs */ #define NORMALIZE_LNG(lng, isTransmeridian) \ (isTransmeridian && lng < 0 ? lng + (double)M_2PI : lng) /** * pointInside is the core loop of the point-in-poly algorithm * @param loop The loop to check * @param bbox The bbox for the loop being tested * @param coord The coordinate to check * @return Whether the point is contained */ bool GENERIC_LOOP_ALGO(pointInside)(const TYPE *loop, const BBox *bbox, const LatLng *coord) { // fail fast if we're outside the bounding box if (!bboxContains(bbox, coord)) { return false; } bool isTransmeridian = bboxIsTransmeridian(bbox); bool contains = false; double lat = coord->lat; double lng = NORMALIZE_LNG(coord->lng, isTransmeridian); LatLng a; LatLng b; INIT_ITERATION; while (true) { ITERATE(loop, a, b); // Ray casting algo requires the second point to always be higher // than the first, so swap if needed if (a.lat > b.lat) { LatLng tmp = a; a = b; b = tmp; } // If the latitude matches exactly, we'll hit an edge case where // the ray passes through the vertex twice on successive segment // checks. To avoid this, adjust the latiude northward if needed. // // NOTE: This currently means that a point at the north pole cannot // be contained in any polygon. This is acceptable in current usage, // because the point we test in this function at present is always // a cell center or vertex, and no cell has a center or vertex on the // north pole. If we need to expand this algo to more generic uses we // might need to handle this edge case. if (lat == a.lat || lat == b.lat) { lat += DBL_EPSILON; } // If we're totally above or below the latitude ranges, the test // ray cannot intersect the line segment, so let's move on if (lat < a.lat || lat > b.lat) { continue; } double aLng = NORMALIZE_LNG(a.lng, isTransmeridian); double bLng = NORMALIZE_LNG(b.lng, isTransmeridian); // Rays are cast in the longitudinal direction, in case a point // exactly matches, to decide tiebreakers, bias westerly if (aLng == lng || bLng == lng) { lng -= DBL_EPSILON; } // For the latitude of the point, compute the longitude of the // point that lies on the line segment defined by a and b // This is done by computing the percent above a the lat is, // and traversing the same percent in the longitudinal direction // of a to b double ratio = (lat - a.lat) / (b.lat - a.lat); double testLng = NORMALIZE_LNG(aLng + (bLng - aLng) * ratio, isTransmeridian); // Intersection of the ray if (testLng > lng) { contains = !contains; } } return contains; } /** * Create a bounding box from a simple polygon loop. * Known limitations: * - Does not support polygons with two adjacent points > 180 degrees of * longitude apart. These will be interpreted as crossing the antimeridian. * - Does not currently support polygons containing a pole. * @param loop Loop of coordinates * @param bbox Output bbox */ void GENERIC_LOOP_ALGO(bboxFrom)(const TYPE *loop, BBox *bbox) { // Early exit if there are no vertices if (IS_EMPTY(loop)) { *bbox = (BBox){0}; return; } bbox->south = DBL_MAX; bbox->west = DBL_MAX; bbox->north = -DBL_MAX; bbox->east = -DBL_MAX; double minPosLng = DBL_MAX; double maxNegLng = -DBL_MAX; bool isTransmeridian = false; double lat; double lng; LatLng coord; LatLng next; INIT_ITERATION; while (true) { ITERATE(loop, coord, next); lat = coord.lat; lng = coord.lng; if (lat < bbox->south) bbox->south = lat; if (lng < bbox->west) bbox->west = lng; if (lat > bbox->north) bbox->north = lat; if (lng > bbox->east) bbox->east = lng; // Save the min positive and max negative longitude for // use in the transmeridian case if (lng > 0 && lng < minPosLng) minPosLng = lng; if (lng < 0 && lng > maxNegLng) maxNegLng = lng; // check for arcs > 180 degrees longitude, flagging as transmeridian if (fabs(lng - next.lng) > M_PI) { isTransmeridian = true; } } // Swap east and west if transmeridian if (isTransmeridian) { bbox->east = maxNegLng; bbox->west = minPosLng; } } /** * Whether the winding order of a given loop is clockwise, with normalization * for loops crossing the antimeridian. * @param loop The loop to check * @param isTransmeridian Whether the loop crosses the antimeridian * @return Whether the loop is clockwise */ static bool GENERIC_LOOP_ALGO(isClockwiseNormalized)(const TYPE *loop, bool isTransmeridian) { double sum = 0; LatLng a; LatLng b; INIT_ITERATION; while (true) { ITERATE(loop, a, b); // If we identify a transmeridian arc (> 180 degrees longitude), // start over with the transmeridian flag set if (!isTransmeridian && fabs(a.lng - b.lng) > M_PI) { return GENERIC_LOOP_ALGO(isClockwiseNormalized)(loop, true); } sum += ((NORMALIZE_LNG(b.lng, isTransmeridian) - NORMALIZE_LNG(a.lng, isTransmeridian)) * (b.lat + a.lat)); } return sum > 0; } /** * Whether the winding order of a given loop is clockwise. In GeoJSON, * clockwise loops are always inner loops (holes). * @param loop The loop to check * @return Whether the loop is clockwise */ bool GENERIC_LOOP_ALGO(isClockwise)(const TYPE *loop) { return GENERIC_LOOP_ALGO(isClockwiseNormalized)(loop, false); }