use geo_types::Coord;
use map_tile_toolkit::{
PolygonSlicerAll, PolygonSlicerOne, SlicerAll, SlicerOne, TileError, TileId,
};
fn line(coords: Vec<(i32, i32)>) -> Vec<Coord<i32>> {
coords.into_iter().map(|(x, y)| Coord { x, y }).collect()
}
fn all(extent: u32, buffer: u16) -> Result<SlicerAll<Coord<i32>>, TileError> {
SlicerAll::new(extent, buffer)
}
fn one(extent: u32, buffer: u16, tile: TileId) -> SlicerOne<Coord<i32>> {
SlicerOne::new(extent, buffer, tile).expect("valid config")
}
fn snapshot(s: &SlicerAll<Coord<i32>>) -> Vec<(TileId, Vec<Vec<Coord<i32>>>)> {
s.iter_tiles()
.map(|t| {
let runs = t
.iter_features()
.flat_map(|f| f.iter_polylines().map(<[_]>::to_vec))
.collect();
(t.tile_id(), runs)
})
.collect()
}
fn errors_after_emitting() -> Vec<Coord<i32>> {
line(vec![
(i32::MIN + 8200, 0), (i32::MIN + 8250, 0),
(i32::MIN + 8, 0), ])
}
#[test]
fn invalid_extent() {
assert_eq!(all(0, 0).err(), Some(TileError::InvalidExtent));
assert_eq!(all(u32::MAX, 0).err(), Some(TileError::InvalidExtent));
assert!(all(1, 0).is_ok());
assert!(all(i32::MAX as u32, u16::MAX).is_ok());
assert_eq!(
SlicerOne::<Coord<i32>>::new(0, 0, TileId::new(0, 0)).err(),
Some(TileError::InvalidExtent)
);
}
#[test]
fn buffer_too_large() {
assert_eq!(all(10, 5).err(), Some(TileError::BufferTooLarge)); assert_eq!(all(10, 6).err(), Some(TileError::BufferTooLarge));
assert!(all(10, 4).is_ok()); assert!(all(1, 0).is_ok());
assert_eq!(all(2, 1).err(), Some(TileError::BufferTooLarge));
assert_eq!(
SlicerOne::<Coord<i32>>::new(10, 5, TileId::new(0, 0)).err(),
Some(TileError::BufferTooLarge)
);
}
#[test]
fn extreme_tile_errors_instead_of_panicking() {
let l = line(vec![(0, 0), (10, 10)]);
assert_eq!(
one(4096, 0, TileId::new(i32::MAX, i32::MAX))
.add_feature(&l)
.err(),
Some(TileError::Overflow)
);
assert_eq!(
one(4096, 0, TileId::new(i32::MIN, 0)).add_feature(&l).err(),
Some(TileError::Overflow)
);
let mut far = one(4096, 0, TileId::new(1000, 1000));
far.add_feature(&l).expect("no error for a far tile");
assert!(far.is_empty());
}
#[test]
fn spanning_too_many_tiles_errors() {
let mut s = all(1, 0).expect("valid config"); assert_eq!(
s.add_feature(line(vec![(0, 0), (40_000_000, 0)])).err(),
Some(TileError::TooManyTiles)
);
assert!(s.is_empty());
let mut s = all(10, 1).expect("valid config");
s.add_feature(line(vec![(0, 0), (1_000_000, 700_010)]))
.expect("a long diagonal is within budget");
assert!(s.len() > 100_000 && s.len() < 200_000, "{}", s.len());
}
#[test]
fn unbuffered_gap_rows_are_charged() {
let mut s = all(1, 0).expect("valid config");
assert_eq!(
s.add_feature(line(vec![(0, 0), (1, 1 << 26)])).err(),
Some(TileError::TooManyTiles)
);
assert!(s.is_empty());
s.add_feature(line(vec![(0, 0), (1, 1000)]))
.expect("a short gap run is within budget");
assert_eq!(s.len(), 2, "only the endpoint tiles are touched");
}
#[test]
fn coordinate_overflow_errors() {
let mut s = all(4096, 8).expect("valid config");
assert_eq!(
s.add_feature(line(vec![(i32::MAX, 0), (i32::MAX, 10)]))
.err(),
Some(TileError::Overflow)
);
}
#[test]
fn long_polylines_are_accepted() {
let mut s = all(1_000_000, 0).expect("valid config");
let coords: Vec<Coord<i32>> = (0..200_000).map(|i| Coord { x: i % 8, y: 0 }).collect();
s.add_feature(&coords).expect("200 000 vertices");
let runs: Vec<usize> = s
.iter_tiles()
.flat_map(|t| {
t.iter_features()
.flat_map(|f| f.iter_polylines().map(<[_]>::len))
.collect::<Vec<_>>()
})
.collect();
assert_eq!(runs, [200_000]);
}
#[test]
fn add_feature_is_atomic_on_error() {
let mut probe = all(4096, 8).expect("valid config");
probe
.add_feature(errors_after_emitting())
.expect_err("must error mid-walk");
assert!(
probe.is_empty(),
"a feature that errors before any other data must leave the slicer empty"
);
let mut s = all(4096, 8).expect("valid config");
s.add_feature(line(vec![(i32::MIN + 8200, 0), (i32::MIN + 8300, 0)]))
.expect("clean feature");
let before = snapshot(&s);
assert_eq!(
s.add_feature(errors_after_emitting()).err(),
Some(TileError::Overflow)
);
assert_eq!(snapshot(&s), before, "errored feature must roll back fully");
s.add_feature(line(vec![(i32::MIN + 8210, 0), (i32::MIN + 8260, 0)]))
.expect("still usable after a rolled-back error");
let mut clean = all(4096, 8).expect("valid config");
clean
.add_feature(line(vec![(i32::MIN + 8200, 0), (i32::MIN + 8300, 0)]))
.expect("clean feature");
clean
.add_feature(line(vec![(i32::MIN + 8210, 0), (i32::MIN + 8260, 0)]))
.expect("clean feature");
assert_eq!(
snapshot(&s),
snapshot(&clean),
"skipping a failed feature matches never adding it"
);
}
#[test]
fn rollback_leaves_untouched_tiles_alone() {
let mut s = all(4096, 8).expect("valid config");
s.add_feature(line(vec![(0, 0), (100, 0)]))
.expect("clean feature near the origin");
let before = snapshot(&s);
assert_eq!(
s.add_feature(errors_after_emitting()).err(),
Some(TileError::Overflow),
"the far feature errors mid-walk"
);
assert_eq!(
snapshot(&s),
before,
"rolling back a feature that touches none of the pre-existing tiles leaves them intact"
);
}
#[test]
fn clear_resets_for_reuse() {
let mut s = all(25, 0).expect("valid config");
s.add_feature(line(vec![(5, 5), (60, 40)])).expect("slice");
assert!(!s.is_empty());
s.clear();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
let mut fresh = all(25, 0).expect("valid config");
s.add_feature(line(vec![(1, 1), (30, 30)])).expect("slice");
fresh
.add_feature(line(vec![(1, 1), (30, 30)]))
.expect("slice");
assert_eq!(snapshot(&s), snapshot(&fresh));
let mut o = one(25, 0, TileId::new(0, 0));
o.add_feature(line(vec![(5, 5), (20, 20)])).expect("slice");
assert!(!o.is_empty());
o.clear();
assert!(o.is_empty());
assert_eq!(o.tile(), TileId::new(0, 0), "clear keeps the bound tile");
}
#[test]
fn empty_and_degenerate_inputs_are_ok() {
let mut s = all(25, 0).expect("valid config");
s.add_feature(Vec::<Coord<i32>>::new())
.expect("empty polyline is ok");
assert!(s.is_empty());
let dot = line(vec![(5, 5)]);
s.add_feature(&dot).expect("single-point polyline is ok");
assert!(s.is_empty());
let mut one = one(25, 0, TileId::new(0, 0));
one.add_feature(&dot).expect("single-point polyline is ok");
assert!(one.is_empty());
}
fn poly(coords: Vec<(i32, i32)>) -> Vec<Vec<Coord<i32>>> {
vec![line(coords)]
}
type PolyState = Vec<(
Vec<(TileId, Vec<Vec<Vec<Coord<i32>>>>)>,
Vec<map_tile_toolkit::FillRun>,
)>;
fn poly_state(s: &PolygonSlicerAll<Coord<i32>>) -> PolyState {
s.iter_features()
.map(|f| {
let tiles = f
.iter_tiles()
.map(|t| {
let polys = t
.iter_polygons()
.map(|p| p.iter_rings().map(|r| r.vertices().to_vec()).collect())
.collect();
(t.tile_id(), polys)
})
.collect();
(tiles, f.iter_fill_runs().collect())
})
.collect()
}
#[test]
fn polygon_all_validates_config() {
assert_eq!(
PolygonSlicerAll::<Coord<i32>>::new(0, 0).err(),
Some(TileError::InvalidExtent)
);
assert_eq!(
PolygonSlicerAll::<Coord<i32>>::new(10, 5).err(),
Some(TileError::BufferTooLarge)
);
}
#[test]
fn polygon_all_too_many_tiles() {
let mut s = PolygonSlicerAll::<Coord<i32>>::new(1, 0).expect("valid config");
assert_eq!(
s.add_feature([poly(vec![(0, 0), (40_000_000, 0), (40_000_000, 5)])])
.err(),
Some(TileError::TooManyTiles)
);
assert!(s.is_empty());
s.add_feature([poly(vec![(0, 0), (40_000, 0), (40_000, 5)])])
.expect("a wide ring is within budget");
s.clear();
let small = poly(vec![(0, 0), (600, 600), (0, 600)]);
let large = poly(vec![(0, 0), (5780, 5780), (0, 5780)]);
s.add_feature([small.clone()])
.expect("one ring is within budget");
assert_eq!(s.len(), 1);
let filled: usize = s
.iter_features()
.flat_map(|f| f.iter_fill_runs())
.map(|r| r.x.len())
.sum();
assert!(filled > 170_000, "{filled}");
s.add_feature([small, large])
.expect("long diagonals are charged per touched tile");
assert_eq!(s.len(), 2);
}
#[test]
fn polygon_all_charges_unbuffered_gap_rows() {
let mut s = PolygonSlicerAll::<Coord<i32>>::new(1, 0).expect("valid config");
assert_eq!(
s.add_feature([poly(vec![(0, 0), (1, 1 << 24), (0, 1)])])
.err(),
Some(TileError::TooManyTiles)
);
assert!(s.is_empty());
s.add_feature([poly(vec![(0, 0), (1, 1000), (0, 1)])])
.expect("a short sliver is within budget");
assert_eq!(s.len(), 1);
}
#[test]
fn polygon_all_is_atomic_on_error() {
let (x0, x1) = (i32::MIN + 10, i32::MIN + 6000);
let bad = poly(vec![(x0, 0), (x1, 0), (x1, 10_000), (x0, 10_000)]);
let mut one =
PolygonSlicerOne::<Coord<i32>>::new(4096, 0, TileId::new(-524_288, 1)).expect("config");
assert_eq!(
one.add_feature(&bad[0], &[]).err(),
Some(TileError::Overflow),
"the single-tile clip agrees"
);
let good = poly(vec![(5, 5), (9000, 5), (9000, 9000), (5, 9000)]);
let mut s = PolygonSlicerAll::<Coord<i32>>::new(4096, 0).expect("valid config");
s.add_feature([good.clone()]).expect("clean feature");
let before = poly_state(&s);
assert_eq!(s.add_feature([bad]).err(), Some(TileError::Overflow));
assert_eq!(
poly_state(&s),
before,
"errored feature must roll back fully"
);
s.add_feature([good.clone()]).expect("still usable");
let mut clean = PolygonSlicerAll::<Coord<i32>>::new(4096, 0).expect("valid config");
clean.add_feature([good.clone()]).expect("clean");
clean.add_feature([good]).expect("clean");
assert_eq!(poly_state(&s), poly_state(&clean));
}
#[test]
fn polygon_all_degenerate_inputs_and_clear() {
let mut s = PolygonSlicerAll::<Coord<i32>>::new(25, 0).expect("valid config");
s.add_feature(Vec::<Vec<Vec<Coord<i32>>>>::new())
.expect("empty");
s.add_feature([Vec::<Vec<Coord<i32>>>::new()])
.expect("no rings");
s.add_feature([vec![
line(vec![(1, 1), (9, 9), (1, 1)]),
line(vec![(2, 2), (8, 2), (8, 8)]),
]])
.expect("degenerate exterior");
assert!(s.is_empty());
let square = line(vec![(2, 2), (60, 2), (60, 60), (2, 60)]);
s.add_feature([vec![square.clone(), line(vec![(30, 30), (31, 31)])]])
.expect("degenerate hole");
let mut plain = PolygonSlicerAll::<Coord<i32>>::new(25, 0).expect("valid config");
plain.add_feature([vec![square.clone()]]).expect("plain");
assert_eq!(poly_state(&s), poly_state(&plain));
s.clear();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
s.add_feature([vec![square]]).expect("reuse");
assert_eq!(poly_state(&s), poly_state(&plain));
}