geojson-tile-renderer 0.1.0

Convert GeoJSON features to PNG tile images with Web Mercator projection
Documentation
use crate::error::{RenderError, Result};

/// Tile coordinate in the Web Mercator tile scheme (z/x/y)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TileCoordinate {
	/// Zoom level (0-30)
	pub z: u8,
	/// X coordinate (column)
	pub x: u32,
	/// Y coordinate (row)
	pub y: u32,
}

impl TileCoordinate {
	/// Create a new tile coordinate
	///
	/// # Arguments
	/// * `z` - Zoom level (0-30)
	/// * `x` - X coordinate (column)
	/// * `y` - Y coordinate (row)
	pub fn new(z: u8, x: u32, y: u32) -> Result<Self> {
		// Validate tile coordinates
		let max_coord = 2u32.pow(z as u32);
		if x >= max_coord || y >= max_coord {
			return Err(RenderError::InvalidCoordinate(format!(
				"Tile coordinate ({}, {}) out of range for zoom level {} (max: {})",
				x, y, z, max_coord - 1
			)));
		}

		Ok(Self { z, x, y })
	}
}

/// RGBA color with values in the range [0, 255]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackgroundColor {
	/// Red component (0-255)
	pub r: u8,
	/// Green component (0-255)
	pub g: u8,
	/// Blue component (0-255)
	pub b: u8,
	/// Alpha component (0-255), where 0 is fully transparent and 255 is fully opaque
	pub a: u8,
}

impl BackgroundColor {
	/// Create a new RGBA color
	pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
		Self { r, g, b, a }
	}

	/// Create a fully opaque color from RGB values
	pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
		Self::new(r, g, b, 255)
	}

	/// Create a transparent color
	pub const fn transparent() -> Self {
		Self::new(0, 0, 0, 0)
	}

	/// Create a white color
	pub const fn white() -> Self {
		Self::rgb(255, 255, 255)
	}

	/// Create a black color
	pub const fn black() -> Self {
		Self::rgb(0, 0, 0)
	}
}

impl Default for BackgroundColor {
	fn default() -> Self {
		Self::transparent()
	}
}

/// Settings for tile rendering
#[derive(Debug, Clone)]
pub struct Settings {
	/// Background color for the tile
	pub background_color: BackgroundColor,
	/// Size of the tile in pixels (width and height)
	pub size: u32,
}

impl Settings {
	/// Create a new Settings instance with default values
	pub fn new() -> Self {
		Self::default()
	}

	/// Create a builder for Settings
	pub fn builder() -> SettingsBuilder {
		SettingsBuilder::default()
	}
}

impl Default for Settings {
	fn default() -> Self {
		Self {
			background_color: BackgroundColor::default(),
			size: 256,
		}
	}
}

/// Builder for Settings
#[derive(Debug, Clone, Default)]
pub struct SettingsBuilder {
	background_color: Option<BackgroundColor>,
	size: Option<u32>,
}

impl SettingsBuilder {
	/// Set the background color
	pub fn background_color(mut self, color: BackgroundColor) -> Self {
		self.background_color = Some(color);
		self
	}

	/// Set the tile size in pixels
	pub fn size(mut self, size: u32) -> Self {
		self.size = Some(size);
		self
	}

	/// Build the Settings
	pub fn build(self) -> Result<Settings> {
		let size = self.size.unwrap_or(256);

		// Validate size
		if size == 0 {
			return Err(RenderError::InvalidSettings(
				"Tile size must be greater than 0".to_string(),
			));
		}

		// Reasonable upper limit to prevent memory issues
		if size > 4096 {
			return Err(RenderError::InvalidSettings(
				"Tile size must be 4096 or less".to_string(),
			));
		}

		Ok(Settings {
			background_color: self.background_color.unwrap_or_default(),
			size,
		})
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_tile_coordinate_new() {
		// Valid coordinates
		assert!(TileCoordinate::new(0, 0, 0).is_ok());
		assert!(TileCoordinate::new(5, 15, 20).is_ok());
		assert!(TileCoordinate::new(10, 1023, 1023).is_ok());

		// Invalid coordinates (out of range)
		assert!(TileCoordinate::new(0, 1, 0).is_err()); // Only one tile at z=0
		assert!(TileCoordinate::new(5, 32, 0).is_err()); // Max is 31 at z=5
		assert!(TileCoordinate::new(5, 0, 32).is_err());
	}

	#[test]
	fn test_background_color() {
		let color = BackgroundColor::new(255, 128, 64, 200);
		assert_eq!(color.r, 255);
		assert_eq!(color.g, 128);
		assert_eq!(color.b, 64);
		assert_eq!(color.a, 200);

		let rgb = BackgroundColor::rgb(100, 150, 200);
		assert_eq!(rgb.a, 255);

		let transparent = BackgroundColor::transparent();
		assert_eq!(transparent.a, 0);
	}

	#[test]
	fn test_settings_builder() {
		let settings = Settings::builder()
			.size(512)
			.background_color(BackgroundColor::white())
			.build()
			.unwrap();

		assert_eq!(settings.size, 512);
		assert_eq!(settings.background_color, BackgroundColor::white());
	}

	#[test]
	fn test_settings_builder_defaults() {
		let settings = Settings::builder().build().unwrap();
		assert_eq!(settings.size, 256);
		assert_eq!(settings.background_color, BackgroundColor::transparent());
	}

	#[test]
	fn test_settings_validation() {
		// Size must be > 0
		assert!(Settings::builder().size(0).build().is_err());

		// Size must be <= 4096
		assert!(Settings::builder().size(4097).build().is_err());

		// Valid sizes
		assert!(Settings::builder().size(1).build().is_ok());
		assert!(Settings::builder().size(256).build().is_ok());
		assert!(Settings::builder().size(4096).build().is_ok());
	}
}