1#![cfg(feature = "elk")]
6
7use std::collections::BTreeMap;
8
9use crate::doc::{EdgeRouting, LayoutAlgorithm, LayoutDirection, LayoutOpts};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DiagramKind {
14 Network,
15 Reaction,
16}
17
18fn direction_elk(d: LayoutDirection) -> &'static str {
19 match d {
20 LayoutDirection::Right => "RIGHT",
21 LayoutDirection::Left => "LEFT",
22 LayoutDirection::Up => "UP",
23 LayoutDirection::Down => "DOWN",
24 }
25}
26
27fn routing_elk(r: EdgeRouting) -> &'static str {
28 match r {
29 EdgeRouting::Orthogonal => "ORTHOGONAL",
30 EdgeRouting::Polyline => "POLYLINE",
31 EdgeRouting::Splines => "SPLINES",
32 }
33}
34
35fn algorithm_elk(a: LayoutAlgorithm) -> &'static str {
36 match a {
37 LayoutAlgorithm::Layered => "layered",
38 LayoutAlgorithm::Radial => "radial",
39 LayoutAlgorithm::Force => "force",
40 LayoutAlgorithm::Stress => "stress",
41 }
42}
43
44pub fn scheme_layout_options(
49 kind: DiagramKind,
50 opts: &LayoutOpts,
51) -> BTreeMap<String, String> {
52 let mut base: BTreeMap<String, String> = BTreeMap::from([
53 ("elk.algorithm".into(), "layered".into()),
54 ("elk.direction".into(), "RIGHT".into()),
55 ("elk.edgeRouting".into(), "POLYLINE".into()),
56 ("elk.layered.unnecessaryBendpoints".into(), "false".into()),
58 ("elk.spacing.nodeNode".into(), "40".into()),
59 ("elk.spacing.edgeEdge".into(), "16".into()),
60 ("elk.spacing.edgeNode".into(), "20".into()),
61 ("elk.layered.spacing.nodeNodeBetweenLayers".into(), "48".into()),
62 ("elk.layered.spacing.edgeNodeBetweenLayers".into(), "24".into()),
63 (
64 "elk.layered.crossingMinimization.strategy".into(),
65 "LAYER_SWEEP".into(),
66 ),
67 (
68 "elk.layered.nodePlacement.strategy".into(),
69 "NETWORK_SIMPLEX".into(),
70 ),
71 ]);
72
73 if kind == DiagramKind::Reaction {
74 base.insert(
75 "elk.spacing.nodeNode".into(),
76 LayoutOpts::DEFAULT_NODE_SPACING.to_string(),
77 );
78 base.insert(
79 "elk.layered.spacing.nodeNodeBetweenLayers".into(),
80 LayoutOpts::DEFAULT_LAYER_SPACING.to_string(),
81 );
82 base.insert(
83 "elk.layered.spacing.edgeNodeBetweenLayers".into(),
84 LayoutOpts::DEFAULT_LAYER_SPACING.to_string(),
85 );
86 base.insert("elk.spacing.edgeEdge".into(), "16".into());
87 base.insert(
88 "elk.layered.crossingMinimization.forceNodeModelOrder".into(),
89 "false".into(),
90 );
91 }
92
93 if opts.algorithm.is_some() {
94 base.insert(
95 "elk.algorithm".into(),
96 algorithm_elk(opts.algorithm_or_default()).into(),
97 );
98 }
99 if opts.direction.is_some() {
100 base.insert(
101 "elk.direction".into(),
102 direction_elk(opts.direction_or_default()).into(),
103 );
104 }
105 if opts.edge_routing.is_some() {
106 base.insert(
107 "elk.edgeRouting".into(),
108 routing_elk(opts.edge_routing_or_default()).into(),
109 );
110 }
111 if let Some(ns) = opts.node_spacing {
112 base.insert("elk.spacing.nodeNode".into(), ns.to_string());
113 }
114 if let Some(ls) = opts.layer_spacing {
115 base.insert(
116 "elk.layered.spacing.nodeNodeBetweenLayers".into(),
117 ls.to_string(),
118 );
119 base.insert(
120 "elk.layered.spacing.edgeNodeBetweenLayers".into(),
121 (ls * 0.5).max(0.0).to_string(),
122 );
123 }
124 base
125}
126
127pub fn layout_json(input: &str) -> Result<String, String> {
131 let value = elkrs::create_elk().layout_json(input)?;
132 Ok(value.to_string())
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn reaction_defaults_tight_pack_no_extra_bends() {
141 let opts = LayoutOpts::default();
142 let m = scheme_layout_options(DiagramKind::Reaction, &opts);
143 assert_eq!(m.get("elk.algorithm").unwrap(), "layered");
144 assert_eq!(m.get("elk.layered.unnecessaryBendpoints").unwrap(), "false");
145 assert_eq!(
146 m.get("elk.spacing.nodeNode").unwrap(),
147 &LayoutOpts::DEFAULT_NODE_SPACING.to_string()
148 );
149 assert_eq!(
150 m.get("elk.layered.spacing.nodeNodeBetweenLayers").unwrap(),
151 &LayoutOpts::DEFAULT_LAYER_SPACING.to_string()
152 );
153 }
154
155 #[test]
156 fn algorithm_override_selects_force() {
157 let opts = LayoutOpts {
158 algorithm: Some(LayoutAlgorithm::Force),
159 ..Default::default()
160 };
161 let m = scheme_layout_options(DiagramKind::Network, &opts);
162 assert_eq!(m.get("elk.algorithm").unwrap(), "force");
163 }
164
165 #[test]
166 fn layout_opts_map_direction_routing_spacing() {
167 for (dir, want) in [
168 (LayoutDirection::Right, "RIGHT"),
169 (LayoutDirection::Left, "LEFT"),
170 (LayoutDirection::Up, "UP"),
171 (LayoutDirection::Down, "DOWN"),
172 ] {
173 let opts = LayoutOpts {
174 direction: Some(dir),
175 ..Default::default()
176 };
177 let m = scheme_layout_options(DiagramKind::Network, &opts);
178 assert_eq!(m.get("elk.direction").unwrap(), want);
179 }
180 for (r, want) in [
181 (EdgeRouting::Orthogonal, "ORTHOGONAL"),
182 (EdgeRouting::Polyline, "POLYLINE"),
183 (EdgeRouting::Splines, "SPLINES"),
184 ] {
185 let opts = LayoutOpts {
186 edge_routing: Some(r),
187 ..Default::default()
188 };
189 let m = scheme_layout_options(DiagramKind::Network, &opts);
190 assert_eq!(m.get("elk.edgeRouting").unwrap(), want);
191 }
192 for (a, want) in [
193 (LayoutAlgorithm::Layered, "layered"),
194 (LayoutAlgorithm::Radial, "radial"),
195 (LayoutAlgorithm::Force, "force"),
196 (LayoutAlgorithm::Stress, "stress"),
197 ] {
198 let opts = LayoutOpts {
199 algorithm: Some(a),
200 ..Default::default()
201 };
202 assert_eq!(
203 scheme_layout_options(DiagramKind::Reaction, &opts)
204 .get("elk.algorithm")
205 .unwrap(),
206 want
207 );
208 }
209 let opts = LayoutOpts {
210 node_spacing: Some(24.0),
211 layer_spacing: Some(40.0),
212 ..Default::default()
213 };
214 let m = scheme_layout_options(DiagramKind::Reaction, &opts);
215 assert_eq!(m.get("elk.spacing.nodeNode").unwrap(), "24");
216 assert_eq!(
217 m.get("elk.layered.spacing.nodeNodeBetweenLayers").unwrap(),
218 "40"
219 );
220 assert_eq!(
221 m.get("elk.layered.spacing.edgeNodeBetweenLayers").unwrap(),
222 "20"
223 );
224 }
225
226 #[test]
227 fn layered_orthogonal_emits_edge_sections() {
228 let input = r#"{
229 "id": "root",
230 "layoutOptions": {
231 "elk.algorithm": "layered",
232 "elk.direction": "RIGHT",
233 "elk.edgeRouting": "ORTHOGONAL"
234 },
235 "children": [
236 {"id": "a", "width": 40.0, "height": 30.0},
237 {"id": "extra", "width": 10.0, "height": 10.0},
238 {"id": "b", "width": 40.0, "height": 30.0}
239 ],
240 "edges": [{"id": "e0", "sources": ["a"], "targets": ["b"]}]
241 }"#;
242 let out = layout_json(input).expect("layout");
243 assert!(out.contains("\"x\""), "{out}");
244 assert!(
245 out.contains("sections") && out.contains("startPoint"),
246 "{out}"
247 );
248 let parsed: serde_json::Value = serde_json::from_str(&out).expect("json");
250 let children = parsed["children"].as_array().expect("children");
251 let mut ax = None;
252 let mut bx = None;
253 for c in children {
254 match c["id"].as_str() {
255 Some("a") => ax = c["x"].as_f64(),
256 Some("b") => bx = c["x"].as_f64(),
257 _ => {}
258 }
259 }
260 assert!(bx.unwrap() > ax.unwrap(), "b.x should be > a.x: {out}");
261 let edge = &parsed["edges"][0];
262 assert!(edge["sections"].as_array().map(|s| !s.is_empty()).unwrap_or(false));
263 assert!(edge["sections"][0].get("startPoint").is_some());
264 }
265
266 #[test]
267 fn layered_ignores_extra_children_in_assert_loop() {
268 let input = r#"{
270 "id": "root",
271 "layoutOptions": {
272 "elk.algorithm": "layered",
273 "elk.direction": "RIGHT",
274 "elk.edgeRouting": "ORTHOGONAL"
275 },
276 "children": [
277 {"id": "a", "width": 40.0, "height": 30.0},
278 {"id": "extra", "width": 20.0, "height": 20.0},
279 {"id": "b", "width": 40.0, "height": 30.0}
280 ],
281 "edges": [{"id": "e0", "sources": ["a"], "targets": ["b"]}]
282 }"#;
283 let out = layout_json(input).expect("layout");
284 let parsed: serde_json::Value = serde_json::from_str(&out).expect("json");
285 assert!(parsed["children"].as_array().unwrap().len() >= 3);
286 }
287}