src/apps/testapps/testGeoLoopArea.c (110 lines of code) (raw):

/* * Copyright 2025 Uber Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** @file * @brief tests H3 GeoLoop area calculation * * usage: `testGeoLoopArea` */ #include <math.h> #include "area.h" #include "h3api.h" #include "test.h" #include "utility.h" static void _compareArea(LatLng *verts, int numVerts, double target_area) { double tol = 1e-14; GeoLoop loop = {.verts = verts, .numVerts = numVerts}; double out; t_assertSuccess(geoLoopAreaRads2(loop, &out)); t_assert(fabs(out - target_area) < tol, "area should match"); } SUITE(geoLoopArea) { TEST(triangle_basic) { /* GeoLoop representing a triangle covering 1/8 of the globe, with points ordered according to right-hand rule (counter-clockwise). The triangle starts at the north pole, moves down 90 degrees to the equator, and then sweeps out 90 degrees along the equator before returning to the north pole. The globe has an area of 4*pi radians, so this 1/8 triangle piece of the globe should have area pi/2. */ LatLng verts[] = { {M_PI_2, 0.0}, {0.0, 0.0}, {0.0, M_PI_2}, }; _compareArea(verts, ARRAY_SIZE(verts), M_PI / 2); } TEST(triangle_reversed) { /* Reverse the order of the points in the triangle from the previous test, so that they are in clockwise order. Since the points are in clockwise order, GeoLoop represents the whole globe minus the triangle above. */ LatLng verts[] = { {0.0, M_PI_2}, {0.0, 0.0}, {M_PI_2, 0.0}, }; _compareArea(verts, ARRAY_SIZE(verts), 7 * M_PI / 2); } TEST(slice) { /* Stitch two 1/8 triangles together, sharing an edge along the equator to create a 1/4 slice of the globe, with vertices at the north and south pole. */ LatLng verts[] = { {M_PI_2, 0.0}, {0.0, 0.0}, {-M_PI_2, 0.0}, {0.0, M_PI_2}, }; _compareArea(verts, ARRAY_SIZE(verts), M_PI); } TEST(slice_reversed) { /* 3/4 slice of the globe, from north to south pole, formed by reversing order of points from example above. */ LatLng verts[] = { {M_PI_2, 0.0}, {0.0, M_PI_2}, {-M_PI_2, 0.0}, {0.0, 0.0}, }; _compareArea(verts, ARRAY_SIZE(verts), 3 * M_PI); } TEST(hemisphere_east) { /* Stitch two 1/4 triangles together to cover the eastern hemisphere. */ LatLng verts[] = { {M_PI_2, 0.0}, {0.0, 0.0}, {-M_PI_2, 0.0}, {0.0, M_PI}, }; _compareArea(verts, ARRAY_SIZE(verts), 2 * M_PI); } TEST(hemisphere_north) { /* Stitch 4 1/8 triangles together to cover the northern hemisphere. */ LatLng verts[] = { {0.0, -M_PI}, {0.0, -M_PI_2}, {0.0, 0.0}, {0.0, M_PI_2}, }; _compareArea(verts, ARRAY_SIZE(verts), 2 * M_PI); } TEST(percentageSlice) { /* Demonstrate that edge arcs between points in a polygon or geoloop should be less than 180 degrees (M_PI radians). Create a triangle from north pole to equator and back to the north pole that sweeps out an edge arc of t * M_PI radians along the equator, so it should have an area of t*M_PI for t \in [0,1]. However, there is a discontinuity at t = 1 (i.e., M_PI radians or 180 degrees), where expected area goes to (2 + t) * M_PI for 1 < t < 2. Recall that the area in stradians of the entire globe is 4*M_PI. */ for (double t = 0; t <= 1.2; t += 0.01) { LatLng verts[] = { {M_PI_2, 0.0}, {0.0, -M_PI_2}, {0.0, t * M_PI - M_PI_2}, }; GeoLoop loop = {.verts = verts, .numVerts = ARRAY_SIZE(verts)}; double out; t_assertSuccess(geoLoopAreaRads2(loop, &out)); double tol = 1e-13; if (t < 0.99) { // When t < 1, the largest angle in the triangle is less than // 180 degrees t_assert(fabs(out - t * M_PI) <= tol, "expected area"); } else if (t > 1.01) { /* Discontinuity at t == 1. For t > 1, the triangle "flips", because the shortest geodesic path is on the other side of the globe. The triangle is now oriented in clockwise order, and the area computed is the area *outside* of the triangle, which starts at 3*pi. */ t_assert(fabs(out - (2 + t) * M_PI) <= tol, "expected area"); } /* Note that we avoid testing t == 1, since the triangle isn't well defined because there are many possible geodesic shortest paths when consecutive points are antipodal (180 degrees apart). */ } } TEST(percentageSlice_large) { /* Continuing from the test above, note that a large polygon with t > 1 is *still* representable and we can compute its area accurately; we just need to add intermediate vertices so that no edge arc is greater than 180 degrees. */ double t = 1.2; LatLng verts[] = { {M_PI_2, 0.0}, {0.0, -M_PI_2}, {0.0, 0.0}, // Extra vertex so every angle is < 180 degrees {0.0, t * M_PI - M_PI_2}, }; _compareArea(verts, ARRAY_SIZE(verts), t * M_PI); } TEST(degenerateLoop2) { // Note that `geoLoopAreaRads2()` works without error on degenerate // loops, returning 0 area. LatLng verts[] = { {M_PI_2, 0.0}, {0.0, -M_PI_2}, }; _compareArea(verts, ARRAY_SIZE(verts), 0.0); } TEST(degenerateLoop1) { // Note that `geoLoopAreaRads2()` works without error on degenerate // loops, returning 0 area. LatLng verts[] = { {0.0, 0.0}, }; _compareArea(verts, ARRAY_SIZE(verts), 0.0); } TEST(degenerateLoop0) { // Note that `geoLoopAreaRads2()` works without error on degenerate // loops, returning 0 area. _compareArea(NULL, 0, 0.0); } }