All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mxgraph.view.mxGraphView Maven / Gradle / Ivy

There is a newer version: 2.2.18
Show newest version
/**
 * $Id: mxGraphView.java,v 1.158 2012-06-20 13:20:09 gaudenz Exp $
 * Copyright (c) 2007-2010, Gaudenz Alder, David Benson
 */
package com.mxgraph.view;

import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUndoableEdit;
import com.mxgraph.util.mxUndoableEdit.mxUndoableChange;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxEdgeStyle.mxEdgeStyleFunction;
import com.mxgraph.view.mxPerimeter.mxPerimeterFunction;

/**
 * Implements a view for the graph. This class is in charge of computing the
 * absolute coordinates for the relative child geometries, the points for
 * perimeters and edge styles and keeping them cached in cell states for
 * faster retrieval. The states are updated whenever the model or the view
 * state (translate, scale) changes. The scale and translate are honoured in
 * the bounds.
 * 
 * This class fires the following events:
 * 
 * mxEvent.UNDO fires after the root was changed in setCurrentRoot. The
 * edit property contains the mxUndoableEdit which contains the
 * mxCurrentRootChange.
 * 
 * mxEvent.SCALE_AND_TRANSLATE fires after the scale and transle have been
 * changed in scaleAndTranslate. The scale, previousScale,
 * translate and previousTranslate properties contain
 * the new and previous scale and translate, respectively.
 * 
 * mxEvent.SCALE fires after the scale was changed in setScale. The
 * scale and previousScale properties contain the
 * new and previous scale.
 * 
 * mxEvent.TRANSLATE fires after the translate was changed in setTranslate. The
 * translate and previousTranslate properties contain
 * the new and previous value for translate.
 * 
 * mxEvent.UP and mxEvent.DOWN fire if the current root is changed by executing
 * a mxCurrentRootChange. The event name depends on the location of the root
 * in the cell hierarchy with respect to the current root. The
 * root and previous properties contain the new and
 * previous root, respectively.
 */
public class mxGraphView extends mxEventSource
{

	/**
	 *
	 */
	protected static mxPoint EMPTY_POINT = new mxPoint();

	/**
	 * Reference to the enclosing graph.
	 */
	protected mxGraph graph;

	/**
	 * mxCell that acts as the root of the displayed cell hierarchy.
	 */
	protected Object currentRoot = null;

	/**
	 * Caches the current bounds of the graph.
	 */
	protected mxRectangle graphBounds = new mxRectangle();

	/**
	 * Specifies the scale. Default is 1 (100%).
	 */
	protected double scale = 1;

	/**
	 * Point that specifies the current translation. Default is a new
	 * empty point.
	 */
	protected mxPoint translate = new mxPoint(0, 0);

	/**
	 * Maps from cells to cell states.
	 */
	protected Hashtable states = new Hashtable();

	/**
	 * Constructs a new view for the given graph.
	 * 
	 * @param graph Reference to the enclosing graph.
	 */
	public mxGraphView(mxGraph graph)
	{
		this.graph = graph;
	}

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

	/**
	 * Returns the dictionary that maps from cells to states.
	 */
	public Hashtable getStates()
	{
		return states;
	}

	/**
	 * Returns the dictionary that maps from cells to states.
	 */
	public void setStates(Hashtable states)
	{
		this.states = states;
	}

	/**
	 * Returns the cached diagram bounds.
	 * 
	 * @return Returns the diagram bounds.
	 */
	public mxRectangle getGraphBounds()
	{
		return graphBounds;
	}

	/**
	 * Sets the graph bounds.
	 */
	public void setGraphBounds(mxRectangle value)
	{
		graphBounds = value;
	}

	/**
	 * Returns the current root.
	 */
	public Object getCurrentRoot()
	{
		return currentRoot;
	}

	/**
	 * Sets and returns the current root and fires an undo event.
	 * 
	 * @param root mxCell that specifies the root of the displayed cell hierarchy.
	 * @return Returns the object that represents the current root.
	 */
	public Object setCurrentRoot(Object root)
	{
		if (currentRoot != root)
		{
			mxCurrentRootChange change = new mxCurrentRootChange(this, root);
			change.execute();
			mxUndoableEdit edit = new mxUndoableEdit(this, false);
			edit.add(change);
			fireEvent(new mxEventObject(mxEvent.UNDO, "edit", edit));
		}

		return root;
	}

	/**
	 * Sets the scale and translation. Fires a "scaleAndTranslate"
	 * event after calling revalidate. Revalidate is only called if
	 * isEventsEnabled.
	 * 
	 * @param scale Decimal value that specifies the new scale (1 is 100%).
	 * @param dx X-coordinate of the translation.
	 * @param dy Y-coordinate of the translation.
	 */
	public void scaleAndTranslate(double scale, double dx, double dy)
	{
		double previousScale = this.scale;
		Object previousTranslate = translate.clone();

		if (scale != this.scale || dx != translate.getX()
				|| dy != translate.getY())
		{
			this.scale = scale;
			translate = new mxPoint(dx, dy);

			if (isEventsEnabled())
			{
				revalidate();
			}
		}

		fireEvent(new mxEventObject(mxEvent.SCALE_AND_TRANSLATE, "scale",
				scale, "previousScale", previousScale, "translate", translate,
				"previousTranslate", previousTranslate));
	}

	/**
	 * Returns the current scale.
	 * 
	 * @return Returns the scale.
	 */
	public double getScale()
	{
		return scale;
	}

	/**
	 * Sets the current scale and revalidates the view. Fires a "scale"
	 * event after calling revalidate. Revalidate is only called if
	 * isEventsEnabled.
	 * 
	 * @param value New scale to be used.
	 */
	public void setScale(double value)
	{
		double previousScale = scale;

		if (scale != value)
		{
			scale = value;

			if (isEventsEnabled())
			{
				revalidate();
			}
		}

		fireEvent(new mxEventObject(mxEvent.SCALE, "scale", scale,
				"previousScale", previousScale));
	}

	/**
	 * Returns the current translation.
	 * 
	 * @return Returns the translation.
	 */
	public mxPoint getTranslate()
	{
		return translate;
	}

