h3_latLng.c (252 lines of code) (raw):

/* * Copyright 2016-2023 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 latLng.c * @brief Functions for working with lat/lng coordinates. */ #include "h3_latLng.h" #include <math.h> #include <stdbool.h> #include "h3_constants.h" #include "h3_h3Assert.h" #include "h3_h3api.h" #include "h3_mathExtensions.h" /** * Normalizes radians to a value between 0.0 and two PI. * * @param rads The input radians value. * @return The normalized radians value. */ double _posAngleRads(double rads) { double tmp = ((rads < 0.0) ? rads + M_2PI : rads); if (rads >= M_2PI) tmp -= M_2PI; return tmp; } /** * Determines if the components of two spherical coordinates are within some * threshold distance of each other. * * @param p1 The first spherical coordinates. * @param p2 The second spherical coordinates. * @param threshold The threshold distance. * @return Whether or not the two coordinates are within the threshold distance * of each other. */ bool geoAlmostEqualThreshold(const LatLng *p1, const LatLng *p2, double threshold) { return fabs(p1->lat - p2->lat) < threshold && fabs(p1->lng - p2->lng) < threshold; } /** * Determines if the components of two spherical coordinates are within our * standard epsilon distance of each other. * * @param p1 The first spherical coordinates. * @param p2 The second spherical coordinates. * @return Whether or not the two coordinates are within the epsilon distance * of each other. */ bool geoAlmostEqual(const LatLng *p1, const LatLng *p2) { return geoAlmostEqualThreshold(p1, p2, EPSILON_RAD); } /** * Set the components of spherical coordinates in decimal degrees. * * @param p The spherical coordinates. * @param latDegs The desired latitude in decimal degrees. * @param lngDegs The desired longitude in decimal degrees. */ void setGeoDegs(LatLng *p, double latDegs, double lngDegs) { _setGeoRads(p, H3_EXPORT(degsToRads)(latDegs), H3_EXPORT(degsToRads)(lngDegs)); } /** * Set the components of spherical coordinates in radians. * * @param p The spherical coordinates. * @param latRads The desired latitude in decimal radians. * @param lngRads The desired longitude in decimal radians. */ void _setGeoRads(LatLng *p, double latRads, double lngRads) { p->lat = latRads; p->lng = lngRads; } /** * Convert from decimal degrees to radians. * * @param degrees The decimal degrees. * @return The corresponding radians. */ double H3_EXPORT(degsToRads)(double degrees) { return degrees * M_PI_180; } /** * Convert from radians to decimal degrees. * * @param radians The radians. * @return The corresponding decimal degrees. */ double H3_EXPORT(radsToDegs)(double radians) { return radians * M_180_PI; } /** * constrainLat makes sure latitudes are in the proper bounds * * @param lat The original lat value * @return The corrected lat value */ double constrainLat(double lat) { while (lat > M_PI_2) { lat = lat - M_PI; } return lat; } /** * constrainLng makes sure longitudes are in the proper bounds * * @param lng The origin lng value * @return The corrected lng value */ double constrainLng(double lng) { while (lng > M_PI) { lng = lng - (2 * M_PI); } while (lng < -M_PI) { lng = lng + (2 * M_PI); } return lng; } /** * Normalize an input longitude according to the specified normalization * @param lng Input longitude * @param normalization Longitude normalization strategy * @return Normalized longitude */ double normalizeLng(const double lng, const LongitudeNormalization normalization) { switch (normalization) { case NORMALIZE_EAST: return lng < 0 ? lng + (double)M_2PI : lng; case NORMALIZE_WEST: return lng > 0 ? lng - (double)M_2PI : lng; default: return lng; } } /** * The great circle distance in radians between two spherical coordinates. * * This function uses the Haversine formula. * For math details, see: * https://en.wikipedia.org/wiki/Haversine_formula * https://www.movable-type.co.uk/scripts/latlong.html * * @param a the first lat/lng pair (in radians) * @param b the second lat/lng pair (in radians) * * @return the great circle distance in radians between a and b */ double H3_EXPORT(greatCircleDistanceRads)(const LatLng *a, const LatLng *b) { double sinLat = sin((b->lat - a->lat) * 0.5); double sinLng = sin((b->lng - a->lng) * 0.5); double A = sinLat * sinLat + cos(a->lat) * cos(b->lat) * sinLng * sinLng; return 2 * atan2(sqrt(A), sqrt(1 - A)); } /** * The great circle distance in kilometers between two spherical coordinates. * * @param a the first lat/lng pair (in radians) * @param b the second lat/lng pair (in radians) * * @return the great circle distance in kilometers between a and b */ double H3_EXPORT(greatCircleDistanceKm)(const LatLng *a, const LatLng *b) { return H3_EXPORT(greatCircleDistanceRads)(a, b) * EARTH_RADIUS_KM; } /** * The great circle distance in meters between two spherical coordinates. * * @param a the first lat/lng pair (in radians) * @param b the second lat/lng pair (in radians) * * @return the great circle distance in meters between a and b */ double H3_EXPORT(greatCircleDistanceM)(const LatLng *a, const LatLng *b) { return H3_EXPORT(greatCircleDistanceKm)(a, b) * 1000; } /** * Determines the azimuth to p2 from p1 in radians. * * @param p1 The first spherical coordinates. * @param p2 The second spherical coordinates. * @return The azimuth in radians from p1 to p2. */ double _geoAzimuthRads(const LatLng *p1, const LatLng *p2) { return atan2(cos(p2->lat) * sin(p2->lng - p1->lng), cos(p1->lat) * sin(p2->lat) - sin(p1->lat) * cos(p2->lat) * cos(p2->lng - p1->lng)); } /** * Computes the point on the sphere a specified azimuth and distance from * another point. * * @param p1 The first spherical coordinates. * @param az The desired azimuth from p1. * @param distance The desired distance from p1, must be non-negative. * @param p2 The spherical coordinates at the desired azimuth and distance from * p1. */ void _geoAzDistanceRads(const LatLng *p1, double az, double distance, LatLng *p2) { if (distance < EPSILON) { *p2 = *p1; return; } double sinlat, sinlng, coslng; az = _posAngleRads(az); // check for due north/south azimuth if (az < EPSILON || fabs(az - M_PI) < EPSILON) { if (az < EPSILON) // due north p2->lat = p1->lat + distance; else // due south p2->lat = p1->lat - distance; if (fabs(p2->lat - M_PI_2) < EPSILON) // north pole { p2->lat = M_PI_2; p2->lng = 0.0; } else if (fabs(p2->lat + M_PI_2) < EPSILON) // south pole { p2->lat = -M_PI_2; p2->lng = 0.0; } else p2->lng = constrainLng(p1->lng); } else // not due north or south { sinlat = sin(p1->lat) * cos(distance) + cos(p1->lat) * sin(distance) * cos(az); if (sinlat > 1.0) sinlat = 1.0; if (sinlat < -1.0) sinlat = -1.0; p2->lat = asin(sinlat); if (fabs(p2->lat - M_PI_2) < EPSILON) // north pole { p2->lat = M_PI_2; p2->lng = 0.0; } else if (fabs(p2->lat + M_PI_2) < EPSILON) // south pole { p2->lat = -M_PI_2; p2->lng = 0.0; } else { double invcosp2lat = 1.0 / cos(p2->lat); sinlng = sin(az) * sin(distance) * invcosp2lat; coslng = (cos(distance) - sin(p1->lat) * sin(p2->lat)) / cos(p1->lat) * invcosp2lat; if (sinlng > 1.0) sinlng = 1.0; if (sinlng < -1.0) sinlng = -1.0; if (coslng > 1.0) coslng = 1.0; if (coslng < -1.0) coslng = -1.0; p2->lng = constrainLng(p1->lng + atan2(sinlng, coslng)); } } } /* * The following functions provide meta information about the H3 hexagons at * each zoom level. Since there are only 16 total levels, these are current * handled with hardwired static values, but it may be worthwhile to put these * static values into another file that can be autogenerated by source code in * the future. */ H3Error H3_EXPORT(getHexagonAreaAvgKm2)(int res, double *out) { static const double areas[] = { 4.357449416078383e+06, 6.097884417941332e+05, 8.680178039899720e+04, 1.239343465508816e+04, 1.770347654491307e+03, 2.529038581819449e+02, 3.612906216441245e+01, 5.161293359717191e+00, 7.373275975944177e-01, 1.053325134272067e-01, 1.504750190766435e-02, 2.149643129451879e-03, 3.070918756316060e-04, 4.387026794728296e-05, 6.267181135324313e-06, 8.953115907605790e-07}; if (res < 0 || res > MAX_H3_RES) { return E_RES_DOMAIN; } *out = areas[res]; return E_SUCCESS; } H3Error H3_EXPORT(getHexagonAreaAvgM2)(int res, double *out) { static const double areas[] = { 4.357449416078390e+12, 6.097884417941339e+11, 8.680178039899731e+10, 1.239343465508818e+10, 1.770347654491309e+09, 2.529038581819452e+08, 3.612906216441250e+07, 5.161293359717198e+06, 7.373275975944188e+05, 1.053325134272069e+05, 1.504750190766437e+04, 2.149643129451882e+03, 3.070918756316063e+02, 4.387026794728301e+01, 6.267181135324322e+00, 8.953115907605802e-01}; if (res < 0 || res > MAX_H3_RES) { return E_RES_DOMAIN; } *out = areas[res]; return E_SUCCESS; } H3Error H3_EXPORT(getHexagonEdgeLengthAvgKm)(int res, double *out) { static const double lens[] = { 1281.256011, 483.0568391, 182.5129565, 68.97922179, 26.07175968, 9.854090990, 3.724532667, 1.406475763, 0.531414010, 0.200786148, 0.075863783, 0.028663897, 0.010830188, 0.004092010, 0.001546100, 0.000584169}; if (res < 0 || res > MAX_H3_RES) { return E_RES_DOMAIN; } *out = lens[res]; return E_SUCCESS; } H3Error H3_EXPORT(getHexagonEdgeLengthAvgM)(int res, double *out) { static const double lens[] = { 1281256.011, 483056.8391, 182512.9565, 68979.22179, 26071.75968, 9854.090990, 3724.532667, 1406.475763, 531.4140101, 200.7861476, 75.86378287, 28.66389748, 10.83018784, 4.092010473, 1.546099657, 0.584168630}; if (res < 0 || res > MAX_H3_RES) { return E_RES_DOMAIN; } *out = lens[res]; return E_SUCCESS; } H3Error H3_EXPORT(getNumCells)(int res, int64_t *out) { if (res < 0 || res > MAX_H3_RES) { return E_RES_DOMAIN; } *out = 2 + 120 * _ipow(7, res); return E_SUCCESS; } /** * Surface area in radians^2 of spherical triangle on unit sphere. * * For the math, see: * https://en.wikipedia.org/wiki/Spherical_trigonometry#Area_and_spherical_excess * * @param a length of triangle side A in radians * @param b length of triangle side B in radians * @param c length of triangle side C in radians * * @return area in radians^2 of triangle on unit sphere */ double triangleEdgeLengthsToArea(double a, double b, double c) { double s = (a + b + c) * 0.5; a = (s - a) * 0.5; b = (s - b) * 0.5; c = (s - c) * 0.5; s = s * 0.5; return 4 * atan(sqrt(tan(s) * tan(a) * tan(b) * tan(c))); } /** * Compute area in radians^2 of a spherical triangle, given its vertices. * * @param a vertex lat/lng in radians * @param b vertex lat/lng in radians * @param c vertex lat/lng in radians * * @return area of triangle on unit sphere, in radians^2 */ double triangleArea(const LatLng *a, const LatLng *b, const LatLng *c) { return triangleEdgeLengthsToArea(H3_EXPORT(greatCircleDistanceRads)(a, b), H3_EXPORT(greatCircleDistanceRads)(b, c), H3_EXPORT(greatCircleDistanceRads)(c, a)); } /** * Area of H3 cell in radians^2. * * The area is calculated by breaking the cell into spherical triangles and * summing up their areas. Note that some H3 cells (hexagons and pentagons) * are irregular, and have more than 6 or 5 sides. * * todo: optimize the computation by re-using the edges shared between triangles * * @param cell H3 cell * @param out cell area in radians^2 * @return E_SUCCESS on success, or an error code otherwise */ H3Error H3_EXPORT(cellAreaRads2)(H3Index cell, double *out) { LatLng c; CellBoundary cb; H3Error err = H3_EXPORT(cellToLatLng)(cell, &c); if (err) { return err; } err = H3_EXPORT(cellToBoundary)(cell, &cb); if (NEVER(err)) { // Uncoverable because cellToLatLng will have returned an error already return err; } double area = 0.0; for (int i = 0; i < cb.numVerts; i++) { int j = (i + 1) % cb.numVerts; area += triangleArea(&cb.verts[i], &cb.verts[j], &c); } *out = area; return E_SUCCESS; } /** * Area of H3 cell in kilometers^2. * * @param cell H3 cell * @param out cell area in kilometers^2 * @return E_SUCCESS on success, or an error code otherwise */ H3Error H3_EXPORT(cellAreaKm2)(H3Index cell, double *out) { H3Error err = H3_EXPORT(cellAreaRads2)(cell, out); if (!err) { *out = *out * EARTH_RADIUS_KM * EARTH_RADIUS_KM; } return err; } /** * Area of H3 cell in meters^2. * * @param cell H3 cell * @param out cell area in meters^2 * @return E_SUCCESS on success, or an error code otherwise */ H3Error H3_EXPORT(cellAreaM2)(H3Index cell, double *out) { H3Error err = H3_EXPORT(cellAreaKm2)(cell, out); if (!err) { *out = *out * 1000 * 1000; } return err; } /** * Length of a directed edge in radians. * * @param edge H3 directed edge * @param length length in radians * @return E_SUCCESS on success, or an error code otherwise */ H3Error H3_EXPORT(edgeLengthRads)(H3Index edge, double *length) { CellBoundary cb; H3Error err = H3_EXPORT(directedEdgeToBoundary)(edge, &cb); if (err) { return err; } *length = 0.0; for (int i = 0; i < cb.numVerts - 1; i++) { *length += H3_EXPORT(greatCircleDistanceRads)(&cb.verts[i], &cb.verts[i + 1]); } return E_SUCCESS; } /** * Length of a directed edge in kilometers. * * @param edge H3 directed edge * @param length length in kilometers * @return E_SUCCESS on success, or an error code otherwise */ H3Error H3_EXPORT(edgeLengthKm)(H3Index edge, double *length) { H3Error err = H3_EXPORT(edgeLengthRads)(edge, length); *length = *length * EARTH_RADIUS_KM; return err; } /** * Length of a directed edge in meters. * * @param edge H3 directed edge * @param length length in meters * @return E_SUCCESS on success, or an error code otherwise */ H3Error H3_EXPORT(edgeLengthM)(H3Index edge, double *length) { H3Error err = H3_EXPORT(edgeLengthKm)(edge, length); *length = *length * 1000; return err; }