use crate::error::Result;
use crate::projection::lat_lon_to_web_mercator;
use crate::svg::types::SvgContext;
use crate::svg::utils::{get_property_num, get_property_str};
use geo_types::Point;
use geojson::Feature;
fn escape_xml(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub fn generate_point_svg_items(
feature: &Feature,
point: &Point<f64>,
context: &SvgContext,
) -> Result<Vec<String>> {
let lon = point.x();
let lat = point.y();
let merc = lat_lon_to_web_mercator(lon, lat)?;
let x = (merc.x - context.mercator_bbox.min_x) * context.x_scaling_factor;
let y = context.size - (merc.y - context.mercator_bbox.min_y) * context.y_scaling_factor;
let text = match feature
.properties
.as_ref()
.and_then(|props| props.get("text"))
.and_then(|val| val.as_str())
{
Some(t) => t,
None => return Ok(vec![]),
};
let font_size = get_property_num(feature, "font-size", 14.0);
let text_anchor = get_property_str(feature, "text-anchor", "middle");
let dominant_baseline = get_property_str(feature, "dominant-baseline", "middle");
let estimated_text_width = font_size * text.len() as f64 * 1.0;
let (left_buffer, right_buffer) = match text_anchor.as_str() {
"start" => (font_size, estimated_text_width),
"end" => (estimated_text_width, font_size),
_ => {
let buffer = estimated_text_width / 2.0 + font_size;
(buffer, buffer)
}
};
let (top_buffer, bottom_buffer) = match dominant_baseline.as_str() {
"top" | "text-before-edge" => (font_size * 0.5, font_size * 2.0),
"bottom" | "text-after-edge" => (font_size * 2.0, font_size * 0.5),
_ => {
let buffer = font_size * 3.0;
(buffer, buffer)
}
};
if x < -left_buffer
|| x > context.size + right_buffer
|| y < -top_buffer
|| y > context.size + bottom_buffer
{
return Ok(vec![]);
}
let font_family = get_property_str(feature, "font-family", "Arial");
let font_weight = get_property_str(feature, "font-weight", "normal");
let color = get_property_str(feature, "color", "black");
let opacity = get_property_num(feature, "opacity", 1.0);
let svg_baseline = match dominant_baseline.as_str() {
"top" => "text-before-edge",
"bottom" => "text-after-edge",
other => other,
};
let escaped_text = escape_xml(text);
Ok(vec![format!(
r#"<text x="{}" y="{}" font-family="{}" font-size="{}" font-weight="{}" fill="{}" fill-opacity="{}" text-anchor="{}" dominant-baseline="{}">{}</text>"#,
x, y, font_family, font_size, font_weight, color, opacity, text_anchor, svg_baseline, escaped_text
)])
}
#[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_generate_point_svg_items_basic() {
let context = create_test_context();
let point = Point::new(0.0, 0.0);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [0.0, 0.0]},
"properties": {
"text": "Test Label"
}
}))
.unwrap();
let items = generate_point_svg_items(&feature, &point, &context).unwrap();
assert_eq!(items.len(), 1);
assert!(items[0].contains("<text"));
assert!(items[0].contains("Test Label"));
assert!(items[0].contains("font-family=\"Arial\""));
assert!(items[0].contains("font-size=\"14\""));
}
#[test]
fn test_generate_point_svg_items_no_text() {
let context = create_test_context();
let point = Point::new(0.0, 0.0);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [0.0, 0.0]},
"properties": {}
}))
.unwrap();
let items = generate_point_svg_items(&feature, &point, &context).unwrap();
assert_eq!(items.len(), 0); }
#[test]
fn test_generate_point_svg_items_with_style() {
let context = create_test_context();
let point = Point::new(0.0, 0.0);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [0.0, 0.0]},
"properties": {
"text": "Styled",
"font-family": "Helvetica",
"font-size": 20,
"font-weight": "bold",
"color": "red",
"opacity": 0.8
}
}))
.unwrap();
let items = generate_point_svg_items(&feature, &point, &context).unwrap();
assert_eq!(items.len(), 1);
assert!(items[0].contains("font-family=\"Helvetica\""));
assert!(items[0].contains("font-size=\"20\""));
assert!(items[0].contains("font-weight=\"bold\""));
assert!(items[0].contains("fill=\"red\""));
assert!(items[0].contains("fill-opacity=\"0.8\""));
}
#[test]
fn test_generate_point_svg_items_text_anchor() {
let context = create_test_context();
let point = Point::new(0.0, 0.0);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [0.0, 0.0]},
"properties": {
"text": "Test",
"text-anchor": "start"
}
}))
.unwrap();
let items = generate_point_svg_items(&feature, &point, &context).unwrap();
assert!(items[0].contains("text-anchor=\"start\""));
}
#[test]
fn test_generate_point_svg_items_out_of_bounds() {
let context = create_test_context();
let point = Point::new(100.0, 100.0);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [100.0, 100.0]},
"properties": {
"text": "Far Away"
}
}))
.unwrap();
let items = generate_point_svg_items(&feature, &point, &context).unwrap();
assert_eq!(items.len(), 0); }
#[test]
fn test_escape_xml() {
assert_eq!(escape_xml("Hello & <World>"), "Hello & <World>");
assert_eq!(escape_xml("Quote \"this\""), "Quote "this"");
assert_eq!(escape_xml("Apostrophe's"), "Apostrophe's");
}
#[test]
fn test_generate_point_svg_items_xml_escape() {
let context = create_test_context();
let point = Point::new(0.0, 0.0);
let feature: Feature = serde_json::from_value(json!({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [0.0, 0.0]},
"properties": {
"text": "Test <>&\""
}
}))
.unwrap();
let items = generate_point_svg_items(&feature, &point, &context).unwrap();
assert!(items[0].contains("Test <>&""));
assert!(!items[0].contains("Test <>&\""));
}
}