flow_rs_core/auto_layout/
manager.rs

1//! Auto Layout Manager - Intelligent layout algorithm selection and management
2
3use 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
17// Layout algorithm names as constants
18const ALGORITHM_HIERARCHICAL: &str = "Hierarchical";
19const ALGORITHM_FORCE_DIRECTED: &str = "ForceDirected";
20const ALGORITHM_GRID: &str = "Grid";
21const ALGORITHM_CIRCULAR: &str = "Circular";
22
23/// Auto Layout Manager - Intelligent layout algorithm selection and management
24///
25/// This manager automatically selects the best layout algorithm based on graph
26/// characteristics and provides smooth transitions between different layouts.
27#[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    /// Create a new auto layout manager with default configuration
37    pub fn new() -> Self {
38        Self::with_config(AutoLayoutConfig::default())
39    }
40
41    /// Create a new auto layout manager with custom configuration
42    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    /// Select the best layout algorithm for the given graph
52    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    /// Apply automatic layout to the graph
66    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        // Check if we need to change algorithms
70        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    /// Get the currently selected layout algorithm name
83    pub fn current_algorithm(&self) -> Option<&str> {
84        self.current_algorithm.as_deref()
85    }
86
87    /// Check if a layout transition is currently in progress
88    pub fn is_transitioning(&self) -> bool {
89        self.transition_state.is_some()
90    }
91
92    /// Get the progress of the current transition (0.0 to 1.0)
93    pub fn transition_progress(&self) -> Option<f64> {
94        self.transition_state.as_ref().map(|state| state.progress)
95    }
96
97    /// Update transition state (should be called each frame during animation)
98    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    // Private implementation methods
107
108    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        // Store current positions for transition if enabled
169        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        // Apply the selected layout algorithm
181        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        // Verify that nodes were actually moved
205        let any_moved = graph.nodes().any(|node| node.position != Position::new(0.0, 0.0));
206        if !any_moved {
207            // Fallback: Simple grid layout
208            self.apply_simple_grid_layout(graph)?;
209        }
210        
211        result
212    }
213    
214    /// Fallback simple grid layout when other algorithms fail
215    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        // Hash node IDs
240        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        // Hash edge connections
247        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}