	/**
	 * Sets the current translation and invalidates the view. Fires
	 * a property change event for "translate" after calling
	 * revalidate. Revalidate is only called if isEventsEnabled.
	 * 
	 * @param value New translation to be used.
	 */
	public void setTranslate(mxPoint value)
	{
		Object previousTranslate = translate.clone();

		if (value != null
				&& (value.getX() != translate.getX() || value.getY() != translate
						.getY()))
		{
			translate = value;

			if (isEventsEnabled())
			{
				revalidate();
			}
		}

		fireEvent(new mxEventObject(mxEvent.TRANSLATE, "translate", translate,
				"previousTranslate", previousTranslate));
	}

	/**
	 * Returns the bounding box for an array of cells or null, if no cells are
	 * specified.
	 * 
	 * @param cells
	 * @return Returns the bounding box for the given cells.
	 */
	public mxRectangle getBounds(Object[] cells)
	{
		return getBounds(cells, false);
	}

	/**
	 * Returns the bounding box for an array of cells or null, if no cells are
	 * specified.
	 * 
	 * @param cells
	 * @return Returns the bounding box for the given cells.
	 */
	public mxRectangle getBoundingBox(Object[] cells)
	{
		return getBounds(cells, true);
	}

	/**
	 * Returns the bounding box for an array of cells or null, if no cells are
	 * specified.
	 * 
	 * @param cells
	 * @return Returns the bounding box for the given cells.
	 */
	public mxRectangle getBounds(Object[] cells, boolean boundingBox)
	{
		mxRectangle result = null;

		if (cells != null && cells.length > 0)
		{
			mxIGraphModel model = graph.getModel();

			for (int i = 0; i < cells.length; i++)
			{
				if (model.isVertex(cells[i]) || model.isEdge(cells[i]))
				{
					mxCellState state = getState(cells[i]);

					if (state != null)
					{
						mxRectangle tmp = (boundingBox) ? state
								.getBoundingBox() : state;

						if (tmp != null)
						{
							if (result == null)
							{
								result = new mxRectangle(tmp);
							}
							else
							{
								result.add(tmp);
							}
						}
					}
				}
			}
		}

		return result;
	}

	/**
	 * Removes all existing cell states and invokes validate.
	 */
	public void reload()
	{
		states.clear();
		validate();
	}

	/**
	 * 
	 */
	public void revalidate()
	{
		invalidate();
		validate();
	}

	/**
	 * Invalidates all cell states.
	 */
	public void invalidate()
	{
		invalidate(null);
	}

	/**
	 * Removes the state of the given cell and all descendants if the given
	 * cell is not the current root.
	 * 
	 * @param cell
	 * @param force
	 * @param recurse
	 */
	public void clear(Object cell, boolean force, boolean recurse)
	{
		removeState(cell);

		if (recurse && (force || cell != currentRoot))
		{
			mxIGraphModel model = graph.getModel();
			int childCount = model.getChildCount(cell);

			for (int i = 0; i < childCount; i++)
			{
				clear(model.getChildAt(cell, i), force, recurse);
			}
		}
		else
		{
			invalidate(cell);
		}
	}

	/**
	 * Invalidates the state of the given cell, all its descendants and
	 * connected edges.
	 */
	public void invalidate(Object cell)
	{
		mxIGraphModel model = graph.getModel();
		cell = (cell != null) ? cell : model.getRoot();
		mxCellState state = getState(cell);

		if (state == null || !state.isInvalid())
		{
			if (state != null)
			{
				state.setInvalid(true);
			}

			// Recursively invalidates all descendants
			int childCount = model.getChildCount(cell);

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

			// Propagates invalidation to all connected edges
			int edgeCount = model.getEdgeCount(cell);

			for (int i = 0; i < edgeCount; i++)
			{
				invalidate(model.getEdgeAt(cell, i));
			}
		}
	}

	/**
	 * First validates all bounds and then validates all points recursively on
	 * all visible cells.
	 */
	public void validate()
	{
		Object cell = (currentRoot != null) ? currentRoot : graph.getModel()
				.getRoot();

		if (cell != null)
		{
			validateBounds(null, cell);
			mxRectangle bounds = validatePoints(null, cell);

			if (bounds == null)
			{
				bounds = new mxRectangle();
			}

			setGraphBounds(bounds);
		}
	}

	/**
	 * Validates the bounds of the given parent's child using the given parent
	 * state as the origin for the child. The validation is carried out
	 * recursively for all non-collapsed descendants.
	 * 
	 * @param parentState Object that represents the state of the parent cell.
	 * @param cell Cell for which the bounds in the state should be updated.
	 */
	public void validateBounds(mxCellState parentState, Object cell)
	{
		mxIGraphModel model = graph.getModel();
		mxCellState state = getState(cell, true);

		if (state != null && state.isInvalid())
		{
			if (!graph.isCellVisible(cell))
			{
				removeState(cell);
			}
			else if (cell != currentRoot && parentState != null)
			{
				state.getAbsoluteOffset().setX(0);
				state.getAbsoluteOffset().setY(0);
				state.setOrigin(new mxPoint(parentState.getOrigin()));
				mxGeometry geo = graph.getCellGeometry(cell);

				if (geo != null)
				{
					if (!model.isEdge(cell))
					{
						mxPoint origin = state.getOrigin();
						mxPoint offset = geo.getOffset();

						if (offset == null)
						{
							offset = EMPTY_POINT;
						}

						if (geo.isRelative())
						{
							origin.setX(origin.getX() + geo.getX()
									* parentState.getWidth() / scale
									+ offset.getX());
							origin.setY(origin.getY() + geo.getY()
									* parentState.getHeight() / scale
									+ offset.getY());
						}
						else
						{
							state.setAbsoluteOffset(new mxPoint(scale
									* offset.getX(), scale * offset.getY()));
							origin.setX(origin.getX() + geo.getX());
							origin.setY(origin.getY() + geo.getY());
						}
					}

					// Updates the cell state's bounds
					state.setX(scale
							* (translate.getX() + state.getOrigin().getX()));
					state.setY(scale
							* (translate.getY() + state.getOrigin().getY()));
					state.setWidth(scale * geo.getWidth());
					state.setHeight(scale * geo.getHeight());

					if (model.isVertex(cell))
					{
						updateVertexLabelOffset(state);
					}

					// Updates the cached label
					updateLabel(state);
				}
			}

			// Applies child offset to origin
			mxPoint offset = graph.getChildOffsetForCell(cell);

			if (offset != null)
			{
				state.getOrigin()
						.setX(state.getOrigin().getX() + offset.getX());
				state.getOrigin()
						.setY(state.getOrigin().getY() + offset.getY());
			}
		}

		// Recursively validates the child bounds
		if (state != null
				&& (!graph.isCellCollapsed(cell) || cell == currentRoot))
		{
			int childCount = model.getChildCount(cell);

			for (int i = 0; i < childCount; i++)
			{
				validateBounds(state, model.getChildAt(cell, i));
			}
		}
	}

