public static Api.H3Index GeoToH3(Code.GeoCoord gc, int res) { var h3 = Code.H3Index.geoToH3(ref gc, res); return(new H3Index { Value = h3.value }); }
public static Api.GeoCoord H3ToGeo(Code.H3Index h3) { var blank = new Code.GeoCoord(); Code.H3Index.h3ToGeo(h3, ref blank); return(new GeoCoord { Latitude = blank.lat, Longitude = blank.lon }); }
/// <summary> /// Encodes a coordinate on the sphere to the FaceIJK address of the containing /// cell at the specified resolution. /// </summary> /// <param name="g">The spherical coordinates to encode.</param> /// <param name="res">The desired H3 resolution for the encoding.</param> /// <param name="h">The FaceIJK address of the containing cell at resolution res.</param> /// <!-- Based off 3.1.1 --> public static void _geoToFaceIjk(GeoCoord g, int res, ref FaceIJK h) { // first convert to hex2d Vec2d v = new Vec2d(); _geoToHex2d(g, res, ref h.face, ref v); // then convert to ijk+ CoordIJK._hex2dToCoordIJK(ref v, ref h.coord); }
/// <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> /// Determines the azimuth to p2 from p1 in radians /// </summary> /// <param name="p1">The first spherical coordinates</param> /// <param name="p2">The second spherical coordinates</param> /// <returns>The azimuth in radians from p1 to p2</returns> /// <!-- Based off 3.1.1 --> public static double _geoAzimuthRads(GeoCoord p1, GeoCoord p2) { return (Math.Atan2 ( Math.Cos(p2.lat) * Math.Sin(p2.lon - p1.lon), Math.Cos(p1.lat) * Math.Sin(p2.lat) - Math.Sin(p1.lat) * Math.Cos(p2.lat) * Math.Cos(p2.lon - p1.lon) )); }
/// <summary> /// Whether the given coordinate has a matching vertex in the given geo boundary. /// </summary> /// <param name="vertex">Coordinate to check</param> /// <param name="boundary">Geo boundary to look in</param> /// <returns>Whether a match was found</returns> /// <!-- Based off 3.1.1 --> public static bool _hasMatchingVertex(GeoCoord vertex, GeoBoundary boundary) { for (int i = 0; i < boundary.numVerts; i++) { if (GeoCoord.geoAlmostEqualThreshold(vertex, boundary.verts[i], 0.000001)) { return(true); } } return(false); }
/// <summary> /// Returns the radius of a given hexagon in kilometers /// </summary> /// <param name="h3Index">Index of the hexagon</param> /// <returns>radius of hexagon in kilometers</returns> /// <!-- Based off 3.1.1 --> static double _hexRadiusKm(H3Index h3Index) { // There is probably a cheaper way to determine the radius of a // hexagon, but this way is conceptually simple GeoCoord h3Center = new GeoCoord(); GeoBoundary h3Boundary = new GeoBoundary(); H3Index.h3ToGeo(h3Index, ref h3Center); H3Index.h3ToGeoBoundary(h3Index, ref h3Boundary); return(GeoCoord._geoDistKm(h3Center, h3Boundary.verts)); }
/// <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> /// Internal: Create a vertex graph from a set of hexagons. It is the /// responsibility of the caller to call destroyVertexGraph on the populated /// graph, otherwise the memory in the graph nodes will not be freed. /// </summary> /// /// <param name="h3Set">Set of hexagons</param> /// <param name="numHexes">Number of hexagons in the set</param> /// <param name="graph">Output graph</param> /// <!-- Based off 3.1.1 --> public static void h3SetToVertexGraph(ref List <H3Index> h3Set, int numHexes, ref VertexGraph graph) { GeoBoundary vertices = new GeoBoundary(); GeoCoord fromVertex = new GeoCoord(); GeoCoord toVertex = new GeoCoord(); VertexGraph.VertexNode edge; if (numHexes < 1) { // We still need to init the graph, or calls to destroyVertexGraph will // fail graph = new VertexGraph(0, 0); return; } int res = H3Index.H3_GET_RESOLUTION(h3Set[0]); const int minBuckets = 6; // TODO: Better way to calculate/guess? int numBuckets = numHexes > minBuckets ? numHexes : minBuckets; graph = new VertexGraph(numBuckets, res); // Iterate through every hexagon for (int i = 0; i < numHexes; i++) { H3Index.h3ToGeoBoundary(h3Set[i], ref vertices); // iterate through every edge for (int j = 0; j < vertices.numVerts; j++) { fromVertex = new GeoCoord(vertices.verts[j].lat, vertices.verts[j].lon); //fromVtx = vertices.verts[j]; int idx = (j + 1) % vertices.numVerts; toVertex = new GeoCoord(vertices.verts[idx].lat, vertices.verts[idx].lon); //toVtx = vertices.verts[(j + 1) % vertices.numVerts]; // If we've seen this edge already, it will be reversed edge = VertexGraph.findNodeForEdge(ref graph, toVertex, fromVertex); if (edge != null) { // If we've seen it, drop it. No edge is shared by more than 2 // hexagons, so we'll never see it again. VertexGraph.removeVertexNode(ref graph, ref edge); } else { // Add a new node for this edge VertexGraph.addVertexNode(ref graph, fromVertex, toVertex); } } } }
/// <summary> /// Determines the center point in spherical coordinates of a cell given by 2D /// hex coordinates on a particular icosahedral face. /// </summary> /// <param name="v">The 2D hex coordinates of the cell.</param> /// <param name="face">The icosahedral face upon which the 2D hex coordinate system is centered</param> /// <param name="res">The H3 resolution of the cell</param> /// <param name="substrate">Indicates whether or not this grid is actually a substrate /// grid relative to the specified resolution.</param> /// <param name="g">The spherical coordinates of the cell center point.</param> /// <!-- Based off 3.1.1 --> public static void _hex2dToGeo(ref Vec2d v, int face, int res, int substrate, ref GeoCoord g) { // calculate (r, theta) in hex2d double r = Vec2d._v2dMag(v); if (r < Constants.EPSILON) { g = faceCenterGeo[face]; return; } double theta = Math.Atan2(v.y, v.x); // scale for current resolution length u for (int i = 0; i < res; i++) { r /= M_SQRT7; } // scale accordingly if this is a substrate grid if (substrate > 0) { r /= 3.0; if (H3Index.isResClassIII(res)) { r /= M_SQRT7; } } r *= Constants.RES0_U_GNOMONIC; // perform inverse gnomonic scaling of r r = Math.Atan(r); // adjust theta for Class III // if a substrate grid, then it's already been adjusted for Class III if (substrate == 0 && H3Index.isResClassIII(res)) { theta = GeoCoord._posAngleRads(theta + Constants.M_AP7_ROT_RADS); } // find theta as an azimuth theta = GeoCoord._posAngleRads(faceAxesAzRadsCII[face, 0] - theta); // now find the point at (r,theta) from the face center GeoCoord._geoAzDistanceRads(ref faceCenterGeo[face], theta, r, ref g); }
/// <summary> /// Find the <see cref="VertexNode"/> for a given edge, if it exists. /// </summary> /// <param name="graph">Graph to look in</param> /// <param name="fromVtx">Start Vertex</param> /// <param name="ToVtx">End Vertex, or null if we don't care</param> /// <returns>Pointer to the vertex node, if found</returns> /// <!-- Based off 3.1.1 --> public static VertexNode findNodeForEdge( ref VertexGraph graph, GeoCoord fromVtx, GeoCoord toVtx) { uint index = _hashVertex(fromVtx, graph.res, graph.numBuckets); var currentBucket = graph.buckets[(int)index]; var nodeIndex = currentBucket.FindIndex( t => GeoCoord.geoAlmostEqual(t.from, fromVtx) && (toVtx == null || GeoCoord.geoAlmostEqual(t.to, toVtx)) ); return(nodeIndex < 0 ? null : currentBucket[nodeIndex]); }
/// <summary> /// Encodes a coordinate on the sphere to the H3 index of the containing cell at /// the specified resolution. /// /// Returns 0 on invalid input. /// </summary> /// <param name="g">The spherical coordinates to encode.</param> /// <param name="res"> The desired H3 resolution for the encoding.</param> /// <returns>The encoded H3Index (or 0 on failure).</returns> /// <!-- Based off 3.1.1 --> public static H3Index geoToH3(ref GeoCoord g, int res) { if (res < 0 || res > Constants.MAX_H3_RES) { return(H3_INVALID_INDEX); } if (Double.IsInfinity(g.lat) || Double.IsNaN(g.lat) || Double.IsInfinity(g.lon) || Double.IsNaN(g.lon)) { return(H3_INVALID_INDEX); } FaceIJK fijk = new FaceIJK(); FaceIJK._geoToFaceIjk(g, res, ref fijk); return(_faceIjkToH3(ref fijk, res)); }
/// <summary> /// Get an integer hash for a lat/lon point, at a precision determined /// by the current hexagon resolution. /// </summary> /// <remarks> /// Light testing suggests this might not be sufficient at resolutions /// finer than 10. Design a better hash function if performance and /// collisions seem to be an issue here. (Modified in code below) /// </remarks> /// <param name="vertex">Lat/lon vertex to hash</param> /// <param name="res">Resolution of the hexagon the vertex belongs to</param> /// <param name="numBuckets">Number of buckets in the graph</param> /// <returns>Integer hash</returns> /// <!-- Based off 3.1.1 --> public static uint _hashVertex(GeoCoord vertex, int res, int numBuckets) { // Simple hash: Take the sum of the lat and lon with a precision level // determined by the resolution, converted to int, modulo bucket count. // return (uint) // Math.IEEERemainder // ( // Math.Abs((vertex.lat + vertex.lon) * Math.Pow(10, 15 - res)), // numBuckets // ); // // I didn't like that one because it caused TestVertexGraph to // fail, so I wrote a new one. // Edge cases for stuff close enough to (0,0) to not matter go straight to bucket 0. if (vertex == null) { return(0); } double start_lat = Math.Abs(vertex.lat); double start_lon = Math.Abs(vertex.lon); if (start_lon < Constants.DBL_EPSILON && start_lat < Constants.DBL_EPSILON) { return(0); } const int keepShifting = 1000000000; double start = 0; while (start < keepShifting) { start_lat *= 9973; start_lon *= 911; start += start_lat; start += start_lon; } var fraction = Math.IEEERemainder(start, 1); return((uint)(Math.Abs(fraction) * numBuckets)); }
/// <summary> /// Provides the coordinates defining the unidirectional edge. /// </summary> /// <param name="edge">The unidirectional edge H3Index</param> /// <param name="gb"> /// The geoboundary object to store the edge coordinates. /// </param> /// <!-- Based off 3.1.1 --> public static void getH3UnidirectionalEdgeBoundary(H3Index edge, ref GeoBoundary gb) { // TODO: More efficient solution :) GeoBoundary origin = new GeoBoundary(); GeoBoundary destination = new GeoBoundary(); GeoCoord postponedVertex = new GeoCoord(); bool hasPostponedVertex = false; H3Index.h3ToGeoBoundary(getOriginH3IndexFromUnidirectionalEdge(edge), ref origin); H3Index.h3ToGeoBoundary(getDestinationH3IndexFromUnidirectionalEdge(edge), ref destination); int k = 0; for (int i = 0; i < origin.numVerts; i++) { if (_hasMatchingVertex(origin.verts[i], destination)) { // If we are on vertex 0, we need to handle the case where it's the // end of the edge, not the beginning. if (i == 0 && !_hasMatchingVertex(origin.verts[i + 1], destination)) { postponedVertex = origin.verts[i]; hasPostponedVertex = true; } else { gb.verts[k] = origin.verts[i]; k++; } } } // If we postponed adding the last vertex, add it now if (hasPostponedVertex) { gb.verts[k] = postponedVertex; k++; } gb.numVerts = k; }
/// <summary> /// Find the great circle distance in radians between two spherical coordinates. /// </summary> /// <param name="p1">The first spherical coordinates</param> /// <param name="p2">The second spherical coordinates</param> /// <returns>The great circle distance between p1 and p2</returns> /// <!-- Based off 3.1.1 --> public static double _geoDistRads(GeoCoord p1, GeoCoord p2) { // use spherical triangle with p1 at A, p2 at B, and north pole at C double bigC = Math.Abs(p2.lon - p1.lon); if (bigC > Constants.M_PI) // assume we want the complement { // note that in this case they can't both be negative double lon1 = p1.lon; if (lon1 < 0.0) { lon1 += 2.0 * Constants.M_PI; } double lon2 = p2.lon; if (lon2 < 0.0) { lon2 += 2.0 * Constants.M_PI; } bigC = Math.Abs(lon2 - lon1); } double b = Constants.M_PI_2 - p1.lat; double a = Constants.M_PI_2 - p2.lat; // use law of cosines to find c double cosc = Math.Cos(a) * Math.Cos(b) + Math.Sin(a) * Math.Sin(b) * Math.Cos(bigC); if (cosc > 1.0) { cosc = 1.0; } if (cosc < -1.0) { cosc = -1.0; } return(Math.Acos(cosc)); }
/// <summary>Add an edge to the graph</summary> /// <param name="graph">Graph to add node to</param> /// <param name="fromVtx">Start vertex</param> /// <param name="toVtx">End vertex</param> /// <returns>new node</returns> /// <!-- Based off 3.1.1 --> public static VertexNode addVertexNode(ref VertexGraph graph, GeoCoord fromVtx, GeoCoord toVtx) { // Make the new node VertexNode node = _initVertexNode(fromVtx, toVtx); // Determine location var index = _hashVertex(fromVtx, graph.res, graph.numBuckets); // Check whether there's an existing node in that spot List <VertexNode> currentNode = graph.buckets[(int)index]; if (currentNode.Count == 0) { // Set bucket to the new node graph.buckets[(int)index].Add(node); } else { // Go through the list to make sure the // edge doesn't already exist // // NOTE: Later, use a Hashset foreach (var vertexNode in graph.buckets[(int)index]) { if (GeoCoord.geoAlmostEqual(vertexNode.from, fromVtx) && GeoCoord.geoAlmostEqual(vertexNode.to, toVtx)) { // already exists, bail. return(vertexNode); } } // Add the new node to the end of the list graph.buckets[(int)index].Add(node); } graph.size++; return(node); }
/// <summary> /// Add a new linked coordinate to the current loop /// </summary> /// <param name="loop">Loop to add coordinate to</param> /// <param name="vertex">Coordinate to add</param> /// <returns>Pointer to the coordinate</returns> /// <!-- Based off 3.1.1 --> public static LinkedGeoCoord addLinkedCoord(ref LinkedGeoLoop loop, ref GeoCoord vertex) { LinkedGeoCoord coord = new LinkedGeoCoord(); coord.vertex = new GeoCoord(vertex.lat, vertex.lon); coord.next = null; LinkedGeoCoord last = loop.last; if (last == null) { if (loop.first != null) { throw new Exception("assert(loop->first == NULL);"); } loop.first = coord; } else { last.next = coord; } loop.last = coord; return(coord); }
/// <summary> /// Determines if the components of two spherical coordinates are within our /// standard epsilon distance of each other. /// </summary> /// <param name="p1">The first spherical coordinates.</param> /// <param name="p2">The second spherical coordinates.</param> /// <returns> /// Whether or not the two coordinates are within the epsilon distance /// of each other. /// </returns> /// <!-- Based off 3.1.1 --> public static bool geoAlmostEqual(GeoCoord v1, GeoCoord v2) { return(geoAlmostEqualThreshold(v1, v2, Constants.EPSILON_RAD)); }
/// <summary> /// Determines if the components of two spherical coordinates are within some /// threshold distance of each other. /// </summary> /// <param name="p1">The first spherical coordinates</param> /// <param name="p2">The second spherical coordinates</param> /// <param name="threshold">The threshold distance</param> /// <returns> /// Whether or not the two coordinates are within the threshold distance /// of each other /// </returns> /// <!-- Based off 3.1.1 --> public static bool geoAlmostEqualThreshold(GeoCoord p1, GeoCoord p2, double threshold) { return(Math.Abs(p1.lat - p2.lat) < threshold && Math.Abs(p1.lon - p2.lon) < threshold); }
/// <summary> /// Computes the point on the sphere a specified azimuth and distance from /// another point. /// </summary> /// <param name="p1">The first spherical coordinates.</param> /// <param name="az">The desired azimuth from p1.</param> /// <param name="distance">The desired distance from p1, must be non-negative.</param> /// <param name="p2"> /// The spherical coordinates at the desired azimuth and distance from p1. /// </param> /// <!-- Based off 3.1.1 --> public static void _geoAzDistanceRads(ref GeoCoord p1, double az, double distance, ref GeoCoord p2) { if (distance < Constants.EPSILON) { p2 = p1; return; } double sinlat, sinlon, coslon; az = _posAngleRads(az); // check for due north/south azimuth if (az < Constants.EPSILON || Math.Abs(az - Constants.M_PI) < Constants.EPSILON) { if (az < Constants.EPSILON) // due north { p2.lat = p1.lat + distance; } else // due south { p2.lat = p1.lat - distance; } if (Math.Abs(p2.lat - Constants.M_PI_2) < Constants.EPSILON) // north pole { p2.lat = Constants.M_PI_2; p2.lon = 0.0; } else if (Math.Abs(p2.lat + Constants.M_PI_2) < Constants.EPSILON) // south pole { p2.lat = -Constants.M_PI_2; p2.lon = 0.0; } else { p2.lon = constrainLng(p1.lon); } } else // not due north or south { sinlat = Math.Sin(p1.lat) * Math.Cos(distance) + Math.Cos(p1.lat) * Math.Sin(distance) * Math.Cos(az); if (sinlat > 1.0) { sinlat = 1.0; } if (sinlat < -1.0) { sinlat = -1.0; } p2.lat = Math.Asin(sinlat); if (Math.Abs(p2.lat - Constants.M_PI_2) < Constants.EPSILON) // north pole { p2.lat = Constants.M_PI_2; p2.lon = 0.0; } else if (Math.Abs(p2.lat + Constants.M_PI_2) < Constants.EPSILON) // south pole { p2.lat = -Constants.M_PI_2; p2.lon = 0.0; } else { sinlon = Math.Sin(az) * Math.Sin(distance) / Math.Cos(p2.lat); coslon = (Math.Cos(distance) - Math.Sin(p1.lat) * Math.Sin(p2.lat)) / Math.Cos(p1.lat) / Math.Cos(p2.lat); if (sinlon > 1.0) { sinlon = 1.0; } if (sinlon < -1.0) { sinlon = -1.0; } if (coslon > 1.0) { sinlon = 1.0; } if (coslon < -1.0) { sinlon = -1.0; } p2.lon = constrainLng(p1.lon + Math.Atan2(sinlon, coslon)); } } }
/// <summary> /// Find a Vertex node starting at the given vertex /// </summary> /// <param name="graph">Graph to look in</param> /// <param name="fromVtx">Start vertex</param> /// <returns>Vertex node, if found</returns> /// <!-- Based off 3.1.1 --> public static VertexNode findNodeForVertex( ref VertexGraph graph, ref GeoCoord fromVtx) { return(findNodeForEdge(ref graph, fromVtx, null)); }
/// <summary> /// Find the great circle distance in kilometers between two spherical /// coordinates /// </summary> /// <param name="p1">The first spherical coordinates</param> /// <param name="p2">The distance in kilometers between p1 and p2</param> /// <!-- Based off 3.1.1 --> public static double _geoDistKm(GeoCoord p1, IEnumerable <GeoCoord> p2) { return(Constants.EARTH_RADIUS_KM * _geoDistRads(p1, p2.First( ))); }
/// <summary> /// Set the components of spherical coordinates in radians. /// </summary> /// <param name="p">The spherical coordinates</param> /// <param name="latDegs">The desired latitude in decimal radians</param> /// <param name="lonDegs">The desired longitude in decimal radians</param> /// <!-- Based off 3.1.1 --> public static void _setGeoRads(ref GeoCoord p, double latRads, double lonRads) { p.lat = latRads; p.lon = lonRads; }
/// <summary> /// Set the components of spherical coordinates in decimal degrees. /// </summary> /// <param name="p">The spherical coordinates</param> /// <param name="latDegs">The desired latitude in decimal degrees</param> /// <param name="lonDegs">The desired longitude in decimal degrees</param> /// <!-- Based off 3.1.1 --> public static void setGeoDegs(ref GeoCoord p, double latDegs, double lonDegs) { _setGeoRads(ref p, degsToRads(latDegs), degsToRads(lonDegs)); }
/// <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> /// takes a given GeoPolygon data structure and /// checks if it contains a given geo coordinate. /// </summary> /// <param name="geoPolygon">The Geofence and holes defining the relevant area</param> /// <param name="bboxes">The bboxes for the main Geofence and each of its holes</param> /// <param name="coord">The coordinate to check</param> /// <returns>Whether the point is contained</returns> /// <!-- Based off 3.1.1 --> public static bool pointInsidePolygon(GeoPolygon geoPolygon, List <BBox> bboxes, GeoCoord coord) { // Start with contains state of primary Geofence var tempBox = bboxes[0]; bool contains = pointInsideGeofence( ref geoPolygon.Geofence, ref tempBox, ref coord); bboxes[0] = tempBox; // If the point is contained in the primary Geofence, but there are holes in // the Geofence iterate through all holes and return false if the point is // contained in any hole if (contains && geoPolygon.numHoles > 0) { for (int i = 0; i < geoPolygon.numHoles; i++) { var hole = geoPolygon.holes[i]; var box = bboxes[i + 1]; var isInside = pointInsideGeofence(ref hole, ref box, ref coord); geoPolygon.holes[i] = hole; bboxes[i + 1] = box; if (isInside) { return(false); } } } 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">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; } }
public GeoCoord(Code.GeoCoord gc) { Latitude = gc.lat; Longitude = gc.lon; }
///<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; } } }