/*
 * $Id: JGraphExampleGraph.java,v 1.7 2008/02/25 09:22:27 david Exp $
 * Copyright (c) 2001-2005, Gaudenz Alder
 * 
 * All rights reserved. 
 * 
 * This file is licensed under the JGraph software license, a copy of which
 * will have been provided to you in the file LICENSE at the root of your
 * installation directory. If you are unable to locate this file please
 * contact JGraph sales for another copy.
 */
package com.jgraph.layout;

import java.awt.event.ActionEvent;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.Timer;

import org.jgraph.example.GraphEd.MyGraph;
import org.jgraph.graph.CellView;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;
import org.jgraph.graph.GraphModel;

/**
 * A graph that can animate changes (morph).
 */
public class JGraphExampleGraph extends MyGraph {

	/**
	 * Specifies the delay between morphing steps
	 */
	protected int delay = 30;

	/**
	 * Specified the number of steps in the morphing process
	 */
	protected int steps = 10;

	/**
	 * Specified the current morhing step
	 */
	protected int step = 0;

	/**
	 * Stores the previous bounds of morphed cells
	 */
	protected Map oldBounds = new Hashtable();

	/**
	 * Stores the future bounds of morphed cells
	 */
	protected Map newBounds = new Hashtable();

	/**
	 * Stores the previous collective bounds of the morphed cells
	 */
	protected Rectangle2D oldClipBounds;
	
	/**
	 * Stores the new collective bounds of the morphed cells
	 */
	protected Rectangle2D newClipBounds;

	/**
	 * Constructs an example graph for the specified graph model.
	 * 
	 * @param model
	 */
	public JGraphExampleGraph(GraphModel model) {
		super(model);
		setGraphLayoutCache(new JGraphExampleLayoutCache(this));
	}

	public JGraphExampleGraph(GraphModel model, GraphLayoutCache cache) {
		super(model, cache);
	}

	public void morph(final Map nestedMap, Set nomorph) {
		Set parents = initMorphing(nestedMap, nomorph);
		if (!newBounds.isEmpty()) {
			final Object[] cells = parents.toArray();
			Object[] edges = DefaultGraphModel.getEdges(getModel(), cells)
					.toArray();
			final CellView[] edgeViews = getGraphLayoutCache()
					.getMapping(edges);

			// Execute the morphing. This spawns a timer
			// to not block the dispatcher thread (repaint)
			Timer timer = new Timer(delay, new AbstractAction() {
				public void actionPerformed(ActionEvent e) {
					if (step >= steps) {
						Timer timer = (Timer) e.getSource();
						timer.stop();
						restore();
						getGraphLayoutCache().edit(nestedMap, null, null, null);
					} else {
						step++;
						Iterator it = newBounds.keySet().iterator();
						while (it.hasNext()) {
							morphCell(it.next(), step);
						}
						getGraphLayoutCache().refresh(edgeViews, false);
						addOffscreenDirty(oldClipBounds);
						addOffscreenDirty(newClipBounds);
						oldClipBounds = newClipBounds = null;
						repaint();
					}
				}
			});
			timer.start();
		} else {
			getGraphLayoutCache().edit(nestedMap, null, null, null);
		}
	}

	/**
	 * Initial step of the morphing process. Analyses the arguments and prepares
	 * internal datastructures for the morphing.
	 * 
	 * @return Returns the set of all cells and ancestors to determine the dirty
	 *         region and connected edges.
	 */
	protected Set initMorphing(Map nestedMap, Set nomorph) {
		oldBounds.clear();
		newBounds.clear();
		step = 0;
		Iterator it = nestedMap.entrySet().iterator();
		while (it.hasNext()) {
			Map.Entry entry = (Map.Entry) it.next();
			Object cell = entry.getKey();
			Map attrs = (Map) entry.getValue();
			Rectangle2D rect = GraphConstants.getBounds(attrs);
			if (rect != null) {
				Rectangle2D old = getCellBounds(cell);
				if (old != null && !old.equals(rect)) {
					newBounds.put(cell, rect);
					oldBounds.put(cell, old.clone());
				}
			}
		}

		// Make sure the cells in nomorph are at their future
		// locations and fetches the set of all parents.
		HashSet parents = new HashSet();
		it = oldBounds.keySet().iterator();
		while (it.hasNext()) {
			Object cell = it.next();
			Object parent = getModel().getParent(cell);
			if (nomorph != null && nomorph.contains(cell)) {
				Rectangle2D rect = (Rectangle2D) newBounds.remove(cell);
				setCellBounds(cell, rect);
			}
			while (parent != null) {
				parents.add(parent);
				parent = getModel().getParent(parent);
			}
		}
		parents.addAll(oldBounds.keySet());
		return parents;
	}

	/**
	 * Restore the old bounds values for all cells. (This is required at the end
	 * of the morphing animation and before calling the edit method for the
	 * command history to work correctly.)
	 * 
	 */
	protected void restore() {
		Iterator it = oldBounds.entrySet().iterator();
		while (it.hasNext()) {
			Map.Entry entry = (Map.Entry) it.next();
			setCellBounds(entry.getKey(), (Rectangle2D) entry.getValue());
		}
	}

	/**
	 * Performs the morph positionon a cell for a particular step
	 * 
	 * @param cell
	 *            the cell being morphed
	 * @param step
	 *            the number step into morph process
	 */
	protected void morphCell(Object cell, int step) {
		Rectangle2D old = (Rectangle2D) oldBounds.get(cell);
		Rectangle2D rect = (Rectangle2D) newBounds.get(cell);
		// Add to total clip bounds
		if (old != null) {
			if (oldClipBounds == null) {
				oldClipBounds = (Rectangle2D)old.clone();
			} else {
				oldClipBounds.add(old);
			}
		}
		if (rect != null) {
			if (newClipBounds == null) {
				newClipBounds = (Rectangle2D)rect.clone();
			} else {
				newClipBounds.add(rect);
			}
		}		double dx = (rect.getX() - old.getX()) * step / steps;
		double dy = (rect.getY() - old.getY()) * step / steps;
		Rectangle2D pos = new Rectangle2D.Double(old.getX() + dx, old.getY()
				+ dy, old.getWidth(), old.getHeight());
		setCellBounds(cell, pos);
	}

	/**
	 * Set the new cell bounds
	 * 
	 * @param cell
	 *            the cell whose bounds to set
	 * @param bounds
	 *            the new bounds of the cell
	 */
	protected void setCellBounds(Object cell, Rectangle2D bounds) {
		Rectangle2D rect = getCellBounds(cell);
		if (rect != null && bounds != null) {
			rect.setFrame(bounds.getX(), bounds.getY(), bounds.getWidth(),
					bounds.getHeight());
			CellView view = getGraphLayoutCache().getMapping(cell, false);
			if (view != null)
				view.update(getGraphLayoutCache());
		}
	}

}