	/**
	 * Updates the absoluteOffset of the given vertex cell state. This takes
	 * into account the label position styles.
	 * 
	 * @param state Cell state whose absolute offset should be updated.
	 */
	public void updateVertexLabelOffset(mxCellState state)
	{
		String horizontal = mxUtils.getString(state.getStyle(),
				mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);

		if (horizontal.equals(mxConstants.ALIGN_LEFT))
		{
			state.absoluteOffset.setX(state.absoluteOffset.getX()
					- state.getWidth());
		}
		else if (horizontal.equals(mxConstants.ALIGN_RIGHT))
		{
			state.absoluteOffset.setX(state.absoluteOffset.getX()
					+ state.getWidth());
		}

		String vertical = mxUtils.getString(state.getStyle(),
				mxConstants.STYLE_VERTICAL_LABEL_POSITION,
				mxConstants.ALIGN_MIDDLE);

		if (vertical.equals(mxConstants.ALIGN_TOP))
		{
			state.absoluteOffset.setY(state.absoluteOffset.getY()
					- state.getHeight());
		}
		else if (vertical.equals(mxConstants.ALIGN_BOTTOM))
		{
			state.absoluteOffset.setY(state.absoluteOffset.getY()
					+ state.getHeight());
		}
	}

	/**
	 * Validates the points for the state of the given cell recursively if the
	 * cell is not collapsed and returns the bounding box of all visited states
	 * as a rectangle.
	 * 
	 * @param parentState Object that represents the state of the parent cell.
	 * @param cell Cell for which the points in the state should be updated.
	 * @return Returns the bounding box for the given cell.
	 */
	public mxRectangle validatePoints(mxCellState parentState, Object cell)
	{
		mxIGraphModel model = graph.getModel();
		mxCellState state = getState(cell);
		mxRectangle bbox = null;

		if (state != null)
		{
			if (state.isInvalid())
			{
				mxGeometry geo = graph.getCellGeometry(cell);

				if (geo != null && model.isEdge(cell))
				{
					// Updates the points on the source terminal if its an edge
					mxCellState source = getState(getVisibleTerminal(cell, true));
					state.setVisibleTerminalState(source, true);

					if (source != null && model.isEdge(source.getCell())
							&& !model.isAncestor(source, cell))
					{
						mxCellState tmp = getState(model.getParent(source
								.getCell()));
						validatePoints(tmp, source);
					}

					// Updates the points on the source terminal if its an edge
					mxCellState target = getState(getVisibleTerminal(cell,
							false));
					state.setVisibleTerminalState(target, false);

					if (target != null && model.isEdge(target.getCell())
							&& !model.isAncestor(target, cell))
					{
						mxCellState tmp = getState(model.getParent(target
								.getCell()));
						validatePoints(tmp, target);
					}

					updateFixedTerminalPoints(state, source, target);
					updatePoints(state, geo.getPoints(), source, target);
					updateFloatingTerminalPoints(state, source, target);
					updateEdgeBounds(state);
					state.setAbsoluteOffset(getPoint(state, geo));
				}
				else if (geo != null && geo.isRelative() && parentState != null
						&& model.isEdge(parentState.getCell()))
				{
					mxPoint origin = getPoint(parentState, geo);

					if (origin != null)
					{
						state.setX(origin.getX());
						state.setY(origin.getY());

						origin.setX((origin.getX() / scale) - translate.getX());
						origin.setY((origin.getY() / scale) - translate.getY());
						state.setOrigin(origin);

						childMoved(parentState, state);
					}
				}

				state.setInvalid(false);
			}

			if (model.isEdge(cell) || model.isVertex(cell))
			{
				updateLabelBounds(state);
				bbox = new mxRectangle(updateBoundingBox(state));
			}
		}

		if (state != null
				&& (!graph.isCellCollapsed(cell) || cell == currentRoot))
		{
			int childCount = model.getChildCount(cell);

			for (int i = 0; i < childCount; i++)
			{
				Object child = model.getChildAt(cell, i);
				mxRectangle bounds = validatePoints(state, child);

				if (bounds != null)
				{
					if (bbox == null)
					{
						bbox = bounds;
					}
					else
					{
						bbox.add(bounds);
					}
				}
			}
		}

		return bbox;
	}

	/**
	 * Invoked when a child state was moved as a result of late evaluation
	 * of its position. This is invoked for relative edge children whose
	 * position can only be determined after the points of the parent edge
	 * are updated in validatePoints, and validates the bounds of all
	 * descendants of the child using validateBounds.
	 */
	protected void childMoved(mxCellState parent, mxCellState child)
	{
		Object cell = child.getCell();

		// Children of relative edge children need to validate
		// their bounds after their parent state was updated
		if (!graph.isCellCollapsed(cell) || cell == currentRoot)
		{
			mxIGraphModel model = graph.getModel();
			int childCount = model.getChildCount(cell);

			for (int i = 0; i < childCount; i++)
			{
				validateBounds(child, model.getChildAt(cell, i));
			}
		}
	}

