#![allow(
dead_code,
reason = "shared across the test and bench crates; not every helper is used in each"
)]
use std::fs;
use std::path::Path;
use geo_types::{Coord, Geometry, LineString, MultiLineString};
use geojson::{Feature, FeatureCollection, GeoJson, GeometryValue, JsonObject, JsonValue};
use map_tile_toolkit::{SlicerAll, SlicerOne, TileId};
use serde_json::json;
pub const EXTENT: u32 = 25;
#[derive(Clone, Copy)]
pub struct Cfg {
pub extent: u32,
pub buffer: u16,
}
impl Cfg {
#[must_use]
pub fn all(self) -> SlicerAll<Coord<i32>> {
SlicerAll::new(self.extent, self.buffer).expect("invalid slicer config in test support")
}
#[must_use]
pub fn one(self, tile: TileId) -> SlicerOne<Coord<i32>> {
SlicerOne::new(self.extent, self.buffer, tile)
.expect("invalid slicer config in test support")
}
}
#[must_use]
pub fn slicer(extent: u32, buffer: u16) -> Cfg {
Cfg { extent, buffer }
}
#[must_use]
pub fn permutations(n: usize) -> Vec<Vec<usize>> {
assert!(n <= 10, "permutations of {n} tiles is too many");
fn go(a: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) {
if k == a.len() {
out.push(a.clone());
return;
}
for i in k..a.len() {
a.swap(k, i);
go(a, k + 1, out);
a.swap(k, i);
}
}
let mut idx: Vec<usize> = (0..n).collect();
let mut out = Vec::new();
go(&mut idx, 0, &mut out);
out
}
#[must_use]
pub fn grid() -> Cfg {
slicer(EXTENT, 0)
}
#[must_use]
pub fn grid_buffered() -> Cfg {
slicer(EXTENT, 5)
}
#[must_use]
pub fn big_configs() -> [(&'static str, Cfg); 3] {
[
("multi", slicer(EXTENT, 0)),
("few", slicer(300, 0)),
("single", slicer(1024, 0)),
]
}
pub type Polylines = Vec<Vec<Coord<i32>>>;
pub fn lines_of(geom: &Geometry<i32>) -> Vec<&[Coord<i32>]> {
match geom {
Geometry::LineString(ls) => vec![ls.0.as_slice()],
Geometry::MultiLineString(mls) => mls.0.iter().map(|ls| ls.0.as_slice()).collect(),
other => panic!("expected a polyline geometry, got {other:?}"),
}
}
#[must_use]
pub fn polylines_of(geom: &Geometry<i32>) -> Polylines {
lines_of(geom).into_iter().map(<[_]>::to_vec).collect()
}
pub fn slice_all_runs(
cfg: &Cfg,
polylines: &[Vec<Coord<i32>>],
) -> Vec<(TileId, Vec<Vec<Coord<i32>>>)> {
let mut acc = cfg.all();
for line in polylines {
acc.add_feature(line.as_slice()).expect("slice");
}
acc.iter_tiles()
.map(|tile| (tile.tile_id(), flatten(&tile)))
.filter(|(_, runs)| !runs.is_empty())
.collect()
}
pub fn slice_tile_runs(
cfg: &Cfg,
polylines: &[Vec<Coord<i32>>],
tile: TileId,
) -> Vec<Vec<Coord<i32>>> {
let mut acc = cfg.one(tile);
for line in polylines {
acc.add_feature(line.as_slice()).expect("slice");
}
acc.iter_features()
.flat_map(|f| f.iter_polylines().map(<[_]>::to_vec))
.collect()
}
fn flatten(tile: &map_tile_toolkit::TileView<'_, Coord<i32>>) -> Vec<Vec<Coord<i32>>> {
tile.iter_features()
.flat_map(|f| f.iter_polylines().map(<[_]>::to_vec))
.collect()
}
pub struct TestFeature {
pub line: Vec<Coord<i32>>,
pub properties: JsonObject,
}
impl TestFeature {}
pub fn load_fixture(path: &Path) -> Vec<TestFeature> {
let text = fs::read_to_string(path).expect("readable fixture");
let GeoJson::FeatureCollection(fc) = text.parse().expect("valid GeoJSON") else {
panic!("fixture must be a FeatureCollection: {}", path.display());
};
let features: Vec<TestFeature> = fc
.features
.into_iter()
.map(|f| {
let geom = Geometry::<f64>::try_from(f.geometry.expect("feature has geometry"))
.expect("geometry converts");
let line = match to_i32(&geom) {
Geometry::LineString(ls) => ls.0,
other => panic!(
"fixtures must use LineString features, not {other:?} ({}): express multiple \
polylines as multiple features",
path.display()
),
};
TestFeature {
line,
properties: f.properties.unwrap_or_default(),
}
})
.collect();
assert!(
!features.is_empty(),
"fixture has no features: {}",
path.display()
);
features
}
pub fn load_fixture_geoms(path: &Path) -> Polylines {
load_fixture(path).into_iter().map(|f| f.line).collect()
}
pub fn load_all_fixtures() -> Vec<(String, Polylines)> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let mut out: Vec<(String, Polylines)> = fs::read_dir(&dir)
.expect("fixtures dir exists")
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|e| e == "geojson"))
.map(|p| {
let name = p
.file_stem()
.expect("stem")
.to_str()
.expect("utf8")
.to_owned();
(name, load_fixture_geoms(&p))
})
.collect();
out.sort_by(|a, b| a.0.cmp(&b.0));
assert!(!out.is_empty(), "no fixtures found in {}", dir.display());
out
}
#[must_use]
pub fn big_polyline() -> Geometry<i32> {
const ROWS: i32 = 60;
const COLS: i32 = 60;
const STEP: i32 = 7; const ROW_H: i32 = 9;
let mut coords = Vec::with_capacity(((ROWS * (COLS + 1)) + 1) as usize);
for r in 0..ROWS {
let y0 = r * ROW_H;
for k in 0..=COLS {
let x = if r % 2 == 0 {
k * STEP
} else {
(COLS - k) * STEP
};
let y = y0 + (k * 3) % 5; coords.push(Coord { x, y });
}
}
Geometry::LineString(LineString(coords))
}
fn to_i32(geom: &Geometry<f64>) -> Geometry<i32> {
let ls = |ls: &LineString<f64>| {
LineString(
ls.0.iter()
.map(|c| Coord {
x: c.x as i32,
y: c.y as i32,
})
.collect(),
)
};
match geom {
Geometry::LineString(l) => Geometry::LineString(ls(l)),
Geometry::MultiLineString(m) => {
Geometry::MultiLineString(MultiLineString(m.0.iter().map(ls).collect()))
}
other => panic!("expected a polyline geometry, got {other:?}"),
}
}
pub fn to_f64(geom: &Geometry<i32>) -> Geometry<f64> {
let ls = |ls: &LineString<i32>| {
LineString(
ls.0.iter()
.map(|c| Coord {
x: f64::from(c.x),
y: f64::from(c.y),
})
.collect(),
)
};
match geom {
Geometry::LineString(l) => Geometry::LineString(ls(l)),
Geometry::MultiLineString(m) => {
Geometry::MultiLineString(MultiLineString(m.0.iter().map(ls).collect()))
}
other => panic!("expected a polyline geometry, got {other:?}"),
}
}
pub fn feature(geom: &Geometry<f64>, props: Vec<(&str, JsonValue)>) -> Feature {
let properties = props
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect::<JsonObject>();
Feature {
bbox: None,
geometry: Some(geojson::Geometry::new(GeometryValue::from(geom))),
id: None,
properties: Some(properties),
foreign_members: None,
}
}
pub fn line_feature(run: &[Coord<i32>], props: Vec<(&str, JsonValue)>) -> Feature {
feature(
&to_f64(&Geometry::LineString(LineString(run.to_vec()))),
props,
)
}
fn styled_line(run: &[Coord<i32>], role: &str, stroke: &str, width: u32) -> Feature {
line_feature(
run,
vec![
("role", json!(role)),
("stroke", json!(stroke)),
("stroke-width", json!(width)),
],
)
}
pub fn feature_line(run: &[Coord<i32>], role: &str) -> Feature {
styled_line(run, role, "#261fb5", 2)
}
pub fn input_feature(run: &[Coord<i32>]) -> Feature {
styled_line(run, "input", "#f6fd31", 15)
}
pub fn feature_collection_bytes(features: Vec<Feature>) -> Vec<u8> {
serde_json::to_vec_pretty(&FeatureCollection {
bbox: None,
features,
foreign_members: None,
})
.expect("serializes")
}