/**
 * Copyright (c) 2009-2010, Gaudenz Alder, David Benson
 */
package com.mxgraph.swing;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EventObject;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.swing.BorderFactory;
import javax.swing.BoundedRangeModel;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.TransferHandler;

import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.canvas.mxICanvas;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.model.mxGraphModel.Filter;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.handler.mxCellHandler;
import com.mxgraph.swing.handler.mxConnectionHandler;
import com.mxgraph.swing.handler.mxEdgeHandler;
import com.mxgraph.swing.handler.mxElbowEdgeHandler;
import com.mxgraph.swing.handler.mxGraphHandler;
import com.mxgraph.swing.handler.mxGraphTransferHandler;
import com.mxgraph.swing.handler.mxPanningHandler;
import com.mxgraph.swing.handler.mxSelectionCellsHandler;
import com.mxgraph.swing.handler.mxVertexHandler;
import com.mxgraph.swing.util.mxCellOverlay;
import com.mxgraph.swing.util.mxICellOverlay;
import com.mxgraph.swing.view.mxCellEditor;
import com.mxgraph.swing.view.mxICellEditor;
import com.mxgraph.swing.view.mxInteractiveCanvas;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxResources;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxEdgeStyle;
import com.mxgraph.view.mxEdgeStyle.mxEdgeStyleFunction;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;
import com.mxgraph.view.mxTemporaryCellStates;

/**
 * For setting the preferred size of the viewport for scrolling, use
 * mxGraph.setMinimumGraphSize. This component is a combined scrollpane with an
 * inner mxGraphControl. The control contains the actual graph display.
 * 
 * To set the background color of the graph, use the following code:
 * 
 * <pre>
 * graphComponent.getViewport().setOpaque(true);
 * graphComponent.getViewport().setBackground(newColor);
 * </pre>
 * 
 * This class fires the following events:
 * 
 * mxEvent.START_EDITING fires before starting the in-place editor for an
 * existing cell in startEditingAtCell. The <code>cell</code> property contains
 * the cell that is being edit and the <code>event</code> property contains
 * optional EventObject which was passed to startEditingAtCell.
 * 
 * mxEvent.LABEL_CHANGED fires between begin- and endUpdate after the call to
 * mxGraph.cellLabelChanged in labelChanged. The <code>cell</code> property
 * contains the cell, the <code>value</code> property contains the new value for
 * the cell and the optional <code>event</code> property contains the
 * EventObject that started the edit.
 * 
 * mxEvent.ADD_OVERLAY and mxEvent.REMOVE_OVERLAY fire afer an overlay was added
 * or removed using add-/removeOverlay. The <code>cell</code> property contains
 * the cell for which the overlay was added or removed and the
 * <code>overlay</code> property contain the mxOverlay.
 * 
 * mxEvent.BEFORE_PAINT and mxEvent.AFTER_PAINT fire before and after the paint
 * method is called on the component. The <code>g</code> property contains the
 * graphics context which is used for painting.
 */
public class mxGraphComponent extends JScrollPane implements Printable
{

	/**
	 * 
	 */
	private static final long serialVersionUID = -30203858391633447L;

	/**
	 * 
	 */
	public static final int GRID_STYLE_DOT = 0;

	/**
	 * 
	 */
	public static final int GRID_STYLE_CROSS = 1;

	/**
	 * 
	 */
	public static final int GRID_STYLE_LINE = 2;

	/**
	 * 
	 */
	public static final int GRID_STYLE_DASHED = 3;

	/**
	 * 
	 */
	public static final int ZOOM_POLICY_NONE = 0;

	/**
	 * 
	 */
	public static final int ZOOM_POLICY_PAGE = 1;

	/**
	 * 
	 */
	public static final int ZOOM_POLICY_WIDTH = 2;

	/**
	 * 
	 */
	public static ImageIcon DEFAULT_EXPANDED_ICON = null;

	/**
	 * 
	 */
	public static ImageIcon DEFAULT_COLLAPSED_ICON = null;

	/**
	 * 
	 */
	public static ImageIcon DEFAULT_WARNING_ICON = null;

	/**
	 * Specifies the default page scale. Default is 1.4
	 */
	public static final double DEFAULT_PAGESCALE = 1.4;

	/**
	 * Loads the collapse and expand icons.
	 */
	static
	{
		DEFAULT_EXPANDED_ICON = new ImageIcon(
				mxGraphComponent.class
						.getResource("/com/mxgraph/swing/images/expanded.gif"));
		DEFAULT_COLLAPSED_ICON = new ImageIcon(
				mxGraphComponent.class
						.getResource("/com/mxgraph/swing/images/collapsed.gif"));
		DEFAULT_WARNING_ICON = new ImageIcon(
				mxGraphComponent.class
						.getResource("/com/mxgraph/swing/images/warning.gif"));
	}

	/**
	 * 
	 */
	protected mxGraph graph;

	/**
	 * 
	 */
	protected mxGraphControl graphControl;

	/**
	 * 
	 */
	protected mxEventSource eventSource = new mxEventSource(this);

	/**
	 * 
	 */
	protected mxICellEditor cellEditor;

	/**
	 * 
	 */
	protected mxConnectionHandler connectionHandler;

	/**
	 * 
	 */
	protected mxPanningHandler panningHandler;

	/**
	 * 
	 */
	protected mxSelectionCellsHandler selectionCellsHandler;

	/**
	 * 
	 */
	protected mxGraphHandler graphHandler;

	/**
	 * The transparency of previewed cells from 0.0. to 0.1. 0.0 indicates
	 * transparent, 1.0 indicates opaque. Default is 1.
	 */
	protected float previewAlpha = 0.5f;

	/**
	 * Specifies the <mxImage> to be returned by <getBackgroundImage>. Default
	 * is null.
	 */
	protected ImageIcon backgroundImage;

	/**
	 * Background page format.
	 */
	protected PageFormat pageFormat = new PageFormat();

	/**
	 * 
	 */
	protected mxInteractiveCanvas canvas;

	/**
	 * 
	 */
	protected BufferedImage tripleBuffer;

	/**
	 * 
	 */
	protected Graphics2D tripleBufferGraphics;

	/**
	 * Defines the scaling for the background page metrics. Default is
	 * {@link #DEFAULT_PAGESCALE}.
	 */
	protected double pageScale = DEFAULT_PAGESCALE;

	/**
	 * Specifies if the background page should be visible. Default is false.
	 */
	protected boolean pageVisible = false;

	/**
	 * If the pageFormat should be used to determine the minimal graph bounds
	 * even if the page is not visible (see pageVisible). Default is false.
	 */
	protected boolean preferPageSize = false;

	/**
	 * Specifies if a dashed line should be drawn between multiple pages.
	 */
	protected boolean pageBreaksVisible = true;

	/**
	 * Specifies the color of page breaks
	 */
	protected Color pageBreakColor = Color.darkGray;

	/**
	 * Specifies the number of pages in the horizontal direction.
	 */
	protected int horizontalPageCount = 1;

	/**
	 * Specifies the number of pages in the vertical direction.
	 */
	protected int verticalPageCount = 1;

	/**
	 * Specifies if the background page should be centered by automatically
	 * setting the translate in the view. Default is true. This does only apply
	 * if pageVisible is true.
	 */
	protected boolean centerPage = true;

	/**
	 * Color of the background area if layout view.
	 */
	protected Color pageBackgroundColor = new Color(144, 153, 174);

	/**
	 * 
	 */
	protected Color pageShadowColor = new Color(110, 120, 140);

	/**
	 * 
	 */
	protected Color pageBorderColor = Color.black;

	/**
	 * Specifies if the grid is visible. Default is false.
	 */
	protected boolean gridVisible = false;

	/**
	 * 
	 */
	protected Color gridColor = new Color(192, 192, 192);

	/**
	 * Whether or not to scroll the scrollable container the graph exists in if
	 * a suitable handler is active and the graph bounds already exist extended
	 * in the direction of mouse travel.
	 */
	protected boolean autoScroll = true;

	/**
	 * Whether to extend the graph bounds and scroll towards the limit of those
	 * new bounds in the direction of mouse travel if a handler is active while
	 * the mouse leaves the container that the graph exists in.
	 */
	protected boolean autoExtend = true;

	/**
	 * 
	 */
	protected boolean dragEnabled = true;

	/**
	 * 
	 */
	protected boolean importEnabled = true;

	/**
	 * 
	 */
	protected boolean exportEnabled = true;

	/**
	 * Specifies if folding (collapse and expand via an image icon in the graph
	 * should be enabled). Default is true.
	 */
	protected boolean foldingEnabled = true;

	/**
	 * Specifies the tolerance for mouse clicks. Default is 4.
	 */
	protected int tolerance = 4;

	/**
	 * Specifies if swimlanes are selected when the mouse is released over the
	 * swimlanes content area. Default is true.
	 */
	protected boolean swimlaneSelectionEnabled = true;

	/**
	 * Specifies if the content area should be transparent to events. Default is
	 * true.
	 */
	protected boolean transparentSwimlaneContent = true;

	/**
	 * 
	 */
	protected int gridStyle = GRID_STYLE_DOT;

	/**
	 * 
	 */
	protected ImageIcon expandedIcon = DEFAULT_EXPANDED_ICON;

	/**
	 * 
	 */
	protected ImageIcon collapsedIcon = DEFAULT_COLLAPSED_ICON;

	/**
	 * 
	 */
	protected ImageIcon warningIcon = DEFAULT_WARNING_ICON;

	/**
	 * 
	 */
	protected boolean antiAlias = true;

	/**
	 * 
	 */
	protected boolean textAntiAlias = true;

	/**
	 * Specifies <escape> should be invoked when the escape key is pressed.
	 * Default is true.
	 */
	protected boolean escapeEnabled = true;

	/**
	 * If true, when editing is to be stopped by way of selection changing, data
	 * in diagram changing or other means stopCellEditing is invoked, and
	 * changes are saved. This is implemented in a mouse listener in this class.
	 * Default is true.
	 */
	protected boolean invokesStopCellEditing = true;

	/**
	 * If true, pressing the enter key without pressing control will stop
	 * editing and accept the new value. This is used in <mxKeyHandler> to stop
	 * cell editing. Default is false.
	 */
	protected boolean enterStopsCellEditing = false;

	/**
	 * Specifies the zoom policy. Default is ZOOM_POLICY_PAGE. The zoom policy
	 * does only apply if pageVisible is true.
	 */
	protected int zoomPolicy = ZOOM_POLICY_PAGE;

	/**
	 * Internal flag to not reset zoomPolicy when zoom was set automatically.
	 */
	private transient boolean zooming = false;

	/**
	 * Specifies the factor used for zoomIn and zoomOut. Default is 1.2 (120%).
	 */
	protected double zoomFactor = 1.2;

	/**
	 * Specifies if the viewport should automatically contain the selection
	 * cells after a zoom operation. Default is false.
	 */
	protected boolean keepSelectionVisibleOnZoom = false;

	/**
	 * Specifies if the zoom operations should go into the center of the actual
	 * diagram rather than going from top, left. Default is true.
	 */
	protected boolean centerZoom = true;

	/**
	 * Specifies if an image buffer should be used for painting the component.
	 * Default is false.
	 */
	protected boolean tripleBuffered = false;

	/**
	 * Used for debugging the dirty region.
	 */
	public boolean showDirtyRectangle = false;

	/**
	 * Maps from cells to lists of heavyweights.
	 */
	protected Hashtable<Object, Component[]> components = new Hashtable<Object, Component[]>();

	/**
	 * Maps from cells to lists of overlays.
	 */
	protected Hashtable<Object, mxICellOverlay[]> overlays = new Hashtable<Object, mxICellOverlay[]>();

	/**
	 * Boolean flag to disable centering after the first time.
	 */
	private transient boolean centerOnResize = true;

	/**
	 * Updates the heavyweight component structure after any changes.
	 */
	protected mxIEventListener updateHandler = new mxIEventListener()
	{
		public void invoke(Object sender, mxEventObject evt)
		{
			updateComponents();
			graphControl.updatePreferredSize();
		}
	};

	/**
	 * 
	 */
	protected mxIEventListener repaintHandler = new mxIEventListener()
	{
		public void invoke(Object source, mxEventObject evt)
		{
			mxRectangle dirty = (mxRectangle) evt.getProperty("region");
			Rectangle rect = (dirty != null) ? dirty.getRectangle() : null;

			if (rect != null)
			{
				rect.grow(1, 1);
			}

			// Updates the triple buffer
			repaintTripleBuffer(rect);

			// Repaints the control using the optional triple buffer
			graphControl.repaint((rect != null) ? rect : getViewport()
					.getViewRect());

			// ----------------------------------------------------------
			// Shows the dirty region as a red rectangle (for debugging)
			JPanel panel = (JPanel) getClientProperty("dirty");

			if (showDirtyRectangle)
			{
				if (panel == null)
				{
					panel = new JPanel();
					panel.setOpaque(false);
					panel.setBorder(BorderFactory.createLineBorder(Color.RED));

					putClientProperty("dirty", panel);
					graphControl.add(panel);
				}

				if (dirty != null)
				{
					panel.setBounds(dirty.getRectangle());
				}

				panel.setVisible(dirty != null);
			}
			else if (panel != null && panel.getParent() != null)
			{
				panel.getParent().remove(panel);
				putClientProperty("dirty", null);
				repaint();
			}
			// ----------------------------------------------------------
		}
	};