	/**
	 * Updates the label of the given state.
	 */
	public void updateLabel(mxCellState state)
	{
		String label = graph.getLabel(state.getCell());
		Map style = state.getStyle();

		// Applies word wrapping to non-HTML labels and stores the result in the state
		if (label != null
				&& label.length() > 0
				&& !graph.isHtmlLabel(state.getCell())
				&& !graph.getModel().isEdge(state.getCell())
				&& mxUtils.getString(style, mxConstants.STYLE_WHITE_SPACE,
						"nowrap").equals("wrap"))
		{
			double w = getWordWrapWidth(state);

			// The lines for wrapping within the given width are calculated for no
			// scale. The reason for this is the granularity of actual displayed 
			// font can cause the displayed lines to change based on scale. A factor 
			// is used to allow for different overalls widths, it ensures the largest 
			// font size/scale factor still stays within the bounds. All this ensures
			// the wrapped lines are constant overing scaling, at the expense the 
			// label bounds will vary.
			String[] lines = mxUtils.wordWrap(label,
					mxUtils.getFontMetrics(mxUtils.getFont(state.getStyle())),
					w * mxConstants.LABEL_SCALE_BUFFER);

			if (lines.length > 0)
			{
				StringBuffer buffer = new StringBuffer();

				for (String line : lines)
				{
					buffer.append(line + '\n');
				}

				label = buffer.substring(0, buffer.length() - 1);
			}
		}

		state.setLabel(label);
	}

	/**
	 * Returns the width for wrapping the label of the given state at
	 * scale 1.
	 */
	public double getWordWrapWidth(mxCellState state)
	{
		Map style = state.getStyle();
		boolean horizontal = mxUtils.isTrue(style,
				mxConstants.STYLE_HORIZONTAL, true);
		double w = 0;

		// Computes the available width for the wrapped label
		if (horizontal)
		{
			w = (state.getWidth() / scale) - 2 * mxConstants.LABEL_INSET - 2
					* mxUtils.getDouble(style, mxConstants.STYLE_SPACING)
					- mxUtils.getDouble(style, mxConstants.STYLE_SPACING_LEFT)
					- mxUtils.getDouble(style, mxConstants.STYLE_SPACING_RIGHT);
		}
		else
		{
			w = (state.getHeight() / scale)
					- 2
					* mxConstants.LABEL_INSET
					- 2
					* mxUtils.getDouble(style, mxConstants.STYLE_SPACING)
					- mxUtils.getDouble(style, mxConstants.STYLE_SPACING_TOP)
					+ mxUtils
							.getDouble(style, mxConstants.STYLE_SPACING_BOTTOM);
		}

		return w;
	}

	/**
	 * Updates the label bounds in the given state.
	 */
	public void updateLabelBounds(mxCellState state)
	{
		Object cell = state.getCell();
		Map style = state.getStyle();

		if (mxUtils.getString(style, mxConstants.STYLE_OVERFLOW, "").equals(
				"fill"))
		{
			state.setLabelBounds(new mxRectangle(state));
		}
		else if (state.getLabel() != null)
		{
			mxRectangle vertexBounds = (!graph.getModel().isEdge(cell)) ? state
					: null;
			state.setLabelBounds(mxUtils.getLabelPaintBounds(state.getLabel(),
					style, graph.isHtmlLabel(cell), state.getAbsoluteOffset(),
					vertexBounds, scale));
		}
	}

	/**
	 * Updates the bounding box in the given cell state.
	 *  
	 * @param state Cell state whose bounding box should be
	 * updated.
	 */
	public mxRectangle updateBoundingBox(mxCellState state)
	{
		// Gets the cell bounds and adds shadows and markers
		mxRectangle rect = new mxRectangle(state);
		Map style = state.getStyle();

		// Adds extra pixels for the marker and stroke assuming
		// that the border stroke is centered around the bounds
		// and the first pixel is drawn inside the bounds
		double strokeWidth = Math.max(
				1,
				Math.round(mxUtils.getInt(style, mxConstants.STYLE_STROKEWIDTH,
						1) * scale));
		strokeWidth -= Math.max(1, strokeWidth / 2);

		if (graph.getModel().isEdge(state.getCell()))
		{
			int ms = 0;

			if (style.containsKey(mxConstants.STYLE_ENDARROW)
					|| style.containsKey(mxConstants.STYLE_STARTARROW))
			{
				ms = (int) Math.round(mxConstants.DEFAULT_MARKERSIZE * scale);
			}

			// Adds the strokewidth
			rect.grow(ms + strokeWidth);

			// Adds worst case border for an arrow shape
			if (mxUtils.getString(style, mxConstants.STYLE_SHAPE, "").equals(
					mxConstants.SHAPE_ARROW))
			{
				rect.grow(mxConstants.ARROW_WIDTH / 2);
			}
		}
		else
		{
			rect.grow(strokeWidth);
		}

		// Adds extra pixels for the shadow
		if (mxUtils.isTrue(style, mxConstants.STYLE_SHADOW))
		{
			rect.setWidth(rect.getWidth() + mxConstants.SHADOW_OFFSETX);
			rect.setHeight(rect.getHeight() + mxConstants.SHADOW_OFFSETY);
		}

		// Adds oversize images in labels
		if (mxUtils.getString(style, mxConstants.STYLE_SHAPE, "").equals(
				mxConstants.SHAPE_LABEL))
		{
			if (mxUtils.getString(style, mxConstants.STYLE_IMAGE) != null)
			{
				double w = mxUtils.getInt(style, mxConstants.STYLE_IMAGE_WIDTH,
						mxConstants.DEFAULT_IMAGESIZE) * scale;
				double h = mxUtils.getInt(style,
						mxConstants.STYLE_IMAGE_HEIGHT,
						mxConstants.DEFAULT_IMAGESIZE)
						* scale;

				double x = state.getX();
				double y = 0;

				String imgAlign = mxUtils.getString(style,
						mxConstants.STYLE_IMAGE_ALIGN, mxConstants.ALIGN_LEFT);
				String imgValign = mxUtils.getString(style,
						mxConstants.STYLE_IMAGE_VERTICAL_ALIGN,
						mxConstants.ALIGN_MIDDLE);

				if (imgAlign.equals(mxConstants.ALIGN_RIGHT))
				{
					x += state.getWidth() - w;
				}
				else if (imgAlign.equals(mxConstants.ALIGN_CENTER))
				{
					x += (state.getWidth() - w) / 2;
				}

				if (imgValign.equals(mxConstants.ALIGN_TOP))
				{
					y = state.getY();
				}
				else if (imgValign.equals(mxConstants.ALIGN_BOTTOM))
				{
					y = state.getY() + state.getHeight() - h;
				}
				else
				// MIDDLE
				{
					y = state.getY() + (state.getHeight() - h) / 2;
				}

				rect.add(new mxRectangle(x, y, w, h));
			}
		}

		// Adds the rotated bounds to the bounding box if the
		// shape is rotated
		double rotation = mxUtils.getDouble(style, mxConstants.STYLE_ROTATION);
		mxRectangle bbox = mxUtils.getBoundingBox(rect, rotation);

		// Add the rotated bounding box to the non-rotated so
		// that all handles are also covered
		rect.add(bbox);

		// Unifies the cell bounds and the label bounds
		if (!graph.isLabelClipped(state.getCell()))
		{
			rect.add(state.getLabelBounds());
		}

		state.setBoundingBox(rect);

		return rect;
	}

