1use crate::{
2 bounds::{Bounds, BoundsBuilder},
3 tools::Tool,
4 Error, Result,
5};
6use attohttpc::{Method, RequestBuilder, Response};
7use rayon::prelude::*;
8use tiny_skia::{Pixmap, PixmapMut, PixmapPaint, Transform};
9
10pub struct StaticMap {
28 url_template: String,
29 tools: Vec<Box<dyn Tool>>,
30 bounds: BoundsBuilder,
31}
32
33pub struct StaticMapBuilder {
35 width: u32,
36 height: u32,
37 padding: (u32, u32),
38 zoom: Option<u8>,
39 lat_center: Option<f64>,
40 lon_center: Option<f64>,
41 url_template: String,
42 tile_size: u32,
43}
44
45impl Default for StaticMapBuilder {
46 fn default() -> Self {
47 Self {
48 width: 300,
49 height: 300,
50 padding: (0, 0),
51 zoom: None,
52 lat_center: None,
53 lon_center: None,
54 url_template: "https://a.tile.osm.org/{z}/{x}/{y}.png".to_string(),
55 tile_size: 256,
56 }
57 }
58}
59
60impl StaticMapBuilder {
61 pub fn new() -> Self {
63 Default::default()
64 }
65
66 pub fn width(mut self, width: u32) -> Self {
69 self.width = width;
70 self
71 }
72
73 pub fn height(mut self, height: u32) -> Self {
76 self.height = height;
77 self
78 }
79
80 pub fn padding(mut self, padding: (u32, u32)) -> Self {
83 self.padding = padding;
84 self
85 }
86
87 pub fn zoom(mut self, zoom: u8) -> Self {
90 self.zoom = Some(zoom);
91 self
92 }
93
94 pub fn lat_center(mut self, coordinate: f64) -> Self {
97 self.lat_center = Some(coordinate);
98 self
99 }
100
101 pub fn lon_center(mut self, coordinate: f64) -> Self {
104 self.lon_center = Some(coordinate);
105 self
106 }
107
108 pub fn url_template<I: Into<String>>(mut self, url_template: I) -> Self {
111 self.url_template = url_template.into();
112 self
113 }
114
115 pub fn tile_size(mut self, tile_size: u32) -> Self {
118 self.tile_size = tile_size;
119 self
120 }
121
122 pub fn build(self) -> Result<StaticMap> {
124 let bounds = BoundsBuilder::new()
125 .zoom(self.zoom)
126 .tile_size(self.tile_size)
127 .lon_center(self.lon_center)
128 .lat_center(self.lat_center)
129 .padding(self.padding)
130 .height(self.height)
131 .width(self.width);
132
133 Ok(StaticMap {
134 url_template: self.url_template,
135 tools: Vec::new(),
136 bounds,
137 })
138 }
139}
140
141impl StaticMap {
142 pub fn add_tool(&mut self, tool: impl Tool + 'static) {
144 self.tools.push(Box::new(tool));
145 }
146
147 pub fn encode_png(&mut self) -> Result<Vec<u8>> {
151 Ok(self.render()?.encode_png()?)
152 }
153
154 pub fn save_png<P: AsRef<::std::path::Path>>(&mut self, path: P) -> Result<()> {
158 self.render()?.save_png(path)?;
159 Ok(())
160 }
161
162 fn render(&mut self) -> Result<Pixmap> {
163 let bounds = self.bounds.build(&self.tools);
164
165 let mut image = Pixmap::new(bounds.width, bounds.height).ok_or(Error::InvalidSize)?;
166
167 self.draw_base_layer(image.as_mut(), &bounds)?;
168
169 for tool in self.tools.iter() {
170 tool.draw(&bounds, image.as_mut());
171 }
172
173 Ok(image)
174 }
175
176 fn draw_base_layer(&self, mut image: PixmapMut, bounds: &Bounds) -> Result<()> {
177 let max_tile: i32 = 2_i32.pow(bounds.zoom.into());
178
179 let tiles: Vec<(i32, i32, String)> = (bounds.x_min..bounds.x_max)
180 .map(|x| (x, bounds.y_min..bounds.y_max))
181 .flat_map(|(x, y_r)| {
182 y_r.map(move |y| {
183 let tile_x = (x + max_tile) % max_tile;
184 let tile_y = (y + max_tile) % max_tile;
185
186 (
187 x,
188 y,
189 self.url_template
190 .replace("{z}", &bounds.zoom.to_string())
191 .replace("{x}", &tile_x.to_string())
192 .replace("{y}", &tile_y.to_string()),
193 )
194 })
195 })
196 .collect();
197
198 let tile_images: Vec<_> = tiles
199 .par_iter()
200 .map(|x| {
201 RequestBuilder::try_new(Method::GET, &x.2)
202 .and_then(RequestBuilder::send)
203 .and_then(Response::bytes)
204 .map_err(|error| Error::TileError {
205 error,
206 url: x.2.clone(),
207 })
208 })
209 .collect();
210
211 for (tile, tile_image) in tiles.iter().zip(tile_images) {
212 let (x, y) = (tile.0, tile.1);
213 let (x_px, y_px) = (bounds.x_to_px(x.into()), bounds.y_to_px(y.into()));
214
215 let pixmap = Pixmap::decode_png(&tile_image?)?;
216
217 image.draw_pixmap(
218 x_px as i32,
219 y_px as i32,
220 pixmap.as_ref(),
221 &PixmapPaint::default(),
222 Transform::default(),
223 None,
224 );
225 }
226
227 Ok(())
228 }
229}