	/**
	 * 
	 */
	protected PropertyChangeListener viewChangeHandler = new PropertyChangeListener()
	{
		/**
		 * 
		 */
		public void propertyChange(PropertyChangeEvent evt)
		{
			if (evt.getPropertyName().equals("view"))
			{
				mxGraphView oldView = (mxGraphView) evt.getOldValue();
				mxGraphView newView = (mxGraphView) evt.getNewValue();

				if (oldView != null)
				{
					oldView.removeListener(updateHandler);
				}

				if (newView != null)
				{
					newView.addListener(mxEvent.SCALE, updateHandler);
					newView.addListener(mxEvent.TRANSLATE, updateHandler);
					newView.addListener(mxEvent.SCALE_AND_TRANSLATE,
							updateHandler);
					newView.addListener(mxEvent.UP, updateHandler);
					newView.addListener(mxEvent.DOWN, updateHandler);
				}
			}
			else if (evt.getPropertyName().equals("model"))
			{
				mxGraphModel oldModel = (mxGraphModel) evt.getOldValue();
				mxGraphModel newModel = (mxGraphModel) evt.getNewValue();

				if (oldModel != null)
				{
					oldModel.removeListener(updateHandler);
				}

				if (newModel != null)
				{
					newModel.addListener(mxEvent.CHANGE, updateHandler);
				}
			}
		}

	};

	/**
	 * Resets the zoom policy if the scale is changed manually.
	 */
	protected mxIEventListener scaleHandler = new mxIEventListener()
	{
		/**
		 * 
		 */
		public void invoke(Object sender, mxEventObject evt)
		{
			if (!zooming)
			{
				zoomPolicy = ZOOM_POLICY_NONE;
			}
		}
	};

	/**
	 * 
	 * @param graph
	 */
	public mxGraphComponent(mxGraph graph)
	{
		setCellEditor(createCellEditor());
		canvas = createCanvas();

		// Initializes the buffered view and
		graphControl = createGraphControl();
		installFocusHandler();
		installKeyHandler();
		installResizeHandler();
		setGraph(graph);

		// Adds the viewport view and initializes handlers
		setViewportView(graphControl);
		createHandlers();
		installDoubleClickHandler();
	}

	/**
	 * installs a handler to set the focus to the container.
	 */
	protected void installFocusHandler()
	{
		graphControl.addMouseListener(new MouseAdapter()
		{
			public void mousePressed(MouseEvent e)
			{
				if (!hasFocus())
				{
					requestFocus();
				}
			}
		});
	}

	/**
	 * Handles escape keystrokes.
	 */
	protected void installKeyHandler()
	{
		addKeyListener(new KeyAdapter()
		{
			public void keyPressed(KeyEvent e)
			{
				if (e.getKeyCode() == KeyEvent.VK_ESCAPE && isEscapeEnabled())
				{
					escape(e);
				}
			}
		});
	}

	/**
	 * Applies the zoom policy if the size of the component changes.
	 */
	protected void installResizeHandler()
	{
		addComponentListener(new ComponentAdapter()
		{
			public void componentResized(ComponentEvent e)
			{
				zoomAndCenter();
			}
		});
	}

	/**
	 * Adds handling of edit and stop-edit events after all other handlers have
	 * been installed.
	 */
	protected void installDoubleClickHandler()
	{
		graphControl.addMouseListener(new MouseAdapter()
		{
			public void mouseReleased(MouseEvent e)
			{
				if (isEnabled())
				{
					if (!e.isConsumed() && isEditEvent(e))
					{
						Object cell = getCellAt(e.getX(), e.getY(), false);

						if (cell != null && getGraph().isCellEditable(cell))
						{
							startEditingAtCell(cell, e);
						}
					}
					else
					{
						// Other languages use focus traversal here, in Java
						// we explicitely stop editing after a click elsewhere
						stopEditing(!invokesStopCellEditing);
					}
				}
			}

		});
	}

	/**
	 * 
	 */
	protected mxICellEditor createCellEditor()
	{
		return new mxCellEditor(this);
	}

	/**
	 * 
	 */
	public void setGraph(mxGraph value)
	{
		mxGraph oldValue = graph;

		// Uninstalls listeners for existing graph
		if (graph != null)
		{
			graph.removeListener(repaintHandler);
			graph.getModel().removeListener(updateHandler);
			graph.getView().removeListener(updateHandler);
			graph.removePropertyChangeListener(viewChangeHandler);
			graph.getView().removeListener(scaleHandler);
		}

		graph = value;

		// Updates the buffer if the model changes
		graph.addListener(mxEvent.REPAINT, repaintHandler);

		// Installs the update handler to sync the overlays and controls
		graph.getModel().addListener(mxEvent.CHANGE, updateHandler);

		// Repaint after the following events is handled via
		// mxGraph.repaint-events
		// The respective handlers are installed in mxGraph.setView
		mxGraphView view = graph.getView();

		view.addListener(mxEvent.SCALE, updateHandler);
		view.addListener(mxEvent.TRANSLATE, updateHandler);
		view.addListener(mxEvent.SCALE_AND_TRANSLATE, updateHandler);
		view.addListener(mxEvent.UP, updateHandler);
		view.addListener(mxEvent.DOWN, updateHandler);

		graph.addPropertyChangeListener(viewChangeHandler);

		// Resets the zoom policy if the scale changes
		graph.getView().addListener(mxEvent.SCALE, scaleHandler);
		graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, scaleHandler);

		// Invoke the update handler once for initial state
		updateHandler.invoke(graph.getView(), null);

