001: /*
002: * $Id: JGraphpadMorphingManager.java,v 1.5 2007/03/25 13:11:08 david Exp $
003: * Copyright (c) 2001-2005, Gaudenz Alder
004: *
005: * All rights reserved.
006: *
007: * This file is licensed under the JGraph software license, a copy of which
008: * will have been provided to you in the file LICENSE at the root of your
009: * installation directory. If you are unable to locate this file please
010: * contact JGraph sales for another copy.
011: */
012: package com.jgraph.pad.util;
013:
014: import java.awt.Shape;
015: import java.awt.event.ActionEvent;
016: import java.awt.event.ActionListener;
017: import java.awt.geom.Rectangle2D;
018: import java.util.HashSet;
019: import java.util.Hashtable;
020: import java.util.Iterator;
021: import java.util.Map;
022:
023: import javax.swing.Timer;
024:
025: import org.jgraph.JGraph;
026: import org.jgraph.graph.CellView;
027: import org.jgraph.graph.DefaultGraphModel;
028: import org.jgraph.graph.GraphConstants;
029:
030: /**
031: * Animation for simple graph changes (moves). This takes a nested map and
032: * animates the change visually so the vertices appear to float to their new
033: * locations. This implementation only takes into account new positions of
034: * vertices, sizes, colors etc are changed after the animation in a single step.<br>
035: * Note: This class is not thread-safe.
036: */
037: public class JGraphpadMorphingManager implements ActionListener {
038:
039: /**
040: * Specifies the delay between morphing steps in milliseconds. Default is
041: * 30.
042: */
043: protected int delay = 30;
044:
045: /**
046: * Specifies the number of animation steps. Default is 10.
047: */
048: protected int steps = 10;
049:
050: /**
051: * References the graph to be morphed.
052: */
053: protected JGraph graph;
054:
055: /**
056: * Holds the current morhing step.
057: */
058: protected transient int step = 0;
059:
060: /**
061: * Holds the current and final bounds of the animation.
062: */
063: protected transient Map oldBounds = new Hashtable(),
064: newBounds = new Hashtable();
065:
066: /**
067: * Holds the context cells, eg the edges connected to the animated cells or
068: * one of their parents.
069: */
070: protected transient CellView[] context;
071:
072: /**
073: * Holds the clipping shape to be used for repainting the graph.
074: */
075: protected transient Shape clip;
076:
077: /**
078: * Holds the original nested map for the final execute step.
079: */
080: protected transient Map nestedMap;
081:
082: /**
083: * Animates the graph so that all vertices move from their current location
084: * to the new location stored in the nested map. This sets the
085: * {@link #nestedMap} and {@link #graph} variable and spawns a timer
086: * process. While the timer is running, further method calls are ignored.
087: * The call will return immediately.
088: *
089: * @param nestedMap
090: * The nested map that defines the new locations.
091: */
092: public synchronized void morph(JGraph graph, Map nestedMap) {
093: if (this .graph == null && this .nestedMap == null
094: && graph != null && nestedMap != null) {
095: this .graph = graph;
096: this .nestedMap = nestedMap;
097: initialize();
098: // Execute the morphing. This spawns a timer
099: // to not block the dispatcher thread (repaint).
100: if (!newBounds.isEmpty()) {
101: Timer timer = new Timer(delay, this );
102: timer.start();
103: } else {
104: execute();
105: }
106: }
107: }
108:
109: /**
110: * Hook for subclassers to determine whether the specified cell should be
111: * animated. This implementation returns true for all cells.
112: *
113: * @param cell
114: * The cells to be checked.
115: * @return Returns true if the cell may be animated.
116: */
117: protected boolean isAnimatable(Object cell) {
118: return true;
119: }
120:
121: /**
122: * Initializes the datastructures required for the animation. This
123: * implementation sets the current and final location for the cells to be
124: * animated using the specified nestedMap to get the new locations. If a
125: * cell is in the nested map but {@link #isAnimatable(Object)} returns false
126: * then the cell is moved to it's final location in the first animation
127: * step.
128: */
129: protected void initialize() {
130: // Initialize the old (current) and new (final) bounds
131: // hashtables if the bounds differ. For the non-animatable
132: // cells this will temporily apply the new bounds.
133: Iterator it = nestedMap.entrySet().iterator();
134: while (it.hasNext()) {
135: Map.Entry entry = (Map.Entry) it.next();
136: Object cell = entry.getKey();
137: Rectangle2D rect = GraphConstants.getBounds((Map) entry
138: .getValue());
139: if (rect != null) {
140: Rectangle2D old = graph.getCellBounds(cell);
141: if (old != null && !old.equals(rect)) {
142: oldBounds.put(cell, old.clone());
143: if (!isAnimatable(cell))
144: setCellBounds(cell, rect);
145: else
146: newBounds.put(cell, rect);
147: }
148: }
149: }
150:
151: // Finds the set of parents to determine the clipping region
152: // and context of the animation. To make sure the complete
153: // graph is painted including the non-animatable cells
154: // this uses the cells from the oldbounds map.
155: HashSet parents = new HashSet();
156: it = oldBounds.keySet().iterator();
157: while (it.hasNext()) {
158: Object cell = it.next();
159:
160: // Fetches all parents of the cell and adds them to the parent set
161: Object parent = graph.getModel().getParent(cell);
162: while (parent != null) {
163: parents.add(parent);
164: parent = graph.getModel().getParent(parent);
165: }
166: }
167: parents.addAll(oldBounds.keySet());
168: Object[] cells = parents.toArray();
169:
170: // Initializes the clipping region
171: clip = graph.getCellBounds(cells);
172:
173: // Initializes the context of the animation. The context consists
174: // of all cells connected to either a cell or one of its parents.
175: Object[] edges = DefaultGraphModel.getEdges(graph.getModel(),
176: cells).toArray();
177: context = graph.getGraphLayoutCache().getMapping(edges);
178: }
179:
180: /**
181: * Invoked to perform an animation step and stop the timer if all animation
182: * steps have been performed.
183: *
184: * @param event
185: * The object that describes the event.
186: */
187: public void actionPerformed(ActionEvent event) {
188: if (step >= steps) {
189: Timer timer = (Timer) event.getSource();
190: timer.stop();
191: execute();
192: } else {
193: step++;
194: Iterator it = newBounds.keySet().iterator();
195: while (it.hasNext())
196: updateCell(it.next());
197: graph.getGraphLayoutCache().update(context);
198: graph.getGraphics().setClip(clip);
199: graph.repaint();
200: }
201: }
202:
203: /**
204: * Executes the actual change on the graph layout cache. This implementation
205: * restored the bounds on the modified cells to their old values for correct
206: * undo of the change, then calls the graph layout cache's edit method with
207: * the original nested map and cleans up the datastructures. This implements
208: * the final step of the animation.
209: */
210: protected void execute() {
211: try {
212: Iterator it = oldBounds.entrySet().iterator();
213: while (it.hasNext()) {
214: Map.Entry entry = (Map.Entry) it.next();
215: setCellBounds(entry.getKey(), (Rectangle2D) entry
216: .getValue());
217: }
218: graph.getGraphLayoutCache().edit(nestedMap, null, null,
219: null);
220: } finally {
221: graph = null;
222: nestedMap = null;
223: oldBounds.clear();
224: newBounds.clear();
225: context = null;
226: clip = null;
227: step = 0;
228: }
229: }
230:
231: /**
232: * Updates the specified cell for {@link #step}. This implementation moves
233: * the cell by a single increment towards it's final location using
234: * {@link #setCellBounds(Object, Rectangle2D)} to update the cell's bounds.
235: *
236: * @param cell
237: * The cell to be updated.
238: */
239: protected void updateCell(Object cell) {
240: if (isAnimatable(cell)) {
241: Rectangle2D old = (Rectangle2D) oldBounds.get(cell);
242: Rectangle2D rect = (Rectangle2D) newBounds.get(cell);
243: double dx = (rect.getX() - old.getX()) * step / steps;
244: double dy = (rect.getY() - old.getY()) * step / steps;
245: Rectangle2D pos = new Rectangle2D.Double(old.getX() + dx,
246: old.getY() + dy, old.getWidth(), old.getHeight());
247: setCellBounds(cell, pos);
248: }
249: }
250:
251: /**
252: * Sets the bounds for the specified cell.
253: *
254: * @param cell
255: * The cell whose bounds to set.
256: * @param bounds
257: * The new bounds of the cell.
258: */
259: protected void setCellBounds(Object cell, Rectangle2D bounds) {
260: Rectangle2D rect = graph.getCellBounds(cell);
261: if (rect != null && bounds != null) {
262: rect.setFrame(bounds.getX(), bounds.getY(), bounds
263: .getWidth(), bounds.getHeight());
264: CellView view = graph.getGraphLayoutCache().getMapping(
265: cell, false);
266: if (view != null)
267: view.update(graph.getGraphLayoutCache());
268: }
269: }
270:
271: }
|