use crate::error::{RenderError, Result};
use crate::projection::{lat_lon_to_web_mercator, tile_to_bounds, BoundingBox};
use crate::svg::{generate_svg_items, SvgContext};
use crate::types::{Settings, TileCoordinate};
use geojson::{Feature, FeatureCollection};
use rayon::prelude::*;
pub struct TileRenderer {
settings: Settings,
}
pub struct TileRendererBuilder {
settings: Option<Settings>,
}
impl Default for TileRendererBuilder {
fn default() -> Self {
Self { settings: None }
}
}
impl TileRendererBuilder {
pub fn settings(mut self, settings: Settings) -> Self {
self.settings = Some(settings);
self
}
pub fn build(self) -> Result<TileRenderer> {
let settings = self.settings.unwrap_or_default();
Ok(TileRenderer { settings })
}
}
impl TileRenderer {
pub fn new() -> Self {
Self {
settings: Settings::default(),
}
}
pub fn builder() -> TileRendererBuilder {
TileRendererBuilder::default()
}
pub fn render(&self, geojson: &FeatureCollection, tile: TileCoordinate) -> Result<Vec<u8>> {
self.render_internal(&geojson.features, tile)
}
pub fn render_feature(&self, feature: &Feature, tile: TileCoordinate) -> Result<Vec<u8>> {
self.render_internal(&[feature.clone()], tile)
}
pub fn render_many(&self, geojson: &FeatureCollection, tiles: &[TileCoordinate]) -> Result<Vec<Vec<u8>>> {
tiles
.par_iter()
.map(|&tile| self.render(geojson, tile))
.collect()
}
fn render_internal(&self, features: &[Feature], tile: TileCoordinate) -> Result<Vec<u8>> {
let size = self.settings.size as f64;
let image_polygon = tile_to_bounds(tile);
let mercator_bbox = {
let exterior = image_polygon.exterior();
let coords: Vec<_> = exterior.coords().collect();
let min_coord = lat_lon_to_web_mercator(coords[0].x, coords[0].y)?;
let max_coord = lat_lon_to_web_mercator(coords[2].x, coords[2].y)?;
BoundingBox::new(min_coord.x, min_coord.y, max_coord.x, max_coord.y)
};
let x_scaling_factor = size / mercator_bbox.width();
let y_scaling_factor = size / mercator_bbox.height();
let context = SvgContext::new(
mercator_bbox,
size,
x_scaling_factor,
y_scaling_factor,
image_polygon,
);
let svg_items = generate_svg_items(features, &context)?;
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">{}</svg>"#,
self.settings.size,
self.settings.size,
svg_items.join("")
);
self.svg_to_png(&svg)
}
fn svg_to_png(&self, svg: &str) -> Result<Vec<u8>> {
let opts = usvg::Options::default();
let tree = usvg::Tree::from_str(svg, &opts)
.map_err(|e| RenderError::SvgGeneration(format!("Failed to parse SVG: {}", e)))?;
let size = self.settings.size;
let mut pixmap = tiny_skia::Pixmap::new(size, size)
.ok_or_else(|| RenderError::ImageRendering("Failed to create pixmap".to_string()))?;
let bg = &self.settings.background_color;
let color = tiny_skia::Color::from_rgba8(bg.r, bg.g, bg.b, bg.a);
pixmap.fill(color);
resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
pixmap
.encode_png()
.map_err(|e| RenderError::ImageRendering(format!("Failed to encode PNG: {}", e)))
}
}
impl Default for TileRenderer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::BackgroundColor;
use serde_json::json;
#[test]
fn test_tile_renderer_new() {
let renderer = TileRenderer::new();
assert_eq!(renderer.settings.size, 256);
}
#[test]
fn test_tile_renderer_builder() {
let renderer = TileRenderer::builder()
.settings(
Settings::builder()
.size(512)
.background_color(BackgroundColor::white())
.build()
.unwrap(),
)
.build()
.unwrap();
assert_eq!(renderer.settings.size, 512);
assert_eq!(renderer.settings.background_color, BackgroundColor::white());
}
#[test]
fn test_render_empty_collection() {
let renderer = TileRenderer::new();
let collection = FeatureCollection {
bbox: None,
features: vec![],
foreign_members: None,
};
let tile = TileCoordinate::new(10, 163, 395).unwrap();
let result = renderer.render(&collection, tile);
assert!(result.is_ok());
let png_data = result.unwrap();
assert!(!png_data.is_empty());
assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
}
#[test]
fn test_render_polygon() {
let renderer = TileRenderer::new();
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-122.5, 37.7],
[-122.4, 37.7],
[-122.4, 37.8],
[-122.5, 37.8],
[-122.5, 37.7]
]]
},
"properties": {
"fill": "red",
"fill-opacity": "0.5"
}
}))
.unwrap();
let collection = FeatureCollection {
bbox: None,
features: vec![feature],
foreign_members: None,
};
let tile = TileCoordinate::new(10, 163, 395).unwrap();
let result = renderer.render(&collection, tile);
assert!(result.is_ok());
let png_data = result.unwrap();
assert!(!png_data.is_empty());
assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
}
#[test]
fn test_render_feature() {
let renderer = TileRenderer::new();
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-122.45, 37.75]
},
"properties": {
"text": "San Francisco"
}
}))
.unwrap();
let tile = TileCoordinate::new(10, 163, 395).unwrap();
let result = renderer.render_feature(&feature, tile);
assert!(result.is_ok());
let png_data = result.unwrap();
assert!(!png_data.is_empty());
}
#[test]
fn test_render_many() {
let renderer = TileRenderer::new();
let collection = FeatureCollection {
bbox: None,
features: vec![],
foreign_members: None,
};
let tiles = vec![
TileCoordinate::new(10, 163, 395).unwrap(),
TileCoordinate::new(10, 164, 395).unwrap(),
TileCoordinate::new(10, 163, 396).unwrap(),
];
let results = renderer.render_many(&collection, &tiles);
assert!(results.is_ok());
let png_buffers = results.unwrap();
assert_eq!(png_buffers.len(), 3);
for png_data in png_buffers {
assert!(!png_data.is_empty());
assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
}
}
#[test]
fn test_render_with_background_color() {
let renderer = TileRenderer::builder()
.settings(
Settings::builder()
.background_color(BackgroundColor::rgb(255, 0, 0))
.build()
.unwrap(),
)
.build()
.unwrap();
let collection = FeatureCollection {
bbox: None,
features: vec![],
foreign_members: None,
};
let tile = TileCoordinate::new(0, 0, 0).unwrap();
let result = renderer.render(&collection, tile);
assert!(result.is_ok());
let png_data = result.unwrap();
assert!(!png_data.is_empty());
}
#[test]
fn test_render_different_tile_sizes() {
for size in [128, 256, 512] {
let renderer = TileRenderer::builder()
.settings(Settings::builder().size(size).build().unwrap())
.build()
.unwrap();
let collection = FeatureCollection {
bbox: None,
features: vec![],
foreign_members: None,
};
let tile = TileCoordinate::new(5, 10, 12).unwrap();
let result = renderer.render(&collection, tile);
assert!(result.is_ok());
let png_data = result.unwrap();
assert!(!png_data.is_empty());
}
}
}