Skip to main content

gmt_dos_actors/
graph.rs

1//! Actors graph
2
3use std::{
4    collections::{hash_map::DefaultHasher, HashMap},
5    env,
6    fs::File,
7    hash::{Hash, Hasher},
8    io::{self, Write},
9    path::Path,
10    sync::{LazyLock, Mutex},
11};
12
13use crate::{model::PlainModel, trim};
14mod render;
15pub use render::{Render, RenderError};
16
17#[derive(Debug)]
18pub struct ColorMap {
19    lookup: HashMap<usize, usize>,
20    colors: Vec<usize>,
21}
22impl Default for ColorMap {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl ColorMap {
29    pub fn new() -> Self {
30        Self {
31            lookup: HashMap::new(),
32            colors: (1usize..=8).collect(),
33        }
34    }
35    pub fn get(&mut self, rate: usize) -> usize {
36        *self.lookup.entry(rate).or_insert_with(|| {
37            let color = self.colors[0];
38            self.colors.rotate_left(1);
39            color
40        })
41    }
42}
43pub static COLORMAP: LazyLock<Mutex<ColorMap>> = LazyLock::new(|| Mutex::new(ColorMap::new()));
44
45#[derive(Debug, thiserror::Error)]
46pub enum GraphError {
47    #[error("failed to write Graphviz file")]
48    ToDot(#[from] io::Error),
49}
50
51#[derive(Debug, Default, Clone)]
52enum GraphTheme {
53    #[default]
54    Screen,
55    Paper,
56}
57impl GraphTheme {
58    pub fn new() -> Self {
59        match env::var("FLOWCHART_THEME") {
60            Ok(var) => match var.to_lowercase().as_str() {
61                "screen" => Self::Screen,
62                "paper" => Self::Paper,
63                _ => Self::default(),
64            },
65            Err(_) => Self::default(),
66        }
67    }
68    pub fn into_string(self, actors: String, inputs: String, outputs: String) -> String {
69        match self {
70            Self::Screen => format!(
71                r#"
72    digraph  G {{
73      overlap = false;
74      splines = true;
75      bgcolor = gray24;
76      {{node [shape=box, width=0.75, margin="0.025", style="rounded,filled", fillcolor=lightgray]; {};}}
77      node [shape=point, fillcolor=gray24, color=lightgray];
78    
79      /* Outputs */
80    {{
81      edge [arrowhead=none,colorscheme=dark28,fontsize=9, fontcolor=lightgray,fontname="times:italic"];
82      {}
83    }}
84      /* Inputs */
85    {{
86      edge [arrowhead=vee, colorscheme=dark28]
87      {}
88    }}
89    }}
90    "#,
91                actors, outputs, inputs,
92            ),
93            Self::Paper => format!(
94                r#"
95    digraph  G {{
96      overlap = scale;
97      splines = true;
98      {{node [shape=box, width=0.75, margin="0.025", style="rounded,filled"]; {};}}
99      node [shape=point, fillcolor=gray24, color=lightgray];
100    
101      /* Outputs */
102    {{
103      edge [arrowhead=none,colorscheme=dark28,fontsize=9,fontname="times:italic"];
104      {}
105    }}
106      /* Inputs */
107    {{
108      edge [arrowhead=vee, colorscheme=dark28]
109      {}
110    }}
111    }}
112    "#,
113                actors, outputs, inputs,
114            ),
115        }
116    }
117}
118
119/// [Model](crate::model::Model) network mapping
120///
121/// The structure is used to build a [Graphviz](https://www.graphviz.org/) diagram of a [Model](crate::model::Model).
122/// A new [Graph] is created with `Model::graph()`.
123///
124/// There are 2 themes for the flowcharts: `Screen` with a dark background and `Paper` with a light background.
125/// The theme can be set with the environment variable `FLOWCHART_THEME` with the values `Screen` and `Paper`.
126///
127/// The graph layout is either
128///  [dot](https://www.graphviz.org/docs/layouts/dot/),
129///  [neato](https://www.graphviz.org/docs/layouts/neato/),
130/// or [fdp](https://www.graphviz.org/docs/layouts/fdp/).
131/// The default layout is `neato` and it can be change by setting the environment variable `FLOWCHART`
132/// to `dot`, `neato` or `fdp`.
133
134#[derive(Debug, Hash, Default, Clone)]
135pub struct Graph {
136    pub(crate) name: String,
137    actors: PlainModel,
138    to_dot: bool,
139}
140impl Graph {
141    pub fn new(name: String, actors: impl Into<PlainModel>) -> Self {
142        let mut hasher = DefaultHasher::new();
143        let mut actors: PlainModel = actors.into();
144        actors.iter_mut().for_each(|actor| {
145            actor.client = trim(&actor.client);
146            actor.hash(&mut hasher);
147            actor.hash = hasher.finish();
148        });
149        Self {
150            name,
151            actors,
152            to_dot: env::var("TO_DOT").is_ok(),
153        }
154    }
155    /// Returns the diagram in the [Graphviz](https://www.graphviz.org/) dot language
156    pub fn to_string(&self) -> String {
157        let color_map = &*COLORMAP;
158        let inputs: Vec<_> = self
159            .actors
160            .iter()
161            .filter_map(|actor| {
162                actor.inputs.as_ref().map(|inputs| {
163                    inputs
164                        .iter()
165                        .map(|input| {
166                            let color = color_map.lock().unwrap().get(input.rate());
167                            input.as_formatted_input(actor.hash, color)
168                        })
169                        .collect::<Vec<String>>()
170                })
171            })
172            .flatten()
173            .collect();
174        let outputs: Vec<_> = self
175            .actors
176            .iter()
177            .filter_map(|actor| {
178                actor.outputs.as_ref().map(|outputs| {
179                    outputs
180                        .iter()
181                        .map(|output| {
182                            let color = color_map.lock().unwrap().get(output.rate());
183                            output.as_formatted_output(actor.hash, color)
184                        })
185                        .collect::<Vec<String>>()
186                })
187            })
188            .flatten()
189            .collect();
190        GraphTheme::new().into_string(
191            self.actors
192                .iter()
193                .map(|actor| {
194                    if let Some(image) = actor.image.as_ref() {
195                        format!(
196                            r#"{} [label="{}", labelloc=t, image="{}"]"#,
197                            actor.hash, actor.client, image
198                        )
199                    } else {
200                        format!(r#"{} [label="{}"]"#, actor.hash, actor.client)
201                    }
202                })
203                .collect::<Vec<String>>()
204                .join("; "),
205            outputs.join("\n"),
206            inputs.join("\n"),
207        )
208    }
209    /// Writes the output of [Graph::to_string()] to a file
210    pub fn to_dot(&self) -> std::result::Result<&Self, GraphError> {
211        if self.to_dot {
212            let data_repo = env::var("DATA_REPO").unwrap_or(".".into());
213            let path = Path::new(&data_repo).join(format!("{}.dot", self.name));
214            let mut file = File::create(&path)?;
215            write!(&mut file, "{}", self.to_string())?;
216            for actor in &self.actors {
217                if let Some(graph) = actor.graph.as_ref() {
218                    graph.to_dot()?;
219                }
220            }
221        }
222        Ok(self)
223    }
224    pub fn walk(&self) -> Render {
225        let mut render = Render::from(self);
226        for actor in &self.actors {
227            if let Some(graph) = actor.graph.as_ref() {
228                render
229                    .child
230                    .get_or_insert(Vec::new())
231                    .push(Box::new(graph.walk()));
232            }
233        }
234        log::debug!("{:}", render);
235        render
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::trim;
242
243    #[test]
244    fn parse_client_name() {
245        let a = trim("print");
246        dbg!(&a);
247        let a = trim("a::b::print");
248        dbg!(a);
249        let a = trim("a::b::print<w::W,q::s::C>");
250        dbg!(a);
251        // let a = trim("a::b::print<w::W>");
252    }
253}