mono-cpp/tests/unit/test_geo.cpp

210 lines
8.4 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "geo/geo.h"
#include <cmath>
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<Coord> 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<BBox> 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<Coord> 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<Coord> 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<Coord> 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<Coord> 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<Coord> pt = {{5, 5}};
REQUIRE(area_sq_km(pt) == 0.0);
}
// ── Point-in-polygon ────────────────────────────────────────────────────────
TEST_CASE("PIP: point inside a square", "[geo][pip]") {
std::vector<Coord> 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<Coord> 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<Coord> 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);
}