geojson-tile-renderer 0.1.0

Convert GeoJSON features to PNG tile images with Web Mercator projection
Documentation
use crate::error::Result;
use crate::projection::transform_coordinates_to_pixels;
use crate::svg::types::SvgContext;
use crate::svg::utils::get_property_str;
use geo_types::{Coord, LineString, MultiLineString};
use geojson::Feature;

/// Generates SVG items for a LineString feature
///
/// # Arguments
/// * `feature` - GeoJSON LineString feature
/// * `linestring` - The linestring geometry
/// * `context` - SVG context for coordinate transformation
///
/// # Returns
/// Vector of SVG element strings
pub fn generate_linestring_svg_items(
	feature: &Feature,
	linestring: &LineString<f64>,
	context: &SvgContext,
) -> Result<Vec<String>> {
	// Convert to vec of coords
	let coords: Vec<Coord<f64>> = linestring.coords().copied().collect();

	// Transform to pixel coordinates
	let transformed_points = transform_coordinates_to_pixels(
		&coords,
		&context.mercator_bbox,
		context.size,
		context.x_scaling_factor,
		context.y_scaling_factor,
		0.0,
	)?;

	// Get styling properties
	let stroke = get_property_str(feature, "stroke", "black");
	let stroke_width = get_property_str(feature, "stroke-width", "1");
	let stroke_opacity = get_property_str(feature, "stroke-opacity", "1.0");

	// Format points as "x1,y1 x2,y2 ..."
	let points_string = transformed_points
		.iter()
		.map(|coord| format!("{},{}", coord.x, coord.y))
		.collect::<Vec<_>>()
		.join(" ");

	Ok(vec![format!(
		r#"<polyline points="{}" fill="none" stroke="{}" stroke-width="{}" stroke-opacity="{}" />"#,
		points_string, stroke, stroke_width, stroke_opacity
	)])
}

/// Generates SVG items for a MultiLineString feature
///
/// # Arguments
/// * `feature` - GeoJSON MultiLineString feature
/// * `multi_linestring` - The multi-linestring geometry
/// * `context` - SVG context for coordinate transformation
///
/// # Returns
/// Vector of SVG element strings
pub fn generate_multi_linestring_svg_items(
	feature: &Feature,
	multi_linestring: &MultiLineString<f64>,
	context: &SvgContext,
) -> Result<Vec<String>> {
	let mut svg_items = Vec::new();

	// Get styling properties once
	let stroke = get_property_str(feature, "stroke", "black");
	let stroke_width = get_property_str(feature, "stroke-width", "1");
	let stroke_opacity = get_property_str(feature, "stroke-opacity", "1.0");

	// Process each linestring
	for linestring in multi_linestring.0.iter() {
		let coords: Vec<Coord<f64>> = linestring.coords().copied().collect();

		let transformed_points = transform_coordinates_to_pixels(
			&coords,
			&context.mercator_bbox,
			context.size,
			context.x_scaling_factor,
			context.y_scaling_factor,
			0.0,
		)?;

		let points_string = transformed_points
			.iter()
			.map(|coord| format!("{},{}", coord.x, coord.y))
			.collect::<Vec<_>>()
			.join(" ");

		svg_items.push(format!(
			r#"<polyline points="{}" fill="none" stroke="{}" stroke-width="{}" stroke-opacity="{}" />"#,
			points_string, stroke, stroke_width, stroke_opacity
		));
	}

	Ok(svg_items)
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::projection::BoundingBox;
	use geo_types::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(
			geo_types::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_generate_linestring_svg_items_basic() {
		let context = create_test_context();
		let linestring = LineString::from(vec![
			Coord { x: -0.5, y: -0.5 },
			Coord { x: 0.0, y: 0.0 },
			Coord { x: 0.5, y: 0.5 },
		]);

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

		let items = generate_linestring_svg_items(&feature, &linestring, &context).unwrap();
		assert_eq!(items.len(), 1);
		assert!(items[0].contains("<polyline"));
		assert!(items[0].contains("stroke=\"black\""));
		assert!(items[0].contains("stroke-width=\"1\""));
		assert!(items[0].contains("fill=\"none\""));
	}

	#[test]
	fn test_generate_linestring_svg_items_with_style() {
		let context = create_test_context();
		let linestring = LineString::from(vec![
			Coord { x: -0.5, y: -0.5 },
			Coord { x: 0.5, y: 0.5 },
		]);

		let feature: Feature = serde_json::from_value(json!({
			"type": "Feature",
			"geometry": {"type": "LineString", "coordinates": []},
			"properties": {
				"stroke": "red",
				"stroke-width": "2",
				"stroke-opacity": "0.8"
			}
		}))
		.unwrap();

		let items = generate_linestring_svg_items(&feature, &linestring, &context).unwrap();
		assert_eq!(items.len(), 1);
		assert!(items[0].contains("stroke=\"red\""));
		assert!(items[0].contains("stroke-width=\"2\""));
		assert!(items[0].contains("stroke-opacity=\"0.8\""));
	}

	#[test]
	fn test_generate_multi_linestring_svg_items() {
		let context = create_test_context();
		let multi_linestring = MultiLineString(vec![
			LineString::from(vec![Coord { x: -0.8, y: -0.8 }, Coord { x: -0.2, y: -0.2 }]),
			LineString::from(vec![Coord { x: 0.2, y: 0.2 }, Coord { x: 0.8, y: 0.8 }]),
		]);

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

		let items = generate_multi_linestring_svg_items(&feature, &multi_linestring, &context).unwrap();
		assert_eq!(items.len(), 2);
		for item in items {
			assert!(item.contains("<polyline"));
		}
	}
}