use crate::{
bounds::{Bounds, BoundsBuilder},
tools::Tool,
Error, Result,
};
use attohttpc::{Method, RequestBuilder, Response};
use rayon::prelude::*;
use tiny_skia::{Pixmap, PixmapMut, PixmapPaint, Transform};
pub struct StaticMap {
url_template: String,
tools: Vec<Box<dyn Tool>>,
bounds: BoundsBuilder,
}
pub struct StaticMapBuilder {
width: u32,
height: u32,
padding: (u32, u32),
zoom: Option<u8>,
lat_center: Option<f64>,
lon_center: Option<f64>,
url_template: String,
tile_size: u32,
}
impl Default for StaticMapBuilder {
fn default() -> Self {
Self {
width: 300,
height: 300,
padding: (0, 0),
zoom: None,
lat_center: None,
lon_center: None,
url_template: "https://a.tile.osm.org/{z}/{x}/{y}.png".to_string(),
tile_size: 256,
}
}
}
impl StaticMapBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn width(mut self, width: u32) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: u32) -> Self {
self.height = height;
self
}
pub fn padding(mut self, padding: (u32, u32)) -> Self {
self.padding = padding;
self
}
pub fn zoom(mut self, zoom: u8) -> Self {
self.zoom = Some(zoom);
self
}
pub fn lat_center(mut self, coordinate: f64) -> Self {
self.lat_center = Some(coordinate);
self
}
pub fn lon_center(mut self, coordinate: f64) -> Self {
self.lon_center = Some(coordinate);
self
}
pub fn url_template<I: Into<String>>(mut self, url_template: I) -> Self {
self.url_template = url_template.into();
self
}
pub fn tile_size(mut self, tile_size: u32) -> Self {
self.tile_size = tile_size;
self
}
pub fn build(self) -> Result<StaticMap> {
let bounds = BoundsBuilder::new()
.zoom(self.zoom)
.tile_size(self.tile_size)
.lon_center(self.lon_center)
.lat_center(self.lat_center)
.padding(self.padding)
.height(self.height)
.width(self.width);
Ok(StaticMap {
url_template: self.url_template,
tools: Vec::new(),
bounds,
})
}
}
impl StaticMap {
pub fn add_tool(&mut self, tool: impl Tool + 'static) {
self.tools.push(Box::new(tool));
}
pub fn encode_png(&mut self) -> Result<Vec<u8>> {
Ok(self.render()?.encode_png()?)
}
pub fn save_png<P: AsRef<::std::path::Path>>(&mut self, path: P) -> Result<()> {
self.render()?.save_png(path)?;
Ok(())
}
fn render(&mut self) -> Result<Pixmap> {
let bounds = self.bounds.build(&self.tools);
let mut image = Pixmap::new(bounds.width, bounds.height).ok_or(Error::InvalidSize)?;
self.draw_base_layer(image.as_mut(), &bounds)?;
for tool in self.tools.iter() {
tool.draw(&bounds, image.as_mut());
}
Ok(image)
}
fn draw_base_layer(&self, mut image: PixmapMut, bounds: &Bounds) -> Result<()> {
let max_tile: i32 = 2_i32.pow(bounds.zoom.into());
let tiles: Vec<(i32, i32, String)> = (bounds.x_min..bounds.x_max)
.map(|x| (x, bounds.y_min..bounds.y_max))
.flat_map(|(x, y_r)| {
y_r.map(move |y| {
let tile_x = (x + max_tile) % max_tile;
let tile_y = (y + max_tile) % max_tile;
(
x,
y,
self.url_template
.replace("{z}", &bounds.zoom.to_string())
.replace("{x}", &tile_x.to_string())
.replace("{y}", &tile_y.to_string()),
)
})
})
.collect();
let tile_images: Vec<_> = tiles
.par_iter()
.map(|x| {
RequestBuilder::try_new(Method::GET, &x.2)
.and_then(RequestBuilder::send)
.and_then(Response::bytes)
.map_err(|error| Error::TileError {
error,
url: x.2.clone(),
})
})
.collect();
for (tile, tile_image) in tiles.iter().zip(tile_images) {
let (x, y) = (tile.0, tile.1);
let (x_px, y_px) = (bounds.x_to_px(x.into()), bounds.y_to_px(y.into()));
let pixmap = Pixmap::decode_png(&tile_image?)?;
image.draw_pixmap(
x_px as i32,
y_px as i32,
pixmap.as_ref(),
&PixmapPaint::default(),
Transform::default(),
None,
);
}
Ok(())
}
}