	/**
	 * Sets the initial absolute terminal points in the given state before the edge
	 * style is computed.
	 * 
	 * @param edge Cell state whose initial terminal points should be updated.
	 * @param source Cell state which represents the source terminal.
	 * @param target Cell state which represents the target terminal.
	 */
	public void updateFixedTerminalPoints(mxCellState edge, mxCellState source,
			mxCellState target)
	{
		updateFixedTerminalPoint(edge, source, true,
				graph.getConnectionConstraint(edge, source, true));
		updateFixedTerminalPoint(edge, target, false,
				graph.getConnectionConstraint(edge, target, false));
	}

	/**
	 * Sets the fixed source or target terminal point on the given edge.
	 * 
	 * @param edge Cell state whose initial terminal points should be
	 * updated.
	 */
	public void updateFixedTerminalPoint(mxCellState edge,
			mxCellState terminal, boolean source,
			mxConnectionConstraint constraint)
	{
		mxPoint pt = null;

		if (constraint != null)
		{
			pt = graph.getConnectionPoint(terminal, constraint);
		}

		if (pt == null && terminal == null)
		{
			mxPoint orig = edge.getOrigin();
			mxGeometry geo = graph.getCellGeometry(edge.cell);
			pt = geo.getTerminalPoint(source);

			if (pt != null)
			{
				pt = new mxPoint(scale
						* (translate.getX() + pt.getX() + orig.getX()), scale
						* (translate.getY() + pt.getY() + orig.getY()));
			}
		}

		edge.setAbsoluteTerminalPoint(pt, source);
	}

	/**
	 * Updates the absolute points in the given state using the specified array
	 * of points as the relative points.
	 * 
	 * @param edge Cell state whose absolute points should be updated.
	 * @param points Array of points that constitute the relative points.
	 * @param source Cell state that represents the source terminal.
	 * @param target Cell state that represents the target terminal.
	 */
	public void updatePoints(mxCellState edge, List points,
			mxCellState source, mxCellState target)
	{
		if (edge != null)
		{
			List pts = new ArrayList();
			pts.add(edge.getAbsolutePoint(0));
			mxEdgeStyleFunction edgeStyle = getEdgeStyle(edge, points, source,
					target);

			if (edgeStyle != null)
			{
				mxCellState src = getTerminalPort(edge, source, true);
				mxCellState trg = getTerminalPort(edge, target, false);

				edgeStyle.apply(edge, src, trg, points, pts);
			}
			else if (points != null)
			{
				for (int i = 0; i < points.size(); i++)
				{
					pts.add(transformControlPoint(edge, points.get(i)));
				}
			}

			pts.add(edge.getAbsolutePoint(edge.getAbsolutePointCount() - 1));
			edge.setAbsolutePoints(pts);
		}
	}

	/**
	 * Transforms the given control point to an absolute point.
	 */
	public mxPoint transformControlPoint(mxCellState state, mxPoint pt)
	{
		mxPoint origin = state.getOrigin();

		return new mxPoint(scale
				* (pt.getX() + translate.getX() + origin.getX()), scale
				* (pt.getY() + translate.getY() + origin.getY()));
	}

	/**
	 * Returns the edge style function to be used to compute the absolute
	 * points for the given state, control points and terminals.
	 */
	public mxEdgeStyleFunction getEdgeStyle(mxCellState edge,
			List points, Object source, Object target)
	{
		Object edgeStyle = null;

		if (source != null && source == target)
		{
			edgeStyle = edge.getStyle().get(mxConstants.STYLE_LOOP);

			if (edgeStyle == null)
			{
				edgeStyle = graph.getDefaultLoopStyle();
			}
		}
		else if (!mxUtils.isTrue(edge.getStyle(),
				mxConstants.STYLE_NOEDGESTYLE, false))
		{
			edgeStyle = edge.getStyle().get(mxConstants.STYLE_EDGE);
		}

		// Converts string values to objects
		if (edgeStyle instanceof String)
		{
			String str = String.valueOf(edgeStyle);
			Object tmp = mxStyleRegistry.getValue(str);

			if (tmp == null)
			{
				tmp = mxUtils.eval(str);
			}

			edgeStyle = tmp;
		}

		if (edgeStyle instanceof mxEdgeStyleFunction)
		{
			return (mxEdgeStyleFunction) edgeStyle;
		}

		return null;
	}

	/**
	 * Updates the terminal points in the given state after the edge style was
	 * computed for the edge.
	 * 
	 * @param state Cell state whose terminal points should be updated.
	 * @param source Cell state that represents the source terminal.
	 * @param target Cell state that represents the target terminal.
	 */
	public void updateFloatingTerminalPoints(mxCellState state,
			mxCellState source, mxCellState target)
	{
		mxPoint p0 = state.getAbsolutePoint(0);
		mxPoint pe = state.getAbsolutePoint(state.getAbsolutePointCount() - 1);

		if (pe == null && target != null)
		{
			updateFloatingTerminalPoint(state, target, source, false);
		}

		if (p0 == null && source != null)
		{
			updateFloatingTerminalPoint(state, source, target, true);
		}
	}

