#![allow(clippy::pedantic, reason = "test tool")]
use std::collections::BTreeMap;
use std::path::Path;
use geo_types::Coord;
use insta::assert_binary_snapshot;
use map_tile_toolkit::{ClipMode, Local, PolygonSlice, Slicer, TileId};
use serde_json::json;
mod support;
use crate::support::{EXTENT, FixturePolygon};
mod lines {
use test_each_file::test_each_path;
test_each_path! { for ["geojson"] in "./tests/polylines/fixtures" => super::lines }
}
mod polygons {
use test_each_file::test_each_path;
test_each_path! { for ["geojson"] in "./tests/polygons/fixtures" => super::polygons }
}
fn unclipped(buffer: u16) -> Slicer {
Slicer::new(EXTENT, buffer)
.expect("valid config")
.with_clip_mode(ClipMode::Unclipped)
}
fn dedup(ring: &[Coord<i32>]) -> Vec<Coord<i32>> {
let mut out = ring.to_vec();
out.dedup();
out
}
fn whole_rings(polygon: &FixturePolygon) -> Vec<Vec<Coord<i32>>> {
let mut rings = Vec::new();
for (i, ring) in support::rings(polygon).into_iter().enumerate() {
let mut ring = dedup(ring);
while ring.len() >= 2 && ring.last() == ring.first() {
ring.pop();
}
if ring.len() < 3 {
if i == 0 {
return Vec::new();
}
continue;
}
ring.push(ring[0]);
rings.push(ring);
}
rings
}
fn local(tile: TileId, ring: Local<'_, Coord<i32>>) -> Vec<Coord<i32>> {
let origin = tile.origin(EXTENT).expect("tile in range");
assert_eq!(ring.origin(), origin, "tile {tile:?} origin");
let local = ring.to_vec();
let from_global = dedup(
&ring
.global()
.iter()
.map(|&g| g - origin)
.collect::<Vec<_>>(),
);
assert_eq!(
local[..from_global.len()],
from_global[..],
"tile {tile:?} global() is not its local vertices moved by its origin"
);
local
}
fn to_local(tile: TileId, geom: &[Vec<Coord<i32>>]) -> Vec<Vec<Coord<i32>>> {
let origin = tile.origin(EXTENT).expect("tile in range");
geom.iter()
.map(|ring| ring.iter().map(|&c| c - origin).collect())
.collect()
}
fn padded_tiles(rings: &[Vec<Coord<i32>>]) -> impl Iterator<Item = TileId> {
let (lo, hi) = support::padded_tile_span(rings);
(lo.y..=hi.y).flat_map(move |y| (lo.x..=hi.x).map(move |x| TileId::new(x, y)))
}
fn lines([path]: [&Path; 1]) {
let stem = path.file_stem().and_then(|s| s.to_str()).expect("stem");
let lines = support::load_fixture_geoms(path);
for buffer in [0, 5] {
let what = format!("{stem} (buffer {buffer})");
let slicer = unclipped(buffer);
let clipped = support::slicer(EXTENT, buffer);
let mut tiles = BTreeMap::<TileId, u32>::new();
for line in &lines {
let mut reached = Vec::new();
for slice in slicer.line(line).expect("line") {
let tile = slice.tile();
assert_eq!(
local(tile, slice.run()),
to_local(tile, &[dedup(line)])[0],
"{what}: tile {tile:?} is not the whole line"
);
assert_eq!(
slice.run().global(),
&line[..],
"{what}: not the whole line"
);
reached.push(tile);
}
let yielded = reached.len();
reached.sort_unstable();
reached.dedup();
assert_eq!(yielded, reached.len(), "{what}: a tile came twice");
let expected: Vec<_> = support::slice_all_runs(&clipped, std::slice::from_ref(line))
.into_iter()
.map(|(tile, _)| tile)
.collect();
assert_eq!(reached, expected, "{what}: not the clipped slicer's tiles");
for tile in padded_tiles(std::slice::from_ref(line)) {
let one: Vec<_> = slicer
.tile_line(tile, line)
.expect("tile_line")
.map(|s| local(tile, s.run()))
.collect();
let expected = if reached.binary_search(&tile).is_ok() {
to_local(tile, &[dedup(line)])
} else {
Vec::new()
};
assert_eq!(one, expected, "{what}: tile_line differs at {tile:?}");
}
for tile in reached {
*tiles.entry(tile).or_default() += 1;
}
}
let inputs = lines.iter().map(|l| support::input_feature(l));
snapshot("polylines", stem, buffer, inputs.collect(), &tiles);
}
}
fn polygons([path]: [&Path; 1]) {
let stem = path.file_stem().and_then(|s| s.to_str()).expect("stem");
let polygons = support::load_polygon_fixture(path);
for buffer in [0, 5] {
let what = format!("{stem} (buffer {buffer})");
let mut slicer = unclipped(buffer);
let mut clipped = Slicer::new(EXTENT, buffer).expect("valid config");
let mut tiles = BTreeMap::<TileId, u32>::new();
for polygon in &polygons {
let rings = support::rings(polygon);
let whole = whole_rings(polygon);
let mut reached = Vec::new();
let mut slices = slicer.polygon(&rings).expect("polygon");
while let Some(slice) = slices.next() {
let PolygonSlice::Piece(piece) = slice.expect("slice") else {
panic!("{what}: an unclipped slicer yielded a fill run");
};
let tile = piece.tile();
let got: Vec<_> = piece.rings().map(|r| local(tile, r)).collect();
assert_eq!(
got,
to_local(tile, &whole),
"{what}: tile {tile:?} is not whole"
);
reached.push(tile);
}
let yielded = reached.len();
reached.sort_unstable();
reached.dedup();
assert_eq!(yielded, reached.len(), "{what}: a tile came twice");
let expected: Vec<_> =
support::slice_polygons(&mut clipped, std::slice::from_ref(polygon))
.expect("clipped")
.into_keys()
.collect();
assert_eq!(reached, expected, "{what}: not the clipped slicer's tiles");
for tile in padded_tiles(std::slice::from_ref(&polygon.exterior)) {
let one = slicer.tile_polygon(tile, &rings).expect("tile_polygon");
let one = one.map(|p| p.rings().map(|r| local(tile, r)).collect::<Vec<_>>());
let expected = reached
.binary_search(&tile)
.is_ok()
.then(|| to_local(tile, &whole));
assert_eq!(one, expected, "{what}: tile_polygon differs at {tile:?}");
}
for tile in reached {
*tiles.entry(tile).or_default() += 1;
}
}
let inputs = polygons
.iter()
.map(|p| support::input_polygon(&p.exterior, &p.holes));
snapshot("polygons", stem, buffer, inputs.collect(), &tiles);
}
}
fn snapshot(
kind: &str,
stem: &str,
buffer: u16,
mut features: Vec<geojson::Feature>,
tiles: &BTreeMap<TileId, u32>,
) {
if buffer > 0 {
return;
}
let (e, b) = (EXTENT as i32, i32::from(buffer));
for (&tile, &count) in tiles {
let (x0, y0) = (tile.x * e - b, tile.y * e - b);
let (x1, y1) = (x0 + e - 1 + 2 * b, y0 + e - 1 + 2 * b);
let ring = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)].map(|(x, y)| Coord { x, y });
let mut f = support::tile_polygon(&ring, &[], tile);
f.set_property("geometries", json!(count));
features.push(f);
}
let (dir, name) = (
format!("{kind}/snapshots-unclipped"),
format!("{stem}.geojson"),
);
insta::with_settings!({
snapshot_path => dir,
prepend_module_to_snapshot => false,
}, {
assert_binary_snapshot!(&name, support::feature_collection_bytes(features));
});
}