use crate::error::{Result, SammError};
use crate::graph_analytics::ModelGraph;
use std::fmt::Write as FmtWrite;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VisualizationStyle {
Compact,
#[default]
Detailed,
Hierarchical,
}
#[derive(Debug, Clone)]
pub struct ColorScheme {
pub aspect_color: String,
pub property_color: String,
pub characteristic_color: String,
pub edge_color: String,
}
impl Default for ColorScheme {
fn default() -> Self {
Self {
aspect_color: "#E8F4F8".to_string(), property_color: "#FFF4E6".to_string(), characteristic_color: "#F0F0F0".to_string(), edge_color: "#666666".to_string(), }
}
}
impl ModelGraph {
pub fn to_dot(&self, style: VisualizationStyle) -> Result<String> {
self.to_dot_with_colors(style, ColorScheme::default())
}
pub fn to_dot_with_colors(
&self,
style: VisualizationStyle,
colors: ColorScheme,
) -> Result<String> {
let mut dot = String::new();
writeln!(dot, "digraph SAMM_Model {{")
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
writeln!(dot, " // Generated by OxiRS SAMM")
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
writeln!(
dot,
" rankdir={}; // Layout direction",
if style == VisualizationStyle::Hierarchical {
"TB"
} else {
"LR"
}
)
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
writeln!(dot, " node [shape=box, style=filled, fontname=\"Arial\"];")
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
writeln!(dot, " edge [color=\"{}\"];", colors.edge_color)
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
writeln!(dot).map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
for name in self.nodes() {
let (color, shape, label) = self.get_node_attributes(name, style, &colors);
writeln!(
dot,
" \"{}\" [fillcolor=\"{}\", shape={}, label=\"{}\"];",
name, color, shape, label
)
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
}
writeln!(dot).map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
for (src, tgt) in self.edges() {
writeln!(dot, " \"{}\" -> \"{}\";", src, tgt)
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
}
writeln!(dot, "}}")
.map_err(|e| SammError::GraphError(format!("Failed to write DOT: {}", e)))?;
Ok(dot)
}
fn get_node_attributes(
&self,
name: &str,
style: VisualizationStyle,
colors: &ColorScheme,
) -> (String, &'static str, String) {
let (color, shape) = if name.contains("Aspect") {
(colors.aspect_color.clone(), "box")
} else if name.contains("Characteristic") || name.contains("Char") {
(colors.characteristic_color.clone(), "ellipse")
} else {
(colors.property_color.clone(), "rectangle")
};
let label = match style {
VisualizationStyle::Compact => {
name.to_string()
}
VisualizationStyle::Detailed => {
name.to_string()
}
VisualizationStyle::Hierarchical => {
name.to_string()
}
};
(color, shape, label)
}
#[cfg(feature = "graphviz")]
pub fn render_svg(&self, output_path: &str, style: VisualizationStyle) -> Result<()> {
use graphviz_rust::cmd::{CommandArg, Format};
use graphviz_rust::exec;
use graphviz_rust::parse;
use graphviz_rust::printer::PrinterContext;
let dot_string = self.to_dot(style)?;
let graph = parse(&dot_string)
.map_err(|e| SammError::GraphError(format!("Failed to parse DOT: {:?}", e)))?;
let svg = exec(
graph,
&mut PrinterContext::default(),
vec![CommandArg::Format(Format::Svg)],
)
.map_err(|e| SammError::GraphError(format!("Failed to render SVG: {:?}", e)))?;
std::fs::write(output_path, svg)
.map_err(|e| SammError::GraphError(format!("Failed to write SVG: {}", e)))?;
Ok(())
}
#[cfg(feature = "graphviz")]
pub fn render_png(&self, output_path: &str, style: VisualizationStyle) -> Result<()> {
use graphviz_rust::cmd::{CommandArg, Format};
use graphviz_rust::exec;
use graphviz_rust::parse;
use graphviz_rust::printer::PrinterContext;
let dot_string = self.to_dot(style)?;
let graph = parse(&dot_string)
.map_err(|e| SammError::GraphError(format!("Failed to parse DOT: {:?}", e)))?;
let png = exec(
graph,
&mut PrinterContext::default(),
vec![CommandArg::Format(Format::Png)],
)
.map_err(|e| SammError::GraphError(format!("Failed to render PNG: {:?}", e)))?;
std::fs::write(output_path, png)
.map_err(|e| SammError::GraphError(format!("Failed to write PNG: {}", e)))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metamodel::{Aspect, Characteristic, CharacteristicKind, Property};
fn create_test_aspect() -> Aspect {
let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
for i in 1..=3 {
let characteristic = Characteristic {
metadata: crate::metamodel::ElementMetadata::new(format!(
"urn:samm:test:1.0.0#Char{}",
i
)),
data_type: Some("string".to_string()),
kind: CharacteristicKind::Trait,
constraints: vec![],
};
let property = Property::new(format!("urn:samm:test:1.0.0#Property{}", i))
.with_characteristic(characteristic);
aspect.add_property(property);
}
aspect
}
#[test]
fn test_dot_generation_compact() {
let aspect = create_test_aspect();
let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
let dot = graph
.to_dot(VisualizationStyle::Compact)
.expect("operation should succeed");
assert!(dot.contains("digraph SAMM_Model"));
assert!(dot.contains("TestAspect"));
assert!(dot.contains("Property1"));
assert!(dot.contains("Char1"));
}
#[test]
fn test_dot_generation_detailed() {
let aspect = create_test_aspect();
let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
let dot = graph
.to_dot(VisualizationStyle::Detailed)
.expect("operation should succeed");
assert!(dot.contains("digraph SAMM_Model"));
assert!(dot.contains("fillcolor"));
assert!(dot.contains("->"));
}
#[test]
fn test_dot_generation_hierarchical() {
let aspect = create_test_aspect();
let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
let dot = graph
.to_dot(VisualizationStyle::Hierarchical)
.expect("operation should succeed");
assert!(dot.contains("rankdir=TB"));
}
#[test]
fn test_custom_colors() {
let aspect = create_test_aspect();
let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
let colors = ColorScheme {
aspect_color: "#FF0000".to_string(),
property_color: "#00FF00".to_string(),
characteristic_color: "#0000FF".to_string(),
edge_color: "#000000".to_string(),
};
let dot = graph
.to_dot_with_colors(VisualizationStyle::Detailed, colors)
.expect("operation should succeed");
assert!(dot.contains("#FF0000") || dot.contains("#00FF00") || dot.contains("#0000FF"));
}
#[test]
fn test_node_attributes() {
let aspect = create_test_aspect();
let graph = ModelGraph::from_aspect(&aspect).expect("conversion should succeed");
let colors = ColorScheme::default();
let (color, shape, _label) =
graph.get_node_attributes("TestAspect", VisualizationStyle::Detailed, &colors);
assert_eq!(color, colors.aspect_color);
assert_eq!(shape, "box");
let (color, shape, _label) =
graph.get_node_attributes("Char1", VisualizationStyle::Detailed, &colors);
assert_eq!(color, colors.characteristic_color);
assert_eq!(shape, "ellipse");
let (color, shape, _label) =
graph.get_node_attributes("Property1", VisualizationStyle::Detailed, &colors);
assert_eq!(color, colors.property_color);
assert_eq!(shape, "rectangle");
}
}