	/**
	 * Updates the absolute terminal point in the given state for the given
	 * start and end state, where start is the source if source is true.
	 * 
	 * @param edge Cell state whose terminal point should be updated.
	 * @param start Cell state for the terminal on "this" side of the edge.
	 * @param end Cell state for the terminal on the other side of the edge.
	 * @param source Boolean indicating if start is the source terminal state.
	 */
	public void updateFloatingTerminalPoint(mxCellState edge,
			mxCellState start, mxCellState end, boolean source)
	{
		start = getTerminalPort(edge, start, source);
		mxPoint next = getNextPoint(edge, end, source);
		double border = mxUtils.getDouble(edge.getStyle(),
				mxConstants.STYLE_PERIMETER_SPACING);
		border += mxUtils.getDouble(edge.getStyle(),
				(source) ? mxConstants.STYLE_SOURCE_PERIMETER_SPACING
						: mxConstants.STYLE_TARGET_PERIMETER_SPACING);
		mxPoint pt = getPerimeterPoint(start, next, graph.isOrthogonal(edge),
				border);
		edge.setAbsoluteTerminalPoint(pt, source);
	}

	/**
	 * Returns a cell state that represents the source or target terminal or
	 * port for the given edge.
	 */
	public mxCellState getTerminalPort(mxCellState state, mxCellState terminal,
			boolean source)
	{
		String key = (source) ? mxConstants.STYLE_SOURCE_PORT
				: mxConstants.STYLE_TARGET_PORT;
		String id = mxUtils.getString(state.style, key);

		if (id != null && graph.getModel() instanceof mxGraphModel)
		{
			mxCellState tmp = getState(((mxGraphModel) graph.getModel())
					.getCell(id));

			// Only uses ports where a cell state exists
			if (tmp != null)
			{
				terminal = tmp;
			}
		}

		return terminal;
	}

	/**
	 * Returns a point that defines the location of the intersection point between
	 * the perimeter and the line between the center of the shape and the given point.
	 */
	public mxPoint getPerimeterPoint(mxCellState terminal, mxPoint next,
			boolean orthogonal)
	{
		return getPerimeterPoint(terminal, next, orthogonal, 0);
	}

	/**
	 * Returns a point that defines the location of the intersection point between
	 * the perimeter and the line between the center of the shape and the given point.
	 * 
	 * @param terminal Cell state for the source or target terminal.
	 * @param next Point that lies outside of the given terminal.
	 * @param orthogonal Boolean that specifies if the orthogonal projection onto
	 * the perimeter should be returned. If this is false then the intersection
	 * of the perimeter and the line between the next and the center point is
	 * returned.
	 * @param border Optional border between the perimeter and the shape.
	 */
	public mxPoint getPerimeterPoint(mxCellState terminal, mxPoint next,
			boolean orthogonal, double border)
	{
		mxPoint point = null;

		if (terminal != null)
		{
			mxPerimeterFunction perimeter = getPerimeterFunction(terminal);

			if (perimeter != null && next != null)
			{
				mxRectangle bounds = getPerimeterBounds(terminal, border);

				if (bounds.getWidth() > 0 || bounds.getHeight() > 0)
				{
					point = perimeter.apply(bounds, terminal, next, orthogonal);
				}
			}

			if (point == null)
			{
				point = getPoint(terminal);
			}
		}

		return point;
	}

	/**
	 * Returns the x-coordinate of the center point for automatic routing.
	 * 
	 * @return Returns the x-coordinate of the routing center point.
	 */
	public double getRoutingCenterX(mxCellState state)
	{
		float f = (state.getStyle() != null) ? mxUtils.getFloat(
				state.getStyle(), mxConstants.STYLE_ROUTING_CENTER_X) : 0;

		return state.getCenterX() + f * state.getWidth();
	}

	/**
	 * Returns the y-coordinate of the center point for automatic routing.
	 * 
	 * @return Returns the y-coordinate of the routing center point.
	 */
	public double getRoutingCenterY(mxCellState state)
	{
		float f = (state.getStyle() != null) ? mxUtils.getFloat(
				state.getStyle(), mxConstants.STYLE_ROUTING_CENTER_Y) : 0;

		return state.getCenterY() + f * state.getHeight();
	}

	/**
	 * Returns the perimeter bounds for the given terminal, edge pair.
	 */
	public mxRectangle getPerimeterBounds(mxCellState terminal, double border)
	{
		if (terminal != null)
		{
			border += mxUtils.getDouble(terminal.getStyle(),
					mxConstants.STYLE_PERIMETER_SPACING);
		}

		return terminal.getPerimeterBounds(border * scale);
	}

	/**
	 * Returns the perimeter function for the given state.
	 */
	public mxPerimeterFunction getPerimeterFunction(mxCellState state)
	{
		Object perimeter = state.getStyle().get(mxConstants.STYLE_PERIMETER);

		// Converts string values to objects
		if (perimeter instanceof String)
		{
			String str = String.valueOf(perimeter);
			Object tmp = mxStyleRegistry.getValue(str);

			if (tmp == null)
			{
				tmp = mxUtils.eval(str);
			}

			perimeter = tmp;
		}

		if (perimeter instanceof mxPerimeterFunction)
		{
			return (mxPerimeterFunction) perimeter;
		}

		return null;
	}

	/**
	 * Returns the nearest point in the list of absolute points or the center
	 * of the opposite terminal.
	 * 
	 * @param edge Cell state that represents the edge.
	 * @param opposite Cell state that represents the opposite terminal.
	 * @param source Boolean indicating if the next point for the source or target
	 * should be returned.
	 * @return Returns the nearest point of the opposite side.
	 */
	public mxPoint getNextPoint(mxCellState edge, mxCellState opposite,
			boolean source)
	{
		List pts = edge.getAbsolutePoints();
		mxPoint point = null;

		if (pts != null && (source || pts.size() > 2 || opposite == null))
		{
			int count = pts.size();
			int index = (source) ? Math.min(1, count - 1) : Math.max(0,
					count - 2);
			point = pts.get(index);
		}

		if (point == null && opposite != null)
		{
			point = new mxPoint(opposite.getCenterX(), opposite.getCenterY());
		}

		return point;
	}

