package fi.csc.microarray.client.dataview; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JToolBar; import javax.swing.JViewport; import javax.swing.SwingUtilities; import org.apache.log4j.Logger; import org.jgraph.event.GraphSelectionEvent; import org.jgraph.event.GraphSelectionListener; import org.jgraph.graph.BasicMarqueeHandler; import org.jgraph.graph.CellViewFactory; import org.jgraph.graph.DefaultCellViewFactory; import org.jgraph.graph.DefaultGraphModel; import org.jgraph.graph.GraphLayoutCache; import org.jgraph.graph.GraphModel; import org.jgraph.graph.VertexView; import com.jgoodies.looks.HeaderStyle; import com.jgoodies.looks.Options; import fi.csc.microarray.client.Session; import fi.csc.microarray.client.SwingClientApplication; import fi.csc.microarray.client.ToolBarComponentFactory; import fi.csc.microarray.client.dataviews.vertexes.AbstractGraphVertex; import fi.csc.microarray.client.dataviews.vertexes.GraphRenderer; import fi.csc.microarray.client.dataviews.vertexes.GraphVertex; import fi.csc.microarray.client.dataviews.vertexes.GroupVertex; import fi.csc.microarray.client.selection.DatasetChoiceEvent; import fi.csc.microarray.client.visualisation.VisualisationFrameManager.FrameType; import fi.csc.microarray.constants.VisualConstants; import fi.csc.microarray.databeans.ContentChangedEvent; import fi.csc.microarray.databeans.DataBean; import fi.csc.microarray.databeans.DataChangeEvent; import fi.csc.microarray.databeans.DataChangeListener; import fi.csc.microarray.databeans.DataItem; import fi.csc.microarray.databeans.DataItemCreatedEvent; import fi.csc.microarray.databeans.LinksChangedEvent; import fi.csc.microarray.util.SwingTools; /** * A GUI component for viewing the data in a graph presentation, where each dataset is represented by a graphical vertex (an instance of * GraphVertex). The graph is created by using the JGraph application framework. * * @author Janne Käki, Aleksi Kallio, Petri Klemelä * */ public class GraphPanel extends JPanel implements ActionListener, PropertyChangeListener, DataChangeListener, AnimatorScrollable { public final float ZOOM_FACTOR = 1.2f; public final float ZOOM_IN_LIMIT = 1.0f; public final float ZOOM_OUT_LIMIT = 0.2f; private MicroarrayGraph graph = null; private SwingClientApplication application = (SwingClientApplication)Session.getSession().getApplication(); private GraphModel model = new DefaultGraphModel(); // FIXME memory leak private JScrollPane graphScroller = null; private JToolBar buttonToolBar = null; private JButton zoomInButton; private JButton zoomOutButton; private JCheckBox autoZoomChecBbox; private boolean internalSelection = false; private static final Logger logger = Logger.getLogger(GraphPanel.class); public void setInternalSelection(boolean internalSelection) { this.internalSelection = internalSelection; } private static final double DEFAULT_GRID_SIZE = 10.0; private static final double BIGGER_GRID_SIZE = 20.0; private static final double HUGE_GRID_SIZE = 40.0; /** * Creates a new GraphPanel with default contents and appearance. */ public GraphPanel() { getGraph().getSelectionModel().addGraphSelectionListener(new WorkflowSelectionListener()); getGraph().setMarqueeHandler(new BasicMarqueeHandler()); // getGraph().setDebugGraphicsOptions(DebugGraphics.LOG_OPTION); // getGraph().setDoubleBuffered(false); this.setMinimumSize(new Dimension(0, 0)); this.setLayout(new GridBagLayout()); buttonToolBar = this.getButtonToolBar(); graphScroller = this.getGraphScroller(); this.setPreferredSize(new Dimension(VisualConstants.LEFT_PANEL_WIDTH, VisualConstants.GRAPH_PANEL_HEIGHT)); graphScroller.setPreferredSize(new Dimension(VisualConstants.LEFT_PANEL_WIDTH, VisualConstants.GRAPH_PANEL_HEIGHT)); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.HORIZONTAL; c.anchor = GridBagConstraints.SOUTH; c.gridy = 1; this.add(buttonToolBar, c); c.gridx = 0; c.gridy = 2; c.fill = GridBagConstraints.BOTH; c.anchor = GridBagConstraints.NORTH; c.weightx = 1.0; c.weighty = 1.0; this.add(graphScroller, c); // start listening application.addClientEventListener(this); application.getDataManager().addDataChangeListener(this); graph.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); graph.setScale(graph.getScale() / ZOOM_FACTOR); // adds vertex renderer which adds the small '+' and '-' buttons to group VertexView.renderer = new GraphRenderer(); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == zoomInButton) { autoZoomChecBbox.setSelected(false); this.zoomInToolUsed(); } else if (source == zoomOutButton) { autoZoomChecBbox.setSelected(false); this.zoomOutToolUsed(); } else if (source == autoZoomChecBbox) { autoZoom(); } } public void autoZoom() { if (autoZoomChecBbox.isSelected()) { Dimension dim = graph.getGraphSize(); double xScale = graphScroller.getSize().getWidth() / (double) dim.width; double yScale = graphScroller.getSize().getHeight() / (double) dim.height; if (xScale < yScale) { this.setGraphScale(xScale); } else { this.setGraphScale(yScale); } this.updateGridSize(); logger.debug("Graph size: " + dim.width + " , " + dim.height + " Scroller size: " + graphScroller.getSize().getWidth() + " , " + graphScroller.getSize().getHeight() + " Scales: " + xScale + " , " + yScale); graph.repaint(); } } /** * Will return (and if needed, create) the JGraph component that is the very heart of this GraphPanel. Initializes an empty graph and * enforces differend kind of UI constraints on it (not editable, not connectable or disconnectable, not sizeable, etc., by user). Also * makes the graph antialiased, which has a creat impact on the looks of this application :) * * @return The graph component of this panel. */ public MicroarrayGraph getGraph() { if (graph == null) { // Sets missing constructor parameter for GraphLayout CellViewFactory factory = new DefaultCellViewFactory(); boolean partial = true; // This must be true to allow group collapsing GraphLayoutCache cache = new GraphLayoutCache(model, factory, partial); graph = new MicroarrayGraph(model, cache, this); // Adds mouse listener which opens popup menu this.graph.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } public void mouseClicked(MouseEvent e) { logger.debug("mouseClicked"); // Double click if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() > 1) { // Change the cursor back from marquee cross graph.setCursor(Cursor.getDefaultCursor()); mouseButtonDoubleClicked(e); } } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { showPopupMenu(e); } } }); graph.setAntiAliased(true); graph.setBendable(false); graph.setConnectable(false); graph.setDisconnectable(false); graph.setEditable(false); graph.setMoveable(true); graph.setPortsVisible(false); graph.setSizeable(false); graph.setGridColor(Color.LIGHT_GRAY); graph.setGridSize(DEFAULT_GRID_SIZE); graph.setGridVisible(true); graph.setGridEnabled(true); } return graph; } private void mouseButtonDoubleClicked(MouseEvent e) { // Get the clicked cell Object cell = graph.getFirstCellForLocation(e.getX(), e.getY()); logger.debug("Selected cell: " + cell); // Do not visualise collapsed group if (cell instanceof GraphVertex) { application.visualiseWithBestMethod(FrameType.MAIN); } } protected GraphModel getGraphModel() { return model; } /** * Creates the surrounding scroller component for the graph. When called again, will simply return the existing scroller. The JGraph * component will also be initialized from this method by calling getGraph(), if not previously created. * * @return A JScrollPane which contains the graph component with appropriate margins. */ private JScrollPane getGraphScroller() { if (graphScroller == null) { graphScroller = new JScrollPane(this.getGraph()); graphScroller.setBorder(BorderFactory.createEmptyBorder()); graphScroller.setMinimumSize(new Dimension(0, 0)); ScrollListener scrollListener = new ScrollListener(); graphScroller.getHorizontalScrollBar().addAdjustmentListener(scrollListener); graphScroller.getVerticalScrollBar().addAdjustmentListener(scrollListener); } return graphScroller; } /** * Repaints the graph after scroll to ensure the visibility of all vertexes */ private class ScrollListener implements AdjustmentListener { // FIXME check if needed still with the new version of JGraph public void adjustmentValueChanged(AdjustmentEvent e) { GraphPanel.this.getGraph().repaint(); } } /** * When called for first time, creates button panel for the buttons of workflow -view. * * @return The history panel of this GraphPanel. */ public JToolBar getButtonToolBar() { if (buttonToolBar == null) { zoomInButton = ToolBarComponentFactory.createButton(false, false); zoomInButton.setToolTipText("Zoom in"); this.initialiseToolBarButton(zoomInButton); zoomInButton.setIcon(VisualConstants.getIcon(VisualConstants.ZOOM_IN_ICON)); zoomOutButton = ToolBarComponentFactory.createButton(false, false); zoomOutButton.setToolTipText("Zoom out"); this.initialiseToolBarButton(zoomOutButton); zoomOutButton.setIcon(VisualConstants.getIcon(VisualConstants.ZOOM_OUT_ICON)); autoZoomChecBbox = ToolBarComponentFactory.createCheckBox("Fit"); autoZoomChecBbox.setToolTipText("Scale workflow to show all datasets"); autoZoomChecBbox.setSelected(true); this.initialiseToolBarButton(autoZoomChecBbox); buttonToolBar = new JToolBar(); buttonToolBar.setFloatable(false); buttonToolBar.setMinimumSize(new Dimension(0, 0)); buttonToolBar.putClientProperty(Options.HEADER_STYLE_KEY, HeaderStyle.SINGLE); buttonToolBar.add(zoomInButton); buttonToolBar.add(zoomOutButton); buttonToolBar.add(autoZoomChecBbox); buttonToolBar.add(Box.createHorizontalGlue()); } return buttonToolBar; } private void initialiseToolBarButton(AbstractButton button) { button.addActionListener(this); button.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); } /** * Centres the workflow view to given point. * * @param p * Point must be given in the pixel coordinates of the graph. If this is done after scale of the graph, make sure that the * scaling has been done before calling this. */ public void pointToCenter(Point2D target) { Point newViewPos = new Point(); newViewPos.x = (int) (target.getX() - graphScroller.getViewport().getWidth() / 2.0); newViewPos.y = (int) (target.getY() - graphScroller.getViewport().getHeight() / 2.0); this.setViewPosition(newViewPos); } /** * Sets viewport position but checks that graph isn't moved outside the scrolling area * * @param p * New View position */ public void setViewPosition(Point p) { Point2D p2 = graph.toScreen(p); JScrollPane scroller = this.getScroller(); double newX = p2.getX(); double newY = p2.getY(); // magic numbers make some extra margin to avoid clipping int graphW = graph.getWidth(); int graphH = graph.getHeight(); int viewW = scroller.getViewport().getWidth(); int viewH = scroller.getViewport().getHeight(); // set the limits to prevent scrolling out of the area from the lower right corner int xLimit = graphW - viewW; int yLimit = graphH - viewH; // these are checked first, so that they are overriden if the graph is too small if (newX > xLimit) { newX = xLimit; } if (newY > yLimit) { newY = yLimit; } // set the limits to prevent scrolling out of the area from the upper left corner if (newX < 0) { newX = 0; } if (newY < 0) { newY = 0; } // apply chanches scroller.getViewport().setViewPosition(new Point((int) newX, (int) newY)); graph.repaint(); } public JScrollPane getScroller() { return this.graphScroller; } /** * @param scale * This is set for the scale of graph if it is between zoom limits. Returns true if the scale was appropriate. */ public boolean setGraphScale(double scale) { double newScale = scale; if (scale >= ZOOM_IN_LIMIT) { newScale = ZOOM_IN_LIMIT; zoomInButton.setEnabled(false); } else { zoomInButton.setEnabled(true); } if (scale <= ZOOM_OUT_LIMIT) { newScale = ZOOM_OUT_LIMIT; zoomOutButton.setEnabled(false); } else { zoomOutButton.setEnabled(true); } graph.setScale(newScale); graphScroller.repaint(); return newScale == scale; } private void showPopupMenu(MouseEvent e) { // right click List<AbstractGraphVertex> chosenCells = graph.getVertexesAtPoint(e.getPoint()); if (chosenCells != null && chosenCells.size() > 0) { List<DataItem> items = new ArrayList<DataItem>(); for (DataBean bean : application.getSelectionManager().getSelectedDataBeans()) { items.add(bean); } application.showPopupMenuFor(e, items); } else { DataBean nullBean = null; application.showPopupMenuFor(e, nullBean); } } // used to send values for the new thread in method zoomInToolUsed private Point center; /** * Zoom in * * @param e */ private void zoomInToolUsed() { // If the zoom-limit has been reached, don't do anything if (this.setGraphScale(graph.getScale() * this.ZOOM_FACTOR)) { JViewport view = this.getScroller().getViewport(); Point viewPos = view.getViewPosition(); center = new Point((int) (viewPos.getX() + view.getWidth() / 2), (int) (viewPos.getY() + view.getHeight() / 2)); logger.debug("view.getX: " + view.getX() + ", viewWidth: " + view.getWidth()); // Make the coordinates to correspond the new size of the graph center.x = (int) (center.getX() * this.ZOOM_FACTOR); center.y = (int) (center.getY() * this.ZOOM_FACTOR); /* * The new scale won't be applied before this method is ended. The new scale changes the size of the graph, which affects on the * calculations done in method pointToCenter. We couuld estimate the new size of the graph, but that doesn't help because we * still cant scroll to the new areas. */ Runnable centerer = new Thread() { public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { // Nothing serious happened } GraphPanel.this.pointToCenter(GraphPanel.this.center); } }; updateGridSize(); // try it to avoid flickering when possible this.pointToCenter(GraphPanel.this.center); // and make sure it's done in every situation; SwingUtilities.invokeLater(centerer); } graph.repaint(); } /** * Zooms out. */ private void zoomOutToolUsed() { if (this.setGraphScale(graph.getScale() / this.ZOOM_FACTOR)) { JViewport view = this.getScroller().getViewport(); Point viewPos = view.getViewPosition(); center = new Point((int) (viewPos.getX() + view.getWidth() / 2), (int) (viewPos.getY() + view.getHeight() / 2)); center.x = (int) (center.getX() / this.ZOOM_FACTOR); center.y = (int) (center.getY() / this.ZOOM_FACTOR); updateGridSize(); this.pointToCenter(center); } this.repaint(); this.getScroller().repaint(); graph.repaint(); } private void updateGridSize() { double size = DEFAULT_GRID_SIZE; if (graph.getScale() <= 0.5) { size = BIGGER_GRID_SIZE; } if (graph.getScale() <= 0.25) { size = HUGE_GRID_SIZE; } graph.setGridSize(size); } /** * Enables the history button if only one dataset is selected * * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent) */ public void propertyChange(PropertyChangeEvent e) { if (e instanceof DatasetChoiceEvent) { SwingTools.runInEventDispatchThread(new Runnable() { public void run() { if (application.getSelectionManager().getSelectedDataBean() != null) { DataBean bean = application.getSelectionManager().getSelectedDataBean(); graph.scrollCellToVisibleAnimated(graph.getVertexMap().get(bean)); } graph.repaint(); } }); } } public void dataChanged(DataChangeEvent event) { if (!(event instanceof ContentChangedEvent || event instanceof LinksChangedEvent || !(event instanceof DataItemCreatedEvent))) { SwingTools.runInEventDispatchThread(new Runnable() { public void run() { autoZoom(); } }); } } public class WorkflowSelectionListener implements GraphSelectionListener { public void valueChanged(GraphSelectionEvent e) { if (!internalSelection) { boolean emptySelection = (graph.getSelectionCount() == 0); application.getSelectionManager().clearAll(emptySelection, graph); ArrayList<AbstractGraphVertex> vertexes = new ArrayList<AbstractGraphVertex>(); for (Object obj : graph.getSelectionCells()) { if (obj instanceof GroupVertex) { GroupVertex group = (GroupVertex) obj; vertexes.addAll(group.getChildVertexes()); } if (obj instanceof GraphVertex) { vertexes.add((AbstractGraphVertex) obj); } } ArrayList<DataItem> items = new ArrayList<DataItem>(); for (AbstractGraphVertex vertex : vertexes) { items.add((DataItem) vertex.getUserObject()); } application.getSelectionManager().selectMultiple(items, graph); } } } }