diff --git a/CMakeLists.txt b/CMakeLists.txt index 42ab9fbef9..6a9ad24594 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,7 +180,6 @@ set(LIB_SOURCE_FILES src/h3lib/lib/polyfill.c src/h3lib/lib/h3Index.c src/h3lib/lib/vec2d.c - src/h3lib/lib/vec3d.c src/h3lib/lib/vertex.c src/h3lib/lib/linkedGeo.c src/h3lib/lib/localij.c @@ -255,6 +254,7 @@ set(OTHER_SOURCE_FILES src/apps/testapps/testPolyfillInternal.c src/apps/testapps/testVec2dInternal.c src/apps/testapps/testVec3dInternal.c + src/apps/testapps/testVec3.c src/apps/testapps/testDirectedEdge.c src/apps/testapps/testDirectedEdgeExhaustive.c src/apps/testapps/testLinkedGeoInternal.c diff --git a/CMakeTests.cmake b/CMakeTests.cmake index aec1190c89..7500b47297 100644 --- a/CMakeTests.cmake +++ b/CMakeTests.cmake @@ -242,6 +242,7 @@ add_h3_test(testPolygonInternal src/apps/testapps/testPolygonInternal.c) add_h3_test(testPolyfillInternal src/apps/testapps/testPolyfillInternal.c) add_h3_test(testVec2dInternal src/apps/testapps/testVec2dInternal.c) add_h3_test(testVec3dInternal src/apps/testapps/testVec3dInternal.c) +add_h3_test(testVec3 src/apps/testapps/testVec3.c) add_h3_test(testCellToLocalIj src/apps/testapps/testCellToLocalIj.c) add_h3_test(testCellToLocalIjInternal src/apps/testapps/testCellToLocalIjInternal.c) diff --git a/src/apps/filters/h3.c b/src/apps/filters/h3.c index a3586b7e39..39fbf70393 100644 --- a/src/apps/filters/h3.c +++ b/src/apps/filters/h3.c @@ -2642,7 +2642,7 @@ SUBCOMMAND(edgeLengthM, if (err) { return err; } - printf("%.10lf\n", length); + printf("%.8lf\n", length); return E_SUCCESS; } diff --git a/src/apps/miscapps/generateFaceCenterPoint.c b/src/apps/miscapps/generateFaceCenterPoint.c index 027c6e63c9..73fa9e646e 100644 --- a/src/apps/miscapps/generateFaceCenterPoint.c +++ b/src/apps/miscapps/generateFaceCenterPoint.c @@ -1,5 +1,5 @@ /* - * Copyright 2018, 2020-2021 Uber Technologies, Inc. + * Copyright 2018, 2020-2021, 2026 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. @@ -56,8 +56,7 @@ static void generate(void) { printf("static const Vec3d faceCenterPoint[NUM_ICOSA_FACES] = {\n"); for (int i = 0; i < NUM_ICOSA_FACES; i++) { LatLng centerCoords = faceCenterGeoCopy[i]; - Vec3d centerPoint; - _geoToVec3d(¢erCoords, ¢erPoint); + Vec3d centerPoint = latLngToVec3(centerCoords); printf(" {%.16f, %.16f, %.16f}, // face %2d\n", centerPoint.x, centerPoint.y, centerPoint.z, i); } diff --git a/src/apps/testapps/testLatLngInternal.c b/src/apps/testapps/testLatLngInternal.c index fc8535c478..03949b45d4 100644 --- a/src/apps/testapps/testLatLngInternal.c +++ b/src/apps/testapps/testLatLngInternal.c @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 Uber Technologies, Inc. + * Copyright 2017-2021, 2026 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. @@ -64,94 +64,7 @@ SUITE(latLngInternal) { t_assert(constrainLng(2 * M_PI) == 0, "lng 2pi"); t_assert(constrainLng(3 * M_PI) == M_PI, "lng 2pi"); t_assert(constrainLng(4 * M_PI) == 0, "lng 4pi"); - } - - TEST(_geoAzDistanceRads_noop) { - LatLng start = {15, 10}; - LatLng out; - LatLng expected = {15, 10}; - - _geoAzDistanceRads(&start, 0, 0, &out); - t_assert(geoAlmostEqual(&expected, &out), - "0 distance produces same point"); - } - - TEST(_geoAzDistanceRads_dueNorthSouth) { - LatLng start; - LatLng out; - LatLng expected; - - // Due north to north pole - setGeoDegs(&start, 45, 1); - setGeoDegs(&expected, 90, 0); - _geoAzDistanceRads(&start, 0, H3_EXPORT(degsToRads)(45), &out); - t_assert(geoAlmostEqual(&expected, &out), - "due north to north pole produces north pole"); - - // Due north to south pole, which doesn't get wrapped correctly - setGeoDegs(&start, 45, 1); - setGeoDegs(&expected, 270, 1); - _geoAzDistanceRads(&start, 0, H3_EXPORT(degsToRads)(45 + 180), &out); - t_assert(geoAlmostEqual(&expected, &out), - "due north to south pole produces south pole"); - - // Due south to south pole - setGeoDegs(&start, -45, 2); - setGeoDegs(&expected, -90, 0); - _geoAzDistanceRads(&start, H3_EXPORT(degsToRads)(180), - H3_EXPORT(degsToRads)(45), &out); - t_assert(geoAlmostEqual(&expected, &out), - "due south to south pole produces south pole"); - - // Due north to non-pole - setGeoDegs(&start, -45, 10); - setGeoDegs(&expected, -10, 10); - _geoAzDistanceRads(&start, 0, H3_EXPORT(degsToRads)(35), &out); - t_assert(geoAlmostEqual(&expected, &out), - "due north produces expected result"); - } - - TEST(_geoAzDistanceRads_poleToPole) { - LatLng start; - LatLng out; - LatLng expected; - - // Azimuth doesn't really matter in this case. Any azimuth from the - // north pole is south, any azimuth from the south pole is north. - - setGeoDegs(&start, 90, 0); - setGeoDegs(&expected, -90, 0); - _geoAzDistanceRads(&start, H3_EXPORT(degsToRads)(12), - H3_EXPORT(degsToRads)(180), &out); - t_assert(geoAlmostEqual(&expected, &out), - "some direction to south pole produces south pole"); - - setGeoDegs(&start, -90, 0); - setGeoDegs(&expected, 90, 0); - _geoAzDistanceRads(&start, H3_EXPORT(degsToRads)(34), - H3_EXPORT(degsToRads)(180), &out); - t_assert(geoAlmostEqual(&expected, &out), - "some direction to north pole produces north pole"); - } - - TEST(_geoAzDistanceRads_invertible) { - LatLng start; - setGeoDegs(&start, 15, 10); - LatLng out; - - double azimuth = H3_EXPORT(degsToRads)(20); - double degrees180 = H3_EXPORT(degsToRads)(180); - double distance = H3_EXPORT(degsToRads)(15); - - _geoAzDistanceRads(&start, azimuth, distance, &out); - t_assert(fabs(H3_EXPORT(greatCircleDistanceRads)(&start, &out) - - distance) < EPSILON_RAD, - "moved distance is as expected"); - - LatLng start2 = out; - _geoAzDistanceRads(&start2, azimuth + degrees180, distance, &out); - // TODO: Epsilon is relatively large - t_assert(H3_EXPORT(greatCircleDistanceRads)(&start, &out) < 0.01, - "moved back to origin"); + t_assert(constrainLng(-2 * M_PI) == 0, "lng -2pi"); + t_assert(constrainLng(-3 * M_PI) == -M_PI, "lng -3pi"); } } diff --git a/src/apps/testapps/testVec3.c b/src/apps/testapps/testVec3.c new file mode 100644 index 0000000000..7c78147520 --- /dev/null +++ b/src/apps/testapps/testVec3.c @@ -0,0 +1,148 @@ +/* + * Copyright 2026 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 testVec3.c + * @brief Tests the Vec3d helpers used by the geodesic polyfill path. + */ + +#include +#include + +#include "h3Index.h" +#include "test.h" +#include "vec3d.h" + +SUITE(Vec3d) { + TEST(dotProduct) { + Vec3d a = {.x = 1.0, .y = 0.0, .z = 0.0}; + Vec3d b = {.x = -1.0, .y = 0.0, .z = 0.0}; + t_assert(vec3Dot(a, b) == -1.0, "dot product matches expected value"); + } + + TEST(crossProductOrthogonality) { + Vec3d i = {.x = 1.0, .y = 0.0, .z = 0.0}; + Vec3d j = {.x = 0.0, .y = 1.0, .z = 0.0}; + Vec3d k = vec3Cross(i, j); + t_assert(fabs(k.x - 0.0) < DBL_EPSILON, "x component zero"); + t_assert(fabs(k.y - 0.0) < DBL_EPSILON, "y component zero"); + t_assert(fabs(k.z - 1.0) < DBL_EPSILON, "z component one"); + t_assert(fabs(vec3Dot(k, i)) < DBL_EPSILON, "cross is orthogonal to i"); + t_assert(fabs(vec3Dot(k, j)) < DBL_EPSILON, "cross is orthogonal to j"); + } + + TEST(normalizeAndMagnitude) { + Vec3d v = {.x = 3.0, .y = -4.0, .z = 12.0}; + double magSq = vec3NormSq(v); + t_assert(fabs(magSq - 169.0) < DBL_EPSILON, + "magnitude squared matches"); + t_assert(fabs(vec3Norm(v) - 13.0) < DBL_EPSILON, "magnitude matches"); + + vec3Normalize(&v); + t_assert(fabs(vec3Norm(v) - 1.0) < DBL_EPSILON, + "normalized vector is unit"); + + Vec3d zero = {.x = 0.0, .y = 0.0, .z = 0.0}; + vec3Normalize(&zero); + t_assert(zero.x == 0.0 && zero.y == 0.0 && zero.z == 0.0, + "zero vector remains unchanged when normalizing"); + } + + TEST(distance) { + Vec3d a = {.x = 0.0, .y = 0.0, .z = 0.0}; + Vec3d b = {.x = 1.0, .y = 2.0, .z = 2.0}; + t_assert(fabs(vec3DistSq(a, b) - 9.0) < DBL_EPSILON, + "distance squared matches"); + } + + TEST(latLngToVec3_unitSphere) { + LatLng geo = {.lat = 0.5, .lng = -1.3}; + Vec3d v = latLngToVec3(geo); + t_assert(fabs(vec3Norm(v) - 1.0) < DBL_EPSILON, + "converted vector lives on the unit sphere"); + } + + TEST(vec3ToCell_invalidRes) { + Vec3d v = {.x = 1.0, .y = 0.0, .z = 0.0}; + H3Index out; + t_assert(vec3ToCell(&v, -1, &out) == E_RES_DOMAIN, + "negative resolution is rejected"); + t_assert(vec3ToCell(&v, 16, &out) == E_RES_DOMAIN, + "resolution above max is rejected"); + } + + TEST(cellToVec3_unitSphere) { + // cellToVec3 should return a point on the unit sphere. + LatLng p = {.lat = 0.6, .lng = -1.2}; + H3Index h; + t_assertSuccess(H3_EXPORT(latLngToCell)(&p, 5, &h)); + + Vec3d v; + t_assertSuccess(cellToVec3(h, &v)); + t_assert(fabs(vec3Norm(v) - 1.0) < DBL_EPSILON, + "cellToVec3 result is on the unit sphere"); + } + + TEST(cellToVec3_matchesCellToLatLng) { + // vec3ToLatLng(cellToVec3(cell)) should agree with cellToLatLng. + LatLng p = {.lat = 0.3, .lng = 2.1}; + H3Index h; + t_assertSuccess(H3_EXPORT(latLngToCell)(&p, 7, &h)); + + Vec3d v; + t_assertSuccess(cellToVec3(h, &v)); + LatLng fromVec3 = vec3ToLatLng(v); + + LatLng fromCell; + t_assertSuccess(H3_EXPORT(cellToLatLng)(h, &fromCell)); + + t_assert(fabs(fromVec3.lat - fromCell.lat) < DBL_EPSILON, + "lat matches cellToLatLng"); + t_assert(fabs(fromVec3.lng - fromCell.lng) < DBL_EPSILON, + "lng matches cellToLatLng"); + } + + TEST(cellToVec3_roundTrip) { + // vec3ToCell(cellToVec3(cell)) should return the same cell. + LatLng p = {.lat = -0.4, .lng = 0.8}; + H3Index h; + t_assertSuccess(H3_EXPORT(latLngToCell)(&p, 9, &h)); + + Vec3d v; + t_assertSuccess(cellToVec3(h, &v)); + + H3Index h2; + t_assertSuccess(vec3ToCell(&v, 9, &h2)); + t_assert(h2 == h, "round-trip through Vec3d returns same cell"); + } + + TEST(cellToVec3_invalidCell) { + Vec3d v; + t_assert(cellToVec3(0x7fffffffffffffff, &v) == E_CELL_INVALID, + "invalid cell gives E_CELL_INVALID"); + } + + TEST(vec3ToCell_nonFinite) { + H3Index out; + Vec3d nanX = {.x = NAN, .y = 0.0, .z = 0.0}; + t_assert(vec3ToCell(&nanX, 0, &out) == E_DOMAIN, "NaN x is rejected"); + Vec3d infY = {.x = 0.0, .y = INFINITY, .z = 0.0}; + t_assert(vec3ToCell(&infY, 0, &out) == E_DOMAIN, + "infinite y is rejected"); + Vec3d infZ = {.x = 0.0, .y = 0.0, .z = -INFINITY}; + t_assert(vec3ToCell(&infZ, 0, &out) == E_DOMAIN, + "infinite z is rejected"); + } +} diff --git a/src/apps/testapps/testVec3dInternal.c b/src/apps/testapps/testVec3dInternal.c index 71ffb8a740..f6254538ea 100644 --- a/src/apps/testapps/testVec3dInternal.c +++ b/src/apps/testapps/testVec3dInternal.c @@ -1,5 +1,5 @@ /* - * Copyright 2018, 2020-2021 Uber Technologies, Inc. + * Copyright 2018, 2020-2021, 2026 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. @@ -16,50 +16,70 @@ #include #include -#include #include "test.h" #include "vec3d.h" SUITE(Vec3dInternal) { - TEST(_pointSquareDist) { + TEST(vec3DistSq) { Vec3d v1 = {0, 0, 0}; Vec3d v2 = {1, 0, 0}; Vec3d v3 = {0, 1, 1}; Vec3d v4 = {1, 1, 1}; Vec3d v5 = {1, 1, 2}; - t_assert(fabs(_pointSquareDist(&v1, &v1)) < DBL_EPSILON, + t_assert(fabs(vec3DistSq(v1, v1)) < DBL_EPSILON, "distance to self is 0"); - t_assert(fabs(_pointSquareDist(&v1, &v2) - 1) < DBL_EPSILON, + t_assert(fabs(vec3DistSq(v1, v2) - 1) < DBL_EPSILON, "distance to <1,0,0> is 1"); - t_assert(fabs(_pointSquareDist(&v1, &v3) - 2) < DBL_EPSILON, + t_assert(fabs(vec3DistSq(v1, v3) - 2) < DBL_EPSILON, "distance to <0,1,1> is 2"); - t_assert(fabs(_pointSquareDist(&v1, &v4) - 3) < DBL_EPSILON, + t_assert(fabs(vec3DistSq(v1, v4) - 3) < DBL_EPSILON, "distance to <1,1,1> is 3"); - t_assert(fabs(_pointSquareDist(&v1, &v5) - 6) < DBL_EPSILON, + t_assert(fabs(vec3DistSq(v1, v5) - 6) < DBL_EPSILON, "distance to <1,1,2> is 6"); } - TEST(_geoToVec3d) { + TEST(vec3Normalize_smallNonzero) { + // 1e-163 squared underflows to 0, so norm == 0. + // vec3Normalize should produce the zero vector. + Vec3d v = {1e-163, 0, 0}; + + t_assert(v.x != 0.0, "vector is nonzero"); + t_assert(vec3Norm(v) == 0.0, "norm underflows to zero"); + + vec3Normalize(&v); + t_assert(v.x == 0.0 && v.y == 0.0 && v.z == 0.0, + "underflowed vector normalizes to zero"); + } + + TEST(vec3Normalize_dblEpsilonHalf) { + // DBL_EPSILON/2 is small but normalizes fine. + Vec3d v = {DBL_EPSILON / 2.0, 0, 0}; + + t_assert(vec3Norm(v) < DBL_EPSILON, "norm is small but nonzero"); + + vec3Normalize(&v); + t_assert(fabs(v.x - 1.0) < DBL_EPSILON && v.y == 0 && v.z == 0, + "still normalizable to unit vector"); + } + + TEST(latLngToVec3) { Vec3d origin = {0}; LatLng c1 = {0, 0}; - Vec3d p1; - _geoToVec3d(&c1, &p1); - t_assert(fabs(_pointSquareDist(&origin, &p1) - 1) < EPSILON_RAD, + Vec3d p1 = latLngToVec3(c1); + t_assert(fabs(vec3DistSq(origin, p1) - 1) < EPSILON_RAD, "Geo point is on the unit sphere"); LatLng c2 = {M_PI_2, 0}; - Vec3d p2; - _geoToVec3d(&c2, &p2); - t_assert(fabs(_pointSquareDist(&p1, &p2) - 2) < EPSILON_RAD, + Vec3d p2 = latLngToVec3(c2); + t_assert(fabs(vec3DistSq(p1, p2) - 2) < EPSILON_RAD, "Geo point is on another axis"); LatLng c3 = {M_PI, 0}; - Vec3d p3; - _geoToVec3d(&c3, &p3); - t_assert(fabs(_pointSquareDist(&p1, &p3) - 4) < EPSILON_RAD, + Vec3d p3 = latLngToVec3(c3); + t_assert(fabs(vec3DistSq(p1, p3) - 4) < EPSILON_RAD, "Geo point is the other side of the sphere"); } } diff --git a/src/h3lib/include/faceijk.h b/src/h3lib/include/faceijk.h index c05cbf2988..77ef642bc9 100644 --- a/src/h3lib/include/faceijk.h +++ b/src/h3lib/include/faceijk.h @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 Uber Technologies, Inc. + * Copyright 2016-2021, 2026 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. @@ -27,6 +27,7 @@ #include "coordijk.h" #include "latLng.h" #include "vec2d.h" +#include "vec3d.h" /** @struct FaceIJK * @brief Face number and ijk coordinates on that face-centered coordinate @@ -47,8 +48,6 @@ typedef struct { /// face } FaceOrientIJK; -extern const LatLng faceCenterGeo[NUM_ICOSA_FACES]; - // indexes for faceNeighbors table /** IJ quadrant faceNeighbors table direction */ #define IJ 1 @@ -72,19 +71,16 @@ typedef enum { // Internal functions -void _geoToFaceIjk(const LatLng *g, int res, FaceIJK *h); -void _geoToHex2d(const LatLng *g, int res, int *face, Vec2d *v); -void _faceIjkToGeo(const FaceIJK *h, int res, LatLng *g); +void _vec3ToFaceIjk(Vec3d p, int res, FaceIJK *h); +void _faceIjkToVec3(const FaceIJK *h, int res, Vec3d *g); void _faceIjkToCellBoundary(const FaceIJK *h, int res, int start, int length, CellBoundary *g); void _faceIjkPentToCellBoundary(const FaceIJK *h, int res, int start, int length, CellBoundary *g); void _faceIjkToVerts(FaceIJK *fijk, int *res, FaceIJK *fijkVerts); void _faceIjkPentToVerts(FaceIJK *fijk, int *res, FaceIJK *fijkVerts); -void _hex2dToGeo(const Vec2d *v, int face, int res, int substrate, LatLng *g); Overage _adjustOverageClassII(FaceIJK *fijk, int res, int pentLeading4, int substrate); Overage _adjustPentVertOverage(FaceIJK *fijk, int res); -void _geoToClosestFace(const LatLng *g, int *face, double *sqd); #endif diff --git a/src/h3lib/include/h3Index.h b/src/h3lib/include/h3Index.h index 6ced3c4cd4..a90574e1f8 100644 --- a/src/h3lib/include/h3Index.h +++ b/src/h3lib/include/h3Index.h @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018, 2020 Uber Technologies, Inc. + * Copyright 2016-2018, 2020, 2026 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. @@ -179,4 +179,7 @@ H3Index _h3Rotate60ccw(H3Index h); H3Index _h3Rotate60cw(H3Index h); DECLSPEC H3Index _zeroIndexDigits(H3Index h, int start, int end); +H3Error vec3ToCell(const Vec3d *v, int res, H3Index *out); +H3Error cellToVec3(H3Index h3, Vec3d *v); + #endif diff --git a/src/h3lib/include/latLng.h b/src/h3lib/include/latLng.h index 8d644e578f..bc11df5548 100644 --- a/src/h3lib/include/latLng.h +++ b/src/h3lib/include/latLng.h @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 Uber Technologies, Inc. + * Copyright 2016-2021, 2026 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. @@ -52,8 +52,5 @@ bool geoAlmostEqualThreshold(const LatLng *p1, const LatLng *p2, double _posAngleRads(double rads); void _setGeoRads(LatLng *p, double latRads, double lngRads); -double _geoAzimuthRads(const LatLng *p1, const LatLng *p2); -void _geoAzDistanceRads(const LatLng *p1, double az, double distance, - LatLng *p2); #endif diff --git a/src/h3lib/include/vec3d.h b/src/h3lib/include/vec3d.h index 0bd9ac5a36..9bc3e0f012 100644 --- a/src/h3lib/include/vec3d.h +++ b/src/h3lib/include/vec3d.h @@ -1,5 +1,5 @@ /* - * Copyright 2018, 2020-2021 Uber Technologies, Inc. + * Copyright 2018, 2020-2021, 2026 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. @@ -15,23 +15,96 @@ */ /** @file vec3d.h * @brief 3D floating point vector functions. + * + * Header-only (static inline) so callers in other translation units + * can inline these without requiring LTO. */ #ifndef VEC3D_H #define VEC3D_H +#include + +#include "h3api.h" #include "latLng.h" -/** @struct Vec3D +/** @struct Vec3d * @brief 3D floating point structure + * + * For geodesic calulations represents a point on the surface of the Earth + * as a unit vector in 3D Cartesian space (ECEF-like coordinates). */ typedef struct { - double x; ///< x component - double y; ///< y component - double z; ///< z component + double x; /// towards 0deg lat, 0deg lon + double y; /// towards 0deg lat, 90deg lon + double z; /// towards north pole } Vec3d; -void _geoToVec3d(const LatLng *geo, Vec3d *point); -double _pointSquareDist(const Vec3d *p1, const Vec3d *p2); +/** Convert latitude and longitude to a unit Vec3d on the sphere. */ +static inline Vec3d latLngToVec3(LatLng geo) { + double r = cos(geo.lat); + Vec3d out = { + .x = cos(geo.lng) * r, + .y = sin(geo.lng) * r, + .z = sin(geo.lat), + }; + return out; +} + +static inline LatLng vec3ToLatLng(Vec3d v) { + LatLng out = { + .lat = asin(v.z), + .lng = atan2(v.y, v.x), + }; + return out; +} + +static inline Vec3d vec3LinComb(double a, Vec3d v1, double b, Vec3d v2) { + Vec3d out = { + .x = a * v1.x + b * v2.x, + .y = a * v1.y + b * v2.y, + .z = a * v1.z + b * v2.z, + }; + return out; +} + +static inline Vec3d vec3Cross(Vec3d v1, Vec3d v2) { + Vec3d out = { + .x = v1.y * v2.z - v1.z * v2.y, + .y = v1.z * v2.x - v1.x * v2.z, + .z = v1.x * v2.y - v1.y * v2.x, + }; + return out; +} + +static inline double vec3Dot(Vec3d v1, Vec3d v2) { + return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; +} + +static inline double vec3NormSq(Vec3d v) { return vec3Dot(v, v); } + +static inline double vec3Norm(Vec3d v) { return sqrt(vec3NormSq(v)); } + +static inline void vec3Normalize(Vec3d *v) { + double norm = vec3Norm(*v); + + // Norm can be zero either from true zero vector, or from squaring + // underflowing to zero. + // If the norm is nonzero, we normalize v using it. + // If the norm is zero, we set the vector to be exactly zero. + double s = 0.0; + if (norm > 0.0) { + s = 1.0 / norm; + } + + v->x *= s; + v->y *= s; + v->z *= s; +} + +static inline double vec3DistSq(Vec3d v1, Vec3d v2) { + Vec3d d = vec3LinComb(1.0, v1, -1.0, v2); + return vec3NormSq(d); +} #endif diff --git a/src/h3lib/lib/faceijk.c b/src/h3lib/lib/faceijk.c index a408bf0f3c..6fb54de9a3 100644 --- a/src/h3lib/lib/faceijk.c +++ b/src/h3lib/lib/faceijk.c @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 Uber Technologies, Inc. + * Copyright 2016-2023, 2026 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. @@ -36,30 +36,6 @@ #define M_SQRT7 2.6457513110645905905016157536392604257102 #define M_RSQRT7 0.37796447300922722721451653623418006081576 -/** @brief icosahedron face centers in lat/lng radians */ -const LatLng faceCenterGeo[NUM_ICOSA_FACES] = { - {0.803582649718989942, 1.248397419617396099}, // face 0 - {1.307747883455638156, 2.536945009877921159}, // face 1 - {1.054751253523952054, -1.347517358900396623}, // face 2 - {0.600191595538186799, -0.450603909469755746}, // face 3 - {0.491715428198773866, 0.401988202911306943}, // face 4 - {0.172745327415618701, 1.678146885280433686}, // face 5 - {0.605929321571350690, 2.953923329812411617}, // face 6 - {0.427370518328979641, -1.888876200336285401}, // face 7 - {-0.079066118549212831, -0.733429513380867741}, // face 8 - {-0.230961644455383637, 0.506495587332349035}, // face 9 - {0.079066118549212831, 2.408163140208925497}, // face 10 - {0.230961644455383637, -2.635097066257444203}, // face 11 - {-0.172745327415618701, -1.463445768309359553}, // face 12 - {-0.605929321571350690, -0.187669323777381622}, // face 13 - {-0.427370518328979641, 1.252716453253507838}, // face 14 - {-0.600191595538186799, 2.690988744120037492}, // face 15 - {-0.491715428198773866, -2.739604450678486295}, // face 16 - {-0.803582649718989942, -1.893195233972397139}, // face 17 - {-1.307747883455638156, -0.604647643711872080}, // face 18 - {-1.054751253523952054, 1.794075294689396615}, // face 19 -}; - /** @brief icosahedron face centers in x/y/z on the unit sphere */ static const Vec3d faceCenterPoint[NUM_ICOSA_FACES] = { {0.2199307791404606, 0.6583691780274996, 0.7198475378926182}, // face 0 @@ -361,36 +337,80 @@ static const int unitScaleByCIIres[] = { 5764801 // res 16 }; +// Forward declares to make diff nicer +// TODO: remove and reorder functions after landing +static void _vec3ToHex2d(const Vec3d *p, int res, int *face, Vec2d *v); +static void _vec3ToClosestFace(const Vec3d *v, int *face, double *sqd); + /** - * Encodes a coordinate on the sphere to the FaceIJK address of the containing + * Encodes a Vec3d coordinate to the FaceIJK address of the containing * cell at the specified resolution. * - * @param g The spherical coordinates to encode. + * Vec3d p is expected to be on the unit sphere. + * + * @param p The Vec3d coordinates to encode. * @param res The desired H3 resolution for the encoding. - * @param h The FaceIJK address of the containing cell at resolution res. + * @param h Output: FaceIJK address of the containing cell at resolution res. */ -void _geoToFaceIjk(const LatLng *g, int res, FaceIJK *h) { +void _vec3ToFaceIjk(Vec3d p, int res, FaceIJK *h) { // first convert to hex2d Vec2d v; - _geoToHex2d(g, res, &h->face, &v); + _vec3ToHex2d(&p, res, &h->face, &v); // then convert to ijk+ _hex2dToCoordIJK(&v, &h->coord); } +/** + * Compute the local north and east directions on the tangent plane + * at a point on the unit sphere. + * + * Will not work if p is at a pole, but icosahedron face centers + * are never at the poles. + * + * @param p Unit vector on the sphere. + * @param north Output: local north direction on tangent plane. + * @param east Output: local east direction on tangent plane. + */ +static inline void _vec3TangentBasis(Vec3d p, Vec3d *north, Vec3d *east) { + Vec3d northPole = {0.0, 0.0, 1.0}; + *north = vec3LinComb(1.0, northPole, -vec3Dot(northPole, p), p); + vec3Normalize(north); + *east = vec3Cross(*north, p); +} + +/** + * Calculates the azimuth from p1 to p2. + * @param p1 The first vector. + * @param p2 The second vector. + * @return The azimuth in radians. + */ +static inline double _vec3AzimuthRads(Vec3d p1, Vec3d p2) { + Vec3d northDir, eastDir; + _vec3TangentBasis(p1, &northDir, &eastDir); + + // project p2 onto tangent plane at p1 + Vec3d p2Proj = vec3LinComb(1.0, p2, -vec3Dot(p2, p1), p1); + vec3Normalize(&p2Proj); + + return atan2(vec3Dot(p2Proj, eastDir), vec3Dot(p2Proj, northDir)); +} + /** * Encodes a coordinate on the sphere to the corresponding icosahedral face and * containing 2D hex coordinates relative to that face center. * - * @param g The spherical coordinates to encode. + * Vec3d p is expected to be on the unit sphere. + * + * @param p The Vec3d coordinates to encode. * @param res The desired H3 resolution for the encoding. - * @param face The icosahedral face containing the spherical coordinates. - * @param v The 2D hex coordinates of the cell containing the point. + * @param face Output: The icosahedral face containing the coordinates. + * @param v Output: The 2D hex coordinates of the cell containing the point. */ -void _geoToHex2d(const LatLng *g, int res, int *face, Vec2d *v) { +static void _vec3ToHex2d(const Vec3d *p, int res, int *face, Vec2d *v) { // determine the icosahedron face double sqd; - _geoToClosestFace(g, face, &sqd); + _vec3ToClosestFace(p, face, &sqd); // cos(r) = 1 - 2 * sin^2(r/2) = 1 - 2 * (sqd / 4) = 1 - sqd/2 double r = acos(1 - sqd * 0.5); @@ -401,9 +421,9 @@ void _geoToHex2d(const LatLng *g, int res, int *face, Vec2d *v) { } // now have face and r, now find CCW theta from CII i-axis - double theta = - _posAngleRads(faceAxesAzRadsCII[*face][0] - - _posAngleRads(_geoAzimuthRads(&faceCenterGeo[*face], g))); + double theta = _posAngleRads( + faceAxesAzRadsCII[*face][0] - + _posAngleRads(_vec3AzimuthRads(faceCenterPoint[*face], *p))); // adjust theta for Class III (odd resolutions) if (isResolutionClassIII(res)) @@ -424,7 +444,7 @@ void _geoToHex2d(const LatLng *g, int res, int *face, Vec2d *v) { } /** - * Determines the center point in spherical coordinates of a cell given by 2D + * Determines the 3D coordinates of a cell given by 2D * hex coordinates on a particular icosahedral face. * * @param v The 2D hex coordinates of the cell. @@ -433,14 +453,15 @@ void _geoToHex2d(const LatLng *g, int res, int *face, Vec2d *v) { * @param res The H3 resolution of the cell. * @param substrate Indicates whether or not this grid is actually a substrate * grid relative to the specified resolution. - * @param g The spherical coordinates of the cell center point. + * @param v3 Output: the 3D coordinates of the cell center point */ -void _hex2dToGeo(const Vec2d *v, int face, int res, int substrate, LatLng *g) { +static void _hex2dToVec3(const Vec2d *v, int face, int res, int substrate, + Vec3d *v3) { // calculate (r, theta) in hex2d double r = _v2dMag(v); if (r < EPSILON) { - *g = faceCenterGeo[face]; + *v3 = faceCenterPoint[face]; return; } @@ -469,21 +490,27 @@ void _hex2dToGeo(const Vec2d *v, int face, int res, int substrate, LatLng *g) { theta = _posAngleRads(faceAxesAzRadsCII[face][0] - theta); // now find the point at (r,theta) from the face center - _geoAzDistanceRads(&faceCenterGeo[face], theta, r, g); + Vec3d northDir, eastDir; + _vec3TangentBasis(faceCenterPoint[face], &northDir, &eastDir); + + Vec3d dir = vec3LinComb(cos(theta), northDir, sin(theta), eastDir); + + *v3 = vec3LinComb(cos(r), faceCenterPoint[face], sin(r), dir); + vec3Normalize(v3); } /** - * Determines the center point in spherical coordinates of a cell given by + * Determines the center point in 3D coordinates of a cell given by * a FaceIJK address at a specified resolution. * * @param h The FaceIJK address of the cell. * @param res The H3 resolution of the cell. - * @param g The spherical coordinates of the cell center point. + * @param g Output: The 3D coordinates of the cell center point. */ -void _faceIjkToGeo(const FaceIJK *h, int res, LatLng *g) { +void _faceIjkToVec3(const FaceIJK *h, int res, Vec3d *g) { Vec2d v; _ijkToHex2d(&h->coord, &v); - _hex2dToGeo(&v, h->face, res, 0, g); + _hex2dToVec3(&v, h->face, res, 0, g); } /** @@ -494,7 +521,7 @@ void _faceIjkToGeo(const FaceIJK *h, int res, LatLng *g) { * @param res The H3 resolution of the cell. * @param start The first topological vertex to return. * @param length The number of topological vertexes to return. - * @param g The spherical coordinates of the cell boundary. + * @param g Output: The spherical coordinates of the cell boundary. */ void _faceIjkPentToCellBoundary(const FaceIJK *h, int res, int start, int length, CellBoundary *g) { @@ -578,8 +605,9 @@ void _faceIjkPentToCellBoundary(const FaceIJK *h, int res, int start, // find the intersection and add the lat/lng point to the result Vec2d inter; _v2dIntersect(&orig2d0, &orig2d1, edge0, edge1, &inter); - _hex2dToGeo(&inter, tmpFijk.face, adjRes, 1, - &g->verts[g->numVerts]); + Vec3d v3; + _hex2dToVec3(&inter, tmpFijk.face, adjRes, 1, &v3); + g->verts[g->numVerts] = vec3ToLatLng(v3); g->numVerts++; } @@ -589,7 +617,9 @@ void _faceIjkPentToCellBoundary(const FaceIJK *h, int res, int start, if (vert < start + NUM_PENT_VERTS) { Vec2d vec; _ijkToHex2d(&fijk.coord, &vec); - _hex2dToGeo(&vec, fijk.face, adjRes, 1, &g->verts[g->numVerts]); + Vec3d v3; + _hex2dToVec3(&vec, fijk.face, adjRes, 1, &v3); + g->verts[g->numVerts] = vec3ToLatLng(v3); g->numVerts++; } @@ -601,9 +631,8 @@ void _faceIjkPentToCellBoundary(const FaceIJK *h, int res, int start, * Get the vertices of a pentagon cell as substrate FaceIJK addresses * * @param fijk The FaceIJK address of the cell. - * @param res The H3 resolution of the cell. This may be adjusted if - * necessary for the substrate grid resolution. - * @param fijkVerts Output array for the vertices + * @param res In/out: the H3 resolution of the cell, adjusted for substrate. + * @param fijkVerts Output: array for the vertices. */ void _faceIjkPentToVerts(FaceIJK *fijk, int *res, FaceIJK *fijkVerts) { // the vertexes of an origin-centered pentagon in a Class II resolution on a @@ -667,7 +696,7 @@ void _faceIjkPentToVerts(FaceIJK *fijk, int *res, FaceIJK *fijkVerts) { * @param res The H3 resolution of the cell. * @param start The first topological vertex to return. * @param length The number of topological vertexes to return. - * @param g The spherical coordinates of the cell boundary. + * @param g Output: The spherical coordinates of the cell boundary. */ void _faceIjkToCellBoundary(const FaceIJK *h, int res, int start, int length, CellBoundary *g) { @@ -751,8 +780,9 @@ void _faceIjkToCellBoundary(const FaceIJK *h, int res, int start, int length, bool isIntersectionAtVertex = _v2dAlmostEquals(&orig2d0, &inter) || _v2dAlmostEquals(&orig2d1, &inter); if (!isIntersectionAtVertex) { - _hex2dToGeo(&inter, centerIJK.face, adjRes, 1, - &g->verts[g->numVerts]); + Vec3d v3; + _hex2dToVec3(&inter, centerIJK.face, adjRes, 1, &v3); + g->verts[g->numVerts] = vec3ToLatLng(v3); g->numVerts++; } } @@ -763,7 +793,9 @@ void _faceIjkToCellBoundary(const FaceIJK *h, int res, int start, int length, if (vert < start + NUM_HEX_VERTS) { Vec2d vec; _ijkToHex2d(&fijk.coord, &vec); - _hex2dToGeo(&vec, fijk.face, adjRes, 1, &g->verts[g->numVerts]); + Vec3d v3; + _hex2dToVec3(&vec, fijk.face, adjRes, 1, &v3); + g->verts[g->numVerts] = vec3ToLatLng(v3); g->numVerts++; } @@ -776,9 +808,8 @@ void _faceIjkToCellBoundary(const FaceIJK *h, int res, int start, int length, * Get the vertices of a cell as substrate FaceIJK addresses * * @param fijk The FaceIJK address of the cell. - * @param res The H3 resolution of the cell. This may be adjusted if - * necessary for the substrate grid resolution. - * @param fijkVerts Output array for the vertices + * @param res In/out: the H3 resolution of the cell, adjusted for substrate. + * @param fijkVerts Output: array for the vertices. */ void _faceIjkToVerts(FaceIJK *fijk, int *res, FaceIJK *fijkVerts) { // the vertexes of an origin-centered cell in a Class II resolution on a @@ -930,21 +961,19 @@ Overage _adjustPentVertOverage(FaceIJK *fijk, int res) { * Encodes a coordinate on the sphere to the corresponding icosahedral face and * containing the squared euclidean distance to that face center. * - * @param g The spherical coordinates to encode. - * @param face The icosahedral face containing the spherical coordinates. - * @param sqd The squared euclidean distance to its icosahedral face center. + * Vec3d v is expected to be on the unit sphere. + * + * @param v The Vec3d coordinates to encode. + * @param face Output: The icosahedral face containing the coordinates. + * @param sqd Output: The squared euclidean distance to its face center. */ -void _geoToClosestFace(const LatLng *g, int *face, double *sqd) { - Vec3d v3d; - _geoToVec3d(g, &v3d); - - // determine the icosahedron face +static void _vec3ToClosestFace(const Vec3d *v, int *face, double *sqd) { *face = 0; // The distance between two farthest points is 2.0, therefore the square of // the distance between two points should always be less or equal than 4.0 . *sqd = 5.0; for (int f = 0; f < NUM_ICOSA_FACES; ++f) { - double sqdT = _pointSquareDist(&faceCenterPoint[f], &v3d); + double sqdT = vec3DistSq(faceCenterPoint[f], *v); if (sqdT < *sqd) { *face = f; *sqd = sqdT; diff --git a/src/h3lib/lib/h3Index.c b/src/h3lib/lib/h3Index.c index 7b48114e50..64c11c9ef5 100644 --- a/src/h3lib/lib/h3Index.c +++ b/src/h3lib/lib/h3Index.c @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021, 2024 Uber Technologies, Inc. + * Copyright 2016-2021, 2024, 2026 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. @@ -1044,8 +1044,31 @@ H3Error H3_EXPORT(latLngToCell)(const LatLng *g, int res, H3Index *out) { return E_LATLNG_DOMAIN; } + Vec3d v = latLngToVec3(*g); + return vec3ToCell(&v, res, out); +} + +/** + * Encodes a coordinate on the sphere to the H3 index of the containing cell at + * the specified resolution. + * + * Vec3d v is expected to be on the unit sphere. + * + * @param v The 3D cartesian coordinates to encode. + * @param res The desired H3 resolution for the encoding. + * @param out The encoded H3Index. + * @returns E_SUCCESS on success, another value otherwise + */ +H3Error vec3ToCell(const Vec3d *v, int res, H3Index *out) { + if (res < 0 || res > MAX_H3_RES) { + return E_RES_DOMAIN; + } + if (!isfinite(v->x) || !isfinite(v->y) || !isfinite(v->z)) { + return E_DOMAIN; + } + FaceIJK fijk; - _geoToFaceIjk(g, res, &fijk); + _vec3ToFaceIjk(*v, res, &fijk); *out = _faceIjkToH3(&fijk, res); if (ALWAYS(*out)) { return E_SUCCESS; @@ -1143,6 +1166,23 @@ H3Error _h3ToFaceIjk(H3Index h, FaceIJK *fijk) { return E_SUCCESS; } +/** + * Determines the 3D cartesian coordinates of the center of an H3 cell. + * + * @param h3 The H3 index. + * @param v The 3D cartesian coordinates of the H3 cell center. + * @return E_SUCCESS on success, or another H3Error code on failure. + */ +H3Error cellToVec3(H3Index h3, Vec3d *v) { + FaceIJK fijk; + H3Error e = _h3ToFaceIjk(h3, &fijk); + if (e) { + return e; + } + _faceIjkToVec3(&fijk, H3_GET_RESOLUTION(h3), v); + return E_SUCCESS; +} + /** * Determines the spherical coordinates of the center point of an H3 index. * @@ -1150,12 +1190,12 @@ H3Error _h3ToFaceIjk(H3Index h, FaceIJK *fijk) { * @param g The spherical coordinates of the H3 cell center. */ H3Error H3_EXPORT(cellToLatLng)(H3Index h3, LatLng *g) { - FaceIJK fijk; - H3Error e = _h3ToFaceIjk(h3, &fijk); + Vec3d v; + H3Error e = cellToVec3(h3, &v); if (e) { return e; } - _faceIjkToGeo(&fijk, H3_GET_RESOLUTION(h3), g); + *g = vec3ToLatLng(v); return E_SUCCESS; } diff --git a/src/h3lib/lib/latLng.c b/src/h3lib/lib/latLng.c index 082db77460..53b024ae5a 100644 --- a/src/h3lib/lib/latLng.c +++ b/src/h3lib/lib/latLng.c @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 Uber Technologies, Inc. + * Copyright 2016-2023, 2026 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. @@ -201,86 +201,6 @@ 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 diff --git a/src/h3lib/lib/vec3d.c b/src/h3lib/lib/vec3d.c deleted file mode 100644 index 0b95f1327c..0000000000 --- a/src/h3lib/lib/vec3d.c +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 vec3d.c - * @brief 3D floating point vector functions. - */ - -#include "vec3d.h" - -#include - -/** - * Square of a number - * - * @param x The input number. - * @return The square of the input number. - */ -double _square(double x) { return x * x; } - -/** - * Calculate the square of the distance between two 3D coordinates. - * - * @param v1 The first 3D coordinate. - * @param v2 The second 3D coordinate. - * @return The square of the distance between the given points. - */ -double _pointSquareDist(const Vec3d *v1, const Vec3d *v2) { - return _square(v1->x - v2->x) + _square(v1->y - v2->y) + - _square(v1->z - v2->z); -} - -/** - * Calculate the 3D coordinate on unit sphere from the latitude and longitude. - * - * @param geo The latitude and longitude of the point. - * @param v The 3D coordinate of the point. - */ -void _geoToVec3d(const LatLng *geo, Vec3d *v) { - double r = cos(geo->lat); - - v->z = sin(geo->lat); - v->x = cos(geo->lng) * r; - v->y = sin(geo->lng) * r; -} diff --git a/tests/cli/edgeLengthM.txt b/tests/cli/edgeLengthM.txt index bcddc41be4..74ff16260d 100644 --- a/tests/cli/edgeLengthM.txt +++ b/tests/cli/edgeLengthM.txt @@ -1,4 +1,4 @@ add_h3_cli_test(testCliEdgeLengthM "edgeLengthM -c 115283473fffffff" - "10294.7360861995") + "10294.73608620") add_h3_cli_test(testCliNotEdgeLengthM "edgeLengthM -c 85283473fffffff 2>&1" "Error 6: Directed edge argument was not valid")