	/**
	 * Returns the nearest ancestor terminal that is visible. The edge appears
	 * to be connected to this terminal on the display.
	 * 
	 * @param edge Cell whose visible terminal should be returned.
	 * @param source Boolean that specifies if the source or target terminal
	 * should be returned.
	 * @return Returns the visible source or target terminal.
	 */
	public Object getVisibleTerminal(Object edge, boolean source)
	{
		mxIGraphModel model = graph.getModel();
		Object result = model.getTerminal(edge, source);
		Object best = result;

		while (result != null && result != currentRoot)
		{
			if (!graph.isCellVisible(best) || graph.isCellCollapsed(result))
			{
				best = result;
			}

			result = model.getParent(result);
		}

		// Checks if the result is not a layer
		if (model.getParent(best) == model.getRoot())
		{
			best = null;
		}

		return best;
	}

	/**
	 * Updates the given state using the bounding box of the absolute points.
	 * Also updates terminal distance, length and segments.
	 * 
	 * @param state Cell state whose bounds should be updated.
	 */
	public void updateEdgeBounds(mxCellState state)
	{
		List points = state.getAbsolutePoints();

		if (points != null && points.size() > 0)
		{
			mxPoint p0 = points.get(0);
			mxPoint pe = points.get(points.size() - 1);

			if (p0 == null || pe == null)
			{
				// Note: This is an error that normally occurs
				// if a connected edge has a null-terminal, ie.
				// source is null and/or target is null and no
				// additional control points are defined
				if (state.getCell() != getCurrentRoot())
				{
					removeState(state.getCell());
				}
			}
			else
			{
				if (p0.getX() != pe.getX() || p0.getY() != pe.getY())
				{
					double dx = pe.getX() - p0.getX();
					double dy = pe.getY() - p0.getY();
					state.setTerminalDistance(Math.sqrt(dx * dx + dy * dy));
				}
				else
				{
					state.setTerminalDistance(0);
				}

				double length = 0;
				double[] segments = new double[points.size() - 1];
				mxPoint pt = p0;

				double minX = pt.getX();
				double minY = pt.getY();
				double maxX = minX;
				double maxY = minY;

				for (int i = 1; i < points.size(); i++)
				{
					mxPoint tmp = points.get(i);

					if (tmp != null)
					{
						double dx = pt.getX() - tmp.getX();
						double dy = pt.getY() - tmp.getY();

						double segment = Math.sqrt(dx * dx + dy * dy);
						segments[i - 1] = segment;
						length += segment;
						pt = tmp;

						minX = Math.min(pt.getX(), minX);
						minY = Math.min(pt.getY(), minY);
						maxX = Math.max(pt.getX(), maxX);
						maxY = Math.max(pt.getY(), maxY);
					}
				}

				state.setLength(length);
				state.setSegments(segments);
				double markerSize = 1; // TODO: include marker size

				state.setX(minX);
				state.setY(minY);
				state.setWidth(Math.max(markerSize, maxX - minX));
				state.setHeight(Math.max(markerSize, maxY - minY));
			}
		}
	}

	/**
	 * Returns the absolute center point along the given edge.
	 */
	public mxPoint getPoint(mxCellState state)
	{
		return getPoint(state, null);
	}

	/**
	 * Returns the absolute point on the edge for the given relative
	 * geometry as a point. The edge is represented by the given cell state.
	 * 
	 * @param state Represents the state of the parent edge.
	 * @param geometry Optional geometry that represents the relative location.
	 * @return Returns the mxpoint that represents the absolute location
	 * of the given relative geometry.
	 */
	public mxPoint getPoint(mxCellState state, mxGeometry geometry)
	{
		double x = state.getCenterX();
		double y = state.getCenterY();

		if (state.getSegments() != null
				&& (geometry == null || geometry.isRelative()))
		{
			double gx = (geometry != null) ? geometry.getX() / 2 : 0;
			int pointCount = state.getAbsolutePointCount();
			double dist = (gx + 0.5) * state.getLength();
			double[] segments = state.getSegments();
			double segment = segments[0];
			double length = 0;
			int index = 1;

			while (dist > length + segment && index < pointCount - 1)
			{
				length += segment;
				segment = segments[index++];
			}

			double factor = (segment == 0) ? 0 : (dist - length) / segment;
			mxPoint p0 = state.getAbsolutePoint(index - 1);
			mxPoint pe = state.getAbsolutePoint(index);

			if (p0 != null && pe != null)
			{
				double gy = 0;
				double offsetX = 0;
				double offsetY = 0;

				if (geometry != null)
				{
					gy = geometry.getY();
					mxPoint offset = geometry.getOffset();

					if (offset != null)
					{
						offsetX = offset.getX();
						offsetY = offset.getY();
					}
				}

				double dx = pe.getX() - p0.getX();
				double dy = pe.getY() - p0.getY();
				double nx = (segment == 0) ? 0 : dy / segment;
				double ny = (segment == 0) ? 0 : dx / segment;

				x = p0.getX() + dx * factor + (nx * gy + offsetX) * scale;
				y = p0.getY() + dy * factor - (ny * gy - offsetY) * scale;
			}
		}
		else if (geometry != null)
		{
			mxPoint offset = geometry.getOffset();

			if (offset != null)
			{
				x += offset.getX();
				y += offset.getY();
			}
		}

		return new mxPoint(x, y);
	}

	/**
	 * Gets the relative point that describes the given, absolute label
	 * position for the given edge state.
	 */
	public mxPoint getRelativePoint(mxCellState edgeState, double x, double y)
	{
		mxIGraphModel model = graph.getModel();
		mxGeometry geometry = model.getGeometry(edgeState.getCell());

		if (geometry != null)
		{
			int pointCount = edgeState.getAbsolutePointCount();

			if (geometry.isRelative() && pointCount > 1)
			{
				double totalLength = edgeState.getLength();
				double[] segments = edgeState.getSegments();

				// Works which line segment the point of the label is closest to
				mxPoint p0 = edgeState.getAbsolutePoint(0);
				mxPoint pe = edgeState.getAbsolutePoint(1);
				Line2D line = new Line2D.Double(p0.getPoint(), pe.getPoint());
				double minDist = line.ptSegDistSq(x, y);

				int index = 0;
				double tmp = 0;
				double length = 0;

				for (int i = 2; i < pointCount; i++)
				{
					tmp += segments[i - 2];
					pe = edgeState.getAbsolutePoint(i);

					line = new Line2D.Double(p0.getPoint(), pe.getPoint());
					double dist = line.ptSegDistSq(x, y);

					if (dist < minDist)
					{
						minDist = dist;
						index = i - 1;
						length = tmp;
					}

					p0 = pe;
				}

				double seg = segments[index];
				p0 = edgeState.getAbsolutePoint(index);
				pe = edgeState.getAbsolutePoint(index + 1);

				double x2 = p0.getX();
				double y2 = p0.getY();

				double x1 = pe.getX();
				double y1 = pe.getY();

				double px = x;
				double py = y;

				double xSegment = x2 - x1;
				double ySegment = y2 - y1;

				px -= x1;
				py -= y1;
				double projlenSq = 0;

				px = xSegment - px;
				py = ySegment - py;
				double dotprod = px * xSegment + py * ySegment;

				if (dotprod <= 0.0)
				{
					projlenSq = 0;
				}
				else
				{
					projlenSq = dotprod * dotprod
							/ (xSegment * xSegment + ySegment * ySegment);
				}

				double projlen = Math.sqrt(projlenSq);

				if (projlen > seg)
				{
					projlen = seg;
				}

				double yDistance = Line2D.ptLineDist(p0.getX(), p0.getY(),
						pe.getX(), pe.getY(), x, y);
				int direction = Line2D.relativeCCW(p0.getX(), p0.getY(),
						pe.getX(), pe.getY(), x, y);

				if (direction == -1)
				{
					yDistance = -yDistance;
				}

				// Constructs the relative point for the label
				return new mxPoint(
						Math.round(((totalLength / 2 - length - projlen) / totalLength)
								* -2), Math.round(yDistance / scale));
			}
		}

		return new mxPoint();
	}

	/**
	 * Returns the states for the given array of cells. The array contains all
	 * states that are not null, that is, the returned array may have less
	 * elements than the given array.
	 */
	public mxCellState[] getCellStates(Object[] cells)
	{
		List result = new ArrayList(cells.length);

		for (int i = 0; i < cells.length; i++)
		{
			mxCellState state = getState(cells[i]);

			if (state != null)
			{
				result.add(state);
			}
		}

		mxCellState[] resultArray = new mxCellState[result.size()];
		return result.toArray(resultArray);
	}

	/**
	 * Returns the state for the given cell or null if no state is defined for
	 * the cell.
	 * 
	 * @param cell Cell whose state should be returned.
	 * @return Returns the state for the given cell.
	 */
	public mxCellState getState(Object cell)
	{
		return getState(cell, false);
	}

	/**
	 * Returns the cell state for the given cell. If create is true, then
	 * the state is created if it does not yet exist.
	 * 
	 * @param cell Cell for which a new state should be returned.
	 * @param create Boolean indicating if a new state should be created if it
	 * does not yet exist.
	 * @return Returns the state for the given cell.
	 */
	public mxCellState getState(Object cell, boolean create)
	{
		mxCellState state = null;

		if (cell != null)
		{
			state = states.get(cell);

			if (state == null && create && graph.isCellVisible(cell))
			{
				state = createState(cell);
				states.put(cell, state);
			}
		}

		return state;
	}

	/**
	 * Removes and returns the mxCellState for the given cell.
	 * 
	 * @param cell mxCell for which the mxCellState should be removed.
	 * @return Returns the mxCellState that has been removed.
	 */
	public mxCellState removeState(Object cell)
	{
		return (cell != null) ? states.remove(cell) : null;
	}

	/**
	 * Creates and returns a cell state for the given cell.
	 * 
	 * @param cell Cell for which a new state should be created.
	 * @return Returns a new state for the given cell.
	 */
	public mxCellState createState(Object cell)
	{
		return new mxCellState(this, cell, graph.getCellStyle(cell));
	}

	/**
	 * Action to change the current root in a view.
	 */
	public static class mxCurrentRootChange implements mxUndoableChange
	{

		/**
		 * 
		 */
		protected mxGraphView view;

		/**
		 * 
		 */
		protected Object root, previous;

		/**
		 * 
		 */
		protected boolean up;

		/**
		 * Constructs a change of the current root in the given view.
		 */
		public mxCurrentRootChange(mxGraphView view, Object root)
		{
			this.view = view;
			this.root = root;
			this.previous = this.root;
			this.up = (root == null);

			if (!up)
			{
				Object tmp = view.getCurrentRoot();
				mxIGraphModel model = view.graph.getModel();

				while (tmp != null)
				{
					if (tmp == root)
					{
						up = true;
						break;
					}

					tmp = model.getParent(tmp);
				}
			}
		}

		/**
		 * Returns the graph view where the change happened.
		 */
		public mxGraphView getView()
		{
			return view;
		}

		/**
		 * Returns the root.
		 */
		public Object getRoot()
		{
			return root;
		}

		/**
		 * Returns the previous root.
		 */
		public Object getPrevious()
		{
			return previous;
		}

		/**
		 * Returns true if the drilling went upwards.
		 */
		public boolean isUp()
		{
			return up;
		}

		/**
		 * Changes the current root of the view.
		 */
		public void execute()
		{
			Object tmp = view.getCurrentRoot();
			view.currentRoot = previous;
			previous = tmp;

			mxPoint translate = view.graph.getTranslateForRoot(view
					.getCurrentRoot());

			if (translate != null)
			{
				view.translate = new mxPoint(-translate.getX(),
						translate.getY());
			}

			// Removes all existing cell states and revalidates
			view.reload();
			up = !up;

			String eventName = (up) ? mxEvent.UP : mxEvent.DOWN;
			view.fireEvent(new mxEventObject(eventName, "root",
					view.currentRoot, "previous", previous));
		}

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy