maplibre_legend/lib.rs
1// Modules of the crate containing specific logic for rendering different types of layers.
2mod circle;
3mod common;
4mod default;
5mod error;
6mod fill;
7mod heatmap;
8mod line;
9mod raster;
10mod symbol;
11
12#[cfg(all(feature = "async", feature = "sync"))]
13compile_error!("Features 'async' and 'sync' cannot be enabled at the same time.");
14
15// Imports of required functions and types from the modules.
16use circle::render_circle;
17// #[cfg(feature = "async")]
18// use crate::common::get_sprite_async;
19// #[cfg(feature = "sync")]
20// use crate::common::get_sprite_sync;
21use crate::common::get_sprite;
22use common::{Layer, Style};
23use default::render_default;
24pub use error::LegendError;
25use fill::render_fill;
26use heatmap::render_heatmap;
27use image::DynamicImage;
28use line::render_line;
29use raster::render_raster;
30use serde_json::Value;
31use symbol::render_symbol;
32
33/// Structure representing a MapLibre legend, used to render SVG representations
34/// of style layers based on a JSON specification.
35pub struct MapLibreLegend {
36 /// The style of the legend, deserialized from JSON.
37 style: Style,
38 /// Default width for SVG renderings.
39 pub default_width: u32,
40 /// Default height for SVG renderings.
41 pub default_height: u32,
42 /// Indicates whether labels should be rendered on layers.
43 pub has_label: bool,
44 /// Indicates whether raster layers should be included in rendering.
45 pub include_raster: bool,
46 /// Optional sprite data used for rendering symbol layers, containing the sprite image (PNG)
47 /// and its associated JSON metadata. Populated during initialization if `style.sprite`
48 /// specifies a valid URL; otherwise, `None`. The sprite data is loaded once to optimize
49 /// rendering of multiple symbol layers.
50 sprite_data: Option<(DynamicImage, Value)>,
51}
52
53impl MapLibreLegend {
54 /// Creates a new `MapLibreLegend` instance from a JSON string and configuration parameters.
55 ///
56 /// # Parameters
57 /// - `json`: A string containing the style in JSON format.
58 /// - `default_width`: Default width for SVG renderings.
59 /// - `default_height`: Default height for SVG renderings.
60 /// - `has_label`: Whether to render labels on layers.
61 /// - `include_raster`: Whether to include raster layers in rendering.
62 ///
63 /// # Returns
64 /// - `Result<Self, LegendError>`: A `MapLibreLegend` instance if the JSON is valid,
65 /// or a `LegendError::Deserialization` if it is not.
66 #[cfg(feature = "async")]
67 pub async fn new(
68 json: &str,
69 default_width: u32,
70 default_height: u32,
71 has_label: bool,
72 include_raster: bool,
73 ) -> Result<Self, LegendError> {
74 let style: Style = serde_json::from_str(json).map_err(LegendError::Deserialization)?;
75 let sprite_data = if let Some(sprite_url) = &style.sprite {
76 Some(get_sprite(sprite_url).await?)
77 } else {
78 None
79 };
80 Ok(Self {
81 style,
82 default_width,
83 default_height,
84 has_label,
85 include_raster,
86 sprite_data,
87 })
88 }
89
90 /// Creates a new `MapLibreLegend` instance from a JSON string and configuration parameters.
91 ///
92 /// # Parameters
93 /// - `json`: A string containing the style in JSON format.
94 /// - `default_width`: Default width for SVG renderings.
95 /// - `default_height`: Default height for SVG renderings.
96 /// - `has_label`: Whether to render labels on layers.
97 /// - `include_raster`: Whether to include raster layers in rendering.
98 ///
99 /// # Returns
100 /// - `Result<Self, LegendError>`: A `MapLibreLegend` instance if the JSON is valid,
101 /// or a `LegendError::Deserialization` if it is not.
102 #[cfg(feature = "sync")]
103 pub fn new(
104 json: &str,
105 default_width: u32,
106 default_height: u32,
107 has_label: bool,
108 include_raster: bool,
109 ) -> Result<Self, LegendError> {
110 let style: Style = serde_json::from_str(json).map_err(LegendError::Deserialization)?;
111 let sprite_data = if let Some(sprite_url) = &style.sprite {
112 Some(get_sprite(sprite_url)?)
113 } else {
114 None
115 };
116 Ok(Self {
117 style,
118 default_width,
119 default_height,
120 has_label,
121 include_raster,
122 sprite_data,
123 })
124 }
125
126 /// Renders a specific layer as an SVG string, identified by its ID.
127 ///
128 /// # Parameters
129 /// - `id`: The identifier of the layer to render.
130 /// - `has_label`: An optional boolean indicating whether to render a label for the layer.
131 /// If `Some(true)` or `Some(false)`, uses the specified value; if `None`, falls back to
132 /// the default `self.has_label` value.
133 ///
134 /// # Returns
135 /// - `Result<String, LegendError>`: A string containing the SVG representation of the layer if found and
136 /// renderable, or a `LegendError` if the layer does not exist or cannot be rendered.
137 pub fn render_layer(&self, id: &str, has_label: Option<bool>) -> Result<String, LegendError> {
138 let layer = self
139 .style
140 .layers
141 .iter()
142 .find(|l| l.id == id)
143 .ok_or_else(|| LegendError::InvalidJson(format!("Layer with ID '{}' not found", id)))?;
144 let (svg, _, _) = render_layer_svg(
145 layer,
146 self.default_width,
147 self.default_height,
148 has_label.unwrap_or(self.has_label),
149 self.include_raster,
150 &self.sprite_data,
151 )?;
152 Ok(svg)
153 }
154
155 /// Renders all layers in the style as a single combined SVG.
156 ///
157 /// Layers are stacked vertically with separator lines between them. The resulting SVG
158 /// has a width equal to the maximum layer width and a height equal to the sum of layer heights.
159 ///
160 /// # Parameters
161 /// - `rev`: If true, renders layers in reverse order.
162 ///
163 /// # Returns
164 /// - `Result<String, LegendError>`: A string containing the combined SVG of all layers,
165 /// or a `LegendError` if any layer fails to render.
166 pub fn render_all(&self, rev: bool) -> Result<String, LegendError> {
167 let mut combined_body = String::new();
168 let mut y_offset = 0;
169 let mut max_width = 0;
170 let total_layers = self.style.layers.len();
171
172 // Create an iterator in normal or reversed order
173 let layer_iter: Box<dyn Iterator<Item = (usize, &Layer)>> = if rev {
174 Box::new(self.style.layers.iter().enumerate().rev())
175 } else {
176 Box::new(self.style.layers.iter().enumerate())
177 };
178
179 for (i, layer) in layer_iter {
180 let (svg, w, h) = render_layer_svg(
181 layer,
182 self.default_width,
183 self.default_height,
184 self.has_label,
185 self.include_raster,
186 &self.sprite_data,
187 )?;
188 let inner = svg
189 .lines()
190 .filter(|l| !l.contains("<svg") && !l.contains("</svg>"))
191 .collect::<Vec<_>>()
192 .join("\n");
193 max_width = max_width.max(w);
194 combined_body.push_str(&format!(
195 "<g transform='translate(0,{})'>{}\n</g>\n",
196 y_offset, inner
197 ));
198 let is_last = if rev { i == 0 } else { i == total_layers - 1 };
199 if !is_last {
200 combined_body.push_str(&format!(
201 "<line x1='0' y1='{}' x2='{}' y2='{}' stroke='#333333' stroke-width='0.5'/>\n",
202 y_offset + h,
203 max_width,
204 y_offset + h
205 ));
206 }
207 y_offset += h;
208 }
209
210 Ok(format!(
211 "<svg xmlns='http://www.w3.org/2000/svg' width='{w}' height='{h}' viewBox='0 0 {w} {h}'>\n{body}</svg>",
212 w = max_width,
213 h = y_offset,
214 body = combined_body
215 ))
216 }
217}
218
219/// Renders a single layer as an SVG based on its type and properties.
220///
221/// # Parameters
222/// - `layer`: The layer to render.
223/// - `def_w`: Default width for the SVG.
224/// - `def_h`: Default height for the SVG.
225/// - `render_label`: Whether to render labels for the layer.
226/// - `include_raster`: Whether to include raster layers in rendering.
227/// - `sprite_url`: Optional URL for sprite images used in symbol layers.
228///
229/// # Returns
230/// - `Result<(String, u32, u32), LegendError>`: A tuple containing the SVG string, width, and height
231/// if the layer is renderable, or a `LegendError` if it cannot be rendered.
232fn render_layer_svg(
233 layer: &Layer,
234 def_w: u32,
235 def_h: u32,
236 render_label: bool,
237 include_raster: bool,
238 sprite_data: &Option<(DynamicImage, Value)>,
239) -> Result<(String, u32, u32), LegendError> {
240 match layer.layer_type.as_str() {
241 "fill" | "line" | "circle" => {
242 let paint = layer
243 .paint
244 .as_ref()
245 .ok_or_else(|| {
246 LegendError::InvalidJson(format!(
247 "Missing the 'paint' field for layer '{}'",
248 layer.id
249 ))
250 })?
251 .as_object()
252 .ok_or_else(|| {
253 LegendError::InvalidJson(format!(
254 "The 'paint' field is not an object for layer '{}'",
255 layer.id
256 ))
257 })?;
258 match layer.layer_type.as_str() {
259 "fill" => render_fill(layer, paint, def_w, def_h, render_label),
260 "line" => render_line(layer, paint, def_w, def_h, render_label),
261 "circle" => render_circle(layer, paint, def_w, def_h, render_label),
262 _ => Err(LegendError::InvalidJson(format!(
263 "Unknown layer type '{}'",
264 layer.layer_type
265 ))),
266 }
267 }
268 "heatmap" => render_heatmap(layer, def_w, def_h, render_label),
269 "symbol" => render_symbol(layer, def_w, def_h, render_label, sprite_data.as_ref()),
270 "raster" if include_raster => render_raster(layer, def_w, def_h, render_label),
271 "raster" => Ok(("<svg></svg>".to_string(), 0, 0)),
272 _ => render_default(layer, def_w, def_h, render_label),
273 }
274}