/* * TouchGraph LLC. Apache-Style Software License * * * Copyright (c) 2002 Alexander Shapiro. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by * TouchGraph LLC (http://www.touchgraph.com/)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "TouchGraph" or "TouchGraph LLC" must not be used to endorse * or promote products derived from this software without prior written * permission. For written permission, please contact * alex@touchgraph.com * * 5. Products derived from this software may not be called "TouchGraph", * nor may "TouchGraph" appear in their name, without prior written * permission of alex@touchgraph.com. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL TOUCHGRAPH OR ITS CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * */ /* * Modifications by Miika Nurminen (minurmin@cc.jyu.fi) for KeyGraph. * Added methods distanceFromCenter and centerNode. * 31.12.2003. */ package com.touchgraph.graphlayout; import com.touchgraph.graphlayout.interaction.*; import com.touchgraph.graphlayout.graphelements.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.Vector; import java.util.Hashtable; /** TGPanel contains code for drawing the graph, and storing which nodes * are selected, and which ones the mouse is over. * * It houses methods to activate TGLayout, which performs dynamic layout. * Whenever the graph is moved, or repainted, TGPanel fires listner * methods on associated objects. * *

* Parts of this code build upon Sun's Graph Layout example. * http://java.sun.com/applets/jdk/1.1/demo/GraphLayout/Graph.java *

* * @author Alexander Shapiro * @author Murray Altheim (2001-11-06; cleanup) * @version 1.20 */ public class TGPanel extends JPanel { // static variables for use within the package public static Color BACK_COLOR = Color.white; // .... private GraphEltSet completeEltSet; private VisibleLocality visibleLocality; private LocalityUtils localityUtils; public TGLayout tgLayout; protected BasicMouseMotionListener basicMML; protected Edge mouseOverE; //mouseOverE is the edge the mouse is over protected Node mouseOverN; //mouseOverN is the node the mouse is over protected boolean maintainMouseOver = false; //If true, then don't change mouseOverN or mouseOverE protected Node select; Node dragNode; //Node currently being dragged protected Point mousePos; //Mouse location, updated in the mouseMotionListener Image offscreen; Dimension offscreensize; Graphics offgraphics; private Vector graphListeners; private Vector paintListeners; TGLensSet tgLensSet; // Converts between a nodes visual position (drawx, drawy), // and its absolute position (x,y). AdjustOriginLens adjustOriginLens; SwitchSelectUI switchSelectUI; // ............ /** Default constructor. */ public TGPanel() { setLayout(null); setGraphEltSet(new GraphEltSet()); addMouseListener(new BasicMouseListener()); basicMML = new BasicMouseMotionListener(); addMouseMotionListener(basicMML); graphListeners=new Vector(); paintListeners=new Vector(); adjustOriginLens = new AdjustOriginLens(); switchSelectUI = new SwitchSelectUI(); TGLayout tgLayout = new TGLayout(this); setTGLayout(tgLayout); tgLayout.start(); setGraphEltSet(new GraphEltSet()); } public void setLensSet( TGLensSet lensSet ) { tgLensSet = lensSet; } protected void setTGLayout( TGLayout tgl ) { tgLayout = tgl; } public void setGraphEltSet( GraphEltSet ges ) { completeEltSet = ges; visibleLocality = new VisibleLocality(completeEltSet); localityUtils = new LocalityUtils(visibleLocality, this); } public AdjustOriginLens getAdjustOriginLens() { return adjustOriginLens; } public SwitchSelectUI getSwitchSelectUI() { return switchSelectUI; } // color and font setters ...................... public void setBackColor( Color color ) { BACK_COLOR = color; } // Node manipulation ........................... /** Returns the current node count. */ public int getNodeCount() { return completeEltSet.nodeCount; } /** Returns the current node count within the VisibleLocality. */ public int nodeNum() { return visibleLocality.nodeNum(); } /** Return the Node whose ID matches the String id, null if no match is found. * * @param id The ID identifier used as a query. * @return The Node whose ID matches the provided 'id', null if no match is found. */ public Node findNode( String id ) { if ( id == null ) return null; // ignore return completeEltSet.findNode(id); } /** Adds a Node, with its ID and label being the current node count plus 1. * @see com.touchgraph.graphlayout.Node */ public Node addNode() throws TGException { String id = String.valueOf(getNodeCount()+1); return addNode(id,null); } /** Adds a Node, provided its label. The node is assigned a unique ID, * by appending an underscore followed by an integer until a unique ID is found. * @see com.touchgraph.graphlayout.Node */ public Node addNode( String label ) throws TGException { String id; if ( findNode(label) != null ) { id = label; } else { int i; for( i = 1; findNode( label +"-"+ i ) != null; i++ ); id = label + "-" + i; // if label is a valid XML Name, then so is ID. } return addNode(id,label); } /** Adds a Node, provided its ID and label. * @see com.touchgraph.graphlayout.Node */ public Node addNode( String id, String label ) throws TGException { Node node; if (label==null) node = new Node(id); else node = new Node(id, label); updateDrawPos(node); // The addNode() call should probably take a position, this just sets it at 0,0 addNode(node); return node; } /** Add the Node node to the visibleLocality, checking for ID uniqueness. */ public void addNode( final Node node ) throws TGException { synchronized (localityUtils) { visibleLocality.addNode(node); resetDamper(); } } /** Remove the Node object matching the ID id, returning true if the * deletion occurred, false if a Node matching the ID does not exist (or if the ID * value was null). * * @param id The ID identifier used as a query. * @return true if the deletion occurred. */ public boolean deleteNodeById( String id ) { if ( id == null ) return false; // ignore Node node = findNode(id); if ( node == null ) return false; else return deleteNode(node); } public boolean deleteNode( Node node ) { synchronized (localityUtils) { if (visibleLocality.deleteNode(node)) { // delete from visibleLocality, *AND completeEltSet if ( node == select ) clearSelect(); resetDamper(); return true; } return false; } } public void clearAll() { synchronized (localityUtils) { visibleLocality.clearAll(); } } public Node getSelect() { return select; } public Node getMouseOverN() { return mouseOverN; } public synchronized void setMouseOverN( Node node ) { if ( dragNode != null || maintainMouseOver ) return; // So you don't accidentally switch nodes while dragging if ( mouseOverN != node ) { Node oldMouseOverN = mouseOverN; mouseOverN=node; } } // Edge manipulation ........................... public void deleteEdge( Edge edge ) { synchronized (localityUtils) { visibleLocality.deleteEdge(edge); resetDamper(); } } public void deleteEdge( Node from, Node to ) { synchronized (localityUtils) { visibleLocality.deleteEdge(from,to); } } public int edgeNum() { return visibleLocality.edgeNum(); } public Edge findEdge( Node f, Node t ) { return visibleLocality.findEdge(f, t); } public void addEdge(Edge e) { synchronized (localityUtils) { visibleLocality.addEdge(e); resetDamper(); } } public Edge addEdge( Node f, Node t, int tens ) { synchronized (localityUtils) { return visibleLocality.addEdge(f, t, tens); } } public Edge getMouseOverE() { return mouseOverE; } public synchronized void setMouseOverE( Edge edge ) { if ( dragNode != null || maintainMouseOver ) return; // No funny business while dragging if ( mouseOverE != edge ) { Edge oldMouseOverE = mouseOverE; mouseOverE = edge; } } // miscellany .................................. protected class AdjustOriginLens extends TGAbstractLens { protected void applyLens(TGPoint2D p) { p.x=p.x+TGPanel.this.getSize().width/2; p.y=p.y+TGPanel.this.getSize().height/2; } protected void undoLens(TGPoint2D p) { p.x=p.x-TGPanel.this.getSize().width/2; p.y=p.y-TGPanel.this.getSize().height/2; } } public class SwitchSelectUI extends TGAbstractClickUI { public void mouseClicked(MouseEvent e) { if ( mouseOverN != null ) { if ( mouseOverN != select ) setSelect(mouseOverN); else clearSelect(); } } } void fireMovedEvent() { Vector listeners; synchronized(this) { listeners = (Vector)graphListeners.clone(); } for (int i=0;i to.x ) { maxX = from.x; minX = to.x; } else { minX = from.x; maxX = to.x; } if ( from.y > to.y ) { maxY = from.y; minY = to.y; } else { minY = from.y; maxY = to.y; } final Vector selectedNodes = new Vector(); TGForEachNode fen = new TGForEachNode() { public void forEachNode( Node node ) { double x = node.drawx; double y = node.drawy; if ( x > minX && x < maxX && y > minY && y < maxY ) { selectedNodes.addElement(node); } } }; visibleLocality.forAllNodes(fen); if ( selectedNodes.size() > 0 ) { int r = (int)( Math.random()*selectedNodes.size() ); setSelect((Node)selectedNodes.elementAt(r)); } else { clearSelect(); } } public void updateLocalityFromVisibility() throws TGException { visibleLocality.updateLocalityFromVisibility(); } public void setLocale( Node node, int radius ) { localityUtils.setLocale(node,radius); } public void expandNode( Node node ) { localityUtils.expandNode(node); } public void hideNode( Node hideNode, Node focusNode ) { localityUtils.hideNode(hideNode,focusNode); } public void hideEdge( Edge hideEdge ) { visibleLocality.removeEdge(hideEdge); if ( mouseOverE == hideEdge ) setMouseOverE(null); resetDamper(); } public void setDragNode( Node node ) { dragNode = node; tgLayout.setDragNode(node); } public Node getDragNode() { return dragNode; } void setMousePos( Point p ) { mousePos = p; } public Point getMousePos() { return mousePos; } /** Start and stop the damper. Should be placed in the TGPanel too. */ public void startDamper() { tgLayout.startDamper(); } public void stopDamper() { tgLayout.stopDamper(); } /** Makes the graph mobile, and slowly slows it down. */ public void resetDamper() { tgLayout.resetDamper(); } class BasicMouseListener extends MouseAdapter { public void mouseEntered(MouseEvent e) { addMouseMotionListener(basicMML); } public void mouseExited(MouseEvent e) { removeMouseMotionListener(basicMML); mousePos = null; setMouseOverN(null); setMouseOverE(null); repaint(); } } class BasicMouseMotionListener implements MouseMotionListener { public void mouseDragged(MouseEvent e) { mousePos = e.getPoint(); findMouseOver(); try { Thread.currentThread().sleep(6); //An attempt to make the cursor flicker less } catch (InterruptedException ex) { //break; } } public void mouseMoved( MouseEvent e ) { mousePos = e.getPoint(); synchronized (this) { Edge oldMouseOverE = mouseOverE; Node oldMouseOverN = mouseOverN; findMouseOver(); if (oldMouseOverE!=mouseOverE || oldMouseOverN!=mouseOverN) { repaint(); } // Replace the above lines with the commented portion below to prevent whole graph // from being repainted simply to highlight a node On mouseOver. // This causes some annoying flickering though. /* if(oldMouseOverE!=mouseOverE) { if (oldMouseOverE!=null) { synchronized(oldMouseOverE) { oldMouseOverE.paint(TGPanel.this.getGraphics(),TGPanel.this); oldMouseOverE.from.paint(TGPanel.this.getGraphics(),TGPanel.this); oldMouseOverE.to.paint(TGPanel.this.getGraphics(),TGPanel.this); } } if (mouseOverE!=null) { synchronized(mouseOverE) { mouseOverE.paint(TGPanel.this.getGraphics(),TGPanel.this); mouseOverE.from.paint(TGPanel.this.getGraphics(),TGPanel.this); mouseOverE.to.paint(TGPanel.this.getGraphics(),TGPanel.this); } } } if(oldMouseOverN!=mouseOverN) { if (oldMouseOverN!=null) oldMouseOverN.paint(TGPanel.this.getGraphics(),TGPanel.this); if (mouseOverN!=null) mouseOverN.paint(TGPanel.this.getGraphics(),TGPanel.this); } */ } } } protected synchronized void findMouseOver() { if ( mousePos == null ) { setMouseOverN(null); setMouseOverE(null); return; } final int mpx=mousePos.x; final int mpy=mousePos.y; final Node[] monA = new Node[1]; final Edge[] moeA = new Edge[1]; TGForEachNode fen = new TGForEachNode() { double minoverdist = 100; //Kind of a hack (see second if statement) //Nodes can be as wide as 200 (=2*100) public void forEachNode( Node node ) { double x = node.drawx; double y = node.drawy; double dist = Math.sqrt((mpx-x)*(mpx-x)+(mpy-y)*(mpy-y)); if ( ( dist < minoverdist ) && node.containsPoint(mpx,mpy) ) { minoverdist = dist; monA[0] = node; } } }; visibleLocality.forAllNodes(fen); TGForEachEdge fee = new TGForEachEdge() { double minDist = 8; // Tangential distance to the edge double minFromDist = 1000; // Distance to the edge's "from" node public void forEachEdge( Edge edge ) { double x = edge.from.drawx; double y = edge.from.drawy; double dist = edge.distFromPoint(mpx,mpy); if ( dist < minDist ) { // Set the over edge to the edge with the minimun tangential distance minDist = dist; minFromDist = Math.sqrt((mpx-x)*(mpx-x)+(mpy-y)*(mpy-y)); moeA[0] = edge; } else if ( dist == minDist ) { // If tangential distances are identical, chose // the edge whose "from" node is closest. double fromDist = Math.sqrt((mpx-x)*(mpx-x)+(mpy-y)*(mpy-y)); if ( fromDist < minFromDist ) { minFromDist = fromDist; moeA[0] = edge; } } } }; visibleLocality.forAllEdges(fee); setMouseOverN(monA[0]); if ( monA[0] == null ) setMouseOverE(moeA[0]); else setMouseOverE(null); } TGPoint2D topLeftDraw = null; TGPoint2D bottomRightDraw = null; public TGPoint2D getTopLeftDraw() { return new TGPoint2D(topLeftDraw); } public TGPoint2D getBottomRightDraw() { return new TGPoint2D(bottomRightDraw); } public TGPoint2D getCenter() { return tgLensSet.convDrawToReal(getSize().width/2,getSize().height/2); } public TGPoint2D getDrawCenter() { return new TGPoint2D(getSize().width/2,getSize().height/2); } public double distanceFromCenter(Node n) { double xs = getSize().width/2; double ys = getSize().height/2; return Math.sqrt(Math.pow((n.drawx-xs),2)+Math.pow((n.drawy-ys),2)); } public void centerNode(Node from,Node n) { if (distanceFromCenter(n)