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};
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, };
let items = match &geometry.value {
Value::Polygon(coords) => {
let polygon = geojson_polygon_to_geo(coords)?;
generate_polygon_svg_items(feature, &polygon, context)?
}
Value::MultiPolygon(coords) => {
let multi_polygon = geojson_multi_polygon_to_geo(coords)?;
generate_multi_polygon_svg_items(feature, &multi_polygon, context)?
}
Value::LineString(coords) => {
let linestring = geojson_linestring_to_geo(coords)?;
generate_linestring_svg_items(feature, &linestring, context)?
}
Value::MultiLineString(coords) => {
let multi_linestring = geojson_multi_linestring_to_geo(coords)?;
generate_multi_linestring_svg_items(feature, &multi_linestring, context)?
}
Value::Point(coords) => {
let point = geojson_point_to_geo(coords)?;
generate_point_svg_items(feature, &point, context)?
}
Value::MultiPoint(_) => {
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)
}
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]))
}
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?))
}
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?))
}
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?))
}
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);
}
}