use crate::error::MilsymbolError;
use crate::options::MilsymbolOptions;
use crate::types::{
ColorMode, DashArrays, SymbolColors, SymbolMetadata, SymbolOutput, SymbolStyle,
ValidationDetails,
};
use deno_core::{JsRuntime, RuntimeOptions, scope, serde_v8, v8};
use eyre::{Result, WrapErr};
#[cfg(feature = "cache")]
use crate::cache::{CacheData, SymbolCache};
pub struct MilsymbolBuilder {
hq_staff_length: Option<u32>,
standard: Option<String>,
dash_arrays: Option<DashArrays>,
color_modes: Vec<(String, ColorMode)>,
#[cfg(feature = "custom-parts")]
symbol_parts: Vec<crate::types::SymbolPart>,
}
impl Default for MilsymbolBuilder {
fn default() -> Self {
Self::new()
}
}
impl MilsymbolBuilder {
pub fn new() -> Self {
Self {
hq_staff_length: None,
standard: None,
dash_arrays: None,
color_modes: Vec::new(),
#[cfg(feature = "custom-parts")]
symbol_parts: Vec::new(),
}
}
pub fn hq_staff_length(mut self, length: u32) -> Self {
self.hq_staff_length = Some(length);
self
}
pub fn standard(mut self, standard: &str) -> Self {
self.standard = Some(standard.to_string());
self
}
pub fn dash_arrays(mut self, arrays: DashArrays) -> Self {
self.dash_arrays = Some(arrays);
self
}
pub fn color_mode(mut self, name: &str, mode: ColorMode) -> Self {
self.color_modes.push((name.to_string(), mode));
self
}
#[cfg(feature = "custom-parts")]
pub fn add_symbol_part(mut self, part: crate::types::SymbolPart) -> Self {
self.symbol_parts.push(part);
self
}
pub fn build(self) -> Result<Milsymbol> {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let ms_code = include_str!(concat!(env!("OUT_DIR"), "/milsymbol/dist/milsymbol.js"));
runtime
.execute_script("<milsymbol>", ms_code)
.wrap_err("Failed to load milsymbol.js into the V8 runtime")?;
let mut setup_script = String::from(include_str!("wrapper.js"));
let mut color_modes_map = std::collections::HashMap::new();
for (name, mode) in self.color_modes {
color_modes_map.insert(name, mode);
}
let config = serde_json::json!({
"hqStaffLength": self.hq_staff_length,
"standard": self.standard,
"dashArrays": self.dash_arrays,
"colorModes": color_modes_map,
});
let config_json = serde_json::to_string(&config).unwrap();
setup_script.push_str(&format!("\n__ms_setup({});\n", config_json));
#[cfg(feature = "custom-parts")]
{
for part in self.symbol_parts {
let part_json = serde_json::to_string(&part).unwrap();
setup_script.push_str(&format!(
"\nms.addSymbolPart(function(ms) {{ return {}; }});\n",
part_json
));
}
}
runtime
.execute_script("<setup>", setup_script)
.wrap_err("Failed to setup pre-compiled milsymbol functions")?;
Ok(Milsymbol {
runtime,
#[cfg(feature = "cache")]
cache: SymbolCache::new(),
})
}
}
pub struct Milsymbol {
runtime: JsRuntime,
#[cfg(feature = "cache")]
cache: SymbolCache,
}
impl Milsymbol {
pub(crate) fn call_js_function<T: serde::de::DeserializeOwned>(
&mut self,
func_name_str: &str,
sidc: &str,
options: &MilsymbolOptions,
) -> Result<T> {
scope!(scope, &mut self.runtime);
let global = scope.get_current_context().global(scope);
let func_name = v8::String::new(scope, func_name_str).unwrap();
let func_val = global.get(scope, func_name.into()).unwrap();
let func = v8::Local::<v8::Function>::try_from(func_val)
.map_err(|e| MilsymbolError::JsExecutionError(deno_core::anyhow::anyhow!(e)))?;
let sidc_v8 = serde_v8::to_v8(scope, sidc).map_err(MilsymbolError::JsSerializationError)?;
let options_v8 =
serde_v8::to_v8(scope, options).map_err(MilsymbolError::JsSerializationError)?;
let result = func
.call(scope, global.into(), &[sidc_v8, options_v8])
.ok_or_else(|| {
MilsymbolError::JsExecutionError(deno_core::anyhow::anyhow!(
"JS function returned null"
))
})?;
let res = serde_v8::from_v8::<T>(scope, result)
.map_err(MilsymbolError::JsDeserializationError)?;
Ok(res)
}
#[cfg(feature = "cache")]
pub fn clear_cache(&mut self) {
self.cache.clear();
}
#[cfg(feature = "cache")]
pub fn remove_from_cache(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<()> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
self.cache.remove(sidc, options)
}
pub fn as_svg(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<SymbolOutput> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("output", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::Output(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok((**val).clone());
}
let result = self
.call_js_function::<SymbolOutput>("__ms_renderSymbol", sidc, options)
.wrap_err("Failed to extract SymbolOutput from V8 payload")?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::Output(Box::new(result.clone())),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
#[cfg(feature = "image")]
pub fn as_image(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<image::DynamicImage> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("image", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::Image(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok((**val).clone());
}
let output = self.as_svg(sidc, Some(options))?;
let result = output.to_image()?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::Image(Box::new(result.clone())),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
pub fn is_valid(&mut self, sidc: &str, options: Option<&MilsymbolOptions>) -> Result<bool> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("is_valid", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::IsValid(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok(**val);
}
let result = self
.call_js_function::<bool>("__ms_isValid", sidc, options)
.wrap_err("Failed to deserialize validation result")?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::IsValid(Box::new(result)),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
pub fn is_valid_extended(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<ValidationDetails> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("validation_details", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::ValidationDetails(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok((**val).clone());
}
let result = self
.call_js_function::<ValidationDetails>("__ms_isValidExtended", sidc, options)
.wrap_err("Failed to extract ValidationDetails from V8 payload")?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::ValidationDetails(Box::new(result.clone())),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
pub fn get_colors(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<SymbolColors> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("colors", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::Colors(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok((**val).clone());
}
let result = self
.call_js_function::<SymbolColors>("__ms_getColors", sidc, options)
.wrap_err("Failed to extract SymbolColors from V8 payload")?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::Colors(Box::new(result.clone())),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
pub fn get_symbol_metadata(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<SymbolMetadata> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("symbol_metadata", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::SymbolMetadata(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok((**val).clone());
}
let result = self
.call_js_function::<SymbolMetadata>("__ms_getMetadata", sidc, options)
.wrap_err("Failed to extract SymbolMetadata from V8 payload")?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::SymbolMetadata(Box::new(result.clone())),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
pub fn get_style(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<SymbolStyle> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("style", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::Style(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok((**val).clone());
}
let result = self
.call_js_function::<SymbolStyle>("__ms_getStyle", sidc, options)
.wrap_err("Failed to extract SymbolStyle from V8 payload")?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::Style(Box::new(result.clone())),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
pub fn get_draw_instructions(
&mut self,
sidc: &str,
options: Option<&MilsymbolOptions>,
) -> Result<Vec<crate::types::DrawInstruction>> {
let default_opts = MilsymbolOptions::default();
let options = options.unwrap_or(&default_opts);
let options_json =
serde_json::to_string(options).map_err(MilsymbolError::SerializationError)?;
#[cfg(feature = "cache")]
let cache_key = ("draw_instructions", sidc.to_string(), options_json.clone());
#[cfg(feature = "cache")]
if let Some(item) = self.cache.map.get_mut(&cache_key)
&& let CacheData::DrawInstructions(val) = &item.data
{
item.last_accessed = std::time::Instant::now();
return Ok((**val).to_vec());
}
let result = self
.call_js_function::<Vec<crate::types::DrawInstruction>>(
"__ms_getDrawInstructions",
sidc,
options,
)
.wrap_err("Failed to extract DrawInstructions from V8 payload")?;
#[cfg(feature = "cache")]
self.cache.map.insert(
cache_key,
crate::cache::CacheItem {
data: CacheData::DrawInstructions(result.clone()),
last_accessed: std::time::Instant::now(),
},
);
Ok(result)
}
pub fn get_hq_staff_length(&mut self) -> Result<u32> {
let result_global = self
.runtime
.execute_script("<exec>", "ms.getHqStaffLength()")
.map_err(|e| MilsymbolError::JsExecutionError(deno_core::anyhow::anyhow!(e)))?;
scope!(scope, &mut self.runtime);
let local = v8::Local::new(scope, result_global);
serde_v8::from_v8::<u32>(scope, local)
.map_err(MilsymbolError::JsDeserializationError)
.wrap_err("Failed to deserialize HQ staff length")
}
pub fn get_dash_arrays(&mut self) -> Result<DashArrays> {
let result_global = self
.runtime
.execute_script("<exec>", "ms.getDashArrays()")
.map_err(|e| MilsymbolError::JsExecutionError(deno_core::anyhow::anyhow!(e)))?;
scope!(scope, &mut self.runtime);
let local = v8::Local::new(scope, result_global);
serde_v8::from_v8::<DashArrays>(scope, local)
.map_err(MilsymbolError::JsDeserializationError)
.wrap_err("Failed to deserialize dash arrays")
}
pub fn get_color_mode(&mut self, name: &str) -> Result<ColorMode> {
let name_json = serde_json::to_string(name).map_err(MilsymbolError::SerializationError)?;
let script = format!("ms.getColorMode({})", name_json);
let result_global = self
.runtime
.execute_script("<exec>", script)
.map_err(|e| MilsymbolError::JsExecutionError(deno_core::anyhow::anyhow!(e)))?;
scope!(scope, &mut self.runtime);
let local = v8::Local::new(scope, result_global);
serde_v8::from_v8::<ColorMode>(scope, local)
.map_err(MilsymbolError::JsDeserializationError)
.wrap_err(format!("Failed to deserialize color mode '{}'", name))
}
pub fn get_version(&mut self) -> Result<String> {
#[cfg(feature = "cache")]
if let Some(version) = crate::cache::VERSION_CACHE.get() {
return Ok(version.clone());
}
let exec_script = "ms.getVersion()";
let result_global = self
.runtime
.execute_script("<exec>", exec_script)
.map_err(|e| MilsymbolError::JsExecutionError(deno_core::anyhow::anyhow!(e)))?;
scope!(scope, &mut self.runtime);
let local = v8::Local::new(scope, result_global);
let version = serde_v8::from_v8::<String>(scope, local)
.map_err(MilsymbolError::JsDeserializationError)
.wrap_err("Failed to deserialize version string")?;
#[cfg(feature = "cache")]
let _ = crate::cache::VERSION_CACHE.set(version.clone());
Ok(version)
}
pub fn get_sidc_entities_and_modifiers(
&mut self,
symbol_set: &str,
) -> Result<crate::metadata::SidcEntitiesAndModifiers> {
#[cfg(feature = "cache")]
{
let cache_mutex = crate::cache::ENTITIES_CACHE
.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
if let Ok(cache) = cache_mutex.lock()
&& let Some(data) = cache.get(symbol_set)
{
return Ok(data.clone());
}
}
let symbol_set_json =
serde_json::to_string(symbol_set).map_err(MilsymbolError::SerializationError)?;
let script = format!("__ms_getEntitiesAndModifiers({})", symbol_set_json);
let result_global = self
.runtime
.execute_script("<extraction>", script)
.map_err(|e| MilsymbolError::JsExecutionError(deno_core::anyhow::anyhow!(e)))
.wrap_err("Failed to execute extraction script")?;
scope!(scope, &mut self.runtime);
let local = v8::Local::new(scope, result_global);
let json_string = serde_v8::from_v8::<String>(scope, local)
.map_err(MilsymbolError::JsDeserializationError)?;
let data: crate::metadata::SidcEntitiesAndModifiers =
serde_json::from_str(&json_string).wrap_err("Failed to deserialize SIDC data")?;
#[cfg(feature = "cache")]
{
let cache_mutex = crate::cache::ENTITIES_CACHE
.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
if let Ok(mut cache) = cache_mutex.lock() {
cache.insert(symbol_set.to_string(), data.clone());
}
}
Ok(data)
}
pub fn get_sidc_entities(
&mut self,
symbol_set: &str,
) -> Result<Vec<crate::metadata::SidcPart>> {
Ok(self.get_sidc_entities_and_modifiers(symbol_set)?.entities)
}
}