Skip to main content

gmt_dos_actors/graph/
render.rs

1use std::{
2    env,
3    fmt::Display,
4    fs::File,
5    io::Write,
6    path::{Path, PathBuf},
7    process::{Command, Stdio},
8};
9
10use svg::{
11    node::{
12        element::tag::{self, Type},
13        Attributes,
14    },
15    parser::Event,
16    Parser,
17};
18
19use crate::graph::Graph;
20
21const HEAD: &str = r#"
22<head>
23    <meta charset="UTF-8">
24    <meta name="viewport" content="width=device-width, initial-scale=1.0">
25    <title>GRAPH</title>
26    <style>
27        body {
28            background-color: #3d3d3d;
29        }
30
31        .svg-container {
32            display: flex;
33            justify-content: space-around;
34            margin-top: 20px;
35        }
36
37        .info-container {
38            display: flex;
39            justify-content: space-around;
40            font-family: monospace
41        }
42
43        svg {
44            width: auto;
45            /* Adjust the width as needed */
46            height: auto;
47        }
48
49        .hidden {
50            display: none;
51        }
52
53        .highlighted {
54            stroke: hsla(348, 83%, 47%, 0.5);
55            /* Set the stroke color to yellow */
56            /* stroke-width: 2; */
57            /* Set the stroke width */
58        }
59    </style>
60</head>
61"#;
62
63#[derive(Debug, thiserror::Error)]
64pub enum RenderError {
65    #[error("failed to write flowchart")]
66    IO(#[from] std::io::Error),
67    #[error("failed to convert to string")]
68    Utf(#[from] std::string::FromUtf8Error),
69    #[error("flowchart layout is empty")]
70    Layout,
71}
72type Result<T> = std::result::Result<T, RenderError>;
73
74#[derive(Debug, Clone)]
75pub struct Render {
76    name: String,
77    render: String,
78    pub(crate) child: Option<Vec<Box<Render>>>,
79}
80impl From<&Graph> for Render {
81    fn from(graph: &Graph) -> Self {
82        Self {
83            name: graph.name.clone(),
84            render: graph.to_string(),
85            child: None,
86        }
87    }
88}
89
90#[derive(Debug, Clone, Default, PartialEq)]
91#[allow(dead_code)]
92enum GraphLayout {
93    Dot,
94    #[default]
95    Neato,
96    Fdp,
97}
98impl Display for GraphLayout {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            GraphLayout::Dot => write!(f, "dot"),
102            GraphLayout::Neato => write!(f, "neato"),
103            GraphLayout::Fdp => write!(f, "fdp"),
104        }
105    }
106}
107impl GraphLayout {
108    pub fn new() -> Option<Self> {
109        match env::var("FLOWCHART") {
110            Ok(var) => match var.to_lowercase().as_str() {
111                "dot" => Some(Self::Dot),
112                "neato" => Some(Self::Neato),
113                "fdp" => Some(Self::Fdp),
114                _ => None,
115            },
116            Err(_) => Some(Self::default()),
117        }
118    }
119}
120impl Render {
121    fn id(&self) -> String {
122        use std::hash::{DefaultHasher, Hash, Hasher};
123        let mut hasher = DefaultHasher::new();
124        self.name.hash(&mut hasher);
125        let sh = hasher.finish();
126        format!("id{sh:x}")
127    }
128    /// Renders flowchart to SVG
129    pub fn into_svg(&mut self) -> Result<&mut Self> {
130        let mut graph_layout = GraphLayout::new().ok_or(RenderError::Layout)?;
131        let result = loop {
132            let graph = Command::new("echo")
133                .arg(&self.render)
134                .stdout(Stdio::piped())
135                .spawn()?;
136            let svg = Command::new(graph_layout.to_string())
137                .arg("-Tsvg")
138                .stdin(Stdio::from(graph.stdout.unwrap()))
139                .stdout(Stdio::piped())
140                .spawn()?;
141            let output = svg.wait_with_output()?;
142            if output.status.success() {
143                break String::from_utf8(output.stdout)?;
144            } else if graph_layout == GraphLayout::Dot {
145                println!("failed to convert model `{:}` to SVG diagram", self.name);
146                return Ok(self);
147            } else {
148                graph_layout = GraphLayout::Dot;
149            }
150        };
151        log::debug!("{:}", &result[..result.len().min(64)]);
152        self.render = result
153            .lines()
154            .skip(6)
155            .collect::<Vec<_>>()
156            .join("\n")
157            .replace(r#"g id="node"#, &format!(r#"g id="{}_node"#, self.id()));
158        self.child
159            .as_mut()
160            .map(|child| {
161                child
162                    .iter_mut()
163                    .map(|child| child.into_svg())
164                    .collect::<Result<Vec<_>>>()
165            })
166            .transpose()?;
167        log::debug!("{:}", self);
168        Ok(self)
169    }
170    fn hover(&self, child: &str, element: &str) -> String {
171        format!(
172            r#"
173const {0} = document.getElementById('{0}');
174{0}.addEventListener('mouseenter', function () {{
175    {0}.classList.add('highlighted');
176}});
177{0}.addEventListener('mouseleave', function () {{
178    {0}.classList.remove('highlighted');
179}});
180{0}.addEventListener('click', function () {{
181    // Hide graph1
182    {1}.classList.add('hidden');
183    // Show graph2
184    {2}.classList.remove('hidden');
185}});
186{2}.addEventListener('keydown', function (event) {{
187    if (event.key === 'Escape') {{
188        // Show graph1
189        {1}.classList.remove('hidden');
190        // Hide graph2
191        {2}.classList.add('hidden');
192    }}
193}});
194        "#,
195            element,
196            self.id(),
197            child
198        )
199    }
200    /// Parses SVG diagram to identify SVG node names of children graph
201    fn parse(&self) -> Option<String> {
202        let Some(child) = &self.child else {
203            return None;
204        };
205        let parser = Parser::new(&self.render);
206        let mut h = vec![];
207        let mut attributes = Attributes::new();
208        for event in parser {
209            match event {
210                Event::Tag(tag::Group, Type::Start, a) => {
211                    attributes = a;
212                }
213                Event::Text(text) => {
214                    for child in child {
215                        if html_escape::decode_html_entities(text) == child.name {
216                            log::debug!("{:?}", (&child.name, child.id()));
217                            h.push(attributes.get("id").map(|id| self.hover(&child.id(), id)));
218                        }
219                    }
220                }
221                _ => {}
222            }
223        }
224        if h.is_empty() {
225            None
226        } else {
227            h.into_iter()
228                .collect::<Option<Vec<String>>>()
229                .map(|h| h.join("\n"))
230        }
231    }
232    /// Writes highlight script to file
233    fn script_child_hover(&self, file: &mut File) -> Result<()> {
234        log::debug!("{:?}", (&self.name, self.id()));
235        if let Some(h) = self.parse() {
236            writeln!(file, "{}", h)?;
237        }
238        let Some(child) = &self.child else {
239            return Ok(());
240        };
241        for child in child {
242            child.script_child_hover(file)?;
243        }
244        Ok(())
245    }
246    /// Writes SVG diagram to file
247    fn child_svg(&self, file: &mut File, class: Option<&str>) -> Result<()> {
248        match class {
249            Some(class) => writeln!(
250                file,
251                "{}",
252                self.render.replace(
253                    "<svg",
254                    &format!(r#"<svg id="{}" tabindex="0" class="{}""#, self.id(), class)
255                )
256            )?,
257            None => writeln!(
258                file,
259                "{}",
260                self.render
261                    .replace("<svg", &format!(r#"<svg id="{}" tabindex="0""#, self.id()))
262            )?,
263        }
264        let Some(child) = &self.child else {
265            return Ok(());
266        };
267        for child in child {
268            child.child_svg(file, Some("hidden"))?;
269        }
270        Ok(())
271    }
272    /// Writes get element by id script to file
273    fn script_child_const(&self, file: &mut File) -> Result<()> {
274        let Some(child) = &self.child else {
275            return Ok(());
276        };
277        for child in child {
278            writeln!(
279                file,
280                "const {0} = document.getElementById('{0}');",
281                child.id()
282            )?;
283            child.script_child_const(file)?;
284        }
285        Ok(())
286    }
287    /*     /// Return the names of all children
288    fn get_children_name(&self, names: &mut Vec<String>) {
289        let Some(child) = &self.child else {
290            return;
291        };
292        for child in child {
293            names.push(child.name.clone());
294            child.get_children_name(names);
295        }
296    } */
297    /// Return the ids of all children
298    fn get_children_id(&self, ids: &mut Vec<String>) {
299        let Some(child) = &self.child else {
300            return;
301        };
302        for child in child {
303            ids.push(child.id());
304            child.get_children_id(ids);
305        }
306    }
307    /// Homing script
308    fn script_home(&self) -> String {
309        let mut ids = vec![];
310        self.get_children_id(&mut ids);
311        format!(
312            r#"
313document.addEventListener('keydown', function (event) {{
314    if (event.key === 'Home') {{
315// Show graph1
316{0}.classList.remove('hidden');
317// Hide other graphs
318{1}
319    }}
320}});
321        "#,
322            self.id(),
323            ids.into_iter()
324                .map(|id| format!("{0}.classList.add('hidden');", id))
325                .collect::<Vec<String>>()
326                .join("\n")
327        )
328    }
329    /// Writes the flowchart to an HTML file
330    pub fn to_html(&self) -> Result<PathBuf> {
331        log::debug!("{:}", self);
332        let data_repo = env::var("DATA_REPO").unwrap_or(".".into());
333        let path = Path::new(&data_repo).join(format!("{}_flowchart.html", self.name));
334        let mut file = File::create(&path).unwrap();
335        writeln!(file, "<!DOCTYPE html>")?;
336        writeln!(file, r#"<html lang="en">"#)?;
337        writeln!(
338            file,
339            "{}",
340            HEAD.replace("GRAPH", &format!("{} Flowchart", self.name.to_uppercase()))
341        )?;
342        writeln!(file, "<body>")?;
343        writeln!(
344            file,
345            r#"<div class="info-container">Left Click on System: show ; Left Click followed by Escape key: back-up ; Home key: back to root</div>"#
346        )?;
347
348        writeln!(file, r#"    <div class="svg-container">"#)?;
349        self.child_svg(&mut file, None)?;
350        writeln!(file, "      </div>")?;
351        writeln!(file, "<script>")?;
352        writeln!(
353            file,
354            "const {0} = document.getElementById('{0}');",
355            self.id()
356        )?;
357        write!(file, "{}", self.script_home())?;
358        self.script_child_const(&mut file)?;
359        self.script_child_hover(&mut file)?;
360        writeln!(file, "</script>")?;
361        writeln!(file, "</body>")?;
362        writeln!(file, "</html>")?;
363        Ok(path)
364    }
365}
366
367impl Display for Render {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        writeln!(f, "==>> {}", self.name)?;
370        // writeln!(f, "{} ...", &self.render[..self.render.len().min(64)])?;
371        if let Some(child) = &self.child {
372            for (i, child) in child.iter().enumerate() {
373                writeln!(f, "{} child #{i}", self.name)?;
374                writeln!(f, "{}", child)?;
375            }
376        }
377        writeln!(f, " <<== {}", self.name)?;
378        Ok(())
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    #[test]
385    fn hash() {
386        use std::hash::{DefaultHasher, Hash, Hasher};
387        let mut hasher = DefaultHasher::new();
388        let s = String::from("M1@80");
389        s.hash(&mut hasher);
390        let sh = hasher.finish();
391        println!("{s} -> {sh:x}");
392        let s = String::from("GMT Servo-Mechanisms (M1@80)");
393        s.hash(&mut hasher);
394        let sh = hasher.finish();
395        println!("{s} -> {sh:x}");
396    }
397}