use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq)]
pub struct Anchor {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq)]
pub struct BoundingBox {
#[serde(default)]
pub x1: f64,
#[serde(default)]
pub y1: f64,
#[serde(default)]
pub x2: f64,
#[serde(default)]
pub y2: f64,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq)]
pub struct SymbolSize {
pub width: f64,
pub height: f64,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct SymbolOutput {
pub svg: String,
pub anchor: Anchor,
#[serde(rename = "octagonAnchor")]
pub octagon_anchor: Anchor,
pub size: SymbolSize,
pub bbox: BoundingBox,
pub metadata: SymbolMetadata,
pub colors: SymbolColors,
pub style: SymbolStyle,
pub options: crate::options::MilsymbolOptions,
#[serde(rename = "drawInstructions")]
pub draw_instructions: Vec<DrawInstruction>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SymbolStyle {
#[serde(default)]
pub alternate_medal: bool,
#[serde(default = "default_true")]
pub civilian_color: bool,
#[serde(default)]
pub color_mode: serde_json::Value,
#[serde(default = "default_true")]
pub fill: bool,
#[serde(default)]
pub fill_color: String,
#[serde(default = "default_fill_opacity")]
pub fill_opacity: f64,
#[serde(default = "default_font_family")]
pub fontfamily: String,
#[serde(default = "default_true")]
pub frame: bool,
#[serde(default)]
pub frame_color: String,
#[serde(default)]
pub hq_staff_length: f64,
#[serde(default = "default_true")]
pub icon: bool,
#[serde(default)]
pub icon_color: String,
#[serde(default)]
pub info_background: String,
#[serde(default)]
pub info_background_frame: String,
#[serde(default)]
pub info_color: String,
#[serde(default = "default_true")]
pub info_fields: bool,
#[serde(default = "default_info_outline_color")]
pub info_outline_color: String,
#[serde(default)]
pub info_outline_width: serde_json::Value,
#[serde(default = "default_info_size")]
pub info_size: f64,
#[serde(default)]
pub mono_color: String,
#[serde(default = "default_outline_color")]
pub outline_color: String,
#[serde(default)]
pub outline_width: f64,
#[serde(default)]
pub padding: f64,
#[serde(default)]
pub simple_status_modifier: bool,
#[serde(default = "default_size")]
pub size: f64,
#[serde(default)]
pub square: bool,
#[serde(default)]
pub standard: String,
#[serde(default = "default_stroke_width")]
pub stroke_width: f64,
#[serde(default)]
pub style_fill: bool,
}
fn default_true() -> bool {
true
}
fn default_fill_opacity() -> f64 {
1.0
}
fn default_font_family() -> String {
"Arial".to_string()
}
fn default_info_outline_color() -> String {
"rgb(239, 239, 239)".to_string()
}
fn default_info_size() -> f64 {
40.0
}
fn default_outline_color() -> String {
"rgb(239, 239, 239)".to_string()
}
fn default_size() -> f64 {
100.0
}
fn default_stroke_width() -> f64 {
4.0
}
impl Default for SymbolStyle {
fn default() -> Self {
Self {
alternate_medal: false,
civilian_color: default_true(),
color_mode: serde_json::Value::String("Light".to_string()),
fill: default_true(),
fill_color: String::new(),
fill_opacity: default_fill_opacity(),
fontfamily: default_font_family(),
frame: default_true(),
frame_color: String::new(),
hq_staff_length: 0.0,
icon: default_true(),
icon_color: String::new(),
info_background: String::new(),
info_background_frame: String::new(),
info_color: String::new(),
info_fields: default_true(),
info_outline_color: default_info_outline_color(),
info_outline_width: serde_json::Value::Bool(false),
info_size: default_info_size(),
mono_color: String::new(),
outline_color: default_outline_color(),
outline_width: 0.0,
padding: 0.0,
simple_status_modifier: false,
size: default_size(),
square: false,
standard: String::new(),
stroke_width: default_stroke_width(),
style_fill: false,
}
}
}
impl SymbolStyle {
pub fn info_outline_width_f64(&self) -> f64 {
match &self.info_outline_width {
serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0),
_ => 0.0,
}
}
}
impl SymbolOutput {
pub fn to_data_url(&self) -> String {
format!("data:image/svg+xml;utf8,{}", urlencoding::encode(&self.svg))
}
pub fn as_bytes(&self) -> Vec<u8> {
self.svg.as_bytes().to_vec()
}
#[cfg(feature = "image")]
pub fn to_image(&self) -> Result<image::DynamicImage, crate::error::MilsymbolError> {
let opt = resvg::usvg::Options::default();
let rtree = resvg::usvg::Tree::from_str(&self.svg, &opt)?;
let pixmap_size = rtree.size();
let mut pixmap =
resvg::tiny_skia::Pixmap::new(pixmap_size.width() as u32, pixmap_size.height() as u32)
.ok_or(crate::error::MilsymbolError::PixmapCreationError)?;
resvg::render(
&rtree,
resvg::usvg::Transform::default(),
&mut pixmap.as_mut(),
);
let rgba_image =
image::RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())
.ok_or(crate::error::MilsymbolError::RgbaImageCreationError)?;
Ok(image::DynamicImage::ImageRgba8(rgba_image))
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DashArrays {
pub pending: String,
pub anticipated: String,
#[serde(rename = "feintDummy")]
pub feint_dummy: String,
}
fn deserialize_color_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StrOrBool {
Str(String),
Bool(bool),
}
match StrOrBool::deserialize(deserializer)? {
StrOrBool::Str(s) => Ok(s),
StrOrBool::Bool(b) => {
if !b {
Ok(String::new())
} else {
Ok("true".to_string())
}
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub struct ColorMode {
#[serde(deserialize_with = "deserialize_color_string")]
pub civilian: String,
#[serde(deserialize_with = "deserialize_color_string")]
pub friend: String,
#[serde(deserialize_with = "deserialize_color_string")]
pub hostile: String,
#[serde(deserialize_with = "deserialize_color_string")]
pub neutral: String,
#[serde(deserialize_with = "deserialize_color_string")]
pub unknown: String,
#[serde(deserialize_with = "deserialize_color_string")]
pub suspect: String,
}
impl ColorMode {
pub fn new(
civilian: &str,
friend: &str,
hostile: &str,
neutral: &str,
unknown: &str,
suspect: &str,
) -> Result<Self, crate::error::MilsymbolError> {
let parse = |c: &str| -> Result<String, crate::error::MilsymbolError> {
csscolorparser::parse(c).map_err(|e| crate::error::MilsymbolError::InvalidColor {
color: c.to_string(),
source: e,
})?;
Ok(c.to_string())
};
Ok(Self {
civilian: parse(civilian)?,
friend: parse(friend)?,
hostile: parse(hostile)?,
neutral: parse(neutral)?,
unknown: parse(unknown)?,
suspect: parse(suspect)?,
})
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SymbolColors {
pub fill_color: ColorMode,
pub frame_color: ColorMode,
pub icon_color: ColorMode,
pub icon_fill_color: ColorMode,
pub none: ColorMode,
pub black: ColorMode,
pub white: ColorMode,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct BaseGeometry {
pub bbox: BoundingBox,
pub g: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ValidationDetails {
pub affiliation: String,
pub dimension: String,
pub dimension_unknown: bool,
pub draw_instructions: bool,
pub icon: bool,
pub mobility: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SymbolMetadata {
pub activity: bool,
pub affiliation: String,
#[serde(rename = "baseAffilation")]
pub base_affiliation: String,
#[serde(default)]
pub base_geometry: BaseGeometry,
pub base_dimension: String,
pub civilian: bool,
#[serde(default)]
pub condition: String,
pub context: String,
pub dimension: String,
pub dimension_unknown: bool,
#[serde(default)]
pub dismounted: Option<bool>,
#[serde(default)]
pub echelon: String,
pub faker: bool,
#[serde(rename = "fenintDummy")]
pub feint_dummy: bool,
pub fill: bool,
pub frame: bool,
pub functionid: String,
pub headquarters: bool,
pub installation: bool,
pub joker: bool,
#[serde(default)]
pub leadership: Option<String>,
#[serde(default)]
pub mobility: String,
#[serde(default)]
pub notpresent: String,
#[serde(rename = "numberSIDC")]
pub number_sidc: bool,
pub space: bool,
#[serde(default)]
pub suspect: bool,
pub task_force: bool,
pub unit: bool,
#[serde(rename = "STD2525", default)]
pub std2525: bool,
}
#[cfg(feature = "custom-parts")]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum DrawInstruction {
Typed(TypedInstruction),
Unknown(serde_json::Value),
}
#[cfg(feature = "custom-parts")]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum TypedInstruction {
Path {
d: String,
#[serde(skip_serializing_if = "Option::is_none")]
fill: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
stroke: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
strokewidth: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
strokedasharray: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
strokelinecap: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
strokelinejoin: Option<String>,
},
Circle {
cx: f64,
cy: f64,
r: f64,
#[serde(skip_serializing_if = "Option::is_none")]
fill: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
stroke: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
strokewidth: Option<f64>,
},
Text {
x: f64,
y: f64,
text: String,
#[serde(rename = "textanchor", skip_serializing_if = "Option::is_none")]
text_anchor: Option<String>,
#[serde(rename = "fontsize", skip_serializing_if = "Option::is_none")]
font_size: Option<f64>,
#[serde(rename = "fontfamily", skip_serializing_if = "Option::is_none")]
font_family: Option<String>,
#[serde(rename = "fontweight", skip_serializing_if = "Option::is_none")]
font_weight: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
fill: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
stroke: Option<serde_json::Value>,
},
Translate {
x: f64,
y: f64,
draw: Vec<DrawInstruction>,
},
Rotate {
degree: f64,
x: f64,
y: f64,
draw: Vec<DrawInstruction>,
},
Scale {
factor: f64,
draw: Vec<DrawInstruction>,
},
Svg {
svg: String,
},
}
#[cfg(feature = "custom-parts")]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct SymbolPart {
pub pre: Vec<DrawInstruction>,
pub post: Vec<DrawInstruction>,
pub bbox: BoundingBox,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_base_affiliation_wire_key_is_misspelled() {
let meta = SymbolMetadata {
base_affiliation: "Friend".to_string(),
..Default::default()
};
let v = serde_json::to_value(&meta).unwrap();
assert!(
v.get("baseAffilation").is_some(),
"Expected key 'baseAffilation' (one l) in JSON output, found: {:?}",
v.as_object().map(|o| o.keys().collect::<Vec<_>>())
);
assert!(
v.get("baseAffiliation").is_none(),
"Unexpected key 'baseAffiliation' (two l's) in JSON output"
);
assert_eq!(v["baseAffilation"], "Friend");
let input = json!({
"activity": false,
"affiliation": "Friend",
"baseAffilation": "Friend",
"baseDimension": "",
"baseGeometry": { "g": "", "bbox": { "x1": 0.0, "y1": 0.0, "x2": 0.0, "y2": 0.0 } },
"civilian": false,
"condition": "",
"context": "",
"dimension": "",
"dimensionUnknown": false,
"echelon": "",
"faker": false,
"fenintDummy": false,
"fill": false,
"frame": false,
"functionid": "",
"headquarters": false,
"installation": false,
"joker": false,
"mobility": "",
"notpresent": "",
"numberSIDC": false,
"space": false,
"suspect": false,
"taskForce": false,
"unit": false,
"STD2525": false
});
let deserialized: SymbolMetadata = serde_json::from_value(input).unwrap();
assert_eq!(deserialized.base_affiliation, "Friend");
}
#[test]
fn test_bounding_box_empty_object_deserializes_to_zeros() {
let input = json!({});
let bbox: BoundingBox = serde_json::from_value(input).unwrap();
assert_eq!(bbox, BoundingBox { x1: 0.0, y1: 0.0, x2: 0.0, y2: 0.0 });
}
#[test]
fn test_base_geometry_is_non_optional() {
let input = json!({
"activity": false,
"affiliation": "undefined",
"baseAffilation": "",
"baseDimension": "",
"baseGeometry": { "g": "", "bbox": {} },
"civilian": false,
"condition": "",
"context": "",
"dimension": "undefined",
"dimensionUnknown": false,
"echelon": "",
"faker": false,
"fenintDummy": false,
"fill": true,
"frame": true,
"functionid": "",
"headquarters": false,
"installation": false,
"joker": false,
"mobility": "",
"notpresent": "",
"numberSIDC": false,
"space": false,
"suspect": false,
"taskForce": false,
"unit": false,
"STD2525": true
});
let meta: SymbolMetadata = serde_json::from_value(input).unwrap();
assert_eq!(meta.base_geometry.bbox, BoundingBox::default());
}
}