diff --git a/src/main.rs b/src/main.rs index 9c7e33f3..2d07ee55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -724,7 +724,7 @@ fn main() { .id, ); if *interactive { - view_operations(&operation_conn, &operations); + view_operations(&conn, &operation_conn, &operations); } else { let mut indicator = ""; println!( diff --git a/src/models/block_group.rs b/src/models/block_group.rs index 2e3252de..12d93eca 100644 --- a/src/models/block_group.rs +++ b/src/models/block_group.rs @@ -183,6 +183,17 @@ impl BlockGroup { } } + pub fn get_by_ids(conn: &Connection, ids: &[i64]) -> Vec { + let query = "SELECT * FROM block_groups WHERE id IN rarray(?1)"; + Self::query( + conn, + query, + params!(Rc::new( + ids.iter().map(|i| SQLValue::from(*i)).collect::>() + )), + ) + } + pub fn clone(conn: &Connection, source_block_group_id: i64, target_block_group_id: i64) { let existing_paths = Path::query( conn, diff --git a/src/models/operations.rs b/src/models/operations.rs index c2f7a79a..ecc509f5 100644 --- a/src/models/operations.rs +++ b/src/models/operations.rs @@ -1,14 +1,22 @@ -use crate::graph::{all_simple_paths, OperationGraph}; +use crate::graph::{all_simple_paths, GraphEdge, GraphNode, OperationGraph}; use crate::models::file_types::FileTypes; use crate::models::traits::*; -use petgraph::graphmap::UnGraphMap; +use crate::operation_management::{ + load_changeset, load_changeset_dependencies, load_changeset_models, +}; +use crate::views::patch::get_change_graph; +use petgraph::graphmap::{DiGraphMap, UnGraphMap}; use petgraph::visit::{Dfs, Reversed}; use petgraph::Direction; +use rusqlite::session::ChangesetIter; use rusqlite::types::Value; +use rusqlite::Error as SQLError; use rusqlite::{params, params_from_iter, Connection, Result as SQLResult, Row}; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use std::io::Read; use std::string::ToString; +use thiserror::Error; #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct Operation { @@ -19,6 +27,12 @@ pub struct Operation { pub change_type: String, } +#[derive(Debug, Error, PartialEq)] +pub enum OperationError { + #[error("SQLite Error: {0}")] + SqliteError(#[from] SQLError), +} + impl Operation { pub fn create( conn: &Connection, @@ -106,6 +120,23 @@ impl Operation { graph } + pub fn get_change_graph( + conn: &Connection, + hash: &str, + ) -> Result>, OperationError> { + let operation = Operation::get_by_hash(conn, hash)?; + + let changeset = load_changeset(&operation); + let dependencies = load_changeset_dependencies(&operation); + + let input: &mut dyn Read = &mut changeset.as_slice(); + let mut iter = ChangesetIter::start_strm(&input).unwrap(); + + let new_models = load_changeset_models(&mut iter); + + Ok(get_change_graph(&new_models, &dependencies)) + } + pub fn get_path_between( conn: &Connection, source_id: &str, @@ -160,19 +191,11 @@ impl Operation { patch_path } - pub fn get(conn: &Connection, query: &str, placeholders: Vec) -> SQLResult { - let mut stmt = conn.prepare(query).unwrap(); - let mut rows = stmt.query_map(params_from_iter(placeholders), |row| { - Ok(Self::process_row(row)) - })?; - rows.next().unwrap() - } - pub fn get_by_hash(conn: &Connection, op_hash: &str) -> SQLResult { Operation::get( conn, "select * from operation where hash LIKE ?1", - vec![Value::from(format!("{op_hash}%"))], + params![Value::from(format!("{op_hash}%"))], ) } } diff --git a/src/views/block_group.rs b/src/views/block_group.rs index de1bd435..e1348a66 100644 --- a/src/views/block_group.rs +++ b/src/views/block_group.rs @@ -1,5 +1,5 @@ use crate::models::{block_group::BlockGroup, node::Node, traits::Query}; -use crate::views::block_group_viewer::{NavDirection, PlotParameters, Viewer}; +use crate::views::block_group_viewer::{PlotParameters, Viewer}; use rusqlite::{params, Connection}; use core::panic; @@ -7,7 +7,7 @@ use std::error::Error; use std::time::{Duration, Instant}; use crossterm::{ - event::{self, KeyCode, KeyEventKind, KeyModifiers}, + event::{self, KeyCode, KeyEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -93,53 +93,67 @@ pub fn view_block_group( // The outer layout is a vertical split between the canvas and the status bar let outer_layout = ratatui::layout::Layout::default() .direction(ratatui::layout::Direction::Vertical) - .constraints(vec! - [ - ratatui::layout::Constraint::Min(1), - ratatui::layout::Constraint::Length(status_bar_height), - ] - ) + .constraints(vec![ + ratatui::layout::Constraint::Min(1), + ratatui::layout::Constraint::Length(status_bar_height), + ]) .split(frame.area()); // The inner layout is a vertical split between the canvas and the panel let inner_layout = ratatui::layout::Layout::default() .direction(ratatui::layout::Direction::Vertical) - .constraints(vec! - [ - Constraint::Percentage(75), - Constraint::Percentage(25), - ] - ) + .constraints(vec![Constraint::Percentage(75), Constraint::Percentage(25)]) .split(outer_layout[0]); - let canvas_area = if show_panel { inner_layout[0] } else { outer_layout[0] }; - let panel_area = if show_panel { inner_layout[1] } else { Rect::default() }; + let canvas_area = if show_panel { + inner_layout[0] + } else { + outer_layout[0] + }; + let panel_area = if show_panel { + inner_layout[1] + } else { + Rect::default() + }; let status_bar_area = outer_layout[1]; - // Ask the viewer to paint the canvas - viewer.paint_canvas(frame, canvas_area); - + let status_message = format!( + "{message} | return: show information on block | q=quit", + message = Viewer::get_status_line() + ); // Status bar let status_bar_contents = format!( - "{:width$}", - "◀ ▼ ▲ ▶ select blocks (+shift/alt to scroll) | +/- zoom | return: show information on block | q=quit", - width = status_bar_area.width as usize); + "{status_message:width$}", + width = status_bar_area.width as usize + ); - let status_bar = Paragraph::new(Text::styled(status_bar_contents, - Style::default().bg(Color::DarkGray).fg(Color::White))); + let status_bar = Paragraph::new(Text::styled( + status_bar_contents, + Style::default().bg(Color::DarkGray).fg(Color::White), + )); frame.render_widget(status_bar, status_bar_area); + // Ask the viewer to paint the canvas + viewer.draw(frame, canvas_area); + // Panel if show_panel { - let panel_block = Block::bordered().padding(Padding::new(2, 2, 1, 1)).title("Details"); + let panel_block = Block::bordered() + .padding(Padding::new(2, 2, 1, 1)) + .title("Details"); let mut panel_text = Text::from("No content found"); // Get information about the currently selected block if viewer.state.selected_block.is_some() { let selected_block = viewer.state.selected_block.unwrap(); - panel_text = Text::from(format!("Block ID: {}\nNode ID: {}\nStart: {}\nEnd: {}\n", - selected_block.block_id, selected_block.node_id, selected_block.sequence_start, selected_block.sequence_end)); + panel_text = Text::from(format!( + "Block ID: {}\nNode ID: {}\nStart: {}\nEnd: {}\n", + selected_block.block_id, + selected_block.node_id, + selected_block.sequence_start, + selected_block.sequence_end + )); } let panel_content = Paragraph::new(panel_text) @@ -171,111 +185,6 @@ pub fn view_block_group( break; } match key.code { - // Scrolling through the graph - KeyCode::Left => { - if key.modifiers.contains(KeyModifiers::SHIFT) { - viewer.state.offset_x -= viewer.state.viewport.width as i32 / 3; - viewer.unselect_if_not_visible(); - } else if key.modifiers.contains(KeyModifiers::ALT) { - viewer.state.offset_x -= 1; - } else { - // If no block is selected, select the center block - if viewer.state.selected_block.is_none() { - viewer.select_center_block(); - } - viewer.move_selection(NavDirection::Left); - } - } - KeyCode::Right => { - if key.modifiers.contains(KeyModifiers::SHIFT) { - viewer.state.offset_x += viewer.state.viewport.width as i32 / 3; - viewer.unselect_if_not_visible(); - } else if key.modifiers.contains(KeyModifiers::ALT) { - viewer.state.offset_x += 1; - } else { - if viewer.state.selected_block.is_none() { - viewer.select_center_block(); - } - viewer.move_selection(NavDirection::Right); - } - } - KeyCode::Up => { - if key.modifiers.contains(KeyModifiers::SHIFT) { - viewer.state.offset_y += viewer.state.viewport.height as i32 / 3; - viewer.unselect_if_not_visible(); - } else if key.modifiers.contains(KeyModifiers::ALT) { - viewer.state.offset_y += 1; - } else { - if viewer.state.selected_block.is_none() { - viewer.select_center_block(); - } - viewer.move_selection(NavDirection::Down); - } - } - KeyCode::Down => { - if key.modifiers.contains(KeyModifiers::SHIFT) { - viewer.state.offset_y -= viewer.state.viewport.height as i32 / 3; - viewer.unselect_if_not_visible(); - } else if key.modifiers.contains(KeyModifiers::ALT) { - viewer.state.offset_y -= 1; - } else { - if viewer.state.selected_block.is_none() { - viewer.select_center_block(); - } - viewer.move_selection(NavDirection::Up); - } - } - // Zooming in and out - KeyCode::Char('+') | KeyCode::Char('=') => { - // Increase how much of the sequence is shown in each block label. - if viewer.parameters.label_width == u32::MAX { - viewer.parameters.scale += 1; - } else { - viewer.parameters.label_width = match viewer.parameters.label_width - { - 1 => 11, - 11 => 100, - 100 => u32::MAX, - _ => u32::MAX, - } - } - - // If no block is selected, select the center block - if viewer.state.selected_block.is_none() { - viewer.select_center_block(); - } - - // Recalculate the layout. - viewer - .scaled_layout - .refresh(&viewer.base_layout, &viewer.parameters); - viewer.center_on_block(viewer.state.selected_block.unwrap()); - } - KeyCode::Char('-') | KeyCode::Char('_') => { - // Decrease how much of the sequence is shown in each block label. - if viewer.parameters.scale > 2 { - viewer.parameters.scale -= 1; - } else { - viewer.parameters.label_width = match viewer.parameters.label_width - { - u32::MAX => 100, - 100 => 11, - 11 => 1, - _ => 1, - }; - } - - // If no block is selected, select the center block - if viewer.state.selected_block.is_none() { - viewer.select_center_block(); - } - - // Recalculate the layout. - viewer - .scaled_layout - .refresh(&viewer.base_layout, &viewer.parameters); - viewer.center_on_block(viewer.state.selected_block.unwrap()); - } // Performing actions on blocks KeyCode::Tab => { // Future implementation: switch between panels @@ -290,7 +199,9 @@ pub fn view_block_group( // Show information on the selected block, if there is one show_panel = viewer.state.selected_block.is_some(); } - _ => {} + _ => { + viewer.handle_input(key); + } } } } diff --git a/src/views/block_group_viewer.rs b/src/views/block_group_viewer.rs index a60f7342..cea3cf09 100644 --- a/src/views/block_group_viewer.rs +++ b/src/views/block_group_viewer.rs @@ -5,6 +5,7 @@ use crate::models::sequence::Sequence; use crate::views::block_layout::{BaseLayout, ScaledLayout}; use core::panic; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use petgraph::graphmap::DiGraphMap; use petgraph::Direction; use ratatui::{ @@ -92,6 +93,7 @@ pub struct Viewer<'a> { pub state: State, pub parameters: PlotParameters, pub origin_block: Option, + view_block: Block<'a>, } impl<'a> Viewer<'a> { @@ -148,6 +150,7 @@ impl<'a> Viewer<'a> { state: State::default(), parameters: plot_parameters, origin_block, + view_block: Block::default(), } } @@ -157,6 +160,10 @@ impl<'a> Viewer<'a> { .refresh(&self.base_layout, &self.parameters); } + pub fn set_block(&mut self, block: Block<'a>) { + self.view_block = block; + } + /// Check if a block is visible in the viewport. pub fn is_block_visible(&self, block: GraphNode) -> bool { if let Some(((x, y), _)) = self.scaled_layout.labels.get(&block) { @@ -191,7 +198,7 @@ impl<'a> Viewer<'a> { /// Draw and render blocks and lines to a canvas through a scrollable window. /// TODO: turn this into the render function of a custom stateful widget - pub fn paint_canvas(&mut self, frame: &mut ratatui::Frame, area: Rect) { + pub fn draw(&mut self, frame: &mut ratatui::Frame, area: Rect) { // Set up the coordinate systems for the window and the canvas, // we need to keep a 1:1 mapping between coordinates to avoid glitches. @@ -204,7 +211,7 @@ impl<'a> Viewer<'a> { // viewport has data coordinates (0,0). We must keep a 1:1 mapping between the size of the viewport // in data units and in terminal cells to avoid glitches. - let canvas_block = Block::default(); + let canvas_block = self.view_block.clone(); let viewport = canvas_block.inner(area); // Check if the viewport has changed size, and if so, update the offset to keep our reference @@ -565,6 +572,117 @@ impl<'a> Viewer<'a> { self.state.offset_y = new_offset_y.round() as i32; } } + + pub fn handle_input(&mut self, key: KeyEvent) { + // Scrolling through the graph + match key.code { + KeyCode::Left => { + if key.modifiers.contains(KeyModifiers::SHIFT) { + self.state.offset_x -= self.state.viewport.width as i32 / 3; + self.unselect_if_not_visible(); + } else if key.modifiers.contains(KeyModifiers::ALT) { + self.state.offset_x -= 1; + } else { + // If no block is selected, select the center block + if self.state.selected_block.is_none() { + self.select_center_block(); + } + self.move_selection(NavDirection::Left); + } + } + KeyCode::Right => { + if key.modifiers.contains(KeyModifiers::SHIFT) { + self.state.offset_x += self.state.viewport.width as i32 / 3; + self.unselect_if_not_visible(); + } else if key.modifiers.contains(KeyModifiers::ALT) { + self.state.offset_x += 1; + } else { + if self.state.selected_block.is_none() { + self.select_center_block(); + } + self.move_selection(NavDirection::Right); + } + } + KeyCode::Up => { + if key.modifiers.contains(KeyModifiers::SHIFT) { + self.state.offset_y += self.state.viewport.height as i32 / 3; + self.unselect_if_not_visible(); + } else if key.modifiers.contains(KeyModifiers::ALT) { + self.state.offset_y += 1; + } else { + if self.state.selected_block.is_none() { + self.select_center_block(); + } + self.move_selection(NavDirection::Down); + } + } + KeyCode::Down => { + if key.modifiers.contains(KeyModifiers::SHIFT) { + self.state.offset_y -= self.state.viewport.height as i32 / 3; + self.unselect_if_not_visible(); + } else if key.modifiers.contains(KeyModifiers::ALT) { + self.state.offset_y -= 1; + } else { + if self.state.selected_block.is_none() { + self.select_center_block(); + } + self.move_selection(NavDirection::Up); + } + } + // Zooming in and out + KeyCode::Char('+') | KeyCode::Char('=') => { + // Increase how much of the sequence is shown in each block label. + if self.parameters.label_width == u32::MAX { + self.parameters.scale += 1; + } else { + self.parameters.label_width = match self.parameters.label_width { + 1 => 11, + 11 => 100, + 100 => u32::MAX, + _ => u32::MAX, + } + } + + // If no block is selected, select the center block + if self.state.selected_block.is_none() { + self.select_center_block(); + } + + // Recalculate the layout. + self.scaled_layout + .refresh(&self.base_layout, &self.parameters); + self.center_on_block(self.state.selected_block.unwrap()); + } + KeyCode::Char('-') | KeyCode::Char('_') => { + // Decrease how much of the sequence is shown in each block label. + if self.parameters.scale > 2 { + self.parameters.scale -= 1; + } else { + self.parameters.label_width = match self.parameters.label_width { + u32::MAX => 100, + 100 => 11, + 11 => 1, + _ => 1, + }; + } + + // If no block is selected, select the center block + if self.state.selected_block.is_none() { + self.select_center_block(); + } + + // Recalculate the layout. + self.scaled_layout + .refresh(&self.base_layout, &self.parameters); + self.center_on_block(self.state.selected_block.unwrap()); + } + _ => {} + } + } + + pub fn get_status_line() -> String { + "◀ ▼ ▲ ▶ select blocks (+shift/alt to scroll) | +/- zoom".to_string() + } } /// Truncate a string to a certain length, adding an ellipsis in the middle diff --git a/src/views/operations.rs b/src/views/operations.rs index c791f639..930659ea 100644 --- a/src/views/operations.rs +++ b/src/views/operations.rs @@ -1,5 +1,8 @@ +use crate::graph::{GraphEdge, GraphNode}; +use crate::models::block_group::BlockGroup; use crate::models::operations::{Operation, OperationSummary}; use crate::models::traits::Query; +use crate::views::block_group_viewer::{PlotParameters, Viewer}; use crossterm::event::KeyModifiers; use crossterm::{ event::{self, KeyCode}, @@ -7,6 +10,7 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use itertools::Itertools; +use petgraph::prelude::DiGraphMap; use ratatui::prelude::{Color, Style, Text}; use ratatui::style::Modifier; use ratatui::widgets::Paragraph; @@ -17,6 +21,7 @@ use ratatui::{ Terminal, }; use rusqlite::{params, types::Value, Connection}; +use std::backtrace::Backtrace; use std::collections::HashMap; use std::io; use std::rc::Rc; @@ -36,7 +41,23 @@ struct OperationRow<'a> { summary: OperationSummary, } -pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<(), io::Error> { +fn restore_terminal() { + let _ = disable_raw_mode(); + let _ = execute!(io::stdout(), LeaveAlternateScreen); +} + +pub fn view_operations( + conn: &Connection, + op_conn: &Connection, + operations: &[Operation], +) -> Result<(), io::Error> { + std::panic::set_hook(Box::new(|info| { + restore_terminal(); + eprintln!("Application crashed: {info}"); + let backtrace = Backtrace::capture(); + eprintln!("Stack trace:\n{}", backtrace); + })); + let operation_by_hash: HashMap = HashMap::from_iter( operations .iter() @@ -44,7 +65,7 @@ pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<() .collect::>(), ); let summaries = OperationSummary::query( - conn, + op_conn, "select * from operation_summary where operation_hash in rarray(?1)", params![Rc::new( operations @@ -68,7 +89,18 @@ pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<() let mut terminal = Terminal::new(backend)?; let mut textarea = TextArea::default(); + let mut empty_graph: DiGraphMap = DiGraphMap::new(); + let mut blockgroup_graphs: Vec<(i64, String, DiGraphMap)> = vec![]; + let mut selected_blockgroup_graph: usize = 0; + empty_graph.add_node(GraphNode { + node_id: 1, + block_id: 0, + sequence_start: 0, + sequence_end: 1, + }); + let mut graph_viewer = Viewer::new(&empty_graph, conn, PlotParameters::default()); let mut view_message_panel = false; + let mut view_graph = false; let mut panel_focus = "operations"; let mut focus_rotation = vec!["operations"]; let mut focus_index: usize = 0; @@ -143,9 +175,24 @@ pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<() .borders(Borders::ALL) .border_style(unfocused_style), ); + if view_graph { + graph_viewer.set_block( + Block::default() + .title(if blockgroup_graphs.is_empty() { + "Change Graph".to_string() + } else { + format!( + "Change Graph {name}", + name = blockgroup_graphs[selected_blockgroup_graph].1 + ) + }) + .borders(Borders::ALL) + .border_style(unfocused_style), + ); + } if panel_focus == "message_editor" { - panel_messages.push_str(", ctrl+s=save message, esc=close message editor"); + panel_messages.push_str("| ctrl+s=save message | esc=close message editor"); textarea.set_block( Block::default() .title("Operation Summary") @@ -153,16 +200,50 @@ pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<() .border_style(focused_style), ); } else if panel_focus == "operations" { - panel_messages.push_str(", e or enter=edit message, esc or q=exit"); + panel_messages.push_str("| e or enter=edit message | v=view graph | esc or q=exit"); + } else if panel_focus == "graph_view" { + panel_messages.push_str(&format!( + " | tab = cycle block group | {l} | esc or q=exit", + l = Viewer::get_status_line() + )); + graph_viewer.set_block( + Block::default() + .title(if blockgroup_graphs.is_empty() { + "Change Graph".to_string() + } else { + format!( + "Change Graph {name}", + name = blockgroup_graphs[selected_blockgroup_graph].1 + ) + }) + .borders(Borders::ALL) + .border_style(focused_style), + ); } if view_message_panel { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(main_area); + if view_graph { + let sub_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(chunks[1]); + f.render_widget(&textarea, sub_chunk[0]); + graph_viewer.draw(f, sub_chunk[1]); + } else { + f.render_widget(&textarea, chunks[1]); + } + f.render_widget(table, chunks[0]); + } else if view_graph { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(main_area); f.render_widget(table, chunks[0]); - f.render_widget(&textarea, chunks[1]); + graph_viewer.draw(f, chunks[1]); } else { let chunks = Layout::default() .direction(Direction::Vertical) @@ -218,7 +299,7 @@ pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<() { let new_summary = textarea.lines().iter().join("\n"); let _ = OperationSummary::set_message( - conn, + op_conn, operation_summaries[selected].summary.id, &new_summary, ); @@ -226,6 +307,39 @@ pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<() } else { textarea.input(key); } + } else if panel_focus == "graph_view" { + if key.code == KeyCode::Esc || key.code == KeyCode::Char('q') { + view_graph = false; + if let Some((p, _)) = + focus_rotation.iter().find_position(|s| **s == "graph_view") + { + focus_rotation.remove(p); + } + if focus_index >= focus_rotation.len() { + focus_index = 0; + } + panel_focus = focus_rotation[focus_index]; + } else if key.code == KeyCode::Tab || key.code == KeyCode::BackTab { + if key.code == KeyCode::BackTab { + if selected_blockgroup_graph == 0 { + selected_blockgroup_graph = blockgroup_graphs.len() - 1; + } else { + selected_blockgroup_graph -= 1; + } + } else { + selected_blockgroup_graph += 1; + if selected_blockgroup_graph >= blockgroup_graphs.len() { + selected_blockgroup_graph = 0; + } + } + graph_viewer = Viewer::new( + &blockgroup_graphs[selected_blockgroup_graph].2, + conn, + PlotParameters::default(), + ); + } else { + graph_viewer.handle_input(key); + } } else { let code = key.code; match code { @@ -256,6 +370,53 @@ pub fn view_operations(conn: &Connection, operations: &[Operation]) -> Result<() }; panel_focus = focus_rotation[focus_index]; } + KeyCode::Char('v') => { + view_graph = true; + focus_index = if let Some((i, _)) = + focus_rotation.iter().find_position(|s| **s == "graph_view") + { + i + } else { + focus_rotation.push("graph_view"); + focus_rotation.len() - 1 + }; + panel_focus = focus_rotation[focus_index]; + let hash = &operation_summaries[selected].operation.hash; + let graphs = Operation::get_change_graph(op_conn, hash).unwrap(); + blockgroup_graphs.clear(); + let bg_info = BlockGroup::get_by_ids( + conn, + &graphs.keys().copied().collect::>(), + ); + let bg_map: HashMap = + HashMap::from_iter(bg_info.iter().map(|k| (k.id, k))); + for (i, v) in graphs { + blockgroup_graphs.push(( + i, + format!( + "{collection} {sample} {name}", + collection = bg_map[&i].collection_name.clone(), + sample = bg_map[&i] + .sample_name + .clone() + .unwrap_or("Reference".to_string()), + name = bg_map[&i].name.clone() + ), + v, + )); + } + selected_blockgroup_graph = 0; + if blockgroup_graphs.is_empty() { + graph_viewer = + Viewer::new(&empty_graph, conn, PlotParameters::default()); + } else { + graph_viewer = Viewer::new( + &blockgroup_graphs[selected_blockgroup_graph].2, + conn, + PlotParameters::default(), + ); + } + } _ => {} } } diff --git a/src/views/patch.rs b/src/views/patch.rs index 773f3b25..320d5349 100644 --- a/src/views/patch.rs +++ b/src/views/patch.rs @@ -1,9 +1,13 @@ +use crate::graph::{GraphEdge, GraphNode}; use crate::models::block_group_edge::BlockGroupEdge; use crate::models::edge::Edge; use crate::models::node::Node; use crate::models::sequence::Sequence; +use crate::models::strand::Strand; +use crate::models::strand::Strand::Forward; use crate::operation_management::{ - load_changeset, load_changeset_dependencies, load_changeset_models, + load_changeset, load_changeset_dependencies, load_changeset_models, ChangesetModels, + DependencyModels, }; use crate::patch::OperationPatch; use html_escape; @@ -14,11 +18,174 @@ use rusqlite::session::ChangesetIter; use std::collections::{HashMap, HashSet}; use std::io::Read; +pub fn get_change_graph( + changes: &ChangesetModels, + dependencies: &DependencyModels, +) -> HashMap> { + let start_node = Node::get_start_node(); + let end_node = Node::get_end_node(); + let mut bges_by_bg: HashMap> = HashMap::new(); + let mut edges_by_id: HashMap = HashMap::new(); + let mut nodes_by_id: HashMap = HashMap::new(); + nodes_by_id.insert(start_node.id, &start_node); + nodes_by_id.insert(end_node.id, &end_node); + let mut sequences_by_hash: HashMap<&String, &Sequence> = HashMap::new(); + let mut block_graphs: HashMap> = HashMap::new(); + + for bge in changes.block_group_edges.iter() { + bges_by_bg + .entry(bge.block_group_id) + .and_modify(|l| l.push(bge)) + .or_insert_with(|| vec![bge]); + } + for edge in changes.edges.iter().chain(dependencies.edges.iter()) { + edges_by_id.insert(edge.id, edge); + } + for node in changes.nodes.iter().chain(dependencies.nodes.iter()) { + nodes_by_id.insert(node.id, node); + } + for seq in changes + .sequences + .iter() + .chain(dependencies.sequences.iter()) + { + sequences_by_hash.insert(&seq.hash, seq); + } + + for (bg_id, bg_edges) in bges_by_bg.iter() { + // There are 2 graphs created here. The first graph is our normal graph of nodes + // and edges. This graph is then used to make our second graph representing the spans + // of each node (blocks). + let mut graph: DiGraphMap = DiGraphMap::new(); + let mut block_graph: DiGraphMap = DiGraphMap::new(); + block_graph.add_node(GraphNode { + block_id: -1, + node_id: start_node.id, + sequence_start: 0, + sequence_end: 0, + }); + block_graph.add_node(GraphNode { + block_id: -1, + node_id: end_node.id, + sequence_start: 0, + sequence_end: 0, + }); + for bg_edge in bg_edges { + let edge = *edges_by_id.get(&bg_edge.edge_id).unwrap(); + // Because our model is an edge graph, the coordinate where an edge occurs is + // actually offset from the block. So we need to adjust coordinates when going + // to blocks. This isn't true for our start node though, which has a source + // coordinate of 0. + if Node::is_start_node(edge.source_node_id) { + graph.add_edge( + edge.source_node_id, + edge.target_node_id, + (edge.source_coordinate, edge.target_coordinate), + ); + } else { + graph.add_edge( + edge.source_node_id, + edge.target_node_id, + (edge.source_coordinate - 1, edge.target_coordinate), + ); + } + } + + for node in graph.nodes() { + // This is where we make the block graph. For this, we figure out the positions of + // all incoming and outgoing edges from the node. Then we make blocks between those + // positions. + if Node::is_terminal(node) { + continue; + } + let in_ports = graph + .edges_directed(node, Direction::Incoming) + .map(|(_src, _dest, (_fp, tp))| *tp) + .collect::>(); + let out_ports = graph + .edges_directed(node, Direction::Outgoing) + .map(|(_src, _dest, (fp, _tp))| *fp) + .collect::>(); + + let node_obj = *nodes_by_id.get(&node).unwrap(); + let sequence = *sequences_by_hash.get(&node_obj.sequence_hash).unwrap(); + let s_len = sequence.length; + let mut block_starts: HashSet = HashSet::from_iter(in_ports.iter().copied()); + block_starts.insert(0); + for x in out_ports.iter() { + if *x < s_len - 1 { + block_starts.insert(x + 1); + } + } + let mut block_ends: HashSet = HashSet::from_iter(out_ports.iter().copied()); + block_ends.insert(s_len); + for x in in_ports.iter() { + if *x > 0 { + block_ends.insert(x - 1); + } + } + + let block_starts = block_starts.into_iter().sorted().collect::>(); + let block_ends = block_ends.into_iter().sorted().collect::>(); + + let mut blocks = vec![]; + for (i, j) in block_starts.iter().zip(block_ends.iter()) { + let node = GraphNode { + block_id: -1, + node_id: node, + sequence_start: *i, + sequence_end: *j, + }; + block_graph.add_node(node); + blocks.push(node); + } + + for (i, j) in blocks.iter().tuple_windows() { + block_graph.add_edge( + *i, + *j, + GraphEdge { + edge_id: -1, + source_strand: Strand::Forward, + target_strand: Forward, + chromosome_index: 0, + phased: 0, + }, + ); + } + } + + for (src, dest, (fp, tp)) in graph.all_edges() { + if !(Node::is_end_node(src) && Node::is_start_node(dest)) { + let source_block = block_graph + .nodes() + .find(|node| node.node_id == src && node.sequence_end == *fp) + .unwrap(); + let dest_block = block_graph + .nodes() + .find(|node| node.node_id == dest && node.sequence_start == *tp) + .unwrap(); + block_graph.add_edge( + source_block, + dest_block, + GraphEdge { + edge_id: -1, + source_strand: Strand::Forward, + target_strand: Forward, + chromosome_index: 0, + phased: 0, + }, + ); + } + } + block_graphs.insert(*bg_id, block_graph); + } + block_graphs +} + pub fn view_patches(patches: &[OperationPatch]) -> HashMap> { // For each blockgroup in a patch, a .dot file is generated showing how the base sequence // has been updated. - let start_node = Node::get_start_node(); - let end_node = Node::get_end_node(); let mut diagrams: HashMap> = HashMap::new(); for patch in patches { @@ -34,25 +201,10 @@ pub fn view_patches(patches: &[OperationPatch]) -> HashMap> = HashMap::new(); - let mut edges_by_id: HashMap = HashMap::new(); - let mut nodes_by_id: HashMap = HashMap::new(); - nodes_by_id.insert(start_node.id, &start_node); - nodes_by_id.insert(end_node.id, &end_node); - let mut sequences_by_hash: HashMap<&String, &Sequence> = HashMap::new(); - for bge in new_models.block_group_edges.iter() { - bges_by_bg - .entry(bge.block_group_id) - .and_modify(|l| l.push(bge)) - .or_insert_with(|| vec![bge]); - } - for edge in new_models.edges.iter().chain(dependencies.edges.iter()) { - edges_by_id.insert(edge.id, edge); - } - for node in new_models.nodes.iter().chain(dependencies.nodes.iter()) { - nodes_by_id.insert(node.id, node); - } + let block_graphs = get_change_graph(&new_models, &dependencies); + + let mut sequences_by_hash: HashMap<&String, &Sequence> = HashMap::new(); for seq in new_models .sequences .iter() @@ -60,103 +212,20 @@ pub fn view_patches(patches: &[OperationPatch]) -> HashMap = HashMap::new(); + for node in new_models.nodes.iter().chain(dependencies.nodes.iter()) { + node_sequence_hashes.insert(node.id, &node.sequence_hash); + } - for (bg_id, bg_edges) in bges_by_bg.iter() { - // There are 2 graphs created here. The first graph is our normal graph of nodes - // and edges. This graph is then used to make our second graph representing the spans - // of each node (blocks). - let mut graph: DiGraphMap = DiGraphMap::new(); - let mut block_graph: DiGraphMap<(i64, i64, i64), ()> = DiGraphMap::new(); - block_graph.add_node((start_node.id, 0, 0)); - block_graph.add_node((end_node.id, 0, 0)); - for bg_edge in bg_edges { - let edge = *edges_by_id.get(&bg_edge.edge_id).unwrap(); - // Because our model is an edge graph, the coordinate where an edge occurs is - // actually offset from the block. So we need to adjust coordinates when going - // to blocks. This isn't true for our start node though, which has a source - // coordinate of 0. - if Node::is_start_node(edge.source_node_id) { - graph.add_edge( - edge.source_node_id, - edge.target_node_id, - (edge.source_coordinate, edge.target_coordinate), - ); - } else { - graph.add_edge( - edge.source_node_id, - edge.target_node_id, - (edge.source_coordinate - 1, edge.target_coordinate), - ); - } - } - - for node in graph.nodes() { - // This is where we make the block graph. For this, we figure out the positions of - // all incoming and outgoing edges from the node. Then we make blocks between those - // positions. - if Node::is_terminal(node) { - continue; - } - let in_ports = graph - .edges_directed(node, Direction::Incoming) - .map(|(_src, _dest, (_fp, tp))| *tp) - .collect::>(); - let out_ports = graph - .edges_directed(node, Direction::Outgoing) - .map(|(_src, _dest, (fp, _tp))| *fp) - .collect::>(); - - let node_obj = *nodes_by_id.get(&node).unwrap(); - let sequence = *sequences_by_hash.get(&node_obj.sequence_hash).unwrap(); - let s_len = sequence.length; - let mut block_starts: HashSet = HashSet::from_iter(in_ports.iter().copied()); - block_starts.insert(0); - for x in out_ports.iter() { - if *x < s_len - 1 { - block_starts.insert(x + 1); - } - } - let mut block_ends: HashSet = HashSet::from_iter(out_ports.iter().copied()); - block_ends.insert(s_len); - for x in in_ports.iter() { - if *x > 0 { - block_ends.insert(x - 1); - } - } - - let block_starts = block_starts.into_iter().sorted().collect::>(); - let block_ends = block_ends.into_iter().sorted().collect::>(); - - let mut blocks = vec![]; - for (i, j) in block_starts.iter().zip(block_ends.iter()) { - block_graph.add_node((node, *i, *j)); - blocks.push((node, *i, *j)); - } - - for (i, j) in blocks.iter().tuple_windows() { - block_graph.add_edge(*i, *j, ()); - } - } - - for (src, dest, (fp, tp)) in graph.all_edges() { - if !(Node::is_end_node(src) && Node::is_start_node(dest)) { - let source_block = block_graph - .nodes() - .find(|(node, _start, end)| *node == src && end == fp) - .unwrap(); - let dest_block = block_graph - .nodes() - .find(|(node, start, _end)| *node == dest && start == tp) - .unwrap(); - block_graph.add_edge(source_block, dest_block, ()); - } - } - + for (bg_id, block_graph) in block_graphs.iter() { let mut dot = "digraph {\n rankdir=LR\n node [shape=none]\n".to_string(); - for (node_id, start, end) in block_graph.nodes() { + for node in block_graph.nodes() { + let node_id = node.node_id; + let start = node.sequence_start; + let end = node.sequence_end; let block_id = format!("{node_id}.{start}.{end}"); - if Node::is_terminal(node_id) { - let label = if Node::is_start_node(node_id) { + if Node::is_terminal(node.node_id) { + let label = if Node::is_start_node(node.node_id) { "start" } else { "end" @@ -167,8 +236,8 @@ pub fn view_patches(patches: &[OperationPatch]) -> HashMap 7 { @@ -204,7 +273,13 @@ pub fn view_patches(patches: &[OperationPatch]) -> HashMap