#include #include #include "geo/geo.h" #include using namespace geo; using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinRel; // ── Distance ──────────────────────────────────────────────────────────────── TEST_CASE("Haversine distance: known reference values", "[geo][distance]") { // London to Paris: ~343 km Coord london{-0.1278, 51.5074}; Coord paris{2.3522, 48.8566}; double d = distance_km(london, paris); REQUIRE_THAT(d, WithinRel(343.5, 0.02)); // 2% tolerance // Same point should be zero REQUIRE_THAT(distance_km(london, london), WithinAbs(0.0, 1e-10)); // Equatorial points 1 degree apart: ~111.32 km Coord eq0{0, 0}; Coord eq1{1, 0}; REQUIRE_THAT(distance_km(eq0, eq1), WithinRel(111.32, 0.01)); } TEST_CASE("Haversine distance: antipodal points", "[geo][distance]") { // North pole to south pole: ~20015 km (half circumference) Coord north{0, 90}; Coord south{0, -90}; double d = distance_km(north, south); REQUIRE_THAT(d, WithinRel(20015.0, 0.01)); } // ── BBox ──────────────────────────────────────────────────────────────────── TEST_CASE("BBox of a simple triangle", "[geo][bbox]") { std::vector triangle = {{0, 0}, {10, 5}, {5, 10}}; BBox b = bbox(triangle); REQUIRE(b.minLon == 0.0); REQUIRE(b.minLat == 0.0); REQUIRE(b.maxLon == 10.0); REQUIRE(b.maxLat == 10.0); } TEST_CASE("BBox center", "[geo][bbox]") { BBox b{-10, -20, 10, 20}; Coord c = b.center(); REQUIRE(c.lon == 0.0); REQUIRE(c.lat == 0.0); } TEST_CASE("BBox union", "[geo][bbox]") { std::vector boxes = {{0, 0, 5, 5}, {3, 3, 10, 10}}; BBox u = bbox_union(boxes); REQUIRE(u.minLon == 0.0); REQUIRE(u.minLat == 0.0); REQUIRE(u.maxLon == 10.0); REQUIRE(u.maxLat == 10.0); } TEST_CASE("BBox of empty ring returns zeros", "[geo][bbox]") { std::vector empty; BBox b = bbox(empty); REQUIRE(b.minLon == 0.0); REQUIRE(b.maxLon == 0.0); } // ── Centroid ──────────────────────────────────────────────────────────────── TEST_CASE("Centroid of a square", "[geo][centroid]") { std::vector square = {{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}; Coord c = centroid(square); REQUIRE_THAT(c.lon, WithinAbs(5.0, 1e-10)); REQUIRE_THAT(c.lat, WithinAbs(5.0, 1e-10)); } TEST_CASE("Centroid handles closed ring (duplicate first/last)", "[geo][centroid]") { // Closed triangle — first and last point are the same std::vector closed = {{0, 0}, {6, 0}, {3, 6}, {0, 0}}; Coord c = centroid(closed); // Average of 3 unique points: (0+6+3)/3 = 3, (0+0+6)/3 = 2 REQUIRE_THAT(c.lon, WithinAbs(3.0, 1e-10)); REQUIRE_THAT(c.lat, WithinAbs(2.0, 1e-10)); } // ── Area ──────────────────────────────────────────────────────────────────── TEST_CASE("Area of an equatorial 1x1 degree square", "[geo][area]") { // ~111.32 km × ~110.57 km ≈ ~12,308 km² std::vector sq = {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}; double a = area_sq_km(sq); REQUIRE_THAT(a, WithinRel(12308.0, 0.05)); // 5% tolerance } TEST_CASE("Area of a zero-size polygon is zero", "[geo][area]") { std::vector pt = {{5, 5}}; REQUIRE(area_sq_km(pt) == 0.0); } // ── Point-in-polygon ──────────────────────────────────────────────────────── TEST_CASE("PIP: point inside a square", "[geo][pip]") { std::vector sq = {{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}; REQUIRE(point_in_polygon({5, 5}, sq) == true); REQUIRE(point_in_polygon({1, 1}, sq) == true); } TEST_CASE("PIP: point outside a square", "[geo][pip]") { std::vector sq = {{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}; REQUIRE(point_in_polygon({-1, 5}, sq) == false); REQUIRE(point_in_polygon({15, 5}, sq) == false); } TEST_CASE("PIP: point on edge is indeterminate but consistent", "[geo][pip]") { std::vector sq = {{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}; // Edge behavior is implementation-defined but should not crash (void)point_in_polygon({0, 5}, sq); (void)point_in_polygon({5, 0}, sq); } // ── Bearing ───────────────────────────────────────────────────────────────── TEST_CASE("Bearing: due north", "[geo][bearing]") { Coord a{0, 0}; Coord b{0, 10}; REQUIRE_THAT(bearing_deg(a, b), WithinAbs(0.0, 0.1)); } TEST_CASE("Bearing: due east", "[geo][bearing]") { Coord a{0, 0}; Coord b{10, 0}; REQUIRE_THAT(bearing_deg(a, b), WithinAbs(90.0, 0.5)); } // ── Destination ───────────────────────────────────────────────────────────── TEST_CASE("Destination: 100km north from equator", "[geo][destination]") { Coord start{0, 0}; Coord dest = destination(start, 0.0, 100.0); // due north REQUIRE_THAT(dest.lat, WithinRel(0.899, 0.02)); // ~0.9 degrees REQUIRE_THAT(dest.lon, WithinAbs(0.0, 0.01)); } TEST_CASE("Destination roundtrip: go 100km then measure distance", "[geo][destination]") { Coord start{2.3522, 48.8566}; // Paris Coord dest = destination(start, 45.0, 100.0); // 100km northeast double d = distance_km(start, dest); REQUIRE_THAT(d, WithinRel(100.0, 0.01)); // should be ~100km back } // ── Square grid ───────────────────────────────────────────────────────────── TEST_CASE("Square grid: generates cells within bbox", "[geo][grid]") { BBox extent{0, 0, 1, 1}; // ~111km x ~110km auto cells = square_grid(extent, 50.0); // 50km cells → ~4 cells REQUIRE(cells.size() >= 4); for (const auto& c : cells) { REQUIRE(c.lon >= extent.minLon); REQUIRE(c.lon <= extent.maxLon); REQUIRE(c.lat >= extent.minLat); REQUIRE(c.lat <= extent.maxLat); } } TEST_CASE("Square grid: zero cell size returns empty", "[geo][grid]") { BBox extent{0, 0, 10, 10}; auto cells = square_grid(extent, 0.0); REQUIRE(cells.empty()); } // ── Hex grid ──────────────────────────────────────────────────────────────── TEST_CASE("Hex grid: generates cells within bbox", "[geo][grid]") { BBox extent{0, 0, 1, 1}; auto cells = hex_grid(extent, 50.0); REQUIRE(cells.size() >= 4); for (const auto& c : cells) { REQUIRE(c.lon >= extent.minLon); REQUIRE(c.lon <= extent.maxLon); REQUIRE(c.lat >= extent.minLat); REQUIRE(c.lat <= extent.maxLat); } } TEST_CASE("Hex grid: has offset rows", "[geo][grid]") { BBox extent{0, 0, 2, 2}; // large enough for multiple rows auto cells = hex_grid(extent, 30.0); // Find first and second row Y values if (cells.size() >= 3) { // Just verify we got some cells (hex pattern is complex to validate) REQUIRE(cells.size() > 2); } } // ── Viewport estimation ───────────────────────────────────────────────────── TEST_CASE("Viewport estimation at equator zoom 14", "[geo][viewport]") { double sq = estimate_viewport_sq_km(0.0, 14); // At zoom 14, equator: ~9.55 m/px → ~9.78 * 7.33 ≈ 71.7 km² REQUIRE_THAT(sq, WithinRel(71.7, 0.15)); // 15% tolerance } TEST_CASE("Viewport estimation: higher zoom = smaller area", "[geo][viewport]") { double z14 = estimate_viewport_sq_km(40.0, 14); double z16 = estimate_viewport_sq_km(40.0, 16); REQUIRE(z16 < z14); }