use std::collections::HashSet;
use lazy_static::lazy_static;
use regex::Regex;
use crate::dezoomer::{Dezoomer, DezoomerError, DezoomerInput, single_level, TileFetchResult, TileProvider, TileReference, ZoomLevels};
use crate::Vec2d;
mod dichotomy_2d;
#[derive(Default)]
pub struct GenericDezoomer;
impl Dezoomer for GenericDezoomer {
fn name(&self) -> &'static str {
"generic"
}
fn zoom_levels(&mut self, data: &DezoomerInput) -> Result<ZoomLevels, DezoomerError> {
self.assert(TEMPLATE_RE.is_match(&data.uri))?;
let dezoomer = ZoomLevel {
url_template: data.uri.clone(),
dichotomy: Default::default(),
last_tile: (0, 0),
done: HashSet::new(),
tile_size: None,
image_size: None,
};
single_level(dezoomer)
}
}
lazy_static! {
static ref TEMPLATE_RE: Regex = Regex::new(r"(?xi)
\{\{
(?P<dimension>x|y)
(?::0(?P<zeroes>\d+))?
\}\}
").unwrap();
}
struct ZoomLevel {
url_template: String,
dichotomy: dichotomy_2d::Dichotomy2d,
last_tile: (u32, u32),
tile_size: Option<Vec2d>,
image_size: Option<Vec2d>,
done: HashSet<(u32, u32)>,
}
impl ZoomLevel {
fn tile_url_at(&self, x: u32, y: u32) -> String {
TEMPLATE_RE.replace_all(&self.url_template, |caps: ®ex::Captures| {
let dimension = caps.name("dimension")
.expect("missing dimension")
.as_str()
.chars().next().expect("empty dim")
.to_ascii_lowercase();
let num = match dimension {
'x' => x,
'y' => y,
_ => unreachable!("The dimension is either x or y")
};
let padding: usize = caps.name("zeroes")
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0);
format!("{num:0padding$}", num = num, padding = padding)
}).to_string()
}
fn tile_ref_at(&self, x: u32, y: u32) -> TileReference {
let tile_size = self.tile_size.unwrap_or(Vec2d { x: 0, y: 0 });
let position = Vec2d { x, y } * tile_size;
TileReference {
url: self.tile_url_at(x, y),
position,
}
}
}
impl TileProvider for ZoomLevel {
fn next_tiles(&mut self, previous: Option<TileFetchResult>) -> Vec<TileReference> {
if let Some(p) = previous {
self.tile_size = self.tile_size.or(p.tile_size);
if let Some((x, y)) = self.dichotomy.next(p.is_success()) {
self.last_tile = (x, y);
self.done.insert((x, y));
vec![self.tile_ref_at(x, y)]
} else if !self.done.is_empty() {
let last_tile_pos = Vec2d {
x: self.last_tile.0,
y: self.last_tile.1,
};
self.image_size = self.tile_size.map(|s| s * last_tile_pos + s);
let all_tiles = (0..=last_tile_pos.y).flat_map(|y|
(0..=last_tile_pos.x).map(move |x|
(x, y)))
.filter(|pos| !self.done.contains(pos))
.map(|(x, y)| self.tile_ref_at(x, y))
.collect();
self.done.clear();
all_tiles
} else {
vec![]
}
} else {
vec![self.tile_ref_at(self.last_tile.0, self.last_tile.1)]
}
}
fn name(&self) -> String {
format!("Generic image with template {}", self.url_template)
}
fn size_hint(&self) -> Option<Vec2d> {
self.image_size
}
}
impl std::fmt::Debug for ZoomLevel {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Generic level")
}
}
#[test]
fn test_generic_dezoomer() {
use std::collections::HashSet;
use crate::dezoomer::PageContents;
let uri = "{{X}},{{Y}}".to_string();
let mut lvl = GenericDezoomer {}
.zoom_levels(&DezoomerInput {
uri,
contents: PageContents::Unknown,
})
.unwrap()
.into_iter()
.next()
.unwrap();
let existing_tiles = vec!["0,0", "1,0", "2,0", "0,1", "1,1", "2,1"];
let mut all_tiles = HashSet::new();
let mut zoom_level_iter = crate::dezoomer::ZoomLevelIter::new(&mut lvl);
let mut tries = 0;
while let Some(tiles) = zoom_level_iter.next_tile_references() {
let count = tiles.len() as u64;
let successes: Vec<_> = tiles
.into_iter()
.filter(|t| existing_tiles.contains(&t.url.as_str()))
.collect();
zoom_level_iter.set_fetch_result(TileFetchResult {
count,
successes: successes.len() as u64,
tile_size: Some(Vec2d { x: 4, y: 5 }),
});
all_tiles.extend(successes);
tries += 1;
assert!(tries <= 10);
};
let expected: HashSet<TileReference> = vec![
TileReference {
url: "0,0".into(),
position: Vec2d { x: 0, y: 0 },
},
TileReference {
url: "1,0".into(),
position: Vec2d { x: 4, y: 0 },
},
TileReference {
url: "2,0".into(),
position: Vec2d { x: 8, y: 0 },
},
TileReference {
url: "0,1".into(),
position: Vec2d { x: 0, y: 5 },
},
TileReference {
url: "1,1".into(),
position: Vec2d { x: 4, y: 5 },
},
TileReference {
url: "2,1".into(),
position: Vec2d { x: 8, y: 5 },
},
].into_iter().collect();
assert_eq!(all_tiles, expected);
}
#[test]
fn test_url_templating() {
let url_template = "http://x.com/{{x:05}}_{{y}}".to_string();
let lvl: ZoomLevel = ZoomLevel {
url_template,
dichotomy: Default::default(),
last_tile: (0, 0),
tile_size: None,
image_size: None,
done: Default::default(),
};
assert_eq!(lvl.tile_url_at(10, 11), "http://x.com/00010_11");
assert_eq!(lvl.tile_url_at(123, 1), "http://x.com/00123_1");
}