use super::xml::{self, Element};
use std::fmt::Write as _;
pub const FIELD: &str = "OGR_STYLE";
fn color(kml: &str) -> Option<String> {
let h = kml.trim().trim_start_matches('#');
if h.len() != 8 || !h.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let (a, b, g, r) = (&h[0..2], &h[2..4], &h[4..6], &h[6..8]);
Some(format!("#{r}{g}{b}{a}").to_ascii_uppercase())
}
fn kml_color(ogr: &str) -> Option<String> {
let h = ogr.trim().trim_start_matches('#');
if !(h.len() == 6 || h.len() == 8) || !h.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let a = if h.len() == 8 { &h[6..8] } else { "ff" };
Some(format!("{a}{}{}{}", &h[4..6], &h[2..4], &h[0..2]).to_ascii_lowercase())
}
fn num(e: &Element, name: &str) -> Option<f64> {
e.child_text(name)?.parse().ok()
}
pub fn from_kml(style: &Element) -> Option<String> {
let mut tools = Vec::new();
if let Some(l) = style.child("LineStyle") {
let mut p = Vec::new();
if let Some(c) = l.child_text("color").and_then(color) {
p.push(format!("c:{c}"));
}
if let Some(w) = num(l, "width") {
p.push(format!("w:{w:.6}px"));
}
tools.push(("PEN", p));
}
if let Some(poly) = style.child("PolyStyle") {
tools.push(("BRUSH", poly.child_text("color").and_then(color).map(|c| vec![format!("fc:{c}")]).unwrap_or_default()));
}
if let Some(i) = style.child("IconStyle") {
let mut p = Vec::new();
if let Some(href) = i.child("Icon").and_then(|ic| ic.child_text("href")) {
p.push(format!("id:\"{href}\""));
}
if let Some(a) = num(i, "heading") {
p.push(format!("a:{a:.6}"));
}
if let Some(c) = i.child_text("color").and_then(color) {
p.push(format!("c:{c}"));
}
if let Some(s) = num(i, "scale") {
p.push(format!("s:{s:.6}"));
}
if let Some(h) = i.child("hotSpot") {
for (k, a) in [("dx", "x"), ("dy", "y")] {
if let Some(v) = h.attr(a).and_then(|v| v.trim().parse::<f64>().ok()) {
p.push(format!("{k}:{v:.6}"));
}
}
}
tools.push(("SYMBOL", p));
}
if let Some(l) = style.child("LabelStyle") {
let mut p = Vec::new();
if let Some(c) = l.child_text("color").and_then(color) {
p.push(format!("c:{c}"));
}
if let Some(s) = num(l, "scale") {
p.push(format!("w:{:.6}", s * 100.0));
}
tools.push(("LABEL", p));
}
let s: Vec<String> = tools.into_iter().filter(|(_, p)| !p.is_empty()).map(|(t, p)| format!("{t}({})", p.join(","))).collect();
(!s.is_empty()).then(|| s.join(";"))
}
pub fn table(root: &Element) -> Vec<(String, String)> {
let mut styles: Vec<(String, String)> = Vec::new();
let mut maps: Vec<&Element> = Vec::new();
fn walk<'a>(e: &'a Element, styles: &mut Vec<(String, String)>, maps: &mut Vec<&'a Element>) {
for c in &e.children {
match c.name.as_str() {
"Style" => {
if let (Some(id), Some(s)) = (c.attr("id"), from_kml(c)) {
styles.push((id.to_string(), s));
}
}
"StyleMap" if c.attr("id").is_some() => maps.push(c),
"Placemark" => {}
_ => walk(c, styles, maps),
}
}
}
walk(root, &mut styles, &mut maps);
for m in maps {
let normal = m.children_named("Pair").find(|p| p.child_text("key") == Some("normal"));
let s = normal.and_then(|p| match p.child("Style") {
Some(st) => from_kml(st),
None => {
let url = p.child_text("styleUrl")?;
let id = url.strip_prefix('#')?;
styles.iter().find(|(k, _)| k == id).map(|(_, v)| v.clone())
}
});
if let (Some(id), Some(s)) = (m.attr("id"), s) {
styles.push((id.to_string(), s));
}
}
styles
}
pub fn of_feature(f: &Element, table: &[(String, String)]) -> Option<String> {
if let Some(s) = f.child("Style").and_then(from_kml) {
return Some(s);
}
let url = f.child_text("styleUrl")?;
match url.strip_prefix('#') {
Some(id) => Some(table.iter().find(|(k, _)| k == id).map_or_else(|| format!("@{id}"), |(_, v)| v.clone())),
None => Some(url.to_string()),
}
}
fn split_top(s: &str, sep: char) -> Vec<&str> {
let (mut out, mut depth, mut quoted, mut start) = (Vec::new(), 0i32, false, 0);
for (i, c) in s.char_indices() {
match c {
'"' => quoted = !quoted,
'(' if !quoted => depth += 1,
')' if !quoted => depth -= 1,
c if c == sep && !quoted && depth == 0 => {
out.push(&s[start..i]);
start = i + c.len_utf8();
}
_ => {}
}
}
out.push(&s[start..]);
out
}
fn tools(style: &str) -> Vec<(String, Vec<(String, String)>)> {
split_top(style, ';')
.into_iter()
.filter_map(|t| {
let t = t.trim();
let open = t.find('(')?;
let body = t[open + 1..].strip_suffix(')')?;
let params = split_top(body, ',')
.into_iter()
.filter_map(|p| {
let (k, v) = p.split_once(':')?;
Some((k.trim().to_ascii_lowercase(), v.trim().trim_matches('"').to_string()))
})
.collect();
Some((t[..open].trim().to_ascii_uppercase(), params))
})
.collect()
}
fn value_num(v: &str) -> Option<f64> {
let end = v.find(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')).unwrap_or(v.len());
v[..end].parse().ok()
}
pub fn to_kml(style: &str) -> String {
let mut s = String::new();
let get = |p: &[(String, String)], k: &str| p.iter().find(|(n, _)| n == k).map(|(_, v)| v.clone());
for (tool, p) in tools(style) {
match tool.as_str() {
"SYMBOL" => {
s.push_str("<IconStyle>");
if let Some(c) = get(&p, "c").and_then(|c| kml_color(&c)) {
let _ = write!(s, "<color>{c}</color>");
}
if let Some(v) = get(&p, "s").and_then(|v| value_num(&v)) {
let _ = write!(s, "<scale>{v}</scale>");
}
if let Some(v) = get(&p, "a").and_then(|v| value_num(&v)) {
let _ = write!(s, "<heading>{v}</heading>");
}
if let Some(id) = get(&p, "id").filter(|id| !id.starts_with("ogr-sym-")) {
let _ = write!(s, "<Icon><href>{}</href></Icon>", xml::escape(&id));
}
if let (Some(x), Some(y)) = (get(&p, "dx").and_then(|v| value_num(&v)), get(&p, "dy").and_then(|v| value_num(&v))) {
let _ = write!(s, "<hotSpot x=\"{x}\" y=\"{y}\" xunits=\"fraction\" yunits=\"fraction\"/>");
}
s.push_str("</IconStyle>");
}
"LABEL" => {
s.push_str("<LabelStyle>");
if let Some(c) = get(&p, "c").and_then(|c| kml_color(&c)) {
let _ = write!(s, "<color>{c}</color>");
}
if let Some(v) = get(&p, "w").and_then(|v| value_num(&v)) {
let _ = write!(s, "<scale>{}</scale>", v / 100.0);
}
s.push_str("</LabelStyle>");
}
"PEN" => {
s.push_str("<LineStyle>");
if let Some(c) = get(&p, "c").and_then(|c| kml_color(&c)) {
let _ = write!(s, "<color>{c}</color>");
}
if let Some(v) = get(&p, "w").and_then(|v| value_num(&v)) {
let _ = write!(s, "<width>{v}</width>");
}
s.push_str("</LineStyle>");
}
"BRUSH" => {
s.push_str("<PolyStyle>");
if let Some(c) = get(&p, "fc").and_then(|c| kml_color(&c)) {
let _ = write!(s, "<color>{c}</color>");
}
s.push_str("</PolyStyle>");
}
_ => {}
}
}
s
}
pub fn is_reference(v: &str) -> bool {
v.starts_with('@') || !v.contains('(')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_like_gdal() {
let doc = xml::parse(
r#"<kml><Document>
<Style id="s1"><IconStyle><color>ff00ff00</color><scale>1.1</scale><heading>45</heading><Icon><href>http://x/i.png</href></Icon><hotSpot x="0.5" y="0" xunits="fraction" yunits="fraction"/></IconStyle><LabelStyle><color>ff0000ff</color><scale>1.5</scale></LabelStyle></Style>
<Style id="s2"><LineStyle><color>7f00ffff</color><width>3.5</width></LineStyle><PolyStyle><color>4000ff00</color><fill>1</fill></PolyStyle></Style>
<StyleMap id="m"><Pair><key>normal</key><styleUrl>#s2</styleUrl></Pair><Pair><key>highlight</key><styleUrl>#s1</styleUrl></Pair></StyleMap>
</Document></kml>"#,
)
.unwrap();
let t = table(&doc);
let get = |k: &str| t.iter().find(|(n, _)| n == k).map(|(_, v)| v.as_str());
assert_eq!(
get("s1"),
Some(
r#"SYMBOL(id:"http://x/i.png",a:45.000000,c:#00FF00FF,s:1.100000,dx:0.500000,dy:0.000000);LABEL(c:#FF0000FF,w:150.000000)"#
)
);
assert_eq!(get("s2"), Some("PEN(c:#FFFF007F,w:3.500000px);BRUSH(fc:#00FF0040)"));
assert_eq!(get("m"), get("s2"));
let back = xml::parse(&format!("<Style>{}</Style>", to_kml(get("s1").unwrap()))).unwrap();
assert_eq!(from_kml(&back).as_deref(), get("s1"));
let back = xml::parse(&format!("<Style>{}</Style>", to_kml(get("s2").unwrap()))).unwrap();
assert_eq!(from_kml(&back).as_deref(), get("s2"));
}
}