use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MapRendererConfig {
pub width: u32,
pub height: u32,
pub background_color: [u8; 4],
pub route_color: [u8; 4],
pub route_width: f32,
pub node_color: [u8; 4],
pub node_radius: f32,
pub padding: f32,
}
impl Default for MapRendererConfig {
fn default() -> Self {
Self {
width: 1920,
height: 1080,
background_color: [255, 255, 255, 255], route_color: [66, 133, 244, 255], route_width: 3.0,
node_color: [234, 67, 53, 255], node_radius: 5.0,
padding: 50.0,
}
}
}
pub struct MapRenderer {
#[allow(dead_code)]
config: MapRendererConfig,
}
impl MapRenderer {
#[must_use]
pub fn new() -> Self {
Self {
config: MapRendererConfig::default(),
}
}
#[must_use]
pub fn with_config(config: MapRendererConfig) -> Self {
Self { config }
}
#[allow(clippy::unused_self)]
pub fn render_route_to_png<P: AsRef<Path>>(
&self,
route: &[(f64, f64)],
_output_path: P,
) -> Result<()> {
if route.is_empty() {
anyhow::bail!("Cannot render empty route");
}
anyhow::bail!(
"Map rendering requires tiny-skia crate. \
Vendor it with: cd vendor && cargo vendor-tiny-skia"
);
}
#[allow(dead_code)]
#[must_use]
pub fn compute_bbox(route: &[(f64, f64)]) -> (f64, f64, f64, f64) {
let mut min_lon = f64::INFINITY;
let mut min_lat = f64::INFINITY;
let mut max_lon = f64::NEG_INFINITY;
let mut max_lat = f64::NEG_INFINITY;
for &(lon, lat) in route {
min_lon = min_lon.min(lon);
min_lat = min_lat.min(lat);
max_lon = max_lon.max(lon);
max_lat = max_lat.max(lat);
}
let lon_pad = (max_lon - min_lon).max(0.001) * 0.1;
let lat_pad = (max_lat - min_lat).max(0.001) * 0.1;
(
min_lon - lon_pad,
min_lat - lat_pad,
max_lon + lon_pad,
max_lat + lat_pad,
)
}
#[allow(dead_code)]
#[must_use]
pub fn config(&self) -> &MapRendererConfig {
&self.config
}
}
impl Default for MapRenderer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_renderer_creation() {
let renderer = MapRenderer::new();
assert_eq!(renderer.config.width, 1920);
assert_eq!(renderer.config.height, 1080);
}
#[test]
fn test_renderer_custom_config() {
let config = MapRendererConfig {
width: 800,
height: 600,
..Default::default()
};
let renderer = MapRenderer::with_config(config);
assert_eq!(renderer.config.width, 800);
assert_eq!(renderer.config.height, 600);
}
#[test]
fn test_compute_bbox() {
let renderer = MapRenderer::new();
let route = vec![
(-73.5, 45.5),
(-73.6, 45.6),
(-73.7, 45.7),
];
let (min_lon, min_lat, max_lon, max_lat) = MapRenderer::compute_bbox(&route);
assert!((min_lon + 73.72).abs() < 0.01);
assert!((min_lat - 45.48).abs() < 0.01);
assert!((max_lon + 73.48).abs() < 0.01);
assert!((max_lat - 45.72).abs() < 0.01);
}
#[test]
fn test_empty_route_fails() {
let renderer = MapRenderer::new();
let route: Vec<(f64, f64)> = vec![];
assert!(renderer.render_route_to_png(&route, "/tmp/test.png").is_err());
}
}