		firePropertyChange("graph", oldValue, graph);
	}

	/**
	 * 
	 * @return Returns the object that contains the graph.
	 */
	public mxGraph getGraph()
	{
		return graph;
	}

	/**
	 * Creates the inner control that handles tooltips, preferred size and can
	 * draw cells onto a canvas.
	 */
	protected mxGraphControl createGraphControl()
	{
		return new mxGraphControl();
	}

	/**
	 * 
	 * @return Returns the control that renders the graph.
	 */
	public mxGraphControl getGraphControl()
	{
		return graphControl;
	}

	/**
	 * Creates the connection-, panning and graphhandler (in this order).
	 */
	protected void createHandlers()
	{
		setTransferHandler(createTransferHandler());
		panningHandler = createPanningHandler();
		selectionCellsHandler = createSelectionCellsHandler();
		connectionHandler = createConnectionHandler();
		graphHandler = createGraphHandler();
	}

	/**
	 * 
	 */
	protected TransferHandler createTransferHandler()
	{
		return new mxGraphTransferHandler();
	}

	/**
	 *
	 */
	protected mxSelectionCellsHandler createSelectionCellsHandler()
	{
		return new mxSelectionCellsHandler(this);
	}

	/**
	 *
	 */
	protected mxGraphHandler createGraphHandler()
	{
		return new mxGraphHandler(this);
	}

	/**
	 * 
	 */
	public mxSelectionCellsHandler getSelectionCellsHandler()
	{
		return selectionCellsHandler;
	}

	/**
	 * 
	 */
	public mxGraphHandler getGraphHandler()
	{
		return graphHandler;
	}

	/**
	 *
	 */
	protected mxConnectionHandler createConnectionHandler()
	{
		return new mxConnectionHandler(this);
	}

	/**
	 * 
	 */
	public mxConnectionHandler getConnectionHandler()
	{
		return connectionHandler;
	}

	/**
	 *
	 */
	protected mxPanningHandler createPanningHandler()
	{
		return new mxPanningHandler(this);
	}

	/**
	 * 
	 */
	public mxPanningHandler getPanningHandler()
	{
		return panningHandler;
	}

	/**
	 * 
	 */
	public boolean isEditing()
	{
		return getCellEditor().getEditingCell() != null;
	}

	/**
	 * 
	 */
	public mxICellEditor getCellEditor()
	{
		return cellEditor;
	}

	/**
	 * 
	 */
	public void setCellEditor(mxICellEditor value)
	{
		mxICellEditor oldValue = cellEditor;
		cellEditor = value;

		firePropertyChange("cellEditor", oldValue, cellEditor);
	}

	/**
	 * @return the tolerance
	 */
	public int getTolerance()
	{
		return tolerance;
	}

	/**
	 * @param value
	 *            the tolerance to set
	 */
	public void setTolerance(int value)
	{
		int oldValue = tolerance;
		tolerance = value;

		firePropertyChange("tolerance", oldValue, tolerance);
	}

	/**
	 * 
	 */
	public PageFormat getPageFormat()
	{
		return pageFormat;
	}

	/**
	 * 
	 */
	public void setPageFormat(PageFormat value)
	{
		PageFormat oldValue = pageFormat;
		pageFormat = value;

		firePropertyChange("pageFormat", oldValue, pageFormat);
	}

	/**
	 * 
	 */
	public double getPageScale()
	{
		return pageScale;
	}

	/**
	 * 
	 */
	public void setPageScale(double value)
	{
		double oldValue = pageScale;
		pageScale = value;

		firePropertyChange("pageScale", oldValue, pageScale);
	}

	/**
	 * Returns the size of the area that layouts can operate in.
	 */
	public mxRectangle getLayoutAreaSize()
	{
		if (pageVisible)
		{
			Dimension d = getPreferredSizeForPage();

			return new mxRectangle(new Rectangle(d));
		}
		else
		{
			return new mxRectangle(new Rectangle(graphControl.getSize()));
		}
	}

	/**
	 * 
	 */
	public ImageIcon getBackgroundImage()
	{
		return backgroundImage;
	}

	/**
	 * 
	 */
	public void setBackgroundImage(ImageIcon value)
	{
		ImageIcon oldValue = backgroundImage;
		backgroundImage = value;

		firePropertyChange("backgroundImage", oldValue, backgroundImage);
	}

	/**
	 * @return the pageVisible
	 */
	public boolean isPageVisible()
	{
		return pageVisible;
	}

	/**
	 * Fires a property change event for <code>pageVisible</code>. zoomAndCenter
	 * should be called if this is set to true.
	 * 
	 * @param value
	 *            the pageVisible to set
	 */
	public void setPageVisible(boolean value)
	{
		boolean oldValue = pageVisible;
		pageVisible = value;

		firePropertyChange("pageVisible", oldValue, pageVisible);
	}

	/**
	 * @return the preferPageSize
	 */
	public boolean isPreferPageSize()
	{
		return preferPageSize;
	}

	/**
	 * Fires a property change event for <code>preferPageSize</code>.
	 * 
	 * @param value
	 *            the preferPageSize to set
	 */
	public void setPreferPageSize(boolean value)
	{
		boolean oldValue = preferPageSize;
		preferPageSize = value;

		firePropertyChange("preferPageSize", oldValue, preferPageSize);
	}

	/**
	 * @return the pageBreaksVisible
	 */
	public boolean isPageBreaksVisible()
	{
		return pageBreaksVisible;
	}

	/**
	 * @param value
	 *            the pageBreaksVisible to set
	 */
	public void setPageBreaksVisible(boolean value)
	{
		boolean oldValue = pageBreaksVisible;
		pageBreaksVisible = value;

		firePropertyChange("pageBreaksVisible", oldValue, pageBreaksVisible);
	}

	/**
	 * @return the pageBreakColor
	 */
	public Color getPageBreakColor()
	{
		return pageBreakColor;
	}

	/**
	 * @param pageBreakColor the pageBreakColor to set
	 */
	public void setPageBreakColor(Color pageBreakColor)
	{
		this.pageBreakColor = pageBreakColor;
	}

	/**
	 * @param value
	 *            the horizontalPageCount to set
	 */
	public void setHorizontalPageCount(int value)
	{
		int oldValue = horizontalPageCount;
		horizontalPageCount = value;

		firePropertyChange("horizontalPageCount", oldValue, horizontalPageCount);
	}

	/**
	 * 
	 */
	public int getHorizontalPageCount()
	{
		return horizontalPageCount;
	}

	/**
	 * @param value
	 *            the verticalPageCount to set
	 */
	public void setVerticalPageCount(int value)
	{
		int oldValue = verticalPageCount;
		verticalPageCount = value;

		firePropertyChange("verticalPageCount", oldValue, verticalPageCount);
	}

	/**
	 * 
	 */
	public int getVerticalPageCount()
	{
		return verticalPageCount;
	}

	/**
	 * @return the centerPage
	 */
	public boolean isCenterPage()
	{
		return centerPage;
	}

	/**
	 * zoomAndCenter should be called if this is set to true.
	 * 
	 * @param value
	 *            the centerPage to set
	 */
	public void setCenterPage(boolean value)
	{
		boolean oldValue = centerPage;
		centerPage = value;

		firePropertyChange("centerPage", oldValue, centerPage);
	}

	/**
	 * @return the pageBackgroundColor
	 */
	public Color getPageBackgroundColor()
	{
		return pageBackgroundColor;
	}

	/**
	 * Sets the color that appears behind the page.
	 * 
	 * @param value
	 *            the pageBackgroundColor to set
	 */
	public void setPageBackgroundColor(Color value)
	{
		Color oldValue = pageBackgroundColor;
		pageBackgroundColor = value;

		firePropertyChange("pageBackgroundColor", oldValue, pageBackgroundColor);
	}

	/**
	 * @return the pageShadowColor
	 */
	public Color getPageShadowColor()
	{
		return pageShadowColor;
	}

	/**
	 * @param value
	 *            the pageShadowColor to set
	 */
	public void setPageShadowColor(Color value)
	{
		Color oldValue = pageShadowColor;
		pageShadowColor = value;

		firePropertyChange("pageShadowColor", oldValue, pageShadowColor);
	}

	/**
	 * @return the pageShadowColor
	 */
	public Color getPageBorderColor()
	{
		return pageBorderColor;
	}

	/**
	 * @param value
	 *            the pageBorderColor to set
	 */
	public void setPageBorderColor(Color value)
	{
		Color oldValue = pageBorderColor;
		pageBorderColor = value;

		firePropertyChange("pageBorderColor", oldValue, pageBorderColor);
	}

	/**
	 * @return the keepSelectionVisibleOnZoom
	 */
	public boolean isKeepSelectionVisibleOnZoom()
	{
		return keepSelectionVisibleOnZoom;
	}

	/**
	 * @param value
	 *            the keepSelectionVisibleOnZoom to set
	 */
	public void setKeepSelectionVisibleOnZoom(boolean value)
	{
		boolean oldValue = keepSelectionVisibleOnZoom;
		keepSelectionVisibleOnZoom = value;

		firePropertyChange("keepSelectionVisibleOnZoom", oldValue,
				keepSelectionVisibleOnZoom);
	}

	/**
	 * @return the zoomFactor
	 */
	public double getZoomFactor()
	{
		return zoomFactor;
	}

	/**
	 * @param value
	 *            the zoomFactor to set
	 */
	public void setZoomFactor(double value)
	{
		double oldValue = zoomFactor;
		zoomFactor = value;

		firePropertyChange("zoomFactor", oldValue, zoomFactor);
	}

	/**
	 * @return the centerZoom
	 */
	public boolean isCenterZoom()
	{
		return centerZoom;
	}

	/**
	 * @param value
	 *            the centerZoom to set
	 */
	public void setCenterZoom(boolean value)
	{
		boolean oldValue = centerZoom;
		centerZoom = value;

		firePropertyChange("centerZoom", oldValue, centerZoom);
	}

	/**
	 * 
	 */
	public void setZoomPolicy(int value)
	{
		int oldValue = zoomPolicy;
		zoomPolicy = value;

		if (zoomPolicy != ZOOM_POLICY_NONE)
		{
			zoom(zoomPolicy == ZOOM_POLICY_PAGE, true);
		}

		firePropertyChange("zoomPolicy", oldValue, zoomPolicy);
	}

	/**
	 * 
	 */
	public int getZoomPolicy()
	{
		return zoomPolicy;
	}

	/**
	 * Callback to process an escape keystroke.
	 * 
	 * @param e
	 */
	public void escape(KeyEvent e)
	{
		if (selectionCellsHandler != null)
		{
			selectionCellsHandler.reset();
		}

		if (connectionHandler != null)
		{
			connectionHandler.reset();
		}

		if (graphHandler != null)
		{
			graphHandler.reset();
		}

		if (cellEditor != null)
		{
			cellEditor.stopEditing(true);
		}
	}

	/**
	 * Clones and inserts the given cells into the graph using the move method
	 * and returns the inserted cells. This shortcut is used if cells are
	 * inserted via datatransfer.
	 */
	public Object[] importCells(Object[] cells, double dx, double dy,
			Object target, Point location)
	{
		return graph.moveCells(cells, dx, dy, true, target, location);
	}

	/**
	 * Refreshes the display and handles.
	 */
	public void refresh()
	{
		graph.refresh();
		selectionCellsHandler.refresh();
	}

	/**
	 * Returns an mxPoint representing the given event in the unscaled,
	 * non-translated coordinate space and applies the grid.
	 */
	public mxPoint getPointForEvent(MouseEvent e)
	{
		return getPointForEvent(e, true);
	}

	/**
	 * Returns an mxPoint representing the given event in the unscaled,
	 * non-translated coordinate space and applies the grid.
	 */
	public mxPoint getPointForEvent(MouseEvent e, boolean addOffset)
	{
		double s = graph.getView().getScale();
		mxPoint tr = graph.getView().getTranslate();

		double off = (addOffset) ? graph.getGridSize() / 2 : 0;
		double x = graph.snap(e.getX() / s - tr.getX() - off);
		double y = graph.snap(e.getY() / s - tr.getY() - off);

		return new mxPoint(x, y);
	}

	/**
	 * 
	 */
	public void startEditing()
	{
		startEditingAtCell(null);
	}

	/**
	 * 
	 */
	public void startEditingAtCell(Object cell)
	{
		startEditingAtCell(cell, null);
	}

	/**
	 * 
	 */
	public void startEditingAtCell(Object cell, EventObject evt)
	{
		if (cell == null)
		{
			cell = graph.getSelectionCell();

			if (cell != null && !graph.isCellEditable(cell))
			{
				cell = null;
			}
		}

		if (cell != null)
		{
			eventSource.fireEvent(new mxEventObject(mxEvent.START_EDITING,
					"cell", cell, "event", evt));
			cellEditor.startEditing(cell, evt);
		}
	}

	/**
	 * 
	 */
	public String getEditingValue(Object cell, EventObject trigger)
	{
		return graph.convertValueToString(cell);
	}

	/**
	 * 
	 */
	public void stopEditing(boolean cancel)
	{
		cellEditor.stopEditing(cancel);
	}

	/**
	 * Sets the label of the specified cell to the given value using
	 * mxGraph.cellLabelChanged and fires mxEvent.LABEL_CHANGED while the
	 * transaction is in progress. Returns the cell whose label was changed.
	 * 
	 * @param cell
	 *            Cell whose label should be changed.
	 * @param value
	 *            New value of the label.
	 * @param evt
	 *            Optional event that triggered the change.
	 */
	public Object labelChanged(Object cell, Object value, EventObject evt)
	{
		mxIGraphModel model = graph.getModel();

		model.beginUpdate();
		try
		{
			graph.cellLabelChanged(cell, value, graph.isAutoSizeCell(cell));
			eventSource.fireEvent(new mxEventObject(mxEvent.LABEL_CHANGED,
					"cell", cell, "value", value, "event", evt));
		}
		finally
		{
			model.endUpdate();
		}

		return cell;
	}

	/**
	 * Returns the (unscaled) preferred size for the current page format (scaled
	 * by pageScale).
	 */
	protected Dimension getPreferredSizeForPage()
	{
		return new Dimension((int) Math.round(pageFormat.getWidth() * pageScale
				* horizontalPageCount), (int) Math.round(pageFormat.getHeight()
				* pageScale * verticalPageCount));
	}

	/**
	 * Returns the vertical border between the page and the control.
	 */
	public int getVerticalPageBorder()
	{
		return (int) Math.round(pageFormat.getWidth() * pageScale);
	}

	/**
	 * Returns the horizontal border between the page and the control.
	 */
	public int getHorizontalPageBorder()
	{
		return (int) Math.round(0.5 * pageFormat.getHeight() * pageScale);
	}

	/**
	 * Returns the scaled preferred size for the current graph.
	 */
	protected Dimension getScaledPreferredSizeForGraph()
	{
		mxRectangle bounds = graph.getGraphBounds();
		int border = graph.getBorder();

		return new Dimension(
				(int) Math.round(bounds.getX() + bounds.getWidth()) + border
						+ 1, (int) Math.round(bounds.getY()
						+ bounds.getHeight())
						+ border + 1);
	}

	/**
	 * Should be called by a hook inside mxGraphView/mxGraph
	 */
	protected mxPoint getPageTranslate(double scale)
	{
		Dimension d = getPreferredSizeForPage();
		Dimension bd = new Dimension(d);

		if (!preferPageSize)
		{
			bd.width += 2 * getHorizontalPageBorder();
			bd.height += 2 * getVerticalPageBorder();
		}

		double width = Math.max(bd.width, (getViewport().getWidth() - 8)
				/ scale);
		double height = Math.max(bd.height, (getViewport().getHeight() - 8)
				/ scale);

		double dx = Math.max(0, (width - d.width) / 2);
		double dy = Math.max(0, (height - d.height) / 2);

		return new mxPoint(dx, dy);
	}

	/**
	 * Invoked after the component was resized to update the zoom if the zoom
	 * policy is not none and/or update the translation of the diagram if
	 * pageVisible and centerPage are true.
	 */
	public void zoomAndCenter()
	{
		if (zoomPolicy != ZOOM_POLICY_NONE)
		{
			// Centers only on the initial zoom call
			zoom(zoomPolicy == ZOOM_POLICY_PAGE, centerOnResize
					|| zoomPolicy == ZOOM_POLICY_PAGE);
			centerOnResize = false;
		}
		else if (pageVisible && centerPage)
		{
			mxPoint translate = getPageTranslate(graph.getView().getScale());
			graph.getView().setTranslate(translate);
		}
		else
		{
			getGraphControl().updatePreferredSize();
		}
	}

	/**
	 * Zooms into the graph by zoomFactor.
	 */
	public void zoomIn()
	{
		zoom(zoomFactor);
	}

	/**
	 * Function: zoomOut
	 * 
	 * Zooms out of the graph by <zoomFactor>.
	 */
	public void zoomOut()
	{
		zoom(1 / zoomFactor);
	}

	/**
	 * 
	 */
	public void zoom(double factor)
	{
		mxGraphView view = graph.getView();
		double newScale = (double) ((int) (view.getScale() * 100 * factor)) / 100;

		if (newScale != view.getScale() && newScale > 0.04)
		{
			mxPoint translate = (pageVisible && centerPage) ? getPageTranslate(newScale)
					: new mxPoint();
			graph.getView().scaleAndTranslate(newScale, translate.getX(),
					translate.getY());

			if (keepSelectionVisibleOnZoom && !graph.isSelectionEmpty())
			{
				mxRectangle rect = view.getBoundingBox(graph.getSelectionCells());
				if (rect != null)
					getGraphControl().scrollRectToVisible(rect.getRectangle());
			}
			else
			{
				maintainScrollBar(true, factor, centerZoom);
				maintainScrollBar(false, factor, centerZoom);
			}
		}
	}

	/**
	 * 
	 */
	public void zoomTo(final double newScale, final boolean center)
	{
		mxGraphView view = graph.getView();
		final double scale = view.getScale();

		mxPoint translate = (pageVisible && centerPage) ? getPageTranslate(newScale)
				: new mxPoint();
		graph.getView().scaleAndTranslate(newScale, translate.getX(),
				translate.getY());

		// Causes two repaints on the scrollpane, namely one for the scale
		// change with the new preferred size and one for the change of
		// the scrollbar position. The latter cannot be done immediately
		// because the scrollbar keeps the value <= max - extent, and if
		// max is changed the value change will trigger a syncScrollPane
		// WithViewport in BasicScrollPaneUI, which will update the value
		// for the previous maximum (ie. it must be invoked later).
		SwingUtilities.invokeLater(new Runnable()
		{
			public void run()
			{
				maintainScrollBar(true, newScale / scale, center);
				maintainScrollBar(false, newScale / scale, center);
			}
		});
	}

	/**
	 * Function: zoomActual
	 * 
	 * Resets the zoom and panning in the view.
	 */
	public void zoomActual()
	{
		mxPoint translate = (pageVisible && centerPage) ? getPageTranslate(1)
				: new mxPoint();
		graph.getView()
				.scaleAndTranslate(1, translate.getX(), translate.getY());

		if (isPageVisible())
		{
			// Causes two repaints, see zoomTo for more details
			SwingUtilities.invokeLater(new Runnable()
			{
				public void run()
				{
					Dimension pageSize = getPreferredSizeForPage();

					if (getViewport().getWidth() > pageSize.getWidth())
					{
						scrollToCenter(true);
					}
					else
					{
						JScrollBar scrollBar = getHorizontalScrollBar();

						if (scrollBar != null)
						{
							scrollBar.setValue((scrollBar.getMaximum() / 3) - 4);
						}
					}

					if (getViewport().getHeight() > pageSize.getHeight())
					{
						scrollToCenter(false);
					}
					else
					{
						JScrollBar scrollBar = getVerticalScrollBar();

						if (scrollBar != null)
						{
							scrollBar.setValue((scrollBar.getMaximum() / 4) - 4);
						}
					}
				}
			});
		}
	}

	/**
	 * 
	 */
	public void zoom(final boolean page, final boolean center)
	{
		if (pageVisible && !zooming)
		{
			zooming = true;

			try
			{
				int off = (getPageShadowColor() != null) ? 8 : 0;
				
				// Adds some extra space for the shadow and border
				double width = getViewport().getWidth() - off;
				double height = getViewport().getHeight() - off;

				Dimension d = getPreferredSizeForPage();
				double pageWidth = d.width;
				double pageHeight = d.height;

				double scaleX = width / pageWidth;
				double scaleY = (page) ? height / pageHeight : scaleX;

				// Rounds the new scale to 5% steps
				final double newScale = (double) ((int) (Math.min(scaleX,
						scaleY) * 20)) / 20;

				if (newScale > 0)
				{
					mxGraphView graphView = graph.getView();
					final double scale = graphView.getScale();
					mxPoint translate = (centerPage) ? getPageTranslate(newScale)
							: new mxPoint();
					graphView.scaleAndTranslate(newScale, translate.getX(),
							translate.getY());

					// Causes two repaints, see zoomTo for more details
					final double factor = newScale / scale;

					SwingUtilities.invokeLater(new Runnable()
					{
						public void run()
						{
							if (center)
							{
								if (page)
								{
									scrollToCenter(true);
									scrollToCenter(false);
								}
								else
								{
									scrollToCenter(true);
									maintainScrollBar(false, factor, false);
								}
							}
							else if (factor != 1)
							{
								maintainScrollBar(true, factor, false);
								maintainScrollBar(false, factor, false);
							}
						}
					});
				}
			}
			finally
			{
				zooming = false;
			}
		}
	}

	/**
	 *
	 */
	protected void maintainScrollBar(boolean horizontal, double factor,
			boolean center)
	{
		JScrollBar scrollBar = (horizontal) ? getHorizontalScrollBar()
				: getVerticalScrollBar();

		if (scrollBar != null)
		{
			BoundedRangeModel model = scrollBar.getModel();
			int newValue = (int) Math.round(model.getValue() * factor)
					+ (int) Math.round((center) ? (model.getExtent()
							* (factor - 1) / 2) : 0);
			model.setValue(newValue);
		}
	}

	/**
	 * 
	 */
	public void scrollToCenter(boolean horizontal)
	{
		JScrollBar scrollBar = (horizontal) ? getHorizontalScrollBar()
				: getVerticalScrollBar();

		if (scrollBar != null)
		{
			final BoundedRangeModel model = scrollBar.getModel();
			final int newValue = ((model.getMaximum()) / 2) - model.getExtent()
					/ 2;
			model.setValue(newValue);
		}
	}

	/**
	 * Scrolls the graph so that it shows the given cell.
	 * 
	 * @param cell
	 */
	public void scrollCellToVisible(Object cell)
	{
		scrollCellToVisible(cell, false);
	}

	/**
	 * Scrolls the graph so that it shows the given cell.
	 * 
	 * @param cell
	 */
	public void scrollCellToVisible(Object cell, boolean center)
	{
		mxCellState state = graph.getView().getState(cell);

		if (state != null)
		{
			mxRectangle bounds = state;

			if (center)
			{
				bounds = (mxRectangle) bounds.clone();

				bounds.setX(bounds.getCenterX() - getWidth() / 2);
				bounds.setWidth(getWidth());
				bounds.setY(bounds.getCenterY() - getHeight() / 2);
				bounds.setHeight(getHeight());
			}

			getGraphControl().scrollRectToVisible(bounds.getRectangle());
		}
	}

	/**
	 * 
	 * @param x
	 * @param y
	 * @return Returns the cell at the given location.
	 */
	public Object getCellAt(int x, int y)
	{
		return getCellAt(x, y, true);
	}

	/**
	 * 
	 * @param x
	 * @param y
	 * @param hitSwimlaneContent
	 * @return Returns the cell at the given location.
	 */
	public Object getCellAt(int x, int y, boolean hitSwimlaneContent)
	{
		return getCellAt(x, y, hitSwimlaneContent, null);
	}

	/**
	 * Returns the bottom-most cell that intersects the given point (x, y) in
	 * the cell hierarchy starting at the given parent.
	 * 
	 * @param x
	 *            X-coordinate of the location to be checked.
	 * @param y
	 *            Y-coordinate of the location to be checked.
	 * @param parent
	 *            <mxCell> that should be used as the root of the recursion.
	 *            Default is <defaultParent>.
	 * @return Returns the child at the given location.
	 */
	public Object getCellAt(int x, int y, boolean hitSwimlaneContent,
			Object parent)
	{
		if (parent == null)
		{
			parent = graph.getDefaultParent();
		}

		if (parent != null)
		{
			mxPoint previousTranslate = canvas.getTranslate();
			double previousScale = canvas.getScale();

			try
			{
				canvas.setScale(graph.getView().getScale());
				canvas.setTranslate(0, 0);

				mxIGraphModel model = graph.getModel();
				mxGraphView view = graph.getView();

				Rectangle hit = new Rectangle(x, y, 1, 1);
				int childCount = model.getChildCount(parent);

				for (int i = childCount - 1; i >= 0; i--)
				{
					Object cell = model.getChildAt(parent, i);
					Object result = getCellAt(x, y, hitSwimlaneContent, cell);

					if (result != null)
					{
						return result;
					}
					else if (graph.isCellVisible(cell))
					{
						mxCellState state = view.getState(cell);

						if (state != null
								&& canvas.intersects(this, hit, state)
								&& (!graph.isSwimlane(cell)
										|| hitSwimlaneContent || (transparentSwimlaneContent && !canvas
										.hitSwimlaneContent(this, state, x, y))))
						{
							return cell;
						}
					}
				}
			}
			finally
			{
				canvas.setScale(previousScale);
				canvas.setTranslate((int) previousTranslate.getX(), (int) previousTranslate.getY());
			}
		}

		return null;
	}

	/**
	 * 
	 */
	public void setSwimlaneSelectionEnabled(boolean value)
	{
		boolean oldValue = swimlaneSelectionEnabled;
		swimlaneSelectionEnabled = value;

		firePropertyChange("swimlaneSelectionEnabled", oldValue,
				swimlaneSelectionEnabled);
	}

	/**
	 * 
	 */
	public boolean isSwimlaneSelectionEnabled()
	{
		return swimlaneSelectionEnabled;
	}

	/**
	 * 
	 */
	public Object[] selectRegion(Rectangle rect, MouseEvent e)
	{
		Object[] cells = getCells(rect);

		if (cells.length > 0)
		{
			selectCellsForEvent(cells, e);
		}
		else if (!graph.isSelectionEmpty() && !e.isConsumed())
		{
			graph.clearSelection();
		}

		return cells;
	}

	/**
	 * Returns the cells inside the given rectangle.
	 * 
	 * @return Returns the cells inside the given rectangle.
	 */
	public Object[] getCells(Rectangle rect)
	{
		return getCells(rect, null);
	}

	/**
	 * Returns the children of the given parent that are contained in the given
	 * rectangle (x, y, width, height). The result is added to the optional
	 * result array, which is returned from the function. If no result array is
	 * specified then a new array is created and returned.
	 * 
	 * @return Returns the children inside the given rectangle.
	 */
	public Object[] getCells(Rectangle rect, Object parent)
	{
		Collection<Object> result = new ArrayList<Object>();

		if (rect.width > 0 || rect.height > 0)
		{
			if (parent == null)
			{
				parent = graph.getDefaultParent();
			}

			if (parent != null)
			{
				mxPoint previousTranslate = canvas.getTranslate();
				double previousScale = canvas.getScale();

				try
				{
					canvas.setScale(graph.getView().getScale());
					canvas.setTranslate(0, 0);

					mxIGraphModel model = graph.getModel();
					mxGraphView view = graph.getView();

					int childCount = model.getChildCount(parent);

					for (int i = 0; i < childCount; i++)
					{
						Object cell = model.getChildAt(parent, i);
						mxCellState state = view.getState(cell);

						if (graph.isCellVisible(cell) && state != null)
						{
							if (canvas.contains(this, rect, state))
							{
								result.add(cell);
							}
							else
							{
								result.addAll(Arrays
										.asList(getCells(rect, cell)));
							}
						}
					}
				}
				finally
				{
					canvas.setScale(previousScale);
					canvas.setTranslate(previousTranslate.getX(),
							previousTranslate.getY());
				}
			}
		}

		return result.toArray();
	}

	/**
	 * Selects the cells for the given event.
	 */
	public void selectCellsForEvent(Object[] cells, MouseEvent event)
	{
		if (isToggleEvent(event))
		{
			graph.addSelectionCells(cells);
		}
		else
		{
			graph.setSelectionCells(cells);
		}
	}

	/**
	 * Selects the cell for the given event.
	 */
	public void selectCellForEvent(Object cell, MouseEvent e)
	{
		boolean isSelected = graph.isCellSelected(cell);

		if (isToggleEvent(e))
		{
			if (isSelected)
			{
				graph.removeSelectionCell(cell);
			}
			else
			{
				graph.addSelectionCell(cell);
			}
		}
		else if (!isSelected || graph.getSelectionCount() != 1)
		{
			graph.setSelectionCell(cell);
		}
	}

	/**
	 * Returns true if the absolute value of one of the given parameters is
	 * greater than the tolerance.
	 */
	public boolean isSignificant(double dx, double dy)
	{
		return Math.abs(dx) > tolerance || Math.abs(dy) > tolerance;
	}

	/**
	 * Returns the icon used to display the collapsed state of the specified
	 * cell state. This returns null for all edges.
	 */
	public ImageIcon getFoldingIcon(mxCellState state)
	{
		if (state != null && isFoldingEnabled()
				&& !getGraph().getModel().isEdge(state.getCell()))
		{
			Object cell = state.getCell();
			boolean tmp = graph.isCellCollapsed(cell);

			if (graph.isCellFoldable(cell, !tmp))
			{
				return (tmp) ? collapsedIcon : expandedIcon;
			}
		}

		return null;
	}

	/**
	 * 
	 */
	public Rectangle getFoldingIconBounds(mxCellState state, ImageIcon icon)
	{
		mxIGraphModel model = graph.getModel();
		boolean isEdge = model.isEdge(state.getCell());
		double scale = getGraph().getView().getScale();

		int x = (int) Math.round(state.getX() + 4 * scale);
		int y = (int) Math.round(state.getY() + 4 * scale);
		int w = (int) Math.max(8, icon.getIconWidth() * scale);
		int h = (int) Math.max(8, icon.getIconHeight() * scale);

		if (isEdge)
		{
			mxPoint pt = graph.getView().getPoint(state);

			x = (int) pt.getX() - w / 2;
			y = (int) pt.getY() - h / 2;
		}

		return new Rectangle(x, y, w, h);
	}

	/**
	 *
	 */
	public boolean hitFoldingIcon(Object cell, int x, int y)
	{
		if (cell != null)
		{
			mxIGraphModel model = graph.getModel();

			// Draws the collapse/expand icons
			boolean isEdge = model.isEdge(cell);

			if (foldingEnabled && (model.isVertex(cell) || isEdge))
			{
				mxCellState state = graph.getView().getState(cell);

				if (state != null)
				{
					ImageIcon icon = getFoldingIcon(state);

					if (icon != null)
					{
						return getFoldingIconBounds(state, icon).contains(x, y);
					}
				}
			}
		}

		return false;
	}

	/**
	 * 
	 * @param enabled
	 */
	public void setToolTips(boolean enabled)
	{
		if (enabled)
		{
			ToolTipManager.sharedInstance().registerComponent(graphControl);
		}
		else
		{
			ToolTipManager.sharedInstance().unregisterComponent(graphControl);
		}
	}

	/**
	 * 
	 */
	public boolean isConnectable()
	{
		return connectionHandler.isEnabled();
	}

	/**
	 * @param connectable
	 */
	public void setConnectable(boolean connectable)
	{
		connectionHandler.setEnabled(connectable);
	}

	/**
	 * 
	 */
	public boolean isPanning()
	{
		return panningHandler.isEnabled();
	}

	/**
	 * @param enabled
	 */
	public void setPanning(boolean enabled)
	{
		panningHandler.setEnabled(enabled);
	}

	/**
	 * @return the autoScroll
	 */
	public boolean isAutoScroll()
	{
		return autoScroll;
	}

	/**
	 * @param value
	 *            the autoScroll to set
	 */
	public void setAutoScroll(boolean value)
	{
		autoScroll = value;
	}

	/**
	 * @return the autoExtend
	 */
	public boolean isAutoExtend()
	{
		return autoExtend;
	}

	/**
	 * @param value
	 *            the autoExtend to set
	 */
	public void setAutoExtend(boolean value)
	{
		autoExtend = value;
	}

	/**
	 * @return the escapeEnabled
	 */
	public boolean isEscapeEnabled()
	{
		return escapeEnabled;
	}

	/**
	 * @param value
	 *            the escapeEnabled to set
	 */
	public void setEscapeEnabled(boolean value)
	{
		boolean oldValue = escapeEnabled;
		escapeEnabled = value;

		firePropertyChange("escapeEnabled", oldValue, escapeEnabled);
	}

	/**
	 * @return the escapeEnabled
	 */
	public boolean isInvokesStopCellEditing()
	{
		return invokesStopCellEditing;
	}

	/**
	 * @param value
	 *            the invokesStopCellEditing to set
	 */
	public void setInvokesStopCellEditing(boolean value)
	{
		boolean oldValue = invokesStopCellEditing;
		invokesStopCellEditing = value;

		firePropertyChange("invokesStopCellEditing", oldValue,
				invokesStopCellEditing);
	}

	/**
	 * @return the enterStopsCellEditing
	 */
	public boolean isEnterStopsCellEditing()
	{
		return enterStopsCellEditing;
	}

	/**
	 * @param value
	 *            the enterStopsCellEditing to set
	 */
	public void setEnterStopsCellEditing(boolean value)
	{
		boolean oldValue = enterStopsCellEditing;
		enterStopsCellEditing = value;

		firePropertyChange("enterStopsCellEditing", oldValue,
				enterStopsCellEditing);
	}

	/**
	 * @return the dragEnabled
	 */
	public boolean isDragEnabled()
	{
		return dragEnabled;
	}

	/**
	 * @param value
	 *            the dragEnabled to set
	 */
	public void setDragEnabled(boolean value)
	{
		boolean oldValue = dragEnabled;
		dragEnabled = value;

		firePropertyChange("dragEnabled", oldValue, dragEnabled);
	}

	/**
	 * @return the gridVisible
	 */
	public boolean isGridVisible()
	{
		return gridVisible;
	}

	/**
	 * Fires a property change event for <code>gridVisible</code>.
	 * 
	 * @param value
	 *            the gridVisible to set
	 */
	public void setGridVisible(boolean value)
	{
		boolean oldValue = gridVisible;
		gridVisible = value;

		firePropertyChange("gridVisible", oldValue, gridVisible);
	}

	/**
	 * @return the gridVisible
	 */
	public boolean isAntiAlias()
	{
		return antiAlias;
	}

	/**
	 * Fires a property change event for <code>antiAlias</code>.
	 * 
	 * @param value
	 *            the antiAlias to set
	 */
	public void setAntiAlias(boolean value)
	{
		boolean oldValue = antiAlias;
		antiAlias = value;

		firePropertyChange("antiAlias", oldValue, antiAlias);
	}

	/**
	 * @return the gridVisible
	 */
	public boolean isTextAntiAlias()
	{
		return antiAlias;
	}

	/**
	 * Fires a property change event for <code>textAntiAlias</code>.
	 * 
	 * @param value
	 *            the textAntiAlias to set
	 */
	public void setTextAntiAlias(boolean value)
	{
		boolean oldValue = textAntiAlias;
		textAntiAlias = value;

		firePropertyChange("textAntiAlias", oldValue, textAntiAlias);
	}

	/**
	 * 
	 */
	public float getPreviewAlpha()
	{
		return previewAlpha;
	}

	/**
	 * 
	 */
	public void setPreviewAlpha(float value)
	{
		float oldValue = previewAlpha;
		previewAlpha = value;

		firePropertyChange("previewAlpha", oldValue, previewAlpha);
	}

	/**
	 * @return the tripleBuffered
	 */
	public boolean isTripleBuffered()
	{
		return tripleBuffered;
	}

	/**
	 * Hook for dynamic triple buffering condition.
	 */
	public boolean isForceTripleBuffered()
	{
		// LATER: Dynamic condition (cell density) to use triple
		// buffering for a large number of cells on a small rect
		return false;
	}

	/**
	 * @param value
	 *            the tripleBuffered to set
	 */
	public void setTripleBuffered(boolean value)
	{
		boolean oldValue = tripleBuffered;
		tripleBuffered = value;

		firePropertyChange("tripleBuffered", oldValue, tripleBuffered);
	}

	/**
	 * @return the gridColor
	 */
	public Color getGridColor()
	{
		return gridColor;
	}

	/**
	 * Fires a property change event for <code>gridColor</code>.
	 * 
	 * @param value
	 *            the gridColor to set
	 */
	public void setGridColor(Color value)
	{
		Color oldValue = gridColor;
		gridColor = value;

		firePropertyChange("gridColor", oldValue, gridColor);
	}

	/**
	 * @return the gridStyle
	 */
	public int getGridStyle()
	{
		return gridStyle;
	}

	/**
	 * Fires a property change event for <code>gridStyle</code>.
	 * 
	 * @param value
	 *            the gridStyle to set
	 */
	public void setGridStyle(int value)
	{
		int oldValue = gridStyle;
		gridStyle = value;

		firePropertyChange("gridStyle", oldValue, gridStyle);
	}

	/**
	 * Returns importEnabled.
	 */
	public boolean isImportEnabled()
	{
		return importEnabled;
	}

	/**
	 * Sets importEnabled.
	 */
	public void setImportEnabled(boolean value)
	{
		boolean oldValue = importEnabled;
		importEnabled = value;

		firePropertyChange("importEnabled", oldValue, importEnabled);
	}

	/**
	 * Returns all cells which may be imported via datatransfer.
	 */
	public Object[] getImportableCells(Object[] cells)
	{
		return mxGraphModel.filterCells(cells, new Filter()
		{
			public boolean filter(Object cell)
			{
				return canImportCell(cell);
			}
		});
	}

	/**
	 * Returns true if the given cell can be imported via datatransfer. This
	 * returns importEnabled.
	 */
	public boolean canImportCell(Object cell)
	{
		return isImportEnabled();
	}

	/**
	 * @return the exportEnabled
	 */
	public boolean isExportEnabled()
	{
		return exportEnabled;
	}

	/**
	 * @param value
	 *            the exportEnabled to set
	 */
	public void setExportEnabled(boolean value)
	{
		boolean oldValue = exportEnabled;
		exportEnabled = value;

		firePropertyChange("exportEnabled", oldValue, exportEnabled);
	}

	/**
	 * Returns all cells which may be exported via datatransfer.
	 */
	public Object[] getExportableCells(Object[] cells)
	{
		return mxGraphModel.filterCells(cells, new Filter()
		{
			public boolean filter(Object cell)
			{
				return canExportCell(cell);
			}
		});
	}

	/**
	 * Returns true if the given cell can be exported via datatransfer.
	 */
	public boolean canExportCell(Object cell)
	{
		return isExportEnabled();
	}

	/**
	 * @return the foldingEnabled
	 */
	public boolean isFoldingEnabled()
	{
		return foldingEnabled;
	}

	/**
	 * @param value
	 *            the foldingEnabled to set
	 */
	public void setFoldingEnabled(boolean value)
	{
		boolean oldValue = foldingEnabled;
		foldingEnabled = value;

		firePropertyChange("foldingEnabled", oldValue, foldingEnabled);
	}

	/**
	 * 
	 */
	public boolean isEditEvent(MouseEvent e)
	{
		return (e != null) ? e.getClickCount() == 2 : false;
	}

	/**
	 * 
	 * @param event
	 * @return Returns true if the given event should toggle selected cells.
	 */
	public boolean isCloneEvent(MouseEvent event)
	{
		return (event != null) ? event.isControlDown() : false;
	}

	/**
	 * 
	 * @param event
	 * @return Returns true if the given event should toggle selected cells.
	 */
	public boolean isToggleEvent(MouseEvent event)
	{
		// NOTE: IsMetaDown always returns true for right-clicks on the Mac, so
		// toggle selection for left mouse buttons requires CMD key to be pressed,
		// but toggle for right mouse buttons requires CTRL to be pressed.
		return (event != null) ? ((mxUtils.IS_MAC) ? ((SwingUtilities
				.isLeftMouseButton(event) && event.isMetaDown()) || (SwingUtilities
				.isRightMouseButton(event) && event.isControlDown()))
				: event.isControlDown())
				: false;
	}

	/**
	 * 
	 * @param event
	 * @return Returns true if the given event allows the grid to be applied.
	 */
	public boolean isGridEnabledEvent(MouseEvent event)
	{
		return (event != null) ? !event.isAltDown() : false;
	}

	/**
	 * Note: This is not used during drag and drop operations due to limitations
	 * of the underlying API. To enable this for move operations set dragEnabled
	 * to false.
	 * 
	 * @param event
	 * @return Returns true if the given event is a panning event.
	 */
	public boolean isPanningEvent(MouseEvent event)
	{
		return (event != null) ? event.isShiftDown() && event.isControlDown()
				: false;
	}

	/**
	 * Note: This is not used during drag and drop operations due to limitations
	 * of the underlying API. To enable this for move operations set dragEnabled
	 * to false.
	 * 
	 * @param event
	 * @return Returns true if the given event is constrained.
	 */
	public boolean isConstrainedEvent(MouseEvent event)
	{
		return (event != null) ? event.isShiftDown() : false;
	}

	/**
	 * Note: This is not used during drag and drop operations due to limitations
	 * of the underlying API. To enable this for move operations set dragEnabled
	 * to false.
	 * 
	 * @param event
	 * @return Returns true if the given event is constrained.
	 */
	public boolean isForceMarqueeEvent(MouseEvent event)
	{
		return (event != null) ? event.isAltDown() : false;
	}

	/**
	 * 
	 */
	public mxPoint snapScaledPoint(mxPoint pt)
	{
		return snapScaledPoint(pt, 0, 0);
	}

	/**
	 * 
	 */
	public mxPoint snapScaledPoint(mxPoint pt, double dx, double dy)
	{
		if (pt != null)
		{
			double scale = graph.getView().getScale();
			mxPoint trans = graph.getView().getTranslate();

			pt.setX((graph.snap(pt.getX() / scale - trans.getX() + dx / scale) + trans
					.getX()) * scale - dx);
			pt.setY((graph.snap(pt.getY() / scale - trans.getY() + dy / scale) + trans
					.getY()) * scale - dy);
		}

		return pt;
	}

	/**
	 * Prints the specified page on the specified graphics using
	 * <code>pageFormat</code> for the page format.
	 * 
	 * @param g
	 *            The graphics to paint the graph on.
	 * @param printFormat
	 *            The page format to use for printing.
	 * @param page
	 *            The page to print
	 * @return Returns {@link Printable#PAGE_EXISTS} or
	 *         {@link Printable#NO_SUCH_PAGE}.
	 */
	public int print(Graphics g, PageFormat printFormat, int page)
	{
		int result = NO_SUCH_PAGE;

		// Disables double-buffering before printing
		RepaintManager currentManager = RepaintManager
				.currentManager(mxGraphComponent.this);
		currentManager.setDoubleBufferingEnabled(false);

		// Gets the current state of the view
		mxGraphView view = graph.getView();

		// Stores the old state of the view
		boolean eventsEnabled = view.isEventsEnabled();
		mxPoint translate = view.getTranslate();

		// Disables firing of scale events so that there is no
		// repaint or update of the original graph while pages
		// are being printed
		view.setEventsEnabled(false);

		// Uses the view to create temporary cell states for each cell
		mxTemporaryCellStates tempStates = new mxTemporaryCellStates(view,
				1 / pageScale);

		try
		{
			view.setTranslate(new mxPoint(0, 0));

			mxGraphics2DCanvas canvas = createCanvas();
			canvas.setGraphics((Graphics2D) g);
			canvas.setScale(1 / pageScale);

			view.revalidate();

			mxRectangle graphBounds = graph.getGraphBounds();
			Dimension pSize = new Dimension((int) Math.ceil(graphBounds.getX()
					+ graphBounds.getWidth()) + 1, (int) Math.ceil(graphBounds
					.getY() + graphBounds.getHeight()) + 1);

			int w = (int) (printFormat.getImageableWidth());
			int h = (int) (printFormat.getImageableHeight());
			int cols = (int) Math.max(
					Math.ceil((double) (pSize.width - 5) / (double) w), 1);
			int rows = (int) Math.max(
					Math.ceil((double) (pSize.height - 5) / (double) h), 1);

			if (page < cols * rows)
			{
				int dx = (int) ((page % cols) * printFormat.getImageableWidth());
				int dy = (int) (Math.floor(page / cols) * printFormat
						.getImageableHeight());

				g.translate(-dx + (int) printFormat.getImageableX(), -dy
						+ (int) printFormat.getImageableY());
				g.setClip(dx, dy, (int) (dx + printFormat.getWidth()),
						(int) (dy + printFormat.getHeight()));

				graph.drawGraph(canvas);

				result = PAGE_EXISTS;
			}
		}
		finally
		{
			view.setTranslate(translate);

			tempStates.destroy();
			view.setEventsEnabled(eventsEnabled);

			// Enables double-buffering after printing
			currentManager.setDoubleBufferingEnabled(true);
		}

		return result;
	}

	/**
	 * 
	 */
	public mxInteractiveCanvas getCanvas()
	{
		return canvas;
	}

	/**
	 * 
	 */
	public BufferedImage getTripleBuffer()
	{
		return tripleBuffer;
	}

	/**
	 * Hook for subclassers to replace the graphics canvas for rendering and and
	 * printing. This must be overridden to return a custom canvas if there are
	 * any custom shapes.
	 */
	public mxInteractiveCanvas createCanvas()
	{
		// NOTE: http://forum.jgraph.com/questions/3354/ reports that we should not
		// pass image observer here as it will cause JVM to enter infinite loop.
		return new mxInteractiveCanvas();
	}

	/**
	 * 
	 * @param state
	 *            Cell state for which a handler should be created.
	 * @return Returns the handler to be used for the given cell state.
	 */
	public mxCellHandler createHandler(mxCellState state)
	{
		if (graph.getModel().isVertex(state.getCell()))
		{
			return new mxVertexHandler(this, state);
		}
		else if (graph.getModel().isEdge(state.getCell()))
		{
			mxEdgeStyleFunction style = graph.getView().getEdgeStyle(state,
					null, null, null);

			if (graph.isLoop(state) || style == mxEdgeStyle.ElbowConnector
					|| style == mxEdgeStyle.SideToSide
					|| style == mxEdgeStyle.TopToBottom)
			{
				return new mxElbowEdgeHandler(this, state);
			}

			return new mxEdgeHandler(this, state);
		}

		return new mxCellHandler(this, state);
	}

	//
	// Heavyweights
	//

	/**
	 * Hook for subclassers to create the array of heavyweights for the given
	 * state.
	 */
	public Component[] createComponents(mxCellState state)
	{
		return null;
	}

	/**
	 * 
	 */
	public void insertComponent(mxCellState state, Component c)
	{
		getGraphControl().add(c, 0);
	}

	/**
	 * 
	 */
	public void removeComponent(Component c, Object cell)
	{
		if (c.getParent() != null)
		{
			c.getParent().remove(c);
		}
	}

	/**
	 * 
	 */
	public void updateComponent(mxCellState state, Component c)
	{
		int x = (int) state.getX();
		int y = (int) state.getY();
		int width = (int) state.getWidth();
		int height = (int) state.getHeight();

		Dimension s = c.getMinimumSize();

		if (s.width > width)
		{
			x -= (s.width - width) / 2;
			width = s.width;
		}

		if (s.height > height)
		{
			y -= (s.height - height) / 2;
			height = s.height;
		}

		c.setBounds(x, y, width, height);
	}

	/**
	 * 
	 */
	public void updateComponents()
	{
		Object root = graph.getModel().getRoot();
		Hashtable<Object, Component[]> result = updateComponents(root);

		// Components now contains the mappings which are no
		// longer used, the result contains the new mappings
		removeAllComponents(components);
		components = result;

		if (!overlays.isEmpty())
		{
			Hashtable<Object, mxICellOverlay[]> result2 = updateCellOverlays(root);

			// Overlays now contains the mappings from cells which
			// are no longer in the model, the result contains the
			// mappings from cells which still exists, regardless
			// from whether a state exists for a particular cell
			removeAllOverlays(overlays);
			overlays = result2;
		}
	}

	/**
	 * 
	 */
	public void removeAllComponents(Hashtable<Object, Component[]> map)
	{
		Iterator<Map.Entry<Object, Component[]>> it = map.entrySet().iterator();

		while (it.hasNext())
		{
			Map.Entry<Object, Component[]> entry = it.next();
			Component[] c = entry.getValue();

			for (int i = 0; i < c.length; i++)
			{
				removeComponent(c[i], entry.getKey());
			}
		}
	}

	/**
	 * 
	 */
	public void removeAllOverlays(Hashtable<Object, mxICellOverlay[]> map)
	{
		Iterator<Map.Entry<Object, mxICellOverlay[]>> it = map.entrySet()
				.iterator();

		while (it.hasNext())
		{
			Map.Entry<Object, mxICellOverlay[]> entry = it.next();
			mxICellOverlay[] c = entry.getValue();

			for (int i = 0; i < c.length; i++)
			{
				removeCellOverlayComponent(c[i], entry.getKey());
			}
		}
	}

	/**
	 * 
	 */
	public Hashtable<Object, Component[]> updateComponents(Object cell)
	{
		Hashtable<Object, Component[]> result = new Hashtable<Object, Component[]>();
		Component[] c = components.remove(cell);
		mxCellState state = getGraph().getView().getState(cell);

		if (state != null)
		{
			if (c == null)
			{
				c = createComponents(state);

				if (c != null)
				{
					for (int i = 0; i < c.length; i++)
					{
						insertComponent(state, c[i]);
					}
				}
			}

			if (c != null)
			{
				result.put(cell, c);

				for (int i = 0; i < c.length; i++)
				{
					updateComponent(state, c[i]);
				}
			}
		}
		// Puts the component back into the map so that it will be removed
		else if (c != null)
		{
			components.put(cell, c);
		}

		int childCount = getGraph().getModel().getChildCount(cell);

		for (int i = 0; i < childCount; i++)
		{
			result.putAll(updateComponents(getGraph().getModel().getChildAt(
					cell, i)));
		}

		return result;
	}

	//
	// Validation and overlays
	//

	/**
	 * Validates the graph by validating each descendant of the given cell or
	 * the root of the model. Context is an object that contains the validation
	 * state for the complete validation run. The validation errors are attached
	 * to their cells using <setWarning>. This function returns true if no
	 * validation errors exist in the graph.
	 */
	public String validateGraph()
	{
		return validateGraph(graph.getModel().getRoot(),
				new Hashtable<Object, Object>());
	}

	/**
	 * Validates the graph by validating each descendant of the given cell or
	 * the root of the model. Context is an object that contains the validation
	 * state for the complete validation run. The validation errors are attached
	 * to their cells using <setWarning>. This function returns true if no
	 * validation errors exist in the graph.
	 * 
	 * @param cell
	 *            Cell to start the validation recursion.
	 * @param context
	 *            Object that represents the global validation state.
	 */
	public String validateGraph(Object cell, Hashtable<Object, Object> context)
	{
		mxIGraphModel model = graph.getModel();
		mxGraphView view = graph.getView();
		boolean isValid = true;
		int childCount = model.getChildCount(cell);

		for (int i = 0; i < childCount; i++)
		{
			Object tmp = model.getChildAt(cell, i);
			Hashtable<Object, Object> ctx = context;

			if (graph.isValidRoot(tmp))
			{
				ctx = new Hashtable<Object, Object>();
			}

			String warn = validateGraph(tmp, ctx);

			if (warn != null)
			{
				String html = warn.replaceAll("\n", "<br>");
				int len = html.length();
				setCellWarning(tmp, html.substring(0, Math.max(0, len - 4)));
			}
			else
			{
				setCellWarning(tmp, null);
			}

			isValid = isValid && warn == null;
		}

		StringBuffer warning = new StringBuffer();

		// Adds error for invalid children if collapsed (children invisible)
		if (graph.isCellCollapsed(cell) && !isValid)
		{
			warning.append(mxResources.get("containsValidationErrors",
					"Contains Validation Errors") + "\n");
		}

		// Checks edges and cells using the defined multiplicities
		if (model.isEdge(cell))
		{
			String tmp = graph.getEdgeValidationError(cell,
					model.getTerminal(cell, true),
					model.getTerminal(cell, false));

			if (tmp != null)
			{
				warning.append(tmp);
			}
		}
		else
		{
			String tmp = graph.getCellValidationError(cell);

			if (tmp != null)
			{
				warning.append(tmp);
			}
		}

		// Checks custom validation rules
		String err = graph.validateCell(cell, context);

		if (err != null)
		{
			warning.append(err);
		}

		// Updates the display with the warning icons before any potential
		// alerts are displayed
		if (model.getParent(cell) == null)
		{
			view.validate();
		}

		return (warning.length() > 0 || !isValid) ? warning.toString() : null;
	}

	/**
	 * Adds an overlay for the specified cell. This method fires an addoverlay
	 * event and returns the new overlay.
	 * 
	 * @param cell
	 *            Cell to add the overlay for.
	 * @param overlay
	 *            Overlay to be added for the cell.
	 */
	public mxICellOverlay addCellOverlay(Object cell, mxICellOverlay overlay)
	{
		mxICellOverlay[] arr = getCellOverlays(cell);

		if (arr == null)
		{
			arr = new mxICellOverlay[] { overlay };
		}
		else
		{
			mxICellOverlay[] arr2 = new mxICellOverlay[arr.length + 1];
			System.arraycopy(arr, 0, arr2, 0, arr.length);
			arr2[arr.length] = overlay;
			arr = arr2;
		}

		overlays.put(cell, arr);
		mxCellState state = graph.getView().getState(cell);

		if (state != null)
		{
			updateCellOverlayComponent(state, overlay);
		}

		eventSource.fireEvent(new mxEventObject(mxEvent.ADD_OVERLAY, "cell",
				cell, "overlay", overlay));

		return overlay;
	}

	/**
	 * Returns the array of overlays for the given cell or null, if no overlays
	 * are defined.
	 * 
	 * @param cell
	 *            Cell whose overlays should be returned.
	 */
	public mxICellOverlay[] getCellOverlays(Object cell)
	{
		return overlays.get(cell);
	}

	/**
	 * Removes and returns the given overlay from the given cell. This method
	 * fires a remove overlay event. If no overlay is given, then all overlays
	 * are removed using removeOverlays.
	 * 
	 * @param cell
	 *            Cell whose overlay should be removed.
	 * @param overlay
	 *            Optional overlay to be removed.
	 */
	public mxICellOverlay removeCellOverlay(Object cell, mxICellOverlay overlay)
	{
		if (overlay == null)
		{
			removeCellOverlays(cell);
		}
		else
		{
			mxICellOverlay[] arr = getCellOverlays(cell);

			if (arr != null)
			{
				// TODO: Use arraycopy from/to same array to speed this up
				List<mxICellOverlay> list = new ArrayList<mxICellOverlay>(
						Arrays.asList(arr));

				if (list.remove(overlay))
				{
					removeCellOverlayComponent(overlay, cell);
				}

				arr = list.toArray(new mxICellOverlay[list.size()]);
				overlays.put(cell, arr);
			}
		}

		return overlay;
	}

	/**
	 * Removes all overlays from the given cell. This method fires a
	 * removeoverlay event for each removed overlay and returns the array of
	 * overlays that was removed from the cell.
	 * 
	 * @param cell
	 *            Cell whose overlays should be removed.
	 */
	public mxICellOverlay[] removeCellOverlays(Object cell)
	{
		mxICellOverlay[] ovls = overlays.remove(cell);

		if (ovls != null)
		{
			// Removes the overlays from the cell hierarchy
			for (int i = 0; i < ovls.length; i++)
			{
				removeCellOverlayComponent(ovls[i], cell);
			}
		}

		return ovls;
	}

	/**
	 * Notified when an overlay has been removed from the graph. This
	 * implementation removes the given overlay from its parent if it is a
	 * component inside a component hierarchy.
	 */
	protected void removeCellOverlayComponent(mxICellOverlay overlay,
			Object cell)
	{
		if (overlay instanceof Component)
		{
			Component comp = (Component) overlay;

			if (comp.getParent() != null)
			{
				comp.setVisible(false);
				comp.getParent().remove(comp);
				eventSource.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY,
						"cell", cell, "overlay", overlay));
			}
		}
	}

	/**
	 * Notified when an overlay has been removed from the graph. This
	 * implementation removes the given overlay from its parent if it is a
	 * component inside a component hierarchy.
	 */
	protected void updateCellOverlayComponent(mxCellState state,
			mxICellOverlay overlay)
	{
		if (overlay instanceof Component)
		{
			Component comp = (Component) overlay;

			if (comp.getParent() == null)
			{
				getGraphControl().add(comp, 0);
			}

			mxRectangle rect = overlay.getBounds(state);

			if (rect != null)
			{
				comp.setBounds(rect.getRectangle());
				comp.setVisible(true);
			}
			else
			{
				comp.setVisible(false);
			}
		}
	}

	/**
	 * Removes all overlays in the graph.
	 */
	public void clearCellOverlays()
	{
		clearCellOverlays(null);
	}

	/**
	 * Removes all overlays in the graph for the given cell and all its
	 * descendants. If no cell is specified then all overlays are removed from
	 * the graph. This implementation uses removeOverlays to remove the overlays
	 * from the individual cells.
	 * 
	 * @param cell
	 *            Optional cell that represents the root of the subtree to
	 *            remove the overlays from. Default is the root in the model.
	 */
	public void clearCellOverlays(Object cell)
	{
		mxIGraphModel model = graph.getModel();

		if (cell == null)
		{
			cell = model.getRoot();
		}

		removeCellOverlays(cell);

		// Recursively removes all overlays from the children
		int childCount = model.getChildCount(cell);

		for (int i = 0; i < childCount; i++)
		{
			Object child = model.getChildAt(cell, i);
			clearCellOverlays(child); // recurse
		}
	}

	/**
	 * Creates an overlay for the given cell using the warning and image or
	 * warningImage and returns the new overlay. If the warning is null or a
	 * zero length string, then all overlays are removed from the cell instead.
	 * 
	 * @param cell
	 *            Cell whose warning should be set.
	 * @param warning
	 *            String that represents the warning to be displayed.
	 */
	public mxICellOverlay setCellWarning(Object cell, String warning)
	{
		return setCellWarning(cell, warning, null, false);
	}

	/**
	 * Creates an overlay for the given cell using the warning and image or
	 * warningImage and returns the new overlay. If the warning is null or a
	 * zero length string, then all overlays are removed from the cell instead.
	 * 
	 * @param cell
	 *            Cell whose warning should be set.
	 * @param warning
	 *            String that represents the warning to be displayed.
	 * @param icon
	 *            Optional image to be used for the overlay. Default is
	 *            warningImageBasename.
	 */
	public mxICellOverlay setCellWarning(Object cell, String warning,
			ImageIcon icon)
	{
		return setCellWarning(cell, warning, icon, false);
	}

	/**
	 * Creates an overlay for the given cell using the warning and image or
	 * warningImage and returns the new overlay. If the warning is null or a
	 * zero length string, then all overlays are removed from the cell instead.
	 * 
	 * @param cell
	 *            Cell whose warning should be set.
	 * @param warning
	 *            String that represents the warning to be displayed.
	 * @param icon
	 *            Optional image to be used for the overlay. Default is
	 *            warningImageBasename.
	 * @param select
	 *            Optional boolean indicating if a click on the overlay should
	 *            select the corresponding cell. Default is false.
	 */
	public mxICellOverlay setCellWarning(final Object cell, String warning,
			ImageIcon icon, boolean select)
	{
		if (warning != null && warning.length() > 0)
		{
			icon = (icon != null) ? icon : warningIcon;

			// Creates the overlay with the image and warning
			mxCellOverlay overlay = new mxCellOverlay(icon, warning);

			// Adds a handler for single mouseclicks to select the cell
			if (select)
			{
				overlay.addMouseListener(new MouseAdapter()
				{
					/**
					 * Selects the associated cell in the graph
					 */
					public void mousePressed(MouseEvent e)
					{
						if (getGraph().isEnabled())
						{
							getGraph().setSelectionCell(cell);
						}
					}
				});

				overlay.setCursor(new Cursor(Cursor.HAND_CURSOR));
			}

			// Sets and returns the overlay in the graph
			return addCellOverlay(cell, overlay);
		}
		else
		{
			removeCellOverlays(cell);
		}

		return null;
	}

	/**
	 * Returns a hashtable with all entries from the overlays variable where a
	 * cell still exists in the model. The entries are removed from the global
	 * hashtable so that the remaining entries reflect those whose cell have
	 * been removed from the model. If no state is available for a given cell
	 * then its overlays are temporarly removed from the rendering control, but
	 * kept in the result.
	 */
	public Hashtable<Object, mxICellOverlay[]> updateCellOverlays(Object cell)
	{
		Hashtable<Object, mxICellOverlay[]> result = new Hashtable<Object, mxICellOverlay[]>();
		mxICellOverlay[] c = overlays.remove(cell);
		mxCellState state = getGraph().getView().getState(cell);

		if (c != null)
		{
			if (state != null)
			{
				for (int i = 0; i < c.length; i++)
				{
					updateCellOverlayComponent(state, c[i]);
				}
			}
			else
			{
				for (int i = 0; i < c.length; i++)
				{
					removeCellOverlayComponent(c[i], cell);
				}
			}

			result.put(cell, c);
		}

		int childCount = getGraph().getModel().getChildCount(cell);

		for (int i = 0; i < childCount; i++)
		{
			result.putAll(updateCellOverlays(getGraph().getModel().getChildAt(
					cell, i)));
		}

		return result;
	}

	/**
	 * 
	 */
	protected void paintBackground(Graphics g)
	{
		Rectangle clip = g.getClipBounds();
		Rectangle rect = paintBackgroundPage(g);

		if (isPageVisible())
		{
			g.clipRect(rect.x + 1, rect.y + 1, rect.width - 1, rect.height - 1);
		}

		// Paints the clipped background image
		paintBackgroundImage(g);

		// Paints the grid directly onto the graphics
		paintGrid(g);
		g.setClip(clip);
	}

	/**
	 * 
	 */
	protected Rectangle paintBackgroundPage(Graphics g)
	{
		mxPoint translate = graph.getView().getTranslate();
		double scale = graph.getView().getScale();

		int x0 = (int) Math.round(translate.getX() * scale) - 1;
		int y0 = (int) Math.round(translate.getY() * scale) - 1;

		Dimension d = getPreferredSizeForPage();
		int w = (int) Math.round(d.width * scale) + 2;
		int h = (int) Math.round(d.height * scale) + 2;

		if (isPageVisible())
		{
			// Draws the background behind the page
			Color c = getPageBackgroundColor();
			
			if (c != null)
			{
				g.setColor(c);
				mxUtils.fillClippedRect(g, 0, 0, getGraphControl().getWidth(),
						getGraphControl().getHeight());
			}

			// Draws the page drop shadow
			c = getPageShadowColor();
			
			if (c != null)
			{
				g.setColor(c);
				mxUtils.fillClippedRect(g, x0 + w, y0 + 6, 6, h - 6);
				mxUtils.fillClippedRect(g, x0 + 8, y0 + h, w - 2, 6);
			}

			// Draws the page
			Color bg = getBackground();

			if (getViewport().isOpaque())
			{
				bg = getViewport().getBackground();
			}

			g.setColor(bg);
			mxUtils.fillClippedRect(g, x0 + 1, y0 + 1, w, h);

			// Draws the page border
			c = getPageBorderColor();
			
			if (c != null)
			{
				g.setColor(c);
				g.drawRect(x0, y0, w, h);
			}
		}

		if (isPageBreaksVisible()
				&& (horizontalPageCount > 1 || verticalPageCount > 1))
		{
			// Draws the pagebreaks
			// TODO: Use clipping
			Graphics2D g2 = (Graphics2D) g;
			Stroke previousStroke = g2.getStroke();

			g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
					BasicStroke.JOIN_MITER, 10.0f, new float[] { 1, 2 }, 0));
			g2.setColor(pageBreakColor);

			for (int i = 1; i <= horizontalPageCount - 1; i++)
			{
				int dx = i * w / horizontalPageCount;
				g2.drawLine(x0 + dx, y0 + 1, x0 + dx, y0 + h);
			}

			for (int i = 1; i <= verticalPageCount - 1; i++)
			{
				int dy = i * h / verticalPageCount;
				g2.drawLine(x0 + 1, y0 + dy, x0 + w, y0 + dy);
			}

			// Restores the graphics
			g2.setStroke(previousStroke);
		}

		return new Rectangle(x0, y0, w, h);
	}

	/**
	 * 
	 */
	protected void paintBackgroundImage(Graphics g)
	{
		if (backgroundImage != null)
		{
			mxPoint translate = graph.getView().getTranslate();
			double scale = graph.getView().getScale();

			g.drawImage(backgroundImage.getImage(),
					(int) (translate.getX() * scale),
					(int) (translate.getY() * scale),
					(int) (backgroundImage.getIconWidth() * scale),
					(int) (backgroundImage.getIconHeight() * scale), this);
		}
	}

	/**
	 * Paints the grid onto the given graphics object.
	 */
	protected void paintGrid(Graphics g)
	{
		if (isGridVisible())
		{
			g.setColor(getGridColor());
			Rectangle clip = g.getClipBounds();

			if (clip == null)
			{
				clip = getGraphControl().getBounds();
			}

			double left = clip.getX();
			double top = clip.getY();
			double right = left + clip.getWidth();
			double bottom = top + clip.getHeight();

			// Double the grid line spacing if smaller than half the gridsize
			int style = getGridStyle();
			int gridSize = graph.getGridSize();
			int minStepping = gridSize;

			// Smaller stepping for certain styles
			if (style == GRID_STYLE_CROSS || style == GRID_STYLE_DOT)
			{
				minStepping /= 2;
			}

			// Fetches some global display state information
			mxPoint trans = graph.getView().getTranslate();
			double scale = graph.getView().getScale();
			double tx = trans.getX() * scale;
			double ty = trans.getY() * scale;

			// Sets the distance of the grid lines in pixels
			double stepping = gridSize * scale;

			if (stepping < minStepping)
			{
				int count = (int) Math
						.round(Math.ceil(minStepping / stepping) / 2) * 2;
				stepping = count * stepping;
			}

			double xs = Math.floor((left - tx) / stepping) * stepping + tx;
			double xe = Math.ceil(right / stepping) * stepping;
			double ys = Math.floor((top - ty) / stepping) * stepping + ty;
			double ye = Math.ceil(bottom / stepping) * stepping;

			switch (style)
			{
				case GRID_STYLE_CROSS:
				{
					// Sets the dot size
					int cs = (stepping > 16.0) ? 2 : 1;

					for (double x = xs; x <= xe; x += stepping)
					{
						for (double y = ys; y <= ye; y += stepping)
						{
							// FIXME: Workaround for rounding errors when adding
							// stepping to
							// xs or ys multiple times (leads to double grid lines
							// when zoom
							// is set to eg. 121%)
							x = Math.round((x - tx) / stepping) * stepping + tx;
							y = Math.round((y - ty) / stepping) * stepping + ty;

							int ix = (int) Math.round(x);
							int iy = (int) Math.round(y);
							g.drawLine(ix - cs, iy, ix + cs, iy);
							g.drawLine(ix, iy - cs, ix, iy + cs);
						}
					}

					break;
				}
				case GRID_STYLE_LINE:
				{
					xe += (int) Math.ceil(stepping);
					ye += (int) Math.ceil(stepping);

					int ixs = (int) Math.round(xs);
					int ixe = (int) Math.round(xe);
					int iys = (int) Math.round(ys);
					int iye = (int) Math.round(ye);

					for (double x = xs; x <= xe; x += stepping)
					{
						// FIXME: Workaround for rounding errors when adding
						// stepping to
						// xs or ys multiple times (leads to double grid lines when
						// zoom
						// is set to eg. 121%)
						x = Math.round((x - tx) / stepping) * stepping + tx;

						int ix = (int) Math.round(x);
						g.drawLine(ix, iys, ix, iye);
					}

					for (double y = ys; y <= ye; y += stepping)
					{

						// FIXME: Workaround for rounding errors when adding
						// stepping to
						// xs or ys multiple times (leads to double grid lines when
						// zoom
						// is set to eg. 121%)
						y = Math.round((y - ty) / stepping) * stepping + ty;

						int iy = (int) Math.round(y);
						g.drawLine(ixs, iy, ixe, iy);
					}

					break;
				}
				case GRID_STYLE_DASHED:
				{
					Graphics2D g2 = (Graphics2D) g;
					Stroke stroke = g2.getStroke();

					xe += (int) Math.ceil(stepping);
					ye += (int) Math.ceil(stepping);

					int ixs = (int) Math.round(xs);
					int ixe = (int) Math.round(xe);
					int iys = (int) Math.round(ys);
					int iye = (int) Math.round(ye);

					// Creates a set of strokes with individual dash offsets
					// for each direction
					Stroke[] strokes = new Stroke[] {
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 3,
											1 }, Math.max(0, iys) % 4),
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 2,
											2 }, Math.max(0, iys) % 4),
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 1,
											1 }, 0),
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 2,
											2 }, Math.max(0, iys) % 4) };

					for (double x = xs; x <= xe; x += stepping)
					{
						g2.setStroke(strokes[((int) (x / stepping))
								% strokes.length]);

						// FIXME: Workaround for rounding errors when adding
						// stepping to
						// xs or ys multiple times (leads to double grid lines when
						// zoom
						// is set to eg. 121%)
						double xx = Math.round((x - tx) / stepping) * stepping
								+ tx;

						int ix = (int) Math.round(xx);
						g.drawLine(ix, iys, ix, iye);
					}

					strokes = new Stroke[] {
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 3,
											1 }, Math.max(0, ixs) % 4),
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 2,
											2 }, Math.max(0, ixs) % 4),
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 1,
											1 }, 0),
							new BasicStroke(1, BasicStroke.CAP_BUTT,
									BasicStroke.JOIN_MITER, 1, new float[] { 2,
											2 }, Math.max(0, ixs) % 4) };

					for (double y = ys; y <= ye; y += stepping)
					{
						g2.setStroke(strokes[((int) (y / stepping))
								% strokes.length]);

						// FIXME: Workaround for rounding errors when adding
						// stepping to
						// xs or ys multiple times (leads to double grid lines when
						// zoom
						// is set to eg. 121%)
						double yy = Math.round((y - ty) / stepping) * stepping
								+ ty;

						int iy = (int) Math.round(yy);
						g.drawLine(ixs, iy, ixe, iy);
					}

					g2.setStroke(stroke);

					break;
				}
				default: // DOT_GRID_MODE
				{
					for (double x = xs; x <= xe; x += stepping)
					{

						for (double y = ys; y <= ye; y += stepping)
						{
							// FIXME: Workaround for rounding errors when adding
							// stepping to
							// xs or ys multiple times (leads to double grid lines
							// when zoom
							// is set to eg. 121%)
							x = Math.round((x - tx) / stepping) * stepping + tx;
							y = Math.round((y - ty) / stepping) * stepping + ty;

							int ix = (int) Math.round(x);
							int iy = (int) Math.round(y);
							g.drawLine(ix, iy, ix, iy);
						}
					}
				}
			}
		}
	}

	//
	// Triple Buffering
	//

	/**
	 * Updates the buffer (if one exists) and repaints the given cell state.
	 */
	public void redraw(mxCellState state)
	{
		if (state != null)
		{
			Rectangle dirty = state.getBoundingBox().getRectangle();
			repaintTripleBuffer(new Rectangle(dirty));
			dirty = SwingUtilities.convertRectangle(graphControl, dirty, this);
			repaint(dirty);
		}
	}

	/**
	 * Checks if the triple buffer exists and creates a new one if it does not.
	 * Also compares the size of the buffer with the size of the graph and drops
	 * the buffer if it has a different size.
	 */
	public void checkTripleBuffer()
	{
		mxRectangle bounds = graph.getGraphBounds();
		int width = (int) Math.ceil(bounds.getX() + bounds.getWidth() + 2);
		int height = (int) Math.ceil(bounds.getY() + bounds.getHeight() + 2);

		if (tripleBuffer != null)
		{
			if (tripleBuffer.getWidth() != width
					|| tripleBuffer.getHeight() != height)
			{
				// Resizes the buffer (destroys existing and creates new)
				destroyTripleBuffer();
			}
		}

		if (tripleBuffer == null)
		{
			createTripleBuffer(width, height);
		}
	}

	/**
	 * Creates the tripleBufferGraphics and tripleBuffer for the given dimension
	 * and draws the complete graph onto the triplebuffer.
	 * 
	 * @param width
	 * @param height
	 */
	protected void createTripleBuffer(int width, int height)
	{
		try
		{
			tripleBuffer = mxUtils.createBufferedImage(width, height, null);
			if (tripleBuffer != null)
			{
				tripleBufferGraphics = tripleBuffer.createGraphics();
				mxUtils.setAntiAlias(tripleBufferGraphics, antiAlias, textAntiAlias);

				// Repaints the complete buffer
				repaintTripleBuffer(null);
			}
		}
		catch (OutOfMemoryError error)
		{
			// ignore
		}
	}

	/**
	 * Destroys the tripleBuffer and tripleBufferGraphics objects.
	 */
	public void destroyTripleBuffer()
	{
		if (tripleBuffer != null)
		{
			tripleBuffer = null;
			tripleBufferGraphics.dispose();
			tripleBufferGraphics = null;
		}
	}

	/**
	 * Clears and repaints the triple buffer at the given rectangle or repaints
	 * the complete buffer if no rectangle is specified.
	 * 
	 * @param dirty
	 */
	public void repaintTripleBuffer(Rectangle dirty)
	{
		if (tripleBuffered && tripleBufferGraphics != null)
		{
			if (dirty == null)
			{
				dirty = new Rectangle(tripleBuffer.getWidth(),
						tripleBuffer.getHeight());
			}

			// Clears and repaints the dirty rectangle using the
			// graphics canvas as a renderer
			mxUtils.clearRect(tripleBufferGraphics, dirty, null);
			tripleBufferGraphics.setClip(dirty);
			graphControl.drawGraph(tripleBufferGraphics, true);
			tripleBufferGraphics.setClip(null);
		}
	}

	//
	// Redirected to event source
	//

	/**
	 * @return Returns true if event dispatching is enabled in the event source.
	 * @see com.mxgraph.util.mxEventSource#isEventsEnabled()
	 */
	public boolean isEventsEnabled()
	{
		return eventSource.isEventsEnabled();
	}

	/**
	 * @param eventsEnabled
	 * @see com.mxgraph.util.mxEventSource#setEventsEnabled(boolean)
	 */
	public void setEventsEnabled(boolean eventsEnabled)
	{
		eventSource.setEventsEnabled(eventsEnabled);
	}

	/**
	 * @param eventName
	 * @param listener
	 * @see com.mxgraph.util.mxEventSource#addListener(java.lang.String,
	 *      com.mxgraph.util.mxEventSource.mxIEventListener)
	 */
	public void addListener(String eventName, mxIEventListener listener)
	{
		eventSource.addListener(eventName, listener);
	}

	/**
	 * @param listener
	 *            Listener instance.
	 */
	public void removeListener(mxIEventListener listener)
	{
		eventSource.removeListener(listener);
	}

	/**
	 * @param eventName
	 *            Name of the event.
	 * @param listener
	 *            Listener instance.
	 */
	public void removeListener(mxIEventListener listener, String eventName)
	{
		eventSource.removeListener(listener, eventName);
	}

	/**
	 * 
	 * @author gaudenz
	 * 
	 */
	public class mxGraphControl extends JComponent
	{

		/**
		 * 
		 */
		private static final long serialVersionUID = -8916603170766739124L;

		/**
		 * Specifies a translation for painting. This should only be used during
		 * mouse drags and must be reset after any interactive repaints. Default
		 * is (0,0). This should not be null.
		 */
		protected Point translate = new Point(0, 0);

		/**
		 * 
		 */
		public mxGraphControl()
		{
			addMouseListener(new MouseAdapter()
			{
				public void mouseReleased(MouseEvent e)
				{
					if (translate.x != 0 || translate.y != 0)
					{
						translate = new Point(0, 0);
						repaint();
					}
				}
			});
		}

		/**
		 * Returns the translate.
		 */
		public Point getTranslate()
		{
			return translate;
		}

		/**
		 * Sets the translate.
		 */
		public void setTranslate(Point value)
		{
			translate = value;
		}

		/**
		 * 
		 */
		public mxGraphComponent getGraphContainer()
		{
			return mxGraphComponent.this;
		}

		/**
		 * Overrides parent method to add extend flag for making the control
		 * larger during previews.
		 */
		public void scrollRectToVisible(Rectangle aRect, boolean extend)
		{
			super.scrollRectToVisible(aRect);

			if (extend)
			{
				extendComponent(aRect);
			}
		}

		/**
		 * Implements extension of the component in all directions. For
		 * extension below the origin (into negative space) the translate will
		 * temporaly be used and reset with the next mouse released event.
		 */
		protected void extendComponent(Rectangle rect)
		{
			int right = rect.x + rect.width;
			int bottom = rect.y + rect.height;

			Dimension d = new Dimension(getPreferredSize());
			Dimension sp = getScaledPreferredSizeForGraph();
			mxRectangle min = graph.getMinimumGraphSize();
			double scale = graph.getView().getScale();
			boolean update = false;

			if (rect.x < 0)
			{
				translate.x = Math.max(translate.x, Math.max(0, -rect.x));
				d.width = sp.width;

				if (min != null)
				{
					d.width = (int) Math.max(d.width,
							Math.round(min.getWidth() * scale));
				}

				d.width += translate.x;
				update = true;
			}
			else if (right > getWidth())
			{
				d.width = Math.max(right, getWidth());
				update = true;
			}

			if (rect.y < 0)
			{
				translate.y = Math.max(translate.y, Math.max(0, -rect.y));
				d.height = sp.height;

				if (min != null)
				{
					d.height = (int) Math.max(d.height,
							Math.round(min.getHeight() * scale));
				}

				d.height += translate.y;
				update = true;
			}
			else if (bottom > getHeight())
			{
				d.height = Math.max(bottom, getHeight());
				update = true;
			}

			if (update)
			{
				setPreferredSize(d);
				setMinimumSize(d);
				revalidate();
			}
		}

		/**
		 * 
		 */
		public String getToolTipText(MouseEvent e)
		{
			String tip = getSelectionCellsHandler().getToolTipText(e);

			if (tip == null)
			{
				Object cell = getCellAt(e.getX(), e.getY());

				if (cell != null)
				{
					if (hitFoldingIcon(cell, e.getX(), e.getY()))
					{
						tip = mxResources.get("collapse-expand");
					}
					else
					{
						tip = graph.getToolTipForCell(cell);
					}
				}
			}

			if (tip != null && tip.length() > 0)
			{
				return tip;
			}

			return super.getToolTipText(e);
		}

		/**
		 * Updates the preferred size for the given scale if the page size
		 * should be preferred or the page is visible.
		 */
		public void updatePreferredSize()
		{
			double scale = graph.getView().getScale();
			Dimension d = null;

			if (preferPageSize || pageVisible)
			{
				Dimension page = getPreferredSizeForPage();

				if (!preferPageSize)
				{
					page.width += 2 * getHorizontalPageBorder();
					page.height += 2 * getVerticalPageBorder();
				}

				d = new Dimension((int) (page.width * scale),
						(int) (page.height * scale));
			}
			else
			{
				d = getScaledPreferredSizeForGraph();
			}

			mxRectangle min = graph.getMinimumGraphSize();

			if (min != null)
			{
				d.width = (int) Math.max(d.width,
						Math.round(min.getWidth() * scale));
				d.height = (int) Math.max(d.height,
						Math.round(min.getHeight() * scale));
			}

			if (!getPreferredSize().equals(d))
			{
				setPreferredSize(d);
				setMinimumSize(d);
				revalidate();
			}
		}

		/**
		 * 
		 */
		public void paint(Graphics g)
		{
			g.translate(translate.x, translate.y);
			eventSource.fireEvent(new mxEventObject(mxEvent.BEFORE_PAINT, "g",
					g));
			super.paint(g);
			eventSource
					.fireEvent(new mxEventObject(mxEvent.AFTER_PAINT, "g", g));
			g.translate(-translate.x, -translate.y);
		}

		/**
		 * 
		 */
		public void paintComponent(Graphics g)
		{
			super.paintComponent(g);

			// Draws the background
			paintBackground(g);

			// Creates or destroys the triple buffer as needed
			if (tripleBuffered)
			{
				checkTripleBuffer();
			}
			else if (tripleBuffer != null)
			{
				destroyTripleBuffer();
			}

			// Paints the buffer in the canvas onto the dirty region
			if (tripleBuffer != null)
			{
				mxUtils.drawImageClip(g, tripleBuffer, this);
			}

			// Paints the graph directly onto the graphics
			else
			{
				Graphics2D g2 = (Graphics2D) g;
				RenderingHints tmp = g2.getRenderingHints();

				// Sets the graphics in the canvas
				try
				{
					mxUtils.setAntiAlias(g2, antiAlias, textAntiAlias);
					drawGraph(g2, true);
				}
				finally
				{
					// Restores the graphics state
					g2.setRenderingHints(tmp);
				}
			}

			eventSource.fireEvent(new mxEventObject(mxEvent.PAINT, "g", g));
		}

		/**
		 * 
		 */
		public void drawGraph(Graphics2D g, boolean drawLabels)
		{
			Graphics2D previousGraphics = canvas.getGraphics();
			boolean previousDrawLabels = canvas.isDrawLabels();
			mxPoint previousTranslate = canvas.getTranslate();
			double previousScale = canvas.getScale();

			try
			{
				canvas.setScale(graph.getView().getScale());
				canvas.setDrawLabels(drawLabels);
				canvas.setTranslate(0, 0);
				canvas.setGraphics(g);

				// Draws the graph using the graphics canvas
				drawFromRootCell();
			}
			finally
			{
				canvas.setScale(previousScale);
				canvas.setTranslate(previousTranslate.getX(), previousTranslate.getY());
				canvas.setDrawLabels(previousDrawLabels);
				canvas.setGraphics(previousGraphics);
			}
		}

		/**
		 * Hook to draw the root cell into the canvas.
		 */
		protected void drawFromRootCell()
		{
			drawCell(canvas, graph.getModel().getRoot());
		}

		/**
		 * 
		 */
		protected boolean hitClip(mxGraphics2DCanvas canvas, mxCellState state)
		{
			Rectangle rect = getExtendedCellBounds(state);

			return (rect == null || canvas.getGraphics().hitClip(rect.x,
					rect.y, rect.width, rect.height));
		}

		/**
		 * @param state the cached state of the cell whose extended bounds are to be calculated
		 * @return the bounds of the cell, including the label and shadow and allowing for rotation
		 */
		protected Rectangle getExtendedCellBounds(mxCellState state)
		{
			Rectangle rect = null;

			// Takes rotation into account
			double rotation = mxUtils.getDouble(state.getStyle(),
					mxConstants.STYLE_ROTATION);
			mxRectangle tmp = mxUtils.getBoundingBox(new mxRectangle(state),
					rotation);

			// Adds scaled stroke width
			int border = (int) Math
					.ceil(mxUtils.getDouble(state.getStyle(),
							mxConstants.STYLE_STROKEWIDTH)
							* graph.getView().getScale()) + 1;
			tmp.grow(border);

			if (mxUtils.isTrue(state.getStyle(), mxConstants.STYLE_SHADOW))
			{
				tmp.setWidth(tmp.getWidth() + mxConstants.SHADOW_OFFSETX);
				tmp.setHeight(tmp.getHeight() + mxConstants.SHADOW_OFFSETX);
			}

			// Adds the bounds of the label
			if (state.getLabelBounds() != null)
			{
				tmp.add(state.getLabelBounds());
			}

			rect = tmp.getRectangle();
			return rect;
		}

		/**
		 * Draws the given cell onto the specified canvas. This is a modified
		 * version of mxGraph.drawCell which paints the label only if the
		 * corresponding cell is not being edited and invokes the cellDrawn hook
		 * after all descendants have been painted.
		 * 
		 * @param canvas
		 *            Canvas onto which the cell should be drawn.
		 * @param cell
		 *            Cell that should be drawn onto the canvas.
		 */
		public void drawCell(mxICanvas canvas, Object cell)
		{
			mxCellState state = graph.getView().getState(cell);

			if (state != null
					&& isCellDisplayable(state.getCell())
					&& (!(canvas instanceof mxGraphics2DCanvas) || hitClip(
							(mxGraphics2DCanvas) canvas, state)))
			{
				graph.drawState(canvas, state,
						cell != cellEditor.getEditingCell());
			}

			// Handles special ordering for edges (all in foreground
			// or background) or draws all children in order
			boolean edgesFirst = graph.isKeepEdgesInBackground();
			boolean edgesLast = graph.isKeepEdgesInForeground();

			if (edgesFirst)
			{
				drawChildren(cell, true, false);
			}

			drawChildren(cell, !edgesFirst && !edgesLast, true);

			if (edgesLast)
			{
				drawChildren(cell, true, false);
			}

			if (state != null)
			{
				cellDrawn(canvas, state);
			}
		}

		/**
		 * Draws the child edges and/or all other children in the given cell
		 * depending on the boolean arguments.
		 */
		protected void drawChildren(Object cell, boolean edges, boolean others)
		{
			mxIGraphModel model = graph.getModel();
			int childCount = model.getChildCount(cell);

			for (int i = 0; i < childCount; i++)
			{
				Object child = model.getChildAt(cell, i);
				boolean isEdge = model.isEdge(child);

				if ((others && !isEdge) || (edges && isEdge))
				{
					drawCell(canvas, model.getChildAt(cell, i));
				}
			}
		}

		/**
		 * 
		 */
		protected void cellDrawn(mxICanvas canvas, mxCellState state)
		{
			if (isFoldingEnabled() && canvas instanceof mxGraphics2DCanvas)
			{
				mxIGraphModel model = graph.getModel();
				mxGraphics2DCanvas g2c = (mxGraphics2DCanvas) canvas;
				Graphics2D g2 = g2c.getGraphics();

				// Draws the collapse/expand icons
				boolean isEdge = model.isEdge(state.getCell());

				if (state.getCell() != graph.getCurrentRoot()
						&& (model.isVertex(state.getCell()) || isEdge))
				{
					ImageIcon icon = getFoldingIcon(state);

					if (icon != null)
					{
						Rectangle bounds = getFoldingIconBounds(state, icon);
						g2.drawImage(icon.getImage(), bounds.x, bounds.y,
								bounds.width, bounds.height, this);
					}
				}
			}
		}

		/**
		 * Returns true if the given cell is not the current root or the root in
		 * the model. This can be overridden to not render certain cells in the
		 * graph display.
		 */
		protected boolean isCellDisplayable(Object cell)
		{
			return cell != graph.getView().getCurrentRoot()
					&& cell != graph.getModel().getRoot();
		}

	}

	/**
	 * 
	 */
	public static class mxMouseRedirector implements MouseListener,
			MouseMotionListener
	{

		/**
		 * 
		 */
		protected mxGraphComponent graphComponent;

		/**
		 * 
		 */
		public mxMouseRedirector(mxGraphComponent graphComponent)
		{
			this.graphComponent = graphComponent;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
		 */
		public void mouseClicked(MouseEvent e)
		{
			graphComponent.getGraphControl().dispatchEvent(
					SwingUtilities.convertMouseEvent(e.getComponent(), e,
							graphComponent.getGraphControl()));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
		 */
		public void mouseEntered(MouseEvent e)
		{
			// Redirecting this would cause problems on the Mac
			// and is technically incorrect anyway
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
		 */
		public void mouseExited(MouseEvent e)
		{
			mouseClicked(e);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
		 */
		public void mousePressed(MouseEvent e)
		{
			mouseClicked(e);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
		 */
		public void mouseReleased(MouseEvent e)
		{
			mouseClicked(e);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent
		 * )
		 */
		public void mouseDragged(MouseEvent e)
		{
			mouseClicked(e);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent
		 * )
		 */
		public void mouseMoved(MouseEvent e)
		{
			mouseClicked(e);
		}

	}

}
