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}