flow_rs_core/auto_layout/
manager.rs1use std::collections::{hash_map::DefaultHasher, HashMap};
4use std::hash::{Hash, Hasher};
5
6use crate::error::Result;
7use crate::graph::Graph;
8use crate::layout::{
9 CircularLayout, ForceDirectedLayout, GridLayout, HierarchicalLayout, LayoutAlgorithm,
10};
11use crate::types::{NodeId, Position};
12
13use super::analysis::GraphAnalysis;
14use super::config::{AutoLayoutConfig, AutoLayoutStrategy};
15use super::transitions::{TransitionState, update_transition};
16
17const ALGORITHM_HIERARCHICAL: &str = "Hierarchical";
19const ALGORITHM_FORCE_DIRECTED: &str = "ForceDirected";
20const ALGORITHM_GRID: &str = "Grid";
21const ALGORITHM_CIRCULAR: &str = "Circular";
22
23#[derive(Debug)]
28pub struct AutoLayoutManager {
29 config: AutoLayoutConfig,
30 current_algorithm: Option<String>,
31 graph_hash: Option<u64>,
32 transition_state: Option<TransitionState>,
33}
34
35impl AutoLayoutManager {
36 pub fn new() -> Self {
38 Self::with_config(AutoLayoutConfig::default())
39 }
40
41 pub fn with_config(config: AutoLayoutConfig) -> Self {
43 Self {
44 config,
45 current_algorithm: None,
46 graph_hash: None,
47 transition_state: None,
48 }
49 }
50
51 pub fn select_layout_algorithm<N, E>(&self, graph: &Graph<N, E>) -> Result<String> {
53 let analysis = GraphAnalysis::analyze(graph);
54
55 match self.config.strategy {
56 AutoLayoutStrategy::Smart => self.select_smart_algorithm(&analysis),
57 AutoLayoutStrategy::HierarchyFirst => self.select_hierarchy_first_algorithm(&analysis),
58 AutoLayoutStrategy::ForceDirectedFirst => {
59 self.select_force_directed_first_algorithm(&analysis)
60 }
61 AutoLayoutStrategy::SimpleFirst => self.select_simple_first_algorithm(&analysis),
62 }
63 }
64
65 pub fn apply_auto_layout<N: Clone, E>(&mut self, graph: &mut Graph<N, E>) -> Result<()> {
67 let selected_algorithm = self.select_layout_algorithm(graph)?;
68
69 let algorithm_changed = self.current_algorithm.as_ref() != Some(&selected_algorithm);
71 let graph_changed = self.has_graph_changed(graph);
72
73 if algorithm_changed || graph_changed || self.config.force_relayout_on_change {
74 self.apply_layout_with_algorithm(graph, &selected_algorithm)?;
75 self.current_algorithm = Some(selected_algorithm);
76 self.update_graph_hash(graph);
77 }
78
79 Ok(())
80 }
81
82 pub fn current_algorithm(&self) -> Option<&str> {
84 self.current_algorithm.as_deref()
85 }
86
87 pub fn is_transitioning(&self) -> bool {
89 self.transition_state.is_some()
90 }
91
92 pub fn transition_progress(&self) -> Option<f64> {
94 self.transition_state.as_ref().map(|state| state.progress)
95 }
96
97 pub fn update_transition<N, E>(
99 &mut self,
100 graph: &mut Graph<N, E>,
101 delta_time: f64,
102 ) -> Result<bool> {
103 update_transition(&mut self.transition_state, graph, delta_time)
104 }
105
106 fn select_smart_algorithm(&self, analysis: &GraphAnalysis) -> Result<String> {
109 if analysis.node_count <= self.config.small_graph_threshold {
110 if analysis.is_tree {
111 Ok(ALGORITHM_HIERARCHICAL.to_string())
112 } else {
113 Ok(ALGORITHM_GRID.to_string())
114 }
115 } else if analysis.is_dag {
116 Ok(ALGORITHM_HIERARCHICAL.to_string())
117 } else if analysis.density > 0.3 {
118 Ok(ALGORITHM_FORCE_DIRECTED.to_string())
119 } else {
120 Ok(ALGORITHM_GRID.to_string())
121 }
122 }
123
124 fn select_hierarchy_first_algorithm(&self, analysis: &GraphAnalysis) -> Result<String> {
125 if analysis.is_dag {
126 Ok(ALGORITHM_HIERARCHICAL.to_string())
127 } else if analysis.node_count <= self.config.small_graph_threshold {
128 Ok(ALGORITHM_CIRCULAR.to_string())
129 } else {
130 Ok(ALGORITHM_FORCE_DIRECTED.to_string())
131 }
132 }
133
134 fn select_force_directed_first_algorithm(&self, analysis: &GraphAnalysis) -> Result<String> {
135 if analysis.node_count <= self.config.small_graph_threshold {
136 Ok(ALGORITHM_CIRCULAR.to_string())
137 } else {
138 Ok(ALGORITHM_FORCE_DIRECTED.to_string())
139 }
140 }
141
142 fn select_simple_first_algorithm(&self, analysis: &GraphAnalysis) -> Result<String> {
143 if analysis.node_count <= self.config.small_graph_threshold {
144 if analysis.is_tree {
145 Ok(ALGORITHM_HIERARCHICAL.to_string())
146 } else {
147 Ok(ALGORITHM_GRID.to_string())
148 }
149 } else {
150 Ok(ALGORITHM_FORCE_DIRECTED.to_string())
151 }
152 }
153
154 fn has_graph_changed<N, E>(&self, graph: &Graph<N, E>) -> bool {
155 let current_hash = self.calculate_graph_hash(graph);
156 self.graph_hash != Some(current_hash)
157 }
158
159 fn update_graph_hash<N, E>(&mut self, graph: &Graph<N, E>) {
160 self.graph_hash = Some(self.calculate_graph_hash(graph));
161 }
162
163 fn apply_layout_with_algorithm<N: Clone, E>(
164 &mut self,
165 graph: &mut Graph<N, E>,
166 algorithm: &str,
167 ) -> Result<()> {
168 let _from_positions = if self.config.enable_transitions && self.transition_state.is_none() {
170 Some(
171 graph
172 .nodes()
173 .map(|node| (node.id.clone(), node.position))
174 .collect::<HashMap<NodeId, Position>>(),
175 )
176 } else {
177 None
178 };
179
180 let result = match algorithm {
182 ALGORITHM_HIERARCHICAL => {
183 let mut layout = HierarchicalLayout::new();
184 layout.apply(graph)
185 }
186 ALGORITHM_FORCE_DIRECTED => {
187 let mut layout = ForceDirectedLayout::new();
188 layout.apply(graph)
189 }
190 ALGORITHM_GRID => {
191 let mut layout = GridLayout::new();
192 layout.apply(graph)
193 }
194 ALGORITHM_CIRCULAR => {
195 let mut layout = CircularLayout::new();
196 layout.apply(graph)
197 }
198 _ => {
199 let mut layout = ForceDirectedLayout::new();
200 layout.apply(graph)
201 }
202 };
203
204 let any_moved = graph.nodes().any(|node| node.position != Position::new(0.0, 0.0));
206 if !any_moved {
207 self.apply_simple_grid_layout(graph)?;
209 }
210
211 result
212 }
213
214 fn apply_simple_grid_layout<N: Clone, E>(&self, graph: &mut Graph<N, E>) -> Result<()> {
216 let node_ids: Vec<_> = graph.nodes().map(|node| node.id.clone()).collect();
217 let cols = (node_ids.len() as f64).sqrt().ceil() as usize;
218
219 for (i, node_id) in node_ids.iter().enumerate() {
220 let row = i / cols;
221 let col = i % cols;
222 let x = col as f64 * 150.0;
223 let y = row as f64 * 100.0;
224
225 if let Some(graph_node) = graph.get_node_mut(node_id) {
226 graph_node.position = Position::new(x, y);
227 }
228 }
229
230 Ok(())
231 }
232
233
234 fn calculate_graph_hash<N, E>(&self, graph: &Graph<N, E>) -> u64 {
235 let mut hasher = DefaultHasher::new();
236 graph.node_count().hash(&mut hasher);
237 graph.edge_count().hash(&mut hasher);
238
239 let mut node_ids: Vec<_> = graph.nodes().map(|n| &n.id).collect();
241 node_ids.sort();
242 for id in node_ids {
243 id.hash(&mut hasher);
244 }
245
246 let mut edges: Vec<_> = graph.edges().map(|e| (&e.source, &e.target)).collect();
248 edges.sort();
249 for (source, target) in edges {
250 source.hash(&mut hasher);
251 target.hash(&mut hasher);
252 }
253
254 hasher.finish()
255 }
256}
257
258impl Default for AutoLayoutManager {
259 fn default() -> Self {
260 Self::new()
261 }
262}