use crate::config::Config;
use crate::map::{MapRenderer, MapRendererConfig};
use anyhow::{Context, Result};
use clap::Args;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Args)]
pub struct RenderMapArgs {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long, default_value = "1920")]
width: u32,
#[arg(long, default_value = "1080")]
height: u32,
#[arg(long, default_value = "#4285F4")]
route_color: String,
#[arg(long, default_value = "3")]
route_width: f32,
#[arg(long, default_value = "#FFFFFF")]
background: String,
}
pub fn run(args: &RenderMapArgs, _config: &Config) -> Result<()> {
let content = std::fs::read_to_string(&args.input)
.context("Failed to read input file")?;
let route = parse_route(&content)?;
let config = MapRendererConfig {
width: args.width,
height: args.height,
background_color: parse_color(&args.background)?,
route_color: parse_color(&args.route_color)?,
route_width: args.route_width,
..Default::default()
};
let renderer = MapRenderer::with_config(config);
renderer
.render_route_to_png(&route, &args.output)
.context("Failed to render map")?;
eprintln!("Rendered route to: {}", args.output.display());
Ok(())
}
fn parse_route(content: &str) -> Result<Vec<(f64, f64)>> {
#[derive(Deserialize)]
struct SimpleRoute {
route: Vec<[f64; 2]>,
}
if let Ok(simple) = serde_json::from_str::<SimpleRoute>(content) {
return Ok(simple.route.iter().map(|p| (p[0], p[1])).collect());
}
if let Ok(geojson) = serde_json::from_str::<geojson::GeoJson>(content) {
return parse_geojson_route(&geojson);
}
anyhow::bail!("Input must be JSON with 'route' array or valid GeoJSON");
}
fn parse_geojson_route(geojson: &geojson::GeoJson) -> Result<Vec<(f64, f64)>> {
match geojson {
geojson::GeoJson::FeatureCollection(fc) => {
let mut route = Vec::new();
for feature in &fc.features {
if let Some(ref geom) = feature.geometry {
if let geojson::Value::LineString(coords) = &geom.value {
for coord in coords {
route.push((coord[0], coord[1]));
}
}
}
}
if route.is_empty() {
anyhow::bail!("No LineString geometries found in GeoJSON");
}
Ok(route)
}
geojson::GeoJson::Feature(feature) => {
if let Some(ref geom) = feature.geometry {
if let geojson::Value::LineString(coords) = &geom.value {
return Ok(coords.iter().map(|c| (c[0], c[1])).collect());
}
}
anyhow::bail!("Feature does not contain a LineString geometry");
}
geojson::GeoJson::Geometry(geom) => {
if let geojson::Value::LineString(coords) = &geom.value {
return Ok(coords.iter().map(|c| (c[0], c[1])).collect());
}
anyhow::bail!("Geometry is not a LineString");
}
}
}
fn parse_color(color: &str) -> Result<[u8; 4]> {
let color = color.trim_start_matches('#');
match color.len() {
6 => {
let r = u8::from_str_radix(&color[0..2], 16)
.context("Invalid red component in color")?;
let g = u8::from_str_radix(&color[2..4], 16)
.context("Invalid green component in color")?;
let b = u8::from_str_radix(&color[4..6], 16)
.context("Invalid blue component in color")?;
Ok([r, g, b, 255])
}
8 => {
let r = u8::from_str_radix(&color[0..2], 16)
.context("Invalid red component in color")?;
let g = u8::from_str_radix(&color[2..4], 16)
.context("Invalid green component in color")?;
let b = u8::from_str_radix(&color[4..6], 16)
.context("Invalid blue component in color")?;
let a = u8::from_str_radix(&color[6..8], 16)
.context("Invalid alpha component in color")?;
Ok([r, g, b, a])
}
_ => anyhow::bail!("Color must be 6 or 8 hex digits (RRGGBB or RRGGBBAA)"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_color_hex() {
assert_eq!(parse_color("#FF0000").unwrap(), [255, 0, 0, 255]);
assert_eq!(parse_color("#00FF00").unwrap(), [0, 255, 0, 255]);
assert_eq!(parse_color("#0000FF").unwrap(), [0, 0, 255, 255]);
assert_eq!(parse_color("#FF000080").unwrap(), [255, 0, 0, 128]);
}
#[test]
fn test_parse_simple_json_route() {
let json = r#"{"route": [[-73.5, 45.5], [-73.6, 45.6]]}"#;
let route = parse_route(json).unwrap();
assert_eq!(route.len(), 2);
assert_eq!(route[0], (-73.5, 45.5));
}
#[test]
fn test_parse_geojson_linestring() {
let geojson = r#"{"type":"LineString","coordinates":[[-73.5,45.5],[-73.6,45.6]]}"#;
let route = parse_route(geojson).unwrap();
assert_eq!(route.len(), 2);
}
}