236 lines
8.0 KiB
C++
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);
|
|
}
|