mono-cpp/tests/unit/test_grid.cpp

236 lines
8.0 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "grid/grid.h"
#include "gadm_reader/gadm_reader.h"
#include <cmath>
#include <set>
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
static const std::string CACHE_DIR = "cache/gadm";
// ── Helper: load ABW boundary ───────────────────────────────────────────────
static gadm::Feature load_abw() {
auto res = gadm::load_boundary_file(CACHE_DIR + "/boundary_ABW_0.json");
REQUIRE(res.error.empty());
REQUIRE(res.features.size() == 1);
return res.features[0];
}
static gadm::Feature load_afg() {
auto res = gadm::load_boundary_file(CACHE_DIR + "/boundary_AFG.1.1_1_2.json");
REQUIRE(res.error.empty());
REQUIRE(res.features.size() == 1);
return res.features[0];
}
// ── Admin mode ──────────────────────────────────────────────────────────────
TEST_CASE("Grid admin: single feature → one waypoint", "[grid][admin]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "admin";
opts.pathOrder = "zigzag";
auto result = grid::generate({feat}, opts);
REQUIRE(result.error.empty());
REQUIRE(result.validCells == 1);
REQUIRE(result.waypoints.size() == 1);
auto& wp = result.waypoints[0];
REQUIRE(wp.step == 1);
REQUIRE(wp.radius_km > 0);
// ABW centroid should be near [-70.0, 12.5]
REQUIRE_THAT(wp.lng, WithinAbs(-70.0, 0.1));
REQUIRE_THAT(wp.lat, WithinAbs(12.5, 0.1));
}
TEST_CASE("Grid admin: multiple features", "[grid][admin]") {
auto abw = load_abw();
auto afg = load_afg();
grid::GridOptions opts;
opts.gridMode = "admin";
auto result = grid::generate({abw, afg}, opts);
REQUIRE(result.error.empty());
REQUIRE(result.validCells == 2);
REQUIRE(result.waypoints.size() == 2);
REQUIRE(result.waypoints[0].step == 1);
REQUIRE(result.waypoints[1].step == 2);
}
TEST_CASE("Grid admin: empty features → error", "[grid][admin]") {
grid::GridOptions opts;
opts.gridMode = "admin";
auto result = grid::generate({}, opts);
REQUIRE(!result.error.empty());
}
// ── Centers mode ────────────────────────────────────────────────────────────
TEST_CASE("Grid centers: ABW generates waypoints from GHS centers", "[grid][centers]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "centers";
opts.cellSize = 5.0;
opts.centroidOverlap = 0.5;
auto result = grid::generate({feat}, opts);
REQUIRE(result.error.empty());
REQUIRE(result.validCells > 0);
REQUIRE(result.waypoints.size() == static_cast<size_t>(result.validCells));
// All waypoints should be near Aruba
for (const auto& wp : result.waypoints) {
REQUIRE(wp.lng > -70.2);
REQUIRE(wp.lng < -69.8);
REQUIRE(wp.lat > 12.4);
REQUIRE(wp.lat < 12.7);
}
}
TEST_CASE("Grid centers: centroid overlap filters nearby centers", "[grid][centers]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "centers";
opts.cellSize = 20.0; // big cells
opts.centroidOverlap = 0.0; // no overlap allowed → aggressive dedup
auto result_aggressive = grid::generate({feat}, opts);
opts.centroidOverlap = 0.9; // allow almost full overlap → more centers pass
auto result_relaxed = grid::generate({feat}, opts);
REQUIRE(result_relaxed.validCells >= result_aggressive.validCells);
}
// ── Hex grid mode ───────────────────────────────────────────────────────────
TEST_CASE("Grid hex: ABW at 3km cells", "[grid][hex]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "hex";
opts.cellSize = 3.0;
auto result = grid::generate({feat}, opts);
REQUIRE(result.error.empty());
REQUIRE(result.validCells > 0);
// Aruba is ~30x10 km, so with 3km cells we expect ~20-60 cells
REQUIRE(result.validCells > 5);
REQUIRE(result.validCells < 200);
}
TEST_CASE("Grid square: ABW at 5km cells", "[grid][square]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "square";
opts.cellSize = 5.0;
auto result = grid::generate({feat}, opts);
REQUIRE(result.error.empty());
REQUIRE(result.validCells > 0);
REQUIRE(result.validCells < 50); // island is small
}
TEST_CASE("Grid hex: too many cells returns error", "[grid][hex]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "hex";
opts.cellSize = 0.01; // tiny cell → huge grid
opts.maxCellsLimit = 100;
auto result = grid::generate({feat}, opts);
REQUIRE(!result.error.empty());
}
// ── Sorting ─────────────────────────────────────────────────────────────────
TEST_CASE("Grid sort: snake vs zigzag differ for multi-row grid", "[grid][sort]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "hex";
opts.cellSize = 3.0;
opts.pathOrder = "zigzag";
auto r1 = grid::generate({feat}, opts);
opts.pathOrder = "snake";
auto r2 = grid::generate({feat}, opts);
REQUIRE(r1.validCells == r2.validCells);
// Snake reverses every other row, so coordinates should differ in order
if (r1.validCells > 5) {
bool anyDiff = false;
for (size_t i = 0; i < r1.waypoints.size(); ++i) {
if (std::abs(r1.waypoints[i].lng - r2.waypoints[i].lng) > 1e-6) {
anyDiff = true;
break;
}
}
REQUIRE(anyDiff);
}
}
TEST_CASE("Grid sort: spiral-out starts near center", "[grid][sort]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "hex";
opts.cellSize = 3.0;
opts.pathOrder = "spiral-out";
auto result = grid::generate({feat}, opts);
REQUIRE(result.validCells > 3);
// Compute center of all waypoints
double cLon = 0, cLat = 0;
for (const auto& wp : result.waypoints) { cLon += wp.lng; cLat += wp.lat; }
cLon /= result.waypoints.size();
cLat /= result.waypoints.size();
// First waypoint should be closer to center than last
double distFirst = std::hypot(result.waypoints.front().lng - cLon, result.waypoints.front().lat - cLat);
double distLast = std::hypot(result.waypoints.back().lng - cLon, result.waypoints.back().lat - cLat);
REQUIRE(distFirst < distLast);
}
TEST_CASE("Grid sort: steps are sequential after sorting", "[grid][sort]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "hex";
opts.cellSize = 3.0;
opts.pathOrder = "shortest";
auto result = grid::generate({feat}, opts);
for (size_t i = 0; i < result.waypoints.size(); ++i) {
REQUIRE(result.waypoints[i].step == static_cast<int>(i + 1));
}
}
// ── GHS Filtering ───────────────────────────────────────────────────────────
TEST_CASE("Grid admin: GHS pop filter skips low-pop features", "[grid][filter]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "admin";
opts.minGhsPop = 999999999; // impossibly high
auto result = grid::generate({feat}, opts);
REQUIRE(result.validCells == 0);
REQUIRE(result.skippedCells == 1);
}
TEST_CASE("Grid admin: bypass filters passes everything", "[grid][filter]") {
auto feat = load_abw();
grid::GridOptions opts;
opts.gridMode = "admin";
opts.minGhsPop = 999999999;
opts.bypassFilters = true;
auto result = grid::generate({feat}, opts);
REQUIRE(result.validCells == 1);
}