geojson-tile-renderer 0.1.0

Convert GeoJSON features to PNG tile images with Web Mercator projection
Documentation
pub mod linestring;
pub mod point;
pub mod polygon;
pub mod types;
pub mod utils;

pub use types::SvgContext;

use crate::error::{RenderError, Result};
use geojson::{Feature, Value};
use linestring::{generate_linestring_svg_items, generate_multi_linestring_svg_items};
use point::generate_point_svg_items;
use polygon::{generate_multi_polygon_svg_items, generate_polygon_svg_items};

/// Generates SVG items for all features in a collection
///
/// # Arguments
/// * `features` - Slice of GeoJSON features
/// * `context` - SVG context for coordinate transformation
///
/// # Returns
/// Vector of SVG element strings
pub fn generate_svg_items(features: &[Feature], context: &SvgContext) -> Result<Vec<String>> {
	let mut svg_items = Vec::new();

	for feature in features {
		let geometry = match &feature.geometry {
			Some(geom) => geom,
			None => continue, // Skip features without geometry
		};

		let items = match &geometry.value {
			Value::Polygon(coords) => {
				// Convert GeoJSON coordinates to geo_types::Polygon
				let polygon = geojson_polygon_to_geo(coords)?;
				generate_polygon_svg_items(feature, &polygon, context)?
			}
			Value::MultiPolygon(coords) => {
				// Convert GeoJSON coordinates to geo_types::MultiPolygon
				let multi_polygon = geojson_multi_polygon_to_geo(coords)?;
				generate_multi_polygon_svg_items(feature, &multi_polygon, context)?
			}
			Value::LineString(coords) => {
				// Convert GeoJSON coordinates to geo_types::LineString
				let linestring = geojson_linestring_to_geo(coords)?;
				generate_linestring_svg_items(feature, &linestring, context)?
			}
			Value::MultiLineString(coords) => {
				// Convert GeoJSON coordinates to geo_types::MultiLineString
				let multi_linestring = geojson_multi_linestring_to_geo(coords)?;
				generate_multi_linestring_svg_items(feature, &multi_linestring, context)?
			}
			Value::Point(coords) => {
				// Convert GeoJSON coordinates to geo_types::Point
				let point = geojson_point_to_geo(coords)?;
				generate_point_svg_items(feature, &point, context)?
			}
			Value::MultiPoint(_) => {
				// MultiPoint not supported yet - could be added later
				return Err(RenderError::UnsupportedGeometry(
					"MultiPoint not yet supported".to_string(),
				));
			}
			Value::GeometryCollection(_) => {
				return Err(RenderError::UnsupportedGeometry(
					"GeometryCollection not supported".to_string(),
				));
			}
		};

		svg_items.extend(items);
	}

	Ok(svg_items)
}

/// Converts GeoJSON Point coordinates to geo_types::Point
fn geojson_point_to_geo(coords: &[f64]) -> Result<geo_types::Point<f64>> {
	if coords.len() < 2 {
		return Err(RenderError::InvalidCoordinate(
			"Point must have at least 2 coordinates".to_string(),
		));
	}
	Ok(geo_types::Point::new(coords[0], coords[1]))
}

/// Converts GeoJSON LineString coordinates to geo_types::LineString
fn geojson_linestring_to_geo(coords: &[Vec<f64>]) -> Result<geo_types::LineString<f64>> {
	let points: Result<Vec<_>> = coords
		.iter()
		.map(|c| {
			if c.len() < 2 {
				Err(RenderError::InvalidCoordinate(
					"Coordinate must have at least 2 values".to_string(),
				))
			} else {
				Ok(geo_types::Coord { x: c[0], y: c[1] })
			}
		})
		.collect();

	Ok(geo_types::LineString::from(points?))
}

/// Converts GeoJSON MultiLineString coordinates to geo_types::MultiLineString
fn geojson_multi_linestring_to_geo(
	coords: &[Vec<Vec<f64>>],
) -> Result<geo_types::MultiLineString<f64>> {
	let linestrings: Result<Vec<_>> = coords.iter().map(|ls| geojson_linestring_to_geo(ls)).collect();

	Ok(geo_types::MultiLineString(linestrings?))
}

/// Converts GeoJSON Polygon coordinates to geo_types::Polygon
fn geojson_polygon_to_geo(coords: &[Vec<Vec<f64>>]) -> Result<geo_types::Polygon<f64>> {
	if coords.is_empty() {
		return Err(RenderError::InvalidCoordinate(
			"Polygon must have at least one ring".to_string(),
		));
	}

	let exterior = geojson_linestring_to_geo(&coords[0])?;
	let interiors: Result<Vec<_>> = coords[1..].iter().map(|ring| geojson_linestring_to_geo(ring)).collect();

	Ok(geo_types::Polygon::new(exterior, interiors?))
}

/// Converts GeoJSON MultiPolygon coordinates to geo_types::MultiPolygon
fn geojson_multi_polygon_to_geo(
	coords: &[Vec<Vec<Vec<f64>>>],
) -> Result<geo_types::MultiPolygon<f64>> {
	let polygons: Result<Vec<_>> = coords.iter().map(|p| geojson_polygon_to_geo(p)).collect();

	Ok(geo_types::MultiPolygon(polygons?))
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::projection::BoundingBox;
	use geo_types::{Coord, LineString, Polygon};
	use serde_json::json;

	fn create_test_context() -> SvgContext {
		let bbox = BoundingBox::new(-1.0, -1.0, 1.0, 1.0);
		let image_polygon = Polygon::new(
			LineString::from(vec![
				Coord { x: -1.0, y: -1.0 },
				Coord { x: 1.0, y: -1.0 },
				Coord { x: 1.0, y: 1.0 },
				Coord { x: -1.0, y: 1.0 },
				Coord { x: -1.0, y: -1.0 },
			]),
			vec![],
		);

		SvgContext::new(bbox, 256.0, 128.0, 128.0, image_polygon)
	}

	#[test]
	fn test_geojson_point_to_geo() {
		let point = geojson_point_to_geo(&[1.0, 2.0]).unwrap();
		assert_eq!(point.x(), 1.0);
		assert_eq!(point.y(), 2.0);

		assert!(geojson_point_to_geo(&[1.0]).is_err());
	}

	#[test]
	fn test_geojson_linestring_to_geo() {
		let ls = geojson_linestring_to_geo(&[vec![1.0, 2.0], vec![3.0, 4.0]]).unwrap();
		assert_eq!(ls.coords().count(), 2);
	}

	#[test]
	fn test_geojson_polygon_to_geo() {
		let poly =
			geojson_polygon_to_geo(&[vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![1.0, 1.0], vec![0.0, 0.0]]])
				.unwrap();
		assert_eq!(poly.exterior().coords().count(), 4);
	}

	#[test]
	fn test_generate_svg_items_polygon() {
		let context = create_test_context();

		let feature: Feature = serde_json::from_value(json!({
			"type": "Feature",
			"geometry": {
				"type": "Polygon",
				"coordinates": [[
					[-0.5, -0.5],
					[0.5, -0.5],
					[0.5, 0.5],
					[-0.5, 0.5],
					[-0.5, -0.5]
				]]
			},
			"properties": {}
		}))
		.unwrap();

		let items = generate_svg_items(&[feature], &context).unwrap();
		assert_eq!(items.len(), 1);
		assert!(items[0].contains("<path"));
	}

	#[test]
	fn test_generate_svg_items_linestring() {
		let context = create_test_context();

		let feature: Feature = serde_json::from_value(json!({
			"type": "Feature",
			"geometry": {
				"type": "LineString",
				"coordinates": [
					[-0.5, -0.5],
					[0.5, 0.5]
				]
			},
			"properties": {}
		}))
		.unwrap();

		let items = generate_svg_items(&[feature], &context).unwrap();
		assert_eq!(items.len(), 1);
		assert!(items[0].contains("<polyline"));
	}

	#[test]
	fn test_generate_svg_items_point() {
		let context = create_test_context();

		let feature: Feature = serde_json::from_value(json!({
			"type": "Feature",
			"geometry": {
				"type": "Point",
				"coordinates": [0.0, 0.0]
			},
			"properties": {
				"text": "Label"
			}
		}))
		.unwrap();

		let items = generate_svg_items(&[feature], &context).unwrap();
		assert_eq!(items.len(), 1);
		assert!(items[0].contains("<text"));
	}

	#[test]
	fn test_generate_svg_items_unsupported_geometry() {
		let context = create_test_context();

		let feature: Feature = serde_json::from_value(json!({
			"type": "Feature",
			"geometry": {
				"type": "GeometryCollection",
				"geometries": []
			},
			"properties": {}
		}))
		.unwrap();

		assert!(generate_svg_items(&[feature], &context).is_err());
	}

	#[test]
	fn test_generate_svg_items_multiple_features() {
		let context = create_test_context();

		let feature1: Feature = serde_json::from_value(json!({
			"type": "Feature",
			"geometry": {
				"type": "Point",
				"coordinates": [0.0, 0.0]
			},
			"properties": {
				"text": "Label1"
			}
		}))
		.unwrap();

		let feature2: Feature = serde_json::from_value(json!({
			"type": "Feature",
			"geometry": {
				"type": "LineString",
				"coordinates": [
					[-0.5, -0.5],
					[0.5, 0.5]
				]
			},
			"properties": {}
		}))
		.unwrap();

		let items = generate_svg_items(&[feature1, feature2], &context).unwrap();
		assert_eq!(items.len(), 2);
	}
}