UNPKG

mxgraph

Version:

mxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.

1,941 lines (1,708 loc) 74.8 kB
/** * Copyright (c) 2006-2015, JGraph Ltd * Copyright (c) 2006-2015, Gaudenz Alder */ /** * Class: mxGraphView * * Extends <mxEventSource> to implement a view for a 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 <mxCellStates> 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. * * Event: mxEvent.UNDO * * Fires after the root was changed in <setCurrentRoot>. The <code>edit</code> * property contains the <mxUndoableEdit> which contains the * <mxCurrentRootChange>. * * Event: mxEvent.SCALE_AND_TRANSLATE * * Fires after the scale and translate 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. * * Event: 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. * * Event: 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. * * Event: mxEvent.DOWN and mxEvent.UP * * Fire if the current root is changed by executing an <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. * * Constructor: mxGraphView * * Constructs a new view for the given <mxGraph>. * * Parameters: * * graph - Reference to the enclosing <mxGraph>. */ function mxGraphView(graph) { this.graph = graph; this.translate = new mxPoint(); this.graphBounds = new mxRectangle(); this.states = new mxDictionary(); }; /** * Extends mxEventSource. */ mxGraphView.prototype = new mxEventSource(); mxGraphView.prototype.constructor = mxGraphView; /** * */ mxGraphView.prototype.EMPTY_POINT = new mxPoint(); /** * Variable: doneResource * * Specifies the resource key for the status message after a long operation. * If the resource for this key does not exist then the value is used as * the status message. Default is 'done'. */ mxGraphView.prototype.doneResource = (mxClient.language != 'none') ? 'done' : ''; /** * Function: updatingDocumentResource * * Specifies the resource key for the status message while the document is * being updated. If the resource for this key does not exist then the * value is used as the status message. Default is 'updatingDocument'. */ mxGraphView.prototype.updatingDocumentResource = (mxClient.language != 'none') ? 'updatingDocument' : ''; /** * Variable: allowEval * * Specifies if string values in cell styles should be evaluated using * <mxUtils.eval>. This will only be used if the string values can't be mapped * to objects using <mxStyleRegistry>. Default is false. NOTE: Enabling this * switch carries a possible security risk. */ mxGraphView.prototype.allowEval = false; /** * Variable: captureDocumentGesture * * Specifies if a gesture should be captured when it goes outside of the * graph container. Default is true. */ mxGraphView.prototype.captureDocumentGesture = true; /** * Variable: optimizeVmlReflows * * Specifies if the <canvas> should be hidden while rendering in IE8 standards * mode and quirks mode. This will significantly improve rendering performance. * Default is true. */ mxGraphView.prototype.optimizeVmlReflows = true; /** * Variable: rendering * * Specifies if shapes should be created, updated and destroyed using the * methods of <mxCellRenderer> in <graph>. Default is true. */ mxGraphView.prototype.rendering = true; /** * Variable: graph * * Reference to the enclosing <mxGraph>. */ mxGraphView.prototype.graph = null; /** * Variable: currentRoot * * <mxCell> that acts as the root of the displayed cell hierarchy. */ mxGraphView.prototype.currentRoot = null; /** * Variable: graphBounds * * <mxRectangle> that caches the scales, translated bounds of the current view. */ mxGraphView.prototype.graphBounds = null; /** * Variable: scale * * Specifies the scale. Default is 1 (100%). */ mxGraphView.prototype.scale = 1; /** * Variable: translate * * <mxPoint> that specifies the current translation. Default is a new * empty <mxPoint>. */ mxGraphView.prototype.translate = null; /** * Variable: states * * <mxDictionary> that maps from cell IDs to <mxCellStates>. */ mxGraphView.prototype.states = null; /** * Variable: updateStyle * * Specifies if the style should be updated in each validation step. If this * is false then the style is only updated if the state is created or if the * style of the cell was changed. Default is false. */ mxGraphView.prototype.updateStyle = false; /** * Variable: lastNode * * During validation, this contains the last DOM node that was processed. */ mxGraphView.prototype.lastNode = null; /** * Variable: lastHtmlNode * * During validation, this contains the last HTML DOM node that was processed. */ mxGraphView.prototype.lastHtmlNode = null; /** * Variable: lastForegroundNode * * During validation, this contains the last edge's DOM node that was processed. */ mxGraphView.prototype.lastForegroundNode = null; /** * Variable: lastForegroundHtmlNode * * During validation, this contains the last edge HTML DOM node that was processed. */ mxGraphView.prototype.lastForegroundHtmlNode = null; /** * Function: getGraphBounds * * Returns <graphBounds>. */ mxGraphView.prototype.getGraphBounds = function() { return this.graphBounds; }; /** * Function: setGraphBounds * * Sets <graphBounds>. */ mxGraphView.prototype.setGraphBounds = function(value) { this.graphBounds = value; }; /** * Function: getBounds * * Returns the union of all <mxCellStates> for the given array of <mxCells>. * * Parameters: * * cells - Array of <mxCells> whose bounds should be returned. */ mxGraphView.prototype.getBounds = function(cells) { var result = null; if (cells != null && cells.length > 0) { var model = this.graph.getModel(); for (var i = 0; i < cells.length; i++) { if (model.isVertex(cells[i]) || model.isEdge(cells[i])) { var state = this.getState(cells[i]); if (state != null) { if (result == null) { result = mxRectangle.fromRectangle(state); } else { result.add(state); } } } } } return result; }; /** * Function: setCurrentRoot * * Sets and returns the current root and fires an <undo> event before * calling <mxGraph.sizeDidChange>. * * Parameters: * * root - <mxCell> that specifies the root of the displayed cell hierarchy. */ mxGraphView.prototype.setCurrentRoot = function(root) { if (this.currentRoot != root) { var change = new mxCurrentRootChange(this, root); change.execute(); var edit = new mxUndoableEdit(this, true); edit.add(change); this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); this.graph.sizeDidChange(); } return root; }; /** * Function: scaleAndTranslate * * Sets the scale and translation and fires a <scale> and <translate> event * before calling <revalidate> followed by <mxGraph.sizeDidChange>. * * Parameters: * * scale - Decimal value that specifies the new scale (1 is 100%). * dx - X-coordinate of the translation. * dy - Y-coordinate of the translation. */ mxGraphView.prototype.scaleAndTranslate = function(scale, dx, dy) { var previousScale = this.scale; var previousTranslate = new mxPoint(this.translate.x, this.translate.y); if (this.scale != scale || this.translate.x != dx || this.translate.y != dy) { this.scale = scale; this.translate.x = dx; this.translate.y = dy; if (this.isEventsEnabled()) { this.viewStateChanged(); } } this.fireEvent(new mxEventObject(mxEvent.SCALE_AND_TRANSLATE, 'scale', scale, 'previousScale', previousScale, 'translate', this.translate, 'previousTranslate', previousTranslate)); }; /** * Function: getScale * * Returns the <scale>. */ mxGraphView.prototype.getScale = function() { return this.scale; }; /** * Function: setScale * * Sets the scale and fires a <scale> event before calling <revalidate> followed * by <mxGraph.sizeDidChange>. * * Parameters: * * value - Decimal value that specifies the new scale (1 is 100%). */ mxGraphView.prototype.setScale = function(value) { var previousScale = this.scale; if (this.scale != value) { this.scale = value; if (this.isEventsEnabled()) { this.viewStateChanged(); } } this.fireEvent(new mxEventObject(mxEvent.SCALE, 'scale', value, 'previousScale', previousScale)); }; /** * Function: getTranslate * * Returns the <translate>. */ mxGraphView.prototype.getTranslate = function() { return this.translate; }; /** * Function: setTranslate * * Sets the translation and fires a <translate> event before calling * <revalidate> followed by <mxGraph.sizeDidChange>. The translation is the * negative of the origin. * * Parameters: * * dx - X-coordinate of the translation. * dy - Y-coordinate of the translation. */ mxGraphView.prototype.setTranslate = function(dx, dy) { var previousTranslate = new mxPoint(this.translate.x, this.translate.y); if (this.translate.x != dx || this.translate.y != dy) { this.translate.x = dx; this.translate.y = dy; if (this.isEventsEnabled()) { this.viewStateChanged(); } } this.fireEvent(new mxEventObject(mxEvent.TRANSLATE, 'translate', this.translate, 'previousTranslate', previousTranslate)); }; /** * Function: viewStateChanged * * Invoked after <scale> and/or <translate> has changed. */ mxGraphView.prototype.viewStateChanged = function() { this.revalidate(); this.graph.sizeDidChange(); }; /** * Function: refresh * * Clears the view if <currentRoot> is not null and revalidates. */ mxGraphView.prototype.refresh = function() { if (this.currentRoot != null) { this.clear(); } this.revalidate(); }; /** * Function: revalidate * * Revalidates the complete view with all cell states. */ mxGraphView.prototype.revalidate = function() { this.invalidate(); this.validate(); }; /** * Function: clear * * Removes the state of the given cell and all descendants if the given * cell is not the current root. * * Parameters: * * cell - Optional <mxCell> for which the state should be removed. Default * is the root of the model. * force - Boolean indicating if the current root should be ignored for * recursion. */ mxGraphView.prototype.clear = function(cell, force, recurse) { var model = this.graph.getModel(); cell = cell || model.getRoot(); force = (force != null) ? force : false; recurse = (recurse != null) ? recurse : true; this.removeState(cell); if (recurse && (force || cell != this.currentRoot)) { var childCount = model.getChildCount(cell); for (var i = 0; i < childCount; i++) { this.clear(model.getChildAt(cell, i), force); } } else { this.invalidate(cell); } }; /** * Function: invalidate * * Invalidates the state of the given cell, all its descendants and * connected edges. * * Parameters: * * cell - Optional <mxCell> to be invalidated. Default is the root of the * model. */ mxGraphView.prototype.invalidate = function(cell, recurse, includeEdges) { var model = this.graph.getModel(); cell = cell || model.getRoot(); recurse = (recurse != null) ? recurse : true; includeEdges = (includeEdges != null) ? includeEdges : true; var state = this.getState(cell); if (state != null) { state.invalid = true; } // Avoids infinite loops for invalid graphs if (!cell.invalidating) { cell.invalidating = true; // Recursively invalidates all descendants if (recurse) { var childCount = model.getChildCount(cell); for (var i = 0; i < childCount; i++) { var child = model.getChildAt(cell, i); this.invalidate(child, recurse, includeEdges); } } // Propagates invalidation to all connected edges if (includeEdges) { var edgeCount = model.getEdgeCount(cell); for (var i = 0; i < edgeCount; i++) { this.invalidate(model.getEdgeAt(cell, i), recurse, includeEdges); } } delete cell.invalidating; } }; /** * Function: validate * * Calls <validateCell> and <validateCellState> and updates the <graphBounds> * using <getBoundingBox>. Finally the background is validated using * <validateBackground>. * * Parameters: * * cell - Optional <mxCell> to be used as the root of the validation. * Default is <currentRoot> or the root of the model. */ mxGraphView.prototype.validate = function(cell) { var t0 = mxLog.enter('mxGraphView.validate'); window.status = mxResources.get(this.updatingDocumentResource) || this.updatingDocumentResource; this.resetValidationState(); // Improves IE rendering speed by minimizing reflows var prevDisplay = null; if (this.optimizeVmlReflows && this.canvas != null && this.textDiv == null && ((document.documentMode == 8 && !mxClient.IS_EM) || mxClient.IS_QUIRKS)) { // Placeholder keeps scrollbar positions when canvas is hidden this.placeholder = document.createElement('div'); this.placeholder.style.position = 'absolute'; this.placeholder.style.width = this.canvas.clientWidth + 'px'; this.placeholder.style.height = this.canvas.clientHeight + 'px'; this.canvas.parentNode.appendChild(this.placeholder); prevDisplay = this.drawPane.style.display; this.canvas.style.display = 'none'; // Creates temporary DIV used for text measuring in mxText.updateBoundingBox this.textDiv = document.createElement('div'); this.textDiv.style.position = 'absolute'; this.textDiv.style.whiteSpace = 'nowrap'; this.textDiv.style.visibility = 'hidden'; this.textDiv.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; this.textDiv.style.zoom = '1'; document.body.appendChild(this.textDiv); } var graphBounds = this.getBoundingBox(this.validateCellState( this.validateCell(cell || ((this.currentRoot != null) ? this.currentRoot : this.graph.getModel().getRoot())))); this.setGraphBounds((graphBounds != null) ? graphBounds : this.getEmptyBounds()); this.validateBackground(); if (prevDisplay != null) { this.canvas.style.display = prevDisplay; this.textDiv.parentNode.removeChild(this.textDiv); if (this.placeholder != null) { this.placeholder.parentNode.removeChild(this.placeholder); } // Textdiv cannot be reused this.textDiv = null; } this.resetValidationState(); window.status = mxResources.get(this.doneResource) || this.doneResource; mxLog.leave('mxGraphView.validate', t0); }; /** * Function: getEmptyBounds * * Returns the bounds for an empty graph. This returns a rectangle at * <translate> with the size of 0 x 0. */ mxGraphView.prototype.getEmptyBounds = function() { return new mxRectangle(this.translate.x * this.scale, this.translate.y * this.scale); }; /** * Function: getBoundingBox * * Returns the bounding box of the shape and the label for the given * <mxCellState> and its children if recurse is true. * * Parameters: * * state - <mxCellState> whose bounding box should be returned. * recurse - Optional boolean indicating if the children should be included. * Default is true. */ mxGraphView.prototype.getBoundingBox = function(state, recurse) { recurse = (recurse != null) ? recurse : true; var bbox = null; if (state != null) { if (state.shape != null && state.shape.boundingBox != null) { bbox = state.shape.boundingBox.clone(); } // Adds label bounding box to graph bounds if (state.text != null && state.text.boundingBox != null) { if (bbox != null) { bbox.add(state.text.boundingBox); } else { bbox = state.text.boundingBox.clone(); } } if (recurse) { var model = this.graph.getModel(); var childCount = model.getChildCount(state.cell); for (var i = 0; i < childCount; i++) { var bounds = this.getBoundingBox(this.getState(model.getChildAt(state.cell, i))); if (bounds != null) { if (bbox == null) { bbox = bounds; } else { bbox.add(bounds); } } } } } return bbox; }; /** * Function: createBackgroundPageShape * * Creates and returns the shape used as the background page. * * Parameters: * * bounds - <mxRectangle> that represents the bounds of the shape. */ mxGraphView.prototype.createBackgroundPageShape = function(bounds) { return new mxRectangleShape(bounds, 'white', 'black'); }; /** * Function: validateBackground * * Calls <validateBackgroundImage> and <validateBackgroundPage>. */ mxGraphView.prototype.validateBackground = function() { this.validateBackgroundImage(); this.validateBackgroundPage(); }; /** * Function: validateBackgroundImage * * Validates the background image. */ mxGraphView.prototype.validateBackgroundImage = function() { var bg = this.graph.getBackgroundImage(); if (bg != null) { if (this.backgroundImage == null || this.backgroundImage.image != bg.src) { if (this.backgroundImage != null) { this.backgroundImage.destroy(); } var bounds = new mxRectangle(0, 0, 1, 1); this.backgroundImage = new mxImageShape(bounds, bg.src); this.backgroundImage.dialect = this.graph.dialect; this.backgroundImage.init(this.backgroundPane); this.backgroundImage.redraw(); // Workaround for ignored event on background in IE8 standards mode if (document.documentMode == 8 && !mxClient.IS_EM) { mxEvent.addGestureListeners(this.backgroundImage.node, mxUtils.bind(this, function(evt) { this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); }), mxUtils.bind(this, function(evt) { this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt)); }), mxUtils.bind(this, function(evt) { this.graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt)); }) ); } } this.redrawBackgroundImage(this.backgroundImage, bg); } else if (this.backgroundImage != null) { this.backgroundImage.destroy(); this.backgroundImage = null; } }; /** * Function: validateBackgroundPage * * Validates the background page. */ mxGraphView.prototype.validateBackgroundPage = function() { if (this.graph.pageVisible) { var bounds = this.getBackgroundPageBounds(); if (this.backgroundPageShape == null) { this.backgroundPageShape = this.createBackgroundPageShape(bounds); this.backgroundPageShape.scale = this.scale; this.backgroundPageShape.isShadow = true; this.backgroundPageShape.dialect = this.graph.dialect; this.backgroundPageShape.init(this.backgroundPane); this.backgroundPageShape.redraw(); // Adds listener for double click handling on background if (this.graph.nativeDblClickEnabled) { mxEvent.addListener(this.backgroundPageShape.node, 'dblclick', mxUtils.bind(this, function(evt) { this.graph.dblClick(evt); })); } // Adds basic listeners for graph event dispatching outside of the // container and finishing the handling of a single gesture mxEvent.addGestureListeners(this.backgroundPageShape.node, mxUtils.bind(this, function(evt) { this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); }), mxUtils.bind(this, function(evt) { // Hides the tooltip if mouse is outside container if (this.graph.tooltipHandler != null && this.graph.tooltipHandler.isHideOnHover()) { this.graph.tooltipHandler.hide(); } if (this.graph.isMouseDown && !mxEvent.isConsumed(evt)) { this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt)); } }), mxUtils.bind(this, function(evt) { this.graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt)); }) ); } else { this.backgroundPageShape.scale = this.scale; this.backgroundPageShape.bounds = bounds; this.backgroundPageShape.redraw(); } } else if (this.backgroundPageShape != null) { this.backgroundPageShape.destroy(); this.backgroundPageShape = null; } }; /** * Function: getBackgroundPageBounds * * Returns the bounds for the background page. */ mxGraphView.prototype.getBackgroundPageBounds = function() { var fmt = this.graph.pageFormat; var ps = this.scale * this.graph.pageScale; var bounds = new mxRectangle(this.scale * this.translate.x, this.scale * this.translate.y, fmt.width * ps, fmt.height * ps); return bounds; }; /** * Function: redrawBackgroundImage * * Updates the bounds and redraws the background image. * * Example: * * If the background image should not be scaled, this can be replaced with * the following. * * (code) * mxGraphView.prototype.redrawBackground = function(backgroundImage, bg) * { * backgroundImage.bounds.x = this.translate.x; * backgroundImage.bounds.y = this.translate.y; * backgroundImage.bounds.width = bg.width; * backgroundImage.bounds.height = bg.height; * * backgroundImage.redraw(); * }; * (end) * * Parameters: * * backgroundImage - <mxImageShape> that represents the background image. * bg - <mxImage> that specifies the image and its dimensions. */ mxGraphView.prototype.redrawBackgroundImage = function(backgroundImage, bg) { backgroundImage.scale = this.scale; backgroundImage.bounds.x = this.scale * this.translate.x; backgroundImage.bounds.y = this.scale * this.translate.y; backgroundImage.bounds.width = this.scale * bg.width; backgroundImage.bounds.height = this.scale * bg.height; backgroundImage.redraw(); }; /** * Function: validateCell * * 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>. * * Parameters: * * cell - <mxCell> whose <mxCellState> should be created. * visible - Optional boolean indicating if the cell should be visible. Default * is true. */ mxGraphView.prototype.validateCell = function(cell, visible) { visible = (visible != null) ? visible : true; if (cell != null) { visible = visible && this.graph.isCellVisible(cell); var state = this.getState(cell, visible); if (state != null && !visible) { this.removeState(cell); } else { var model = this.graph.getModel(); var childCount = model.getChildCount(cell); for (var i = 0; i < childCount; i++) { this.validateCell(model.getChildAt(cell, i), visible && (!this.isCellCollapsed(cell) || cell == this.currentRoot)); } } } return cell; }; /** * Function: validateCellState * * Validates and repaints the <mxCellState> for the given <mxCell>. * * Parameters: * * cell - <mxCell> whose <mxCellState> should be validated. * recurse - Optional boolean indicating if the children of the cell should be * validated. Default is true. */ mxGraphView.prototype.validateCellState = function(cell, recurse) { recurse = (recurse != null) ? recurse : true; var state = null; if (cell != null) { state = this.getState(cell); if (state != null) { var model = this.graph.getModel(); if (state.invalid) { state.invalid = false; if (state.style == null || state.invalidStyle) { state.style = this.graph.getCellStyle(state.cell); state.invalidStyle = false; } if (cell != this.currentRoot) { this.validateCellState(model.getParent(cell), false); } state.setVisibleTerminalState(this.validateCellState(this.getVisibleTerminal(cell, true), false), true); state.setVisibleTerminalState(this.validateCellState(this.getVisibleTerminal(cell, false), false), false); this.updateCellState(state); // Repaint happens immediately after the cell is validated if (cell != this.currentRoot && !state.invalid) { this.graph.cellRenderer.redraw(state, false, this.isRendering()); // Handles changes to invertex paintbounds after update of rendering shape state.updateCachedBounds(); } } if (recurse && !state.invalid) { // Updates order in DOM if recursively traversing if (state.shape != null) { this.stateValidated(state); } var childCount = model.getChildCount(cell); for (var i = 0; i < childCount; i++) { this.validateCellState(model.getChildAt(cell, i)); } } } } return state; }; /** * Function: updateCellState * * Updates the given <mxCellState>. * * Parameters: * * state - <mxCellState> to be updated. */ mxGraphView.prototype.updateCellState = function(state) { state.absoluteOffset.x = 0; state.absoluteOffset.y = 0; state.origin.x = 0; state.origin.y = 0; state.length = 0; if (state.cell != this.currentRoot) { var model = this.graph.getModel(); var pState = this.getState(model.getParent(state.cell)); if (pState != null && pState.cell != this.currentRoot) { state.origin.x += pState.origin.x; state.origin.y += pState.origin.y; } var offset = this.graph.getChildOffsetForCell(state.cell); if (offset != null) { state.origin.x += offset.x; state.origin.y += offset.y; } var geo = this.graph.getCellGeometry(state.cell); if (geo != null) { if (!model.isEdge(state.cell)) { offset = (geo.offset != null) ? geo.offset : this.EMPTY_POINT; if (geo.relative && pState != null) { if (model.isEdge(pState.cell)) { var origin = this.getPoint(pState, geo); if (origin != null) { state.origin.x += (origin.x / this.scale) - pState.origin.x - this.translate.x; state.origin.y += (origin.y / this.scale) - pState.origin.y - this.translate.y; } } else { state.origin.x += geo.x * pState.unscaledWidth + offset.x; state.origin.y += geo.y * pState.unscaledHeight + offset.y; } } else { state.absoluteOffset.x = this.scale * offset.x; state.absoluteOffset.y = this.scale * offset.y; state.origin.x += geo.x; state.origin.y += geo.y; } } state.x = this.scale * (this.translate.x + state.origin.x); state.y = this.scale * (this.translate.y + state.origin.y); state.width = this.scale * geo.width; state.unscaledWidth = geo.width; state.height = this.scale * geo.height; state.unscaledHeight = geo.height; if (model.isVertex(state.cell)) { this.updateVertexState(state, geo); } if (model.isEdge(state.cell)) { this.updateEdgeState(state, geo); } } } state.updateCachedBounds(); }; /** * Function: isCellCollapsed * * Returns true if the children of the given cell should not be visible in the * view. This implementation uses <mxGraph.isCellVisible> but it can be * overidden to use a separate condition. */ mxGraphView.prototype.isCellCollapsed = function(cell) { return this.graph.isCellCollapsed(cell); }; /** * Function: updateVertexState * * Validates the given cell state. */ mxGraphView.prototype.updateVertexState = function(state, geo) { var model = this.graph.getModel(); var pState = this.getState(model.getParent(state.cell)); if (geo.relative && pState != null && !model.isEdge(pState.cell)) { var alpha = mxUtils.toRadians(pState.style[mxConstants.STYLE_ROTATION] || '0'); if (alpha != 0) { var cos = Math.cos(alpha); var sin = Math.sin(alpha); var ct = new mxPoint(state.getCenterX(), state.getCenterY()); var cx = new mxPoint(pState.getCenterX(), pState.getCenterY()); var pt = mxUtils.getRotatedPoint(ct, cos, sin, cx); state.x = pt.x - state.width / 2; state.y = pt.y - state.height / 2; } } this.updateVertexLabelOffset(state); }; /** * Function: updateEdgeState * * Validates the given cell state. */ mxGraphView.prototype.updateEdgeState = function(state, geo) { var source = state.getVisibleTerminalState(true); var 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 ((this.graph.model.getTerminal(state.cell, true) != null && source == null) || (source == null && geo.getTerminalPoint(true) == null) || (this.graph.model.getTerminal(state.cell, false) != null && target == null) || (target == null && geo.getTerminalPoint(false) == null)) { this.clear(state.cell, true); } else { this.updateFixedTerminalPoints(state, source, target); this.updatePoints(state, geo.points, source, target); this.updateFloatingTerminalPoints(state, source, target); var pts = state.absolutePoints; if (state.cell != this.currentRoot && (pts == null || pts.length < 2 || pts[0] == null || pts[pts.length - 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. this.clear(state.cell, true); } else { this.updateEdgeBounds(state); this.updateEdgeLabelOffset(state); } } }; /** * Function: updateVertexLabelOffset * * Updates the absoluteOffset of the given vertex cell state. This takes * into account the label position styles. * * Parameters: * * state - <mxCellState> whose absolute offset should be updated. */ mxGraphView.prototype.updateVertexLabelOffset = function(state) { var h = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER); if (h == mxConstants.ALIGN_LEFT) { var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null); if (lw != null) { lw *= this.scale; } else { lw = state.width; } state.absoluteOffset.x -= lw; } else if (h == mxConstants.ALIGN_RIGHT) { state.absoluteOffset.x += state.width; } else if (h == mxConstants.ALIGN_CENTER) { var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null); if (lw != null) { // Aligns text block with given width inside the vertex width var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER); var dx = 0; if (align == mxConstants.ALIGN_CENTER) { dx = 0.5; } else if (align == mxConstants.ALIGN_RIGHT) { dx = 1; } if (dx != 0) { state.absoluteOffset.x -= (lw * this.scale - state.width) * dx; } } } var v = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE); if (v == mxConstants.ALIGN_TOP) { state.absoluteOffset.y -= state.height; } else if (v == mxConstants.ALIGN_BOTTOM) { state.absoluteOffset.y += state.height; } }; /** * Function: resetValidationState * * Resets the current validation state. */ mxGraphView.prototype.resetValidationState = function() { this.lastNode = null; this.lastHtmlNode = null; this.lastForegroundNode = null; this.lastForegroundHtmlNode = null; }; /** * Function: stateValidated * * Invoked when a state has been processed in <validatePoints>. This is used * to update the order of the DOM nodes of the shape. * * Parameters: * * state - <mxCellState> that represents the cell state. */ mxGraphView.prototype.stateValidated = function(state) { var fg = (this.graph.getModel().isEdge(state.cell) && this.graph.keepEdgesInForeground) || (this.graph.getModel().isVertex(state.cell) && this.graph.keepEdgesInBackground); var htmlNode = (fg) ? this.lastForegroundHtmlNode || this.lastHtmlNode : this.lastHtmlNode; var node = (fg) ? this.lastForegroundNode || this.lastNode : this.lastNode; var result = this.graph.cellRenderer.insertStateAfter(state, node, htmlNode); if (fg) { this.lastForegroundHtmlNode = result[1]; this.lastForegroundNode = result[0]; } else { this.lastHtmlNode = result[1]; this.lastNode = result[0]; } }; /** * Function: updateFixedTerminalPoints * * Sets the initial absolute terminal points in the given state before the edge * style is computed. * * Parameters: * * edge - <mxCellState> whose initial terminal points should be updated. * source - <mxCellState> which represents the source terminal. * target - <mxCellState> which represents the target terminal. */ mxGraphView.prototype.updateFixedTerminalPoints = function(edge, source, target) { this.updateFixedTerminalPoint(edge, source, true, this.graph.getConnectionConstraint(edge, source, true)); this.updateFixedTerminalPoint(edge, target, false, this.graph.getConnectionConstraint(edge, target, false)); }; /** * Function: updateFixedTerminalPoint * * Sets the fixed source or target terminal point on the given edge. * * Parameters: * * edge - <mxCellState> whose terminal point should be updated. * terminal - <mxCellState> which represents the actual terminal. * source - Boolean that specifies if the terminal is the source. * constraint - <mxConnectionConstraint> that specifies the connection. */ mxGraphView.prototype.updateFixedTerminalPoint = function(edge, terminal, source, constraint) { edge.setAbsoluteTerminalPoint(this.getFixedTerminalPoint(edge, terminal, source, constraint), source); }; /** * Function: getFixedTerminalPoint * * Returns the fixed source or target terminal point for the given edge. * * Parameters: * * edge - <mxCellState> whose terminal point should be returned. * terminal - <mxCellState> which represents the actual terminal. * source - Boolean that specifies if the terminal is the source. * constraint - <mxConnectionConstraint> that specifies the connection. */ mxGraphView.prototype.getFixedTerminalPoint = function(edge, terminal, source, constraint) { var pt = null; if (constraint != null) { pt = this.graph.getConnectionPoint(terminal, constraint, false); // FIXME Rounding introduced bugs when calculating label positions -> , this.graph.isOrthogonal(edge)); } if (pt == null && terminal == null) { var s = this.scale; var tr = this.translate; var orig = edge.origin; var geo = this.graph.getCellGeometry(edge.cell); pt = geo.getTerminalPoint(source); if (pt != null) { pt = new mxPoint(s * (tr.x + pt.x + orig.x), s * (tr.y + pt.y + orig.y)); } } return pt; }; /** * Function: updateBoundsFromStencil * * Updates the bounds of the given cell state to reflect the bounds of the stencil * if it has a fixed aspect and returns the previous bounds as an <mxRectangle> if * the bounds have been modified or null otherwise. * * Parameters: * * edge - <mxCellState> whose bounds should be updated. */ mxGraphView.prototype.updateBoundsFromStencil = function(state) { var previous = null; if (state != null && state.shape != null && state.shape.stencil != null && state.shape.stencil.aspect == 'fixed') { previous = mxRectangle.fromRectangle(state); var asp = state.shape.stencil.computeAspect(state.style, state.x, state.y, state.width, state.height); state.setRect(asp.x, asp.y, state.shape.stencil.w0 * asp.width, state.shape.stencil.h0 * asp.height); } return previous; }; /** * Function: updatePoints * * Updates the absolute points in the given state using the specified array * of <mxPoints> as the relative points. * * Parameters: * * edge - <mxCellState> whose absolute points should be updated. * points - Array of <mxPoints> that constitute the relative points. * source - <mxCellState> that represents the source terminal. * target - <mxCellState> that represents the target terminal. */ mxGraphView.prototype.updatePoints = function(edge, points, source, target) { if (edge != null) { var pts = []; pts.push(edge.absolutePoints[0]); var edgeStyle = this.getEdgeStyle(edge, points, source, target); if (edgeStyle != null) { var src = this.getTerminalPort(edge, source, true); var trg = this.getTerminalPort(edge, target, false); // Uses the stencil bounds for routing and restores after routing var srcBounds = this.updateBoundsFromStencil(src); var trgBounds = this.updateBoundsFromStencil(trg); edgeStyle(edge, src, trg, points, pts); // Restores previous bounds if (srcBounds != null) { src.setRect(srcBounds.x, srcBounds.y, srcBounds.width, srcBounds.height); } if (trgBounds != null) { trg.setRect(trgBounds.x, trgBounds.y, trgBounds.width, trgBounds.height); } } else if (points != null) { for (var i = 0; i < points.length; i++) { if (points[i] != null) { var pt = mxUtils.clone(points[i]); pts.push(this.transformControlPoint(edge, pt)); } } } var tmp = edge.absolutePoints; pts.push(tmp[tmp.length-1]); edge.absolutePoints = pts; } }; /** * Function: transformControlPoint * * Transforms the given control point to an absolute point. */ mxGraphView.prototype.transformControlPoint = function(state, pt, ignoreScale) { if (state != null && pt != null) { var orig = state.origin; var scale = ignoreScale ? 1 : this.scale return new mxPoint(scale * (pt.x + this.translate.x + orig.x), scale * (pt.y + this.translate.y + orig.y)); } return null; }; /** * Function: isLoopStyleEnabled * * Returns true if the given edge should be routed with <mxGraph.defaultLoopStyle> * or the <mxConstants.STYLE_LOOP> defined for the given edge. This implementation * returns true if the given edge is a loop and does not have connections constraints * associated. */ mxGraphView.prototype.isLoopStyleEnabled = function(edge, points, source, target) { var sc = this.graph.getConnectionConstraint(edge, source, true); var tc = this.graph.getConnectionConstraint(edge, target, false); if ((points == null || points.length < 2) && (!mxUtils.getValue(edge.style, mxConstants.STYLE_ORTHOGONAL_LOOP, false) || ((sc == null || sc.point == null) && (tc == null || tc.point == null)))) { return source != null && source == target; } return false; }; /** * Function: getEdgeStyle * * Returns the edge style function to be used to render the given edge state. */ mxGraphView.prototype.getEdgeStyle = function(edge, points, source, target) { var edgeStyle = this.isLoopStyleEnabled(edge, points, source, target) ? mxUtils.getValue(edge.style, mxConstants.STYLE_LOOP, this.graph.defaultLoopStyle) : (!mxUtils.getValue(edge.style, mxConstants.STYLE_NOEDGESTYLE, false) ? edge.style[mxConstants.STYLE_EDGE] : null); // Converts string values to objects if (typeof(edgeStyle) == "string") { var tmp = mxStyleRegistry.getValue(edgeStyle); if (tmp == null && this.isAllowEval()) { tmp = mxUtils.eval(edgeStyle); } edgeStyle = tmp; } if (typeof(edgeStyle) == "function") { return edgeStyle; } return null; }; /** * Function: updateFloatingTerminalPoints * * Updates the terminal points in the given state after the edge style was * computed for the edge. * * Parameters: * * state - <mxCellState> whose terminal points should be updated. * source - <mxCellState> that represents the source terminal. * target - <mxCellState> that represents the target terminal. */ mxGraphView.prototype.updateFloatingTerminalPoints = function(state, source, target) { var pts = state.absolutePoints; var p0 = pts[0]; var pe = pts[pts.length - 1]; if (pe == null && target != null) { this.updateFloatingTerminalPoint(state, target, source, false); } if (p0 == null && source != null) { this.updateFloatingTerminalPoint(state, source, target, true); } }; /** * Function: updateFloatingTerminalPoint * * 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. * * Parameters: * * edge - <mxCellState> whose terminal point should be updated. * start - <mxCellState> for the terminal on "this" side of the edge. * end - <mxCellState> for the terminal on the other side of the edge. * source - Boolean indicating if start is the source terminal state. */ mxGraphView.prototype.updateFloatingTerminalPoint = function(edge, start, end, source) { edge.setAbsoluteTerminalPoint(this.getFloatingTerminalPoint(edge, start, end, source), source); }; /** * Function: getFloatingTerminalPoint * * Returns the floating terminal point for the given edge, start and end * state, where start is the source if source is true. * * Parameters: * * edge - <mxCellState> whose terminal point should be returned. * start - <mxCellState> for the terminal on "this" side of the edge. * end - <mxCellState> for the terminal on the other side of the edge. * source - Boolean indicating if start is the source terminal state. */ mxGraphView.prototype.getFloatingTerminalPoint = function(edge, start, end, source) { start = this.getTerminalPort(edge, start, source); var next = this.getNextPoint(edge, end, source); var orth = this.graph.isOrthogonal(edge); var alpha = mxUtils.toRadians(Number(start.style[mxConstants.STYLE_ROTATION] || '0')); var center = new mxPoint(start.getCenterX(), start.getCenterY()); if (alpha != 0) { var cos = Math.cos(-alpha); var sin = Math.sin(-alpha); next = mxUtils.getRotatedPoint(next, cos, sin, center); } var border = parseFloat(edge.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); border += parseFloat(edge.style[(source) ? mxConstants.STYLE_SOURCE_PERIMETER_SPACING : mxConstants.STYLE_TARGET_PERIMETER_SPACING] || 0); var pt = this.getPerimeterPoint(start, next, alpha == 0 && orth, border); if (alpha != 0) { var cos = Math.cos(alpha); var sin = Math.sin(alpha); pt = mxUtils.getRotatedPoint(pt, cos, sin, center); } return pt; }; /** * Function: getTerminalPort * * Returns an <mxCellState> that represents the source or target terminal or * port for the given edge. * * Parameters: * * state - <mxCellState> that represents the state of the edge. * terminal - <mxCellState> that represents the terminal. * source - Boolean indicating if the given terminal is the source terminal. */ mxGraphView.prototype.getTerminalPort = function(state, terminal, source) { var key = (source) ? mxConstants.STYLE_SOURCE_PORT : mxConstants.STYLE_TARGET_PORT; var id = mxUtils.getValue(state.style, key); if (id != null) { var tmp = this.getState(this.graph.getModel().getCell(id)); // Only uses ports where a cell state exists if (tmp != null) { terminal = tmp; } } return terminal; }; /** * Function: getPerimeterPoint * * Returns an <mxPoint> that defines the location of the intersection point between * the perimeter and the line between the center of the shape and the given point. * * Parameters: * * terminal - <mxCellState> for the source or target terminal. * next - <mxPoint> that lies outside of the given terminal. * 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. * border - Optional border between the perimeter and the shape. */ mxGraphView.prototype.getPerimeterPoint = function(terminal, next, orthogonal, border) { var point = null; if (terminal != null) { var perimeter = this.getPerimeterFunction(terminal); if (perimeter != null && next != null) { var bounds = this.getPerimeterBounds(terminal, border); if (bounds.width > 0 || bounds.height > 0) { point = new mxPoint(next.x, next.y); var flipH = false; var flipV = false; if (this.graph.model.isVertex(terminal.cell)) { flipH = mxUtils.getValue(terminal.style, mxConstants.STYLE_FLIPH, 0) == 1; flipV = mxUtils.getValue(terminal.style, mxConstants.STYLE_FLIPV, 0) == 1; // Legacy support for stencilFlipH/V if (terminal.shape != null && terminal.shape.stencil != null) { flipH = (mxUtils.getValue(terminal.style, 'stencilFlipH', 0) == 1) || flipH; flipV = (mxUtils.getValue(terminal.style, 'stencilFlipV', 0) == 1) || flipV; } if (flipH) { point.x = 2 * bounds.getCenterX() - point.x; } if (flipV) { point.y = 2 * bounds.getCenterY() - point.y; } } point = perimeter(bounds, terminal, point, orthogonal); if (point != null) { if (flipH) { point.x = 2 * bounds.getCenterX() - point.x; } if (flipV) { point.y = 2 * bounds.getCenterY() - point.y; } } } } if (point == null) { point = this.getPoint(terminal); } } return point; }; /** * Function: getRoutingCenterX * * Returns the x-coordinate of the center point for automatic routing. */ mxGraphView.prototype.getRoutingCenterX = function (state) { var f = (state.style != null) ? parseFloat(state.style [mxConstants.STYLE_ROUTING_CENTER_X]) || 0 : 0; return state.getCenterX() + f * state.width; }; /** * Function: getRoutingCenterY * * Returns the y-coordinate of the center point for automatic routing. */ mxGraphView.prototype.getRoutingCenterY = function (state) { var f = (state.style != null) ? parseFloat(state.style [mxConstants.STYLE_ROUTING_CENTER_Y]) || 0 : 0; return state.getCenterY() + f * state.height; }; /** * Function: getPerimeterBounds * * Returns the perimeter bounds for the given terminal, edge pair as an * <mxRectangle>. * * If you have a model where each terminal has a relative child that should * act as the graphical endpoint for a connection from/to the terminal, then * this method can be replaced as follows: * * (code) * var oldGetPerimeterBounds = mxGraphView.prototype.getPerimeterBounds; * mxGraphView.prototype.getPerimeterBounds = function(terminal, edge, isSource) * { * var model = this.graph.getModel(); * var childCount = model.getChildCount(terminal.cell); * * if (childCount > 0) * { * var child = model.getChildAt(terminal.cell, 0); * var geo = model.getGeometry(child); * * if (geo != null && * geo.relative) * { * var state = this.getState(child); * * if (state != null) * { * terminal = state; * } * } * } * * return oldGetPerimeterBounds.apply(this, arguments); * }; * (end) * * Parameters: * * terminal - <mxCellState> that represents the terminal. * border - Number that adds a border between the shape and the perimeter. */ mxGraphView.prototype.getPerimeterBounds = function(terminal, border) { border = (border != null) ? border : 0; if (terminal != null) { border += parseFloat(terminal.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); } return terminal.getPerimeterBounds(border * this.scale); }; /** * Function: getPerimeterFunction * * Returns the perimeter function for the given state. */ mxGraphView.prototype.getPerimeterFunction = function(state) { var perimeter = state.style[mxConstants.STYLE_PERIMETER]; // Converts string values to objects if (typeof(perimeter) == "string") { var tmp = mxStyleRegistry.getValue(perimeter); if (tmp == null && this.isAllowEval()) { tmp = mxUtils.eval(perimeter); } perimeter = tmp; } if (typeof(perimeter) == "function") { return perimeter; } return null; }; /** * Function: getNextPoint * * Returns the nearest point in the list of absolute points or the center * of the opposite terminal. * * Parameters: * * edge - <mxCellState> that represents the edge. * opposite - <mxCellState> that represents the opposite terminal. * source - Boolean indicating if the next point for the source or target * should be returned. */ mxGraphView.prototype.getNextPoint = function(edge, opposite, source) { var pts = edge.absolutePoints; var point = null; if (pts != null && pts.length >= 2) { var count = pts.length; point = pts[(source) ? Math.min(1, count - 1) : Math.max(0, count - 2)]; } if (point == null && opposite != null) { point = new mxPoint(opposite.getCenterX(), opposite.getCenterY()); } return point; }; /** * Function: getVisibleTerminal * * Returns the nearest ancestor terminal that is visible. The edge appears * to be connected to this terminal on the display. The result of this method * is cached in <mxCellState.getVisibleTerminalState>. * * Parameters: * * edge - <mxCell> whose visible terminal should be returned. * source - Boolean that specifies if the source or target terminal * should be returned. */ mxGraphView.prototype.getVisibleTerminal = function(edge, source) { var model = this.graph.getModel(); var result = model.getTerminal(edge, source); var best = result; while (result != null && result != this.currentRoot) { if (!this.graph.isCellVisible(best) || this.isCellCollapsed(result)) { best = result; } result = model.getParent(result); } // Checks if the result is valid for the current view state if (best != null && (!model.contains(best) || model.getParent(best) == model.getRoot() || best == this.currentRoot)) { best = null; } return best; }; /** * Function: updateEdgeBounds * * Updates the given state using the bounding box of t * he absolute points. * Also updates <mxCellState.terminalDistance>, <mxCellState.length> and * <mxCellState.segments>. * * Parameters: * * state - <mxCellState> whose bounds should be updated. */ mxGraphView.prototype.updateEdgeBounds = function(state) { var points = state.absolutePoints; var p0 = points[0];