geojson_tile_renderer/
render.rs

1use crate::error::{RenderError, Result};
2use crate::projection::{lat_lon_to_web_mercator, tile_to_bounds, BoundingBox};
3use crate::svg::{generate_svg_items, SvgContext};
4use crate::types::{Settings, TileCoordinate};
5use geojson::{Feature, FeatureCollection};
6use rayon::prelude::*;
7
8/// Main tile renderer
9pub struct TileRenderer {
10	settings: Settings,
11}
12
13/// Builder for TileRenderer
14pub struct TileRendererBuilder {
15	settings: Option<Settings>,
16}
17
18impl Default for TileRendererBuilder {
19	fn default() -> Self {
20		Self { settings: None }
21	}
22}
23
24impl TileRendererBuilder {
25	/// Set custom settings
26	pub fn settings(mut self, settings: Settings) -> Self {
27		self.settings = Some(settings);
28		self
29	}
30
31	/// Build the TileRenderer
32	pub fn build(self) -> Result<TileRenderer> {
33		let settings = self.settings.unwrap_or_default();
34		Ok(TileRenderer { settings })
35	}
36}
37
38impl TileRenderer {
39	/// Create a new TileRenderer with default settings
40	pub fn new() -> Self {
41		Self {
42			settings: Settings::default(),
43		}
44	}
45
46	/// Create a builder for TileRenderer
47	pub fn builder() -> TileRendererBuilder {
48		TileRendererBuilder::default()
49	}
50
51	/// Renders GeoJSON features to a tile image
52	///
53	/// # Arguments
54	/// * `geojson` - GeoJSON feature collection to render
55	/// * `tile` - Tile coordinates (z/x/y)
56	///
57	/// # Returns
58	/// PNG image buffer of the rendered tile
59	pub fn render(&self, geojson: &FeatureCollection, tile: TileCoordinate) -> Result<Vec<u8>> {
60		self.render_internal(&geojson.features, tile)
61	}
62
63	/// Renders a single GeoJSON feature to a tile image
64	///
65	/// # Arguments
66	/// * `feature` - GeoJSON feature to render
67	/// * `tile` - Tile coordinates (z/x/y)
68	///
69	/// # Returns
70	/// PNG image buffer of the rendered tile
71	pub fn render_feature(&self, feature: &Feature, tile: TileCoordinate) -> Result<Vec<u8>> {
72		self.render_internal(&[feature.clone()], tile)
73	}
74
75	/// Renders multiple tiles in parallel
76	///
77	/// # Arguments
78	/// * `geojson` - GeoJSON feature collection to render
79	/// * `tiles` - Slice of tile coordinates to render
80	///
81	/// # Returns
82	/// Vector of PNG image buffers, one for each tile
83	pub fn render_many(&self, geojson: &FeatureCollection, tiles: &[TileCoordinate]) -> Result<Vec<Vec<u8>>> {
84		// Use rayon to parallelize tile rendering
85		tiles
86			.par_iter()
87			.map(|&tile| self.render(geojson, tile))
88			.collect()
89	}
90
91	/// Internal rendering implementation
92	fn render_internal(&self, features: &[Feature], tile: TileCoordinate) -> Result<Vec<u8>> {
93		let size = self.settings.size as f64;
94
95		// Get the geographic bounds of this tile
96		let image_polygon = tile_to_bounds(tile);
97
98		// Calculate bounding box in Web Mercator projection
99		let mercator_bbox = {
100			let exterior = image_polygon.exterior();
101			let coords: Vec<_> = exterior.coords().collect();
102
103			// Get min/max from corners
104			let min_coord = lat_lon_to_web_mercator(coords[0].x, coords[0].y)?;
105			let max_coord = lat_lon_to_web_mercator(coords[2].x, coords[2].y)?;
106
107			BoundingBox::new(min_coord.x, min_coord.y, max_coord.x, max_coord.y)
108		};
109
110		// Calculate scaling factors to convert from projected coordinates to pixels
111		let x_scaling_factor = size / mercator_bbox.width();
112		let y_scaling_factor = size / mercator_bbox.height();
113
114		// Create SVG context for coordinate transformation
115		let context = SvgContext::new(
116			mercator_bbox,
117			size,
118			x_scaling_factor,
119			y_scaling_factor,
120			image_polygon,
121		);
122
123		// Generate SVG items for all features
124		let svg_items = generate_svg_items(features, &context)?;
125
126		// Build SVG containing all features
127		let svg = format!(
128			r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">{}</svg>"#,
129			self.settings.size,
130			self.settings.size,
131			svg_items.join("")
132		);
133
134		// Render SVG to PNG
135		self.svg_to_png(&svg)
136	}
137
138	/// Convert SVG to PNG using resvg and tiny-skia
139	fn svg_to_png(&self, svg: &str) -> Result<Vec<u8>> {
140		// Parse SVG
141		let opts = usvg::Options::default();
142		let tree = usvg::Tree::from_str(svg, &opts)
143			.map_err(|e| RenderError::SvgGeneration(format!("Failed to parse SVG: {}", e)))?;
144
145		// Create pixmap with background color
146		let size = self.settings.size;
147		let mut pixmap = tiny_skia::Pixmap::new(size, size)
148			.ok_or_else(|| RenderError::ImageRendering("Failed to create pixmap".to_string()))?;
149
150		// Fill with background color
151		let bg = &self.settings.background_color;
152		let color = tiny_skia::Color::from_rgba8(bg.r, bg.g, bg.b, bg.a);
153		pixmap.fill(color);
154
155		// Render SVG onto pixmap
156		resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
157
158		// Encode as PNG
159		pixmap
160			.encode_png()
161			.map_err(|e| RenderError::ImageRendering(format!("Failed to encode PNG: {}", e)))
162	}
163}
164
165impl Default for TileRenderer {
166	fn default() -> Self {
167		Self::new()
168	}
169}
170
171#[cfg(test)]
172mod tests {
173	use super::*;
174	use crate::types::BackgroundColor;
175	use serde_json::json;
176
177	#[test]
178	fn test_tile_renderer_new() {
179		let renderer = TileRenderer::new();
180		assert_eq!(renderer.settings.size, 256);
181	}
182
183	#[test]
184	fn test_tile_renderer_builder() {
185		let renderer = TileRenderer::builder()
186			.settings(
187				Settings::builder()
188					.size(512)
189					.background_color(BackgroundColor::white())
190					.build()
191					.unwrap(),
192			)
193			.build()
194			.unwrap();
195
196		assert_eq!(renderer.settings.size, 512);
197		assert_eq!(renderer.settings.background_color, BackgroundColor::white());
198	}
199
200	#[test]
201	fn test_render_empty_collection() {
202		let renderer = TileRenderer::new();
203		let collection = FeatureCollection {
204			bbox: None,
205			features: vec![],
206			foreign_members: None,
207		};
208
209		let tile = TileCoordinate::new(10, 163, 395).unwrap();
210		let result = renderer.render(&collection, tile);
211
212		assert!(result.is_ok());
213		let png_data = result.unwrap();
214		assert!(!png_data.is_empty());
215
216		// Verify PNG header
217		assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
218	}
219
220	#[test]
221	fn test_render_polygon() {
222		let renderer = TileRenderer::new();
223
224		let feature: Feature = serde_json::from_value(json!({
225			"type": "Feature",
226			"geometry": {
227				"type": "Polygon",
228				"coordinates": [[
229					[-122.5, 37.7],
230					[-122.4, 37.7],
231					[-122.4, 37.8],
232					[-122.5, 37.8],
233					[-122.5, 37.7]
234				]]
235			},
236			"properties": {
237				"fill": "red",
238				"fill-opacity": "0.5"
239			}
240		}))
241		.unwrap();
242
243		let collection = FeatureCollection {
244			bbox: None,
245			features: vec![feature],
246			foreign_members: None,
247		};
248
249		let tile = TileCoordinate::new(10, 163, 395).unwrap();
250		let result = renderer.render(&collection, tile);
251
252		assert!(result.is_ok());
253		let png_data = result.unwrap();
254		assert!(!png_data.is_empty());
255		assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
256	}
257
258	#[test]
259	fn test_render_feature() {
260		let renderer = TileRenderer::new();
261
262		let feature: Feature = serde_json::from_value(json!({
263			"type": "Feature",
264			"geometry": {
265				"type": "Point",
266				"coordinates": [-122.45, 37.75]
267			},
268			"properties": {
269				"text": "San Francisco"
270			}
271		}))
272		.unwrap();
273
274		let tile = TileCoordinate::new(10, 163, 395).unwrap();
275		let result = renderer.render_feature(&feature, tile);
276
277		assert!(result.is_ok());
278		let png_data = result.unwrap();
279		assert!(!png_data.is_empty());
280	}
281
282	#[test]
283	fn test_render_many() {
284		let renderer = TileRenderer::new();
285		let collection = FeatureCollection {
286			bbox: None,
287			features: vec![],
288			foreign_members: None,
289		};
290
291		let tiles = vec![
292			TileCoordinate::new(10, 163, 395).unwrap(),
293			TileCoordinate::new(10, 164, 395).unwrap(),
294			TileCoordinate::new(10, 163, 396).unwrap(),
295		];
296
297		let results = renderer.render_many(&collection, &tiles);
298		assert!(results.is_ok());
299
300		let png_buffers = results.unwrap();
301		assert_eq!(png_buffers.len(), 3);
302
303		for png_data in png_buffers {
304			assert!(!png_data.is_empty());
305			assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
306		}
307	}
308
309	#[test]
310	fn test_render_with_background_color() {
311		let renderer = TileRenderer::builder()
312			.settings(
313				Settings::builder()
314					.background_color(BackgroundColor::rgb(255, 0, 0))
315					.build()
316					.unwrap(),
317			)
318			.build()
319			.unwrap();
320
321		let collection = FeatureCollection {
322			bbox: None,
323			features: vec![],
324			foreign_members: None,
325		};
326
327		let tile = TileCoordinate::new(0, 0, 0).unwrap();
328		let result = renderer.render(&collection, tile);
329
330		assert!(result.is_ok());
331		let png_data = result.unwrap();
332		assert!(!png_data.is_empty());
333	}
334
335	#[test]
336	fn test_render_different_tile_sizes() {
337		for size in [128, 256, 512] {
338			let renderer = TileRenderer::builder()
339				.settings(Settings::builder().size(size).build().unwrap())
340				.build()
341				.unwrap();
342
343			let collection = FeatureCollection {
344				bbox: None,
345				features: vec![],
346				foreign_members: None,
347			};
348
349			let tile = TileCoordinate::new(5, 10, 12).unwrap();
350			let result = renderer.render(&collection, tile);
351
352			assert!(result.is_ok());
353			let png_data = result.unwrap();
354			assert!(!png_data.is_empty());
355		}
356	}
357}