#include #include #include "grid/grid.h" #include "gadm_reader/gadm_reader.h" #include #include 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(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(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); }