/// <summary> /// Gets the center of a bounding box /// </summary> /// <param name="bbox">Input bounding box</param> /// <param name="center">Output center coordinate</param> /// <!-- Based off 3.1.1 --> public static void bboxCenter(BBox bbox, ref GeoCoord center) { center.lat = (bbox.north + bbox.south) / 2.0; // If the bbox crosses the antimeridian, shift east 360 degrees double east = bboxIsTransmeridian(bbox) ? bbox.east + Constants.M_2PI : bbox.east; center.lon = GeoCoord.constrainLng((east + bbox.west) / 2.0); }
/// <summary> /// Create a bounding box from a simple polygon defined as an array of vertices. /// /// 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. /// </summary> /// <param name="verts">Array of vertices</param> /// <param name="numVerts">Number of vertices</param> /// <param name="bbox">Output box</param> /// <!-- Based off 3.1.1 --> static void bboxFromVertices(List <GeoCoord> verts, int numVerts, ref BBox bbox) { // Early exit if there are no vertices if (numVerts == 0) { bbox.north = 0; bbox.south = 0; bbox.east = 0; bbox.west = 0; return; } double lat; double lon; bbox.south = double.MaxValue; bbox.west = double.MaxValue; bbox.north = -double.MaxValue; bbox.east = -double.MaxValue; bool isTransmeridian = false; for (int i = 0; i < numVerts; i++) { lat = verts[i].lat; lon = verts[i].lon; if (lat < bbox.south) { bbox.south = lat; } if (lon < bbox.west) { bbox.west = lon; } if (lat > bbox.north) { bbox.north = lat; } if (lon > bbox.east) { bbox.east = lon; } // check for arcs > 180 degrees longitude, flagging as transmeridian if (Math.Abs(lon - verts[(i + 1) % numVerts].lon) > Constants.M_PI) { isTransmeridian = true; } } // Swap east and west if transmeridian if (isTransmeridian) { double tmp = bbox.east; bbox.east = bbox.west; bbox.west = tmp; } }
/// <summary> /// maxPolyfillSize returns the number of hexagons to allocate space for when /// performing a polyfill on the given GeoJSON-like data structure. /// /// Currently a laughably padded response, being a k-ring that wholly contains /// a bounding box of the GeoJSON, but still less wasted memory than initializing /// a Python application? ;) /// </summary> /// <param name="geoPolygon">A GeoJSON-like data structure indicating the poly to fill</param> /// <param name="res">Hexagon resolution (0-15)</param> /// <returns>number of hexagons to allocate for</returns> /// <!-- Based off 3.1.1 --> internal static int maxPolyfillSize(ref GeoPolygon geoPolygon, int res) { // Get the bounding box for the GeoJSON-like struct BBox bbox = new BBox(); Polygon.bboxFromGeofence(ref geoPolygon.Geofence, ref bbox); int minK = BBox.bboxHexRadius(bbox, res); // The total number of hexagons to allocate can now be determined by // the k-ring hex allocation helper function. return(maxKringSize(minK)); }
/// <summary> /// Whether the bounding box contains a given point /// </summary> /// <param name="bbox">Bounding box</param> /// <param name="point">Point to test</param> /// <returns>true is point is contained</returns> /// <!-- Based off 3.1.1 --> public static bool bboxContains(BBox bbox, GeoCoord point) { return (point.lat >= bbox.south && point.lat <= bbox.north && ( bboxIsTransmeridian(bbox) // transmeridian case ? point.lon >= bbox.west || point.lon <= bbox.east // standard case : point.lon >= bbox.west && point.lon <= bbox.east )); }
///<summary> /// polyfill takes a given GeoJSON-like data structure and preallocated, /// zeroed memory, and fills it with the hexagons that are contained by /// the GeoJSON-like data structure. /// /// The current implementation is very primitive and slow, but correct, /// performing a point-in-poly operation on every hexagon in a k-ring defined /// around the given Geofence. /// </summary> /// <param name="geoPolygon">The Geofence and holes defining the relevant area</param> /// <param name="res"> The Hexagon resolution (0-15)</param> /// <param name="out_hex">The slab of zeroed memory to write to. Assumed to be big enough.</param> /// <!-- Based off 3.1.1 --> internal static void polyfill(GeoPolygon geoPolygon, int res, List <H3Index> out_hex) { // One of the goals of the polyfill algorithm is that two adjacent polygons // with zero overlap have zero overlapping hexagons. That the hexagons are // uniquely assigned. There are a few approaches to take here, such as // deciding based on which polygon has the greatest overlapping area of the // hexagon, or the most number of contained points on the hexagon (using the // center point as a tiebreaker). // // But if the polygons are convex, both of these more complex algorithms can // be reduced down to checking whether or not the center of the hexagon is // contained in the polygon, and so this is the approach that this polyfill // algorithm will follow, as it's simpler, faster, and the error for concave // polygons is still minimal (only affecting concave shapes on the order of // magnitude of the hexagon size or smaller, not impacting larger concave // shapes) // // This first part is identical to the maxPolyfillSize above. // Get the bounding boxes for the polygon and any holes int cnt = geoPolygon.numHoles + 1; List <BBox> bboxes = new List <BBox>(); for (int i = 0; i < cnt; i++) { bboxes.Add(new BBox()); } Polygon.bboxesFromGeoPolygon(geoPolygon, ref bboxes); int minK = BBox.bboxHexRadius(bboxes[0], res); int numHexagons = maxKringSize(minK); // Get the center hex GeoCoord center = new GeoCoord(); BBox.bboxCenter(bboxes[0], ref center); H3Index centerH3 = H3Index.geoToH3(ref center, res); // From here on it works differently, first we get all potential // hexagons inserted into the available memory kRing(centerH3, minK, ref out_hex); // Next we iterate through each hexagon, and test its center point to see if // it's contained in the GeoJSON-like struct for (int i = 0; i < numHexagons; i++) { // Skip records that are already zeroed if (out_hex[i] == 0) { continue; } // Check if hexagon is inside of polygon GeoCoord hexCenter = new GeoCoord(); H3Index.h3ToGeo(out_hex[i], ref hexCenter); hexCenter.lat = GeoCoord.constrainLat(hexCenter.lat); hexCenter.lon = GeoCoord.constrainLng(hexCenter.lon); // And remove from list if not if (!Polygon.pointInsidePolygon(geoPolygon, bboxes, hexCenter)) { out_hex[i] = H3Index.H3_INVALID_INDEX; } } }
/// <summary> /// 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. /// </summary> /// <param name="loop">Loop of coordinates</param> /// <param name="bbox">Output bbox</param> /// <!-- Based off 3.1.1 --> public static void bboxFromGeofence(ref Geofence loop, ref BBox bbox) { // Early exit if there are no vertices if (loop.numVerts == 0) { bbox = new BBox(); return; } bbox.south = Double.MaxValue; bbox.west = Double.MaxValue; bbox.north = -Double.MaxValue; bbox.east = -Double.MaxValue; double minPosLon = Double.MaxValue; double maxNegLon = -Double.MaxValue; bool isTransmeridian = false; double lat; double lon; GeoCoord coord; GeoCoord next; int loopIndex = -1; while (true) { if (++loopIndex >= loop.numVerts) { break; } coord = new GeoCoord(loop.verts[loopIndex].lat, loop.verts[loopIndex].lon); next = new GeoCoord ( loop.verts[(loopIndex + 1) % loop.numVerts].lat, loop.verts[(loopIndex + 1) % loop.numVerts].lon ); lat = coord.lat; lon = coord.lon; if (lat < bbox.south) { bbox.south = lat; } if (lon < bbox.west) { bbox.west = lon; } if (lat > bbox.north) { bbox.north = lat; } if (lon > bbox.east) { bbox.east = lon; } // Save the min positive and max negative longitude for // use in the transmeridian case if (lon > 0 && lon < minPosLon) { minPosLon = lon; } if (lon < 0 && lon > maxNegLon) { maxNegLon = lon; } // check for arcs > 180 degrees longitude, flagging as transmeridian if (Math.Abs(lon - next.lon) > Constants.M_PI) { isTransmeridian = true; } } // Swap east and west if transmeridian if (isTransmeridian) { bbox.east = maxNegLon; bbox.west = minPosLon; } }
/// <summary> /// pointInside is the core loop of the point-in-poly algorithm /// </summary> /// <param name="loop">The loop to check</param> /// <param name="bbox">The bbox for the loop being tested</param> /// <param name="coord">The coordinate to check</param> /// <returns>Whether the point is contained</returns> /// <!-- Based off 3.1.1 --> public static bool pointInsideGeofence(ref Geofence loop, ref BBox bbox, ref GeoCoord coord) { // fail fast if we're outside the bounding box if (!BBox.bboxContains(bbox, coord)) { return(false); } bool isTransmeridian = BBox.bboxIsTransmeridian(bbox); bool contains = false; double lat = coord.lat; double lng = NORMALIZE_LON(coord.lon, isTransmeridian); GeoCoord a; GeoCoord b; int loopIndex = -1; while (true) { if (++loopIndex >= loop.numVerts) { break; } a = new GeoCoord(loop.verts[loopIndex].lat, loop.verts[loopIndex].lon); b = new GeoCoord ( loop.verts[(loopIndex + 1) % loop.numVerts].lat, loop.verts[(loopIndex + 1) % loop.numVerts].lon ); //b = loop.verts[(loopIndex + 1) % loop.numVerts]; // Ray casting algo requires the second point to always be higher // than the first, so swap if needed if (a.lat > b.lat) { GeoCoord tmp = a; a = b; b = tmp; } // 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_LON(a.lon, isTransmeridian); double bLng = NORMALIZE_LON(b.lon, isTransmeridian); // Rays are cast in the longitudinal direction, in case a point // exactly matches, to decide tiebreakers, bias westerly if (Math.Abs(aLng - lng) < Constants.DBL_EPSILON || Math.Abs(bLng - lng) < Constants.DBL_EPSILON) { lng -= Constants.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_LON(aLng + (bLng - aLng) * ratio, isTransmeridian); // Intersection of the ray if (testLng > lng) { contains = !contains; } } return(contains); }
/// <summary> /// 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. /// </summary> /// <param name="loop">Loop of coordinates</param> /// <param name="bbox">bbox</param> /// <!-- Based off 3.1.1 --> public static void bboxFromLinkedGeoLoop(ref LinkedGeoLoop loop, ref BBox bbox) { // Early exit if there are no vertices if (loop.first == null) { bbox = new BBox(); return; } bbox.south = Double.MaxValue; bbox.west = Double.MaxValue; bbox.north = -Double.MaxValue; bbox.east = -Double.MaxValue; double minPosLon = Double.MaxValue; double maxNegLon = -Double.MaxValue; bool isTransmeridian = false; double lat; double lon; GeoCoord coord; GeoCoord next; LinkedGeoCoord currentCoord = null; LinkedGeoCoord nextCoord = null; while (true) { currentCoord = currentCoord == null ? loop.first : currentCoord.next; if (currentCoord == null) { break; } coord = currentCoord.vertex; nextCoord = currentCoord.next == null ? loop.first : currentCoord.next; next = nextCoord.vertex; lat = coord.lat; lon = coord.lon; if (lat < bbox.south) { bbox.south = lat; } if (lon < bbox.west) { bbox.west = lon; } if (lat > bbox.north) { bbox.north = lat; } if (lon > bbox.east) { bbox.east = lon; } // Save the min positive and max negative longitude for // use in the transmeridian case if (lon > 0 && lon < minPosLon) { minPosLon = lon; } if (lon < 0 && lon > maxNegLon) { maxNegLon = lon; } // check for arcs > 180 degrees longitude, flagging as transmeridian if (Math.Abs(lon - next.lon) > Constants.M_PI) { isTransmeridian = true; } } // Swap east and west if transmeridian if (isTransmeridian) { bbox.east = maxNegLon; bbox.west = minPosLon; } }
/// <summary> /// Whether the given bounding box cross the antimeridian /// </summary> /// <param name="bbox">bounding box to inspect</param> /// <returns>true if transmeridian</returns> /// <!-- Based off 3.1.1 --> public static bool bboxIsTransmeridian(BBox bbox) { return(bbox.east < bbox.west); }
/// <summary> /// Create a bounding box from a Geofence /// </summary> /// <param name="Geofence">Input <see cref="Geofence"/></param> /// <param name="bbox">Output bbox</param> /// <!-- Based off 3.1.1 --> public static void bboxFromGeofence(Geofence Geofence, ref BBox bbox) { bboxFromVertices(Geofence.verts.ToList(), Geofence.numVerts, ref bbox); }