use thiserror::Error;
#[derive(Debug, Error)]
pub enum PlannerError {
#[error("zero dimension: {width}x{height}")]
ZeroDimension { width: u32, height: u32 },
#[error("tile size must be > 0, got {0}")]
ZeroTileSize(u32),
#[error("overlap {overlap} must be less than tile size {tile_size}")]
OverlapTooLarge { overlap: u32, tile_size: u32 },
#[error("dimensions too large: {width}x{height} would overflow")]
DimensionOverflow { width: u32, height: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Layout {
DeepZoom,
Xyz,
Google,
}
#[derive(Debug, Clone)]
pub struct PyramidPlanner {
image_width: u32,
image_height: u32,
tile_size: u32,
overlap: u32,
layout: Layout,
centre: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PyramidPlan {
pub image_width: u32,
pub image_height: u32,
pub tile_size: u32,
pub overlap: u32,
pub layout: Layout,
pub levels: Vec<LevelPlan>,
pub canvas_width: u32,
pub canvas_height: u32,
pub centre: bool,
pub centre_offset_x: u32,
pub centre_offset_y: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LevelPlan {
pub level: u32,
pub width: u32,
pub height: u32,
pub cols: u32,
pub rows: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TileCoord {
pub level: u32,
pub col: u32,
pub row: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl PyramidPlanner {
pub fn new(
image_width: u32,
image_height: u32,
tile_size: u32,
overlap: u32,
layout: Layout,
) -> Result<Self, PlannerError> {
if image_width == 0 || image_height == 0 {
return Err(PlannerError::ZeroDimension {
width: image_width,
height: image_height,
});
}
if tile_size == 0 {
return Err(PlannerError::ZeroTileSize(tile_size));
}
if overlap >= tile_size {
return Err(PlannerError::OverlapTooLarge { overlap, tile_size });
}
Ok(Self {
image_width,
image_height,
tile_size,
overlap,
layout,
centre: false,
})
}
pub fn with_centre(mut self, centre: bool) -> Self {
self.centre = centre;
self
}
pub fn plan(&self) -> PyramidPlan {
if self.layout == Layout::Google {
return self.plan_google();
}
let levels = self.compute_levels();
let (canvas_width, canvas_height, offset_x, offset_y) = if self.centre {
let top = levels.last().unwrap();
let grid_w = top.cols * self.tile_size;
let grid_h = top.rows * self.tile_size;
let ox = (grid_w - self.image_width) / 2;
let oy = (grid_h - self.image_height) / 2;
(grid_w, grid_h, ox, oy)
} else {
(self.image_width, self.image_height, 0, 0)
};
PyramidPlan {
image_width: self.image_width,
image_height: self.image_height,
tile_size: self.tile_size,
overlap: self.overlap,
layout: self.layout,
levels,
canvas_width,
canvas_height,
centre: self.centre,
centre_offset_x: offset_x,
centre_offset_y: offset_y,
}
}
pub fn estimate_peak_memory(&self) -> u64 {
let bytes_per_pixel: u64 = 4; let source_bytes = self.image_width as u64 * self.image_height as u64 * bytes_per_pixel;
let (canvas_w, canvas_h) = self.canvas_dimensions();
if !self.centre || (canvas_w == self.image_width && canvas_h == self.image_height) {
source_bytes * 2
} else {
let canvas_bytes = canvas_w as u64 * canvas_h as u64 * bytes_per_pixel;
source_bytes + canvas_bytes
}
}
pub fn canvas_dimensions(&self) -> (u32, u32) {
if !self.centre {
return (self.image_width, self.image_height);
}
if self.layout == Layout::Google {
let ts = self.tile_size;
let cols_needed = ceil_div(self.image_width, ts);
let rows_needed = ceil_div(self.image_height, ts);
let max_grid = cols_needed.max(rows_needed);
let n_levels = if max_grid <= 1 {
1u32
} else {
(32 - (max_grid - 1).leading_zeros()) + 1
};
let canvas = ts * (1u32 << (n_levels - 1));
(canvas, canvas)
} else {
let cols = ceil_div(self.image_width, self.tile_size);
let rows = ceil_div(self.image_height, self.tile_size);
(cols * self.tile_size, rows * self.tile_size)
}
}
fn plan_google(&self) -> PyramidPlan {
let ts = self.tile_size;
let cols_needed = ceil_div(self.image_width, ts);
let rows_needed = ceil_div(self.image_height, ts);
let max_grid = cols_needed.max(rows_needed);
let n_levels = if max_grid <= 1 {
1
} else {
(32 - (max_grid - 1).leading_zeros()) + 1
};
let canvas = ts * (1u32 << (n_levels - 1));
let (offset_x, offset_y) = if self.centre {
(
(canvas - self.image_width) / 2,
(canvas - self.image_height) / 2,
)
} else {
(0, 0)
};
let mut levels = Vec::with_capacity(n_levels as usize);
let mut w = self.image_width;
let mut h = self.image_height;
let mut img_dims = vec![(w, h)];
for _ in 1..n_levels {
w = ceil_div(w, 2);
h = ceil_div(h, 2);
img_dims.push((w, h));
}
img_dims.reverse();
for z in 0..n_levels {
let grid = 1u32 << z; let (iw, ih) = img_dims[z as usize];
levels.push(LevelPlan {
level: z,
width: iw,
height: ih,
cols: grid,
rows: grid,
});
}
PyramidPlan {
image_width: self.image_width,
image_height: self.image_height,
tile_size: ts,
overlap: self.overlap,
layout: Layout::Google,
levels,
canvas_width: canvas,
canvas_height: canvas,
centre: self.centre,
centre_offset_x: offset_x,
centre_offset_y: offset_y,
}
}
fn compute_levels(&self) -> Vec<LevelPlan> {
let mut levels = Vec::new();
let mut w = self.image_width;
let mut h = self.image_height;
let mut dims = vec![(w, h)];
while w > 1 || h > 1 {
w = ceil_div(w, 2);
h = ceil_div(h, 2);
dims.push((w, h));
}
dims.reverse();
for (level, &(w, h)) in dims.iter().enumerate() {
let (cols, rows) = self.tile_grid(w, h);
levels.push(LevelPlan {
level: level as u32,
width: w,
height: h,
cols,
rows,
});
}
levels
}
fn tile_grid(&self, width: u32, height: u32) -> (u32, u32) {
if width == 0 || height == 0 {
return (0, 0);
}
let cols = ceil_div(width, self.tile_size);
let rows = ceil_div(height, self.tile_size);
(cols, rows)
}
}
impl PyramidPlan {
pub fn total_tile_count(&self) -> u64 {
self.levels
.iter()
.map(|l| l.cols as u64 * l.rows as u64)
.sum()
}
pub fn level_count(&self) -> usize {
self.levels.len()
}
pub fn tile_coords(&self) -> impl Iterator<Item = TileCoord> + '_ {
self.levels.iter().flat_map(|level| {
(0..level.rows).flat_map(move |row| {
(0..level.cols).map(move |col| TileCoord {
level: level.level,
col,
row,
})
})
})
}
pub fn tile_rect(&self, coord: TileCoord) -> Option<TileRect> {
let level = self.levels.get(coord.level as usize)?;
if coord.col >= level.cols || coord.row >= level.rows {
return None;
}
if self.layout == Layout::Google {
let ts = self.tile_size;
return Some(TileRect {
x: coord.col * ts,
y: coord.row * ts,
width: ts,
height: ts,
});
}
let x_start = if coord.col == 0 {
0
} else {
coord.col * self.tile_size - self.overlap
};
let y_start = if coord.row == 0 {
0
} else {
coord.row * self.tile_size - self.overlap
};
let x_end_unclipped = if coord.col == 0 {
self.tile_size + self.overlap
} else {
(coord.col + 1) * self.tile_size + self.overlap
};
let y_end_unclipped = if coord.row == 0 {
self.tile_size + self.overlap
} else {
(coord.row + 1) * self.tile_size + self.overlap
};
let x_end = x_end_unclipped.min(level.width);
let y_end = y_end_unclipped.min(level.height);
Some(TileRect {
x: x_start,
y: y_start,
width: x_end - x_start,
height: y_end - y_start,
})
}
pub fn tile_path(&self, coord: TileCoord, extension: &str) -> Option<String> {
let level = self.levels.get(coord.level as usize)?;
if coord.col >= level.cols || coord.row >= level.rows {
return None;
}
match self.layout {
Layout::DeepZoom => Some(format!(
"{}/{}_{}.{}",
coord.level, coord.col, coord.row, extension
)),
Layout::Xyz => Some(format!(
"{}/{}/{}.{}",
coord.level, coord.col, coord.row, extension
)),
Layout::Google => Some(format!(
"{}/{}/{}.{}",
coord.level, coord.row, coord.col, extension
)),
}
}
pub fn dzi_manifest(&self, format: &str) -> Option<String> {
if self.layout != Layout::DeepZoom {
return None;
}
Some(format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<Image xmlns=\"http://schemas.microsoft.com/deepzoom/2008\"\n\
\x20 Format=\"{format}\"\n\
\x20 Overlap=\"{overlap}\"\n\
\x20 TileSize=\"{tile_size}\">\n\
\x20 <Size Width=\"{width}\" Height=\"{height}\"/>\n\
</Image>",
overlap = self.overlap,
tile_size = self.tile_size,
width = self.image_width,
height = self.image_height,
))
}
pub fn canvas_size_at_level(&self, level: u32) -> (u32, u32) {
if self.layout == Layout::Google {
let ts = self.tile_size;
let grid = 1u32 << level;
(ts * grid, ts * grid)
} else {
match self.levels.get(level as usize) {
Some(lp) => (lp.width, lp.height),
None => (0, 0),
}
}
}
pub fn centre_offset_at_level(&self, level: u32) -> (u32, u32) {
if !self.centre || (self.centre_offset_x == 0 && self.centre_offset_y == 0) {
return (0, 0);
}
let top_level = self.levels.len() as u32 - 1;
let shift = top_level - level;
(self.centre_offset_x >> shift, self.centre_offset_y >> shift)
}
pub fn estimate_peak_memory_for_format(&self, format: crate::pixel::PixelFormat) -> u64 {
let bpp = format.bytes_per_pixel() as u64;
let canvas_bytes = self.canvas_width as u64 * self.canvas_height as u64 * bpp;
canvas_bytes + canvas_bytes / 4
}
pub fn estimate_streaming_peak_memory(
&self,
format: crate::pixel::PixelFormat,
strip_height: u32,
) -> u64 {
let bpp = format.bytes_per_pixel() as u64;
let cw = self.canvas_width as u64;
let mut total: u64 = 0;
let mut w = cw;
let mut h = strip_height as u64;
loop {
let strip_bytes = w * h * bpp;
total += strip_bytes * 2;
w = w.div_ceil(2);
h = h.div_ceil(2);
if h <= 1 || w <= 1 {
total += w * h * bpp;
break;
}
}
total + total / 10
}
}
impl LevelPlan {
pub fn tile_count(&self) -> u64 {
self.cols as u64 * self.rows as u64
}
}
impl TileCoord {
pub fn new(level: u32, col: u32, row: u32) -> Self {
Self { level, col, row }
}
}
fn ceil_div(a: u32, b: u32) -> u32 {
a.div_ceil(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_dimension_rejected() {
assert!(PyramidPlanner::new(0, 100, 256, 0, Layout::DeepZoom).is_err());
assert!(PyramidPlanner::new(100, 0, 256, 0, Layout::DeepZoom).is_err());
}
#[test]
fn zero_tile_size_rejected() {
assert!(PyramidPlanner::new(100, 100, 0, 0, Layout::DeepZoom).is_err());
}
#[test]
fn overlap_must_be_less_than_tile_size() {
assert!(PyramidPlanner::new(100, 100, 256, 256, Layout::DeepZoom).is_err());
assert!(PyramidPlanner::new(100, 100, 256, 300, Layout::DeepZoom).is_err());
assert!(PyramidPlanner::new(100, 100, 256, 255, Layout::DeepZoom).is_ok());
}
#[test]
fn single_pixel_image() {
let planner = PyramidPlanner::new(1, 1, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
assert_eq!(plan.level_count(), 1);
assert_eq!(plan.levels[0].width, 1);
assert_eq!(plan.levels[0].height, 1);
assert_eq!(plan.levels[0].cols, 1);
assert_eq!(plan.levels[0].rows, 1);
assert_eq!(plan.total_tile_count(), 1);
}
#[test]
fn image_smaller_than_tile() {
let planner = PyramidPlanner::new(100, 80, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.width, 100);
assert_eq!(top.height, 80);
assert_eq!(top.cols, 1);
assert_eq!(top.rows, 1);
}
#[test]
fn image_exactly_n_tiles() {
let planner = PyramidPlanner::new(512, 512, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.width, 512);
assert_eq!(top.height, 512);
assert_eq!(top.cols, 2);
assert_eq!(top.rows, 2);
}
#[test]
fn image_not_multiple_of_tile() {
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.width, 500);
assert_eq!(top.height, 300);
assert_eq!(top.cols, 2); assert_eq!(top.rows, 2); }
#[test]
fn non_square_image() {
let planner = PyramidPlanner::new(1000, 200, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.width, 1000);
assert_eq!(top.height, 200);
assert_eq!(top.cols, 4);
assert_eq!(top.rows, 1);
}
#[test]
fn level_dimensions_halve_correctly() {
let planner = PyramidPlanner::new(1024, 768, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.width, 1024);
assert_eq!(top.height, 768);
for i in (1..plan.levels.len()).rev() {
let upper = &plan.levels[i];
let lower = &plan.levels[i - 1];
assert_eq!(lower.width, ceil_div(upper.width, 2));
assert_eq!(lower.height, ceil_div(upper.height, 2));
}
assert_eq!(plan.levels[0].width, 1);
assert_eq!(plan.levels[0].height, 1);
}
#[test]
fn level_indices_are_sequential() {
let planner = PyramidPlanner::new(2048, 2048, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
for (i, level) in plan.levels.iter().enumerate() {
assert_eq!(level.level, i as u32);
}
}
#[test]
fn total_tile_count_sums_all_levels() {
let planner = PyramidPlanner::new(512, 512, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let manual_count: u64 = plan
.levels
.iter()
.map(|l| l.cols as u64 * l.rows as u64)
.sum();
assert_eq!(plan.total_tile_count(), manual_count);
}
#[test]
fn tile_coords_count_matches_total() {
let planner = PyramidPlanner::new(800, 600, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let coord_count = plan.tile_coords().count() as u64;
assert_eq!(coord_count, plan.total_tile_count());
}
#[test]
fn tile_rect_no_overlap() {
let planner = PyramidPlanner::new(600, 400, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
let r = plan.tile_rect(TileCoord::new(top.level, 0, 0)).unwrap();
assert_eq!(r.x, 0);
assert_eq!(r.y, 0);
assert_eq!(r.width, 256);
assert_eq!(r.height, 256);
let r = plan.tile_rect(TileCoord::new(top.level, 1, 0)).unwrap();
assert_eq!(r.x, 256);
assert_eq!(r.y, 0);
assert_eq!(r.width, 256);
assert_eq!(r.height, 256);
let r = plan.tile_rect(TileCoord::new(top.level, 2, 1)).unwrap();
assert_eq!(r.x, 512);
assert_eq!(r.y, 256);
assert_eq!(r.width, 600 - 512); assert_eq!(r.height, 400 - 256); }
#[test]
fn tile_rect_with_overlap() {
let planner = PyramidPlanner::new(600, 400, 256, 1, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
let r = plan.tile_rect(TileCoord::new(top.level, 0, 0)).unwrap();
assert_eq!(r.x, 0);
assert_eq!(r.y, 0);
assert_eq!(r.width, 256 + 1); assert_eq!(r.height, 256 + 1);
let r = plan.tile_rect(TileCoord::new(top.level, 1, 0)).unwrap();
assert_eq!(r.x, 256 - 1); assert_eq!(r.width, 258); }
#[test]
fn tile_rect_out_of_bounds_returns_none() {
let planner = PyramidPlanner::new(256, 256, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert!(plan.tile_rect(TileCoord::new(top.level, 1, 0)).is_none());
assert!(plan.tile_rect(TileCoord::new(top.level, 0, 1)).is_none());
assert!(plan.tile_rect(TileCoord::new(999, 0, 0)).is_none());
}
#[test]
fn tile_rects_cover_full_image_no_overlap() {
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
let mut coverage = vec![0u32; top.width as usize * top.height as usize];
for row in 0..top.rows {
for col in 0..top.cols {
let r = plan.tile_rect(TileCoord::new(top.level, col, row)).unwrap();
for y in r.y..r.y + r.height {
for x in r.x..r.x + r.width {
coverage[y as usize * top.width as usize + x as usize] += 1;
}
}
}
}
for (i, &count) in coverage.iter().enumerate() {
assert_eq!(
count,
1,
"Pixel ({}, {}) covered {} times",
i % top.width as usize,
i / top.width as usize,
count
);
}
}
#[test]
fn deep_zoom_tile_path() {
let planner = PyramidPlanner::new(256, 256, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
let path = plan
.tile_path(TileCoord::new(top.level, 0, 0), "jpeg")
.unwrap();
assert_eq!(path, format!("{}/0_0.jpeg", top.level));
}
#[test]
fn xyz_tile_path() {
let planner = PyramidPlanner::new(2048, 2048, 256, 0, Layout::Xyz).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
let path = plan
.tile_path(TileCoord::new(top.level, 3, 5), "png")
.unwrap();
assert_eq!(path, format!("{}/3/5.png", top.level));
}
#[test]
fn tile_path_out_of_bounds_returns_none() {
let planner = PyramidPlanner::new(256, 256, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
assert!(plan.tile_path(TileCoord::new(999, 0, 0), "png").is_none());
}
#[test]
fn dzi_manifest_structure() {
let planner = PyramidPlanner::new(1024, 768, 256, 1, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let manifest = plan.dzi_manifest("jpeg").unwrap();
assert!(manifest.contains("Format=\"jpeg\""));
assert!(manifest.contains("Overlap=\"1\""));
assert!(manifest.contains("TileSize=\"256\""));
assert!(manifest.contains("Width=\"1024\""));
assert!(manifest.contains("Height=\"768\""));
}
#[test]
fn dzi_manifest_returns_none_for_xyz() {
let planner = PyramidPlanner::new(256, 256, 256, 0, Layout::Xyz).unwrap();
let plan = planner.plan();
assert!(plan.dzi_manifest("png").is_none());
}
#[test]
fn plan_is_deterministic() {
let planner = PyramidPlanner::new(4000, 3000, 256, 1, Layout::DeepZoom).unwrap();
let plan_a = planner.plan();
let plan_b = planner.plan();
assert_eq!(plan_a, plan_b);
}
#[test]
fn large_image_level_count() {
let planner = PyramidPlanner::new(50_000, 50_000, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
assert!(plan.level_count() >= 16);
assert!(plan.level_count() <= 18);
let top = plan.levels.last().unwrap();
assert_eq!(top.width, 50_000);
assert_eq!(top.height, 50_000);
}
#[test]
fn different_tile_sizes() {
for tile_size in [64, 128, 256, 512, 1024] {
let planner = PyramidPlanner::new(2048, 1536, tile_size, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.cols, ceil_div(2048, tile_size));
assert_eq!(top.rows, ceil_div(1536, tile_size));
}
}
#[test]
fn google_layout_single_tile() {
let planner = PyramidPlanner::new(100, 80, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
assert_eq!(plan.level_count(), 1);
assert_eq!(plan.levels[0].cols, 1);
assert_eq!(plan.levels[0].rows, 1);
assert_eq!(plan.canvas_width, 256);
assert_eq!(plan.canvas_height, 256);
}
#[test]
fn google_layout_level_count() {
let planner = PyramidPlanner::new(3300, 5024, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
assert_eq!(plan.level_count(), 6);
assert_eq!(plan.canvas_width, 8192);
assert_eq!(plan.canvas_height, 8192);
}
#[test]
fn google_layout_power_of_2_grids() {
let planner = PyramidPlanner::new(1000, 800, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
for (i, level) in plan.levels.iter().enumerate() {
let expected_grid = 1u32 << i;
assert_eq!(level.cols, expected_grid, "Level {} cols", i);
assert_eq!(level.rows, expected_grid, "Level {} rows", i);
}
}
#[test]
fn google_layout_path_format() {
let planner = PyramidPlanner::new(512, 512, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
let path = plan.tile_path(TileCoord::new(1, 1, 0), "png").unwrap();
assert_eq!(path, "1/0/1.png");
}
#[test]
fn google_layout_no_dzi() {
let planner = PyramidPlanner::new(512, 512, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
assert!(plan.dzi_manifest("png").is_none());
}
#[test]
fn google_layout_tile_rect_full_size() {
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
for coord in plan.tile_coords() {
let rect = plan.tile_rect(coord).unwrap();
assert_eq!(rect.width, 256, "Tile {:?} width", coord);
assert_eq!(rect.height, 256, "Tile {:?} height", coord);
}
}
#[test]
fn google_centre_offsets() {
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::Google)
.unwrap()
.with_centre(true);
let plan = planner.plan();
assert_eq!(plan.canvas_width, 512);
assert_eq!(plan.canvas_height, 512);
assert_eq!(plan.centre_offset_x, (512 - 500) / 2); assert_eq!(plan.centre_offset_y, (512 - 300) / 2); }
#[test]
fn google_no_centre_offsets_zero() {
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
assert_eq!(plan.centre_offset_x, 0);
assert_eq!(plan.centre_offset_y, 0);
assert!(!plan.centre);
}
#[test]
fn google_layout_image_dims_halve() {
let planner = PyramidPlanner::new(1000, 800, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.width, 1000);
assert_eq!(top.height, 800);
for i in (1..plan.levels.len()).rev() {
let upper = &plan.levels[i];
let lower = &plan.levels[i - 1];
assert_eq!(lower.width, ceil_div(upper.width, 2));
assert_eq!(lower.height, ceil_div(upper.height, 2));
}
}
#[test]
fn google_total_tile_count() {
let planner = PyramidPlanner::new(300, 300, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
assert_eq!(plan.level_count(), 2);
assert_eq!(plan.total_tile_count(), 1 + 4);
}
#[test]
fn centre_with_deep_zoom() {
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::DeepZoom)
.unwrap()
.with_centre(true);
let plan = planner.plan();
let top = plan.levels.last().unwrap();
assert_eq!(top.cols, 2);
assert_eq!(top.rows, 2);
assert_eq!(plan.canvas_width, 512);
assert_eq!(plan.canvas_height, 512);
assert_eq!(plan.centre_offset_x, 6);
assert_eq!(plan.centre_offset_y, 106);
}
#[test]
fn canvas_size_at_level_google() {
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
assert_eq!(plan.canvas_size_at_level(0), (256, 256));
assert_eq!(plan.canvas_size_at_level(1), (512, 512));
}
#[test]
fn centre_offset_at_level_scales() {
let planner = PyramidPlanner::new(500, 300, 128, 0, Layout::Google)
.unwrap()
.with_centre(true);
let plan = planner.plan();
assert_eq!(plan.level_count(), 3);
assert_eq!(plan.canvas_width, 512);
let (ox_top, oy_top) = plan.centre_offset_at_level(2);
assert_eq!(ox_top, 6);
assert_eq!(oy_top, 106);
let (ox1, oy1) = plan.centre_offset_at_level(1);
assert_eq!(ox1, 3);
assert_eq!(oy1, 53);
let (ox0, oy0) = plan.centre_offset_at_level(0);
assert_eq!(ox0, 1);
assert_eq!(oy0, 26);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn tile_coverage_no_overlap(
w in 1u32..2048,
h in 1u32..2048,
tile_size in 1u32..512,
) {
let planner = PyramidPlanner::new(w, h, tile_size, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
for level in &plan.levels {
let mut min_x_covered = level.width;
let mut min_y_covered = level.height;
let mut max_x_covered = 0u32;
let mut max_y_covered = 0u32;
let mut total_area = 0u64;
for row in 0..level.rows {
for col in 0..level.cols {
let r = plan.tile_rect(TileCoord::new(level.level, col, row)).unwrap();
min_x_covered = min_x_covered.min(r.x);
min_y_covered = min_y_covered.min(r.y);
max_x_covered = max_x_covered.max(r.x + r.width);
max_y_covered = max_y_covered.max(r.y + r.height);
total_area += r.width as u64 * r.height as u64;
}
}
prop_assert_eq!(min_x_covered, 0, "Level {} x gap at start", level.level);
prop_assert_eq!(min_y_covered, 0, "Level {} y gap at start", level.level);
prop_assert_eq!(max_x_covered, level.width, "Level {} x short", level.level);
prop_assert_eq!(max_y_covered, level.height, "Level {} y short", level.level);
prop_assert_eq!(
total_area,
level.width as u64 * level.height as u64,
"Level {} area mismatch (overlap/gap)",
level.level,
);
}
}
#[test]
fn level_halving_invariant(w in 2u32..10000, h in 2u32..10000) {
let planner = PyramidPlanner::new(w, h, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
for i in 1..plan.levels.len() {
let upper = &plan.levels[i];
let lower = &plan.levels[i - 1];
prop_assert_eq!(lower.width, ceil_div(upper.width, 2));
prop_assert_eq!(lower.height, ceil_div(upper.height, 2));
}
prop_assert_eq!(plan.levels[0].width, 1);
prop_assert_eq!(plan.levels[0].height, 1);
let top = plan.levels.last().unwrap();
prop_assert_eq!(top.width, w);
prop_assert_eq!(top.height, h);
}
#[test]
fn plan_determinism(
w in 1u32..5000,
h in 1u32..5000,
tile_size in 1u32..512,
) {
let planner = PyramidPlanner::new(w, h, tile_size, 0, Layout::DeepZoom).unwrap();
let a = planner.plan();
let b = planner.plan();
prop_assert_eq!(a, b);
}
}
}