/** * $Id: mxGraphView.java,v 1.3 2014/02/19 09:40:59 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.mxICell; 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 * <code>edit</code> property contains the mxUndoableEdit which contains the * mxCurrentRootChange. * * mxEvent.SCALE_AND_TRANSLATE fires after the scale and transle have been * changed in scaleAndTranslate. The <code>scale</code>, <code>previousScale</code>, * <code>translate</code> and <code>previousTranslate</code> properties contain * the new and previous scale and translate, respectively. * * mxEvent.SCALE fires after the scale was changed in setScale. The * <code>scale</code> and <code>previousScale</code> properties contain the * new and previous scale. * * mxEvent.TRANSLATE fires after the translate was changed in setTranslate. The * <code>translate</code> and <code>previousTranslate</code> 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 * <code>root</code> and <code>previous</code> properties contain the new and * previous root, respectively. */ public class mxGraphView extends mxEventSource { /** * */ private 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<Object, mxCellState> states = new Hashtable<Object, mxCellState>(); /** * 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<Object, mxCellState> getStates() { return states; } /** * Returns the dictionary that maps from cells to states. */ public void setStates(Hashtable<Object, mxCellState> 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() { mxRectangle graphBounds = getBoundingBox(validateCellState(validateCell((currentRoot != null) ? currentRoot : graph.getModel().getRoot()))); setGraphBounds((graphBounds != null) ? graphBounds : new mxRectangle()); } /** * Shortcut to validateCell with visible set to true. */ public mxRectangle getBoundingBox(mxCellState state) { return getBoundingBox(state, true); } /** * Returns the bounding box of the shape and the label for the given * cell state and its children if recurse is true. * * @param state Cell state whose bounding box should be returned. * @param recurse Boolean indicating if the children should be included. */ public mxRectangle getBoundingBox(mxCellState state, boolean recurse) { mxRectangle bbox = null; if (state != null) { if (state.getBoundingBox() != null) { bbox = (mxRectangle) state.getBoundingBox().clone(); } if (recurse) { mxIGraphModel model = graph.getModel(); int childCount = model.getChildCount(state.cell); for (int i = 0; i < childCount; i++) { mxRectangle bounds = getBoundingBox( getState(model.getChildAt(state.cell, i)), true); if (bounds != null) { if (bbox == null) { bbox = bounds; } else { bbox.add(bounds); } } } } } return bbox; } /** * Shortcut to validateCell with visible set to true. */ public Object validateCell(Object cell) { return validateCell(cell, true); } /** * Recursively creates the cell state for the given cell if visible is true and * the given cell is visible. If the cell is not visible but the state exists * then it is removed using removeState. * * @param cell Cell whose cell state should be created. * @param visible Boolean indicating if the cell should be visible. */ public Object validateCell(Object cell, boolean visible) { if (cell != null) { visible = visible && graph.isCellVisible(cell); mxCellState state = getState(cell, visible); if (state != null && !visible) { removeState(cell); } else { mxIGraphModel model = graph.getModel(); int childCount = model.getChildCount(cell); for (int i = 0; i < childCount; i++) { validateCell( model.getChildAt(cell, i), visible && (!graph.isCellCollapsed(cell) || cell == currentRoot)); } } } return cell; } /** * Shortcut to validateCellState with recurse set to true. */ public mxCellState validateCellState(Object cell) { return validateCellState(cell, true); } /** * Validates the cell state for the given cell. * * @param cell Cell whose cell state should be validated. * @param recurse Boolean indicating if the children of the cell should be * validated. */ public mxCellState validateCellState(Object cell, boolean recurse) { mxCellState state = null; if (cell != null) { state = getState(cell); if (state != null) { mxIGraphModel model = graph.getModel(); if (state.isInvalid()) { state.setInvalid(false); if (cell != currentRoot) { validateCellState(model.getParent(cell), false); } state.setVisibleTerminalState( validateCellState(getVisibleTerminal(cell, true), false), true); state.setVisibleTerminalState( validateCellState(getVisibleTerminal(cell, false), false), false); updateCellState(state); if (model.isEdge(cell) || model.isVertex(cell)) { updateLabelBounds(state); updateBoundingBox(state); } } if (recurse) { int childCount = model.getChildCount(cell); for (int i = 0; i < childCount; i++) { validateCellState(model.getChildAt(cell, i)); } } } } return state; } /** * Updates the given cell state. * * @param state Cell state to be updated. */ public void updateCellState(mxCellState state) { state.getAbsoluteOffset().setX(0); state.getAbsoluteOffset().setY(0); state.getOrigin().setX(0); state.getOrigin().setY(0); state.setLength(0); if (state.getCell() != currentRoot) { mxIGraphModel model = graph.getModel(); mxCellState pState = getState(model.getParent(state.getCell())); if (pState != null && pState.getCell() != currentRoot) { state.getOrigin().setX( state.getOrigin().getX() + pState.getOrigin().getX()); state.getOrigin().setY( state.getOrigin().getY() + pState.getOrigin().getY()); } mxPoint offset = graph.getChildOffsetForCell(state.getCell()); if (offset != null) { state.getOrigin() .setX(state.getOrigin().getX() + offset.getX()); state.getOrigin() .setY(state.getOrigin().getY() + offset.getY()); } mxGeometry geo = graph.getCellGeometry(state.getCell()); if (geo != null) { if (!model.isEdge(state.cell)) { mxPoint origin = state.getOrigin(); offset = geo.getOffset(); if (offset == null) { offset = EMPTY_POINT; } if (geo.isRelative() && pState != null) { if (model.isEdge(pState.cell)) { mxPoint orig = getPoint(pState, geo); if (orig != null) { origin.setX(origin.getX() + (orig.getX() / scale) - translate.getX()); origin.setY(origin.getY() + (orig.getY() / scale) - translate.getY()); } } else { origin.setX(origin.getX() + geo.getX() * pState.getWidth() / scale + offset.getX()); origin.setY(origin.getY() + geo.getY() * pState.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()); } } 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(state.getCell())) { updateVertexState(state, geo); } if (model.isEdge(state.getCell())) { updateEdgeState(state, geo); } // Updates the cached label updateLabel(state); } } } /** * Validates the given cell state. */ public void updateVertexState(mxCellState state, mxGeometry geo) { // LATER: Add support for rotation updateVertexLabelOffset(state); } /** * Validates the given cell state. */ public void updateEdgeState(mxCellState state, mxGeometry geo) { mxCellState source = state.getVisibleTerminalState(true); mxCellState target = state.getVisibleTerminalState(false); // This will remove edges with no terminals and no terminal points // as such edges are invalid and produce NPEs in the edge styles. // Also removes connected edges that have no visible terminals. if ((graph.getModel().getTerminal(state.getCell(), true) != null && source == null) || (source == null && geo.getTerminalPoint(true) == null) || (graph.getModel().getTerminal(state.getCell(), false) != null && target == null) || (target == null && geo.getTerminalPoint(false) == null)) { clear(state.cell, true, true); } else { updateFixedTerminalPoints(state, source, target); updatePoints(state, geo.getPoints(), source, target); updateFloatingTerminalPoints(state, source, target); if (state.getCell() != getCurrentRoot() && (state.getAbsolutePointCount() < 2 || state.getAbsolutePoint(0) == null || state .getAbsolutePoint(state.getAbsolutePointCount() - 1) == null)) { // This will remove edges with invalid points from the list of states in the view. // Happens if the one of the terminals and the corresponding terminal point is null. clear(state.getCell(), true, true); } else { updateEdgeBounds(state); state.setAbsoluteOffset(getPoint(state, geo)); } } } /** * 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()); } } /** * Updates the label of the given state. */ public void updateLabel(mxCellState state) { String label = graph.getLabel(state.getCell()); Map<String, Object> 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<String, Object> 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<String, Object> style = state.getStyle(); String overflow = mxUtils.getString(style, mxConstants.STYLE_OVERFLOW, ""); if (overflow.equals("fill")) { state.setLabelBounds(new mxRectangle(state)); } else if (state.getLabel() != null) { // For edges, the width of the geometry is used for wrapping HTML // labels or no wrapping is applied if the width is set to 0 mxRectangle vertexBounds = state; if (graph.getModel().isEdge(cell)) { mxGeometry geo = graph.getCellGeometry(cell); if (geo != null && geo.getWidth() > 0) { vertexBounds = new mxRectangle(0, 0, geo.getWidth() * this.getScale(), 0); } else { vertexBounds = null; } } state.setLabelBounds(mxUtils.getLabelPaintBounds(state.getLabel(), style, graph.isHtmlLabel(cell), state.getAbsoluteOffset(), vertexBounds, scale, graph.getModel().isEdge(cell))); if (overflow.equals("width")) { state.getLabelBounds().setX(state.getX()); state.getLabelBounds().setWidth(state.getWidth()); } } } /** * 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<String, Object> 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 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<mxPoint> points, mxCellState source, mxCellState target) { if (edge != null) { List<mxPoint> pts = new ArrayList<mxPoint>(); 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<mxPoint> 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<mxPoint> 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<mxPoint> points = state.getAbsolutePoints(); mxPoint p0 = points.get(0); mxPoint pe = points.get(points.size() - 1); 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<mxCellState> result = new ArrayList<mxCellState>(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) ? (mxCellState) 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)); } } }