use crate::error::Result;
use crate::projection::transform_polygon_coordinates_to_pixels;
use crate::svg::types::SvgContext;
use crate::svg::utils::get_property_str;
use geo_types::{MultiPolygon, Polygon};
use geojson::Feature;
fn generate_path_data(polygons: &[Polygon<f64>], context: &SvgContext) -> Result<String> {
let mut path_data = String::new();
for polygon in polygons {
let exterior = polygon.exterior().coords().copied().collect::<Vec<_>>();
let interiors = polygon
.interiors()
.iter()
.map(|ring| ring.coords().copied().collect::<Vec<_>>())
.collect::<Vec<_>>();
let mut all_rings = vec![exterior];
all_rings.extend(interiors);
let transformed_rings = transform_polygon_coordinates_to_pixels(
&all_rings,
&context.mercator_bbox,
context.size,
context.x_scaling_factor,
context.y_scaling_factor,
0.0,
)?;
for ring in transformed_rings {
for (i, coord) in ring.iter().enumerate() {
if i == 0 {
path_data.push_str(&format!("M{},{} ", coord.x, coord.y));
} else {
path_data.push_str(&format!("L{},{} ", coord.x, coord.y));
}
}
path_data.push_str("Z ");
}
}
Ok(path_data)
}
pub fn generate_polygon_svg_items(
feature: &Feature,
polygon: &Polygon<f64>,
context: &SvgContext,
) -> Result<Vec<String>> {
let path_data = generate_path_data(&[polygon.clone()], context)?;
let fill = get_property_str(feature, "fill", "black");
let fill_opacity = get_property_str(feature, "fill-opacity", "1.0");
Ok(vec![format!(
r#"<path d="{}" fill="{}" fill-opacity="{}" fill-rule="evenodd" />"#,
path_data, fill, fill_opacity
)])
}
pub fn generate_multi_polygon_svg_items(
feature: &Feature,
multi_polygon: &MultiPolygon<f64>,
context: &SvgContext,
) -> Result<Vec<String>> {
let mut svg_items = Vec::new();
let fill = get_property_str(feature, "fill", "black");
let fill_opacity = get_property_str(feature, "fill-opacity", "1.0");
for polygon in multi_polygon.0.iter() {
let path_data = generate_path_data(&[polygon.clone()], context)?;
svg_items.push(format!(
r#"<path d="{}" fill="{}" fill-opacity="{}" fill-rule="evenodd" />"#,
path_data, fill, fill_opacity
));
}
Ok(svg_items)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::projection::BoundingBox;
use geo_types::{Coord, LineString};
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_generate_path_data_single_polygon() {
let context = create_test_context();
let polygon = Polygon::new(
LineString::from(vec![
Coord { x: -0.5, y: -0.5 },
Coord { x: 0.5, y: -0.5 },
Coord { x: 0.5, y: 0.5 },
Coord { x: -0.5, y: 0.5 },
Coord { x: -0.5, y: -0.5 },
]),
vec![],
);
let path_data = generate_path_data(&[polygon], &context).unwrap();
assert!(path_data.contains("M"));
assert!(path_data.contains("L"));
assert!(path_data.contains("Z"));
}
#[test]
fn test_generate_polygon_svg_items_basic() {
let context = create_test_context();
let polygon = Polygon::new(
LineString::from(vec![
Coord { x: -0.5, y: -0.5 },
Coord { x: 0.5, y: -0.5 },
Coord { x: 0.5, y: 0.5 },
Coord { x: -0.5, y: 0.5 },
Coord { x: -0.5, y: -0.5 },
]),
vec![],
);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Polygon", "coordinates": []},
"properties": {}
}))
.unwrap();
let items = generate_polygon_svg_items(&feature, &polygon, &context).unwrap();
assert_eq!(items.len(), 1);
assert!(items[0].contains("<path"));
assert!(items[0].contains("fill=\"black\""));
assert!(items[0].contains("fill-opacity=\"1.0\""));
}
#[test]
fn test_generate_polygon_svg_items_with_style() {
let context = create_test_context();
let polygon = Polygon::new(
LineString::from(vec![
Coord { x: -0.5, y: -0.5 },
Coord { x: 0.5, y: -0.5 },
Coord { x: 0.5, y: 0.5 },
Coord { x: -0.5, y: 0.5 },
Coord { x: -0.5, y: -0.5 },
]),
vec![],
);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Polygon", "coordinates": []},
"properties": {
"fill": "red",
"fill-opacity": "0.5"
}
}))
.unwrap();
let items = generate_polygon_svg_items(&feature, &polygon, &context).unwrap();
assert_eq!(items.len(), 1);
assert!(items[0].contains("fill=\"red\""));
assert!(items[0].contains("fill-opacity=\"0.5\""));
}
#[test]
fn test_generate_polygon_svg_items_out_of_bounds() {
let context = create_test_context();
let polygon = Polygon::new(
LineString::from(vec![
Coord { x: 10.0, y: 10.0 },
Coord { x: 11.0, y: 10.0 },
Coord { x: 11.0, y: 11.0 },
Coord { x: 10.0, y: 11.0 },
Coord { x: 10.0, y: 10.0 },
]),
vec![],
);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Polygon", "coordinates": []},
"properties": {}
}))
.unwrap();
let items = generate_polygon_svg_items(&feature, &polygon, &context).unwrap();
assert_eq!(items.len(), 1);
}
#[test]
fn test_generate_multi_polygon_svg_items() {
let context = create_test_context();
let multi_polygon = MultiPolygon(vec![
Polygon::new(
LineString::from(vec![
Coord { x: -0.8, y: -0.8 },
Coord { x: -0.2, y: -0.8 },
Coord { x: -0.2, y: -0.2 },
Coord { x: -0.8, y: -0.2 },
Coord { x: -0.8, y: -0.8 },
]),
vec![],
),
Polygon::new(
LineString::from(vec![
Coord { x: 0.2, y: 0.2 },
Coord { x: 0.8, y: 0.2 },
Coord { x: 0.8, y: 0.8 },
Coord { x: 0.2, y: 0.8 },
Coord { x: 0.2, y: 0.2 },
]),
vec![],
),
]);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "MultiPolygon", "coordinates": []},
"properties": {}
}))
.unwrap();
let items = generate_multi_polygon_svg_items(&feature, &multi_polygon, &context).unwrap();
assert_eq!(items.len(), 2);
}
}