staticmap/
map.rs

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
10/// Main type.
11/// Use [StaticMapBuilder][StaticMapBuilder] as an entrypoint.
12///
13/// ## Example
14/// ```rust
15/// use staticmap::StaticMapBuilder;
16///
17/// let mut map = StaticMapBuilder::new()
18///     .width(300)
19///     .height(300)
20///     .zoom(4)
21///     .lat_center(52.6)
22///     .lon_center(13.4)
23///     .build()
24///     .unwrap();
25///
26/// ```
27pub struct StaticMap {
28    url_template: String,
29    tools: Vec<Box<dyn Tool>>,
30    bounds: BoundsBuilder,
31}
32
33/// Builder for [StaticMap][StaticMap].
34pub 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    /// Create a new builder with defaults.
62    pub fn new() -> Self {
63        Default::default()
64    }
65
66    /// Image width, in pixels.
67    /// Default is 300.
68    pub fn width(mut self, width: u32) -> Self {
69        self.width = width;
70        self
71    }
72
73    /// Image height, in pixels.
74    /// Default is 300.
75    pub fn height(mut self, height: u32) -> Self {
76        self.height = height;
77        self
78    }
79
80    /// Padding between map features and edge of map in x and y direction.
81    /// Default is (0, 0).
82    pub fn padding(mut self, padding: (u32, u32)) -> Self {
83        self.padding = padding;
84        self
85    }
86
87    /// Map zoom, usually between 1-17.
88    /// Determined based on map features if not specified.
89    pub fn zoom(mut self, zoom: u8) -> Self {
90        self.zoom = Some(zoom);
91        self
92    }
93
94    /// Latitude center of the map.
95    /// Determined based on map features if not specified.
96    pub fn lat_center(mut self, coordinate: f64) -> Self {
97        self.lat_center = Some(coordinate);
98        self
99    }
100
101    /// Longitude center of the map.
102    /// Determined based on map features if not specified.
103    pub fn lon_center(mut self, coordinate: f64) -> Self {
104        self.lon_center = Some(coordinate);
105        self
106    }
107
108    /// URL template, e.g. "https://example.com/{z}/{x}/{y}.png".
109    /// Default is "https://a.tile.osm.org/{z}/{x}/{y}.png".
110    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    /// Tile size, in pixels.
116    /// Default is 256.
117    pub fn tile_size(mut self, tile_size: u32) -> Self {
118        self.tile_size = tile_size;
119        self
120    }
121
122    /// Consumes the builder.
123    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    /// Add a type implementing [Tool][Tool]. The map can contain several tools.
143    pub fn add_tool(&mut self, tool: impl Tool + 'static) {
144        self.tools.push(Box::new(tool));
145    }
146
147    /// Render the map and encode as PNG.
148    ///
149    /// May panic if any feature has invalid bounds.
150    pub fn encode_png(&mut self) -> Result<Vec<u8>> {
151        Ok(self.render()?.encode_png()?)
152    }
153
154    /// Render the map and save as PNG to a file.
155    ///
156    /// May panic if any feature has invalid bounds.
157    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}