/** * Copyright (c) 2006-2015, JGraph Ltd * Copyright (c) 2006-2015, Gaudenz Alder */ /** * Class: mxGraphView * * Extends 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 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 . The edit * property contains the which contains the * . * * Event: mxEvent.SCALE_AND_TRANSLATE * * Fires after the scale and translate have been changed in . * The scale, previousScale, translate * and previousTranslate properties contain the new and previous * scale and translate, respectively. * * Event: mxEvent.SCALE * * Fires after the scale was changed in . The scale and * previousScale properties contain the new and previous scale. * * Event: mxEvent.TRANSLATE * * Fires after the translate was changed in . The * translate and previousTranslate 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 . * 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. * * Constructor: mxGraphView * * Constructs a new view for the given . * * Parameters: * * graph - Reference to the enclosing . */ 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 * . This will only be used if the string values can't be mapped * to objects using . 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 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 in . Default is true. */ mxGraphView.prototype.rendering = true; /** * Variable: graph * * Reference to the enclosing . */ mxGraphView.prototype.graph = null; /** * Variable: currentRoot * * that acts as the root of the displayed cell hierarchy. */ mxGraphView.prototype.currentRoot = null; /** * Variable: graphBounds * * 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 * * that specifies the current translation. Default is a new * empty . */ mxGraphView.prototype.translate = null; /** * Variable: states * * that maps from cell IDs to . */ 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 . */ mxGraphView.prototype.getGraphBounds = function() { return this.graphBounds; }; /** * Function: setGraphBounds * * Sets . */ mxGraphView.prototype.setGraphBounds = function(value) { this.graphBounds = value; }; /** * Function: getBounds * * Returns the union of all for the given array of . * * Parameters: * * cells - Array of 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 event before * calling . * * Parameters: * * root - 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 and event * before calling followed by . * * 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 . */ mxGraphView.prototype.getScale = function() { return this.scale; }; /** * Function: setScale * * Sets the scale and fires a event before calling followed * by . * * 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 . */ mxGraphView.prototype.getTranslate = function() { return this.translate; }; /** * Function: setTranslate * * Sets the translation and fires a event before calling * followed by . 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 and/or has changed. */ mxGraphView.prototype.viewStateChanged = function() { this.revalidate(); this.graph.sizeDidChange(); }; /** * Function: refresh * * Clears the view if 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 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 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 and and updates the * using . Finally the background is validated using * . * * Parameters: * * cell - Optional to be used as the root of the validation. * Default is 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 * 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 * and its children if recurse is true. * * Parameters: * * state - 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 - that represents the bounds of the shape. */ mxGraphView.prototype.createBackgroundPageShape = function(bounds) { return new mxRectangleShape(bounds, 'white', 'black'); }; /** * Function: validateBackground * * Calls and . */ 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 - that represents the background image. * bg - 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 . * * Parameters: * * cell - whose 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 for the given . * * Parameters: * * cell - whose 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 . * * Parameters: * * state - 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 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 - 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 . This is used * to update the order of the DOM nodes of the shape. * * Parameters: * * state - 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 - whose initial terminal points should be updated. * source - which represents the source terminal. * target - 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 - whose terminal point should be updated. * terminal - which represents the actual terminal. * source - Boolean that specifies if the terminal is the source. * constraint - 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 - whose terminal point should be returned. * terminal - which represents the actual terminal. * source - Boolean that specifies if the terminal is the source. * constraint - 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 if * the bounds have been modified or null otherwise. * * Parameters: * * edge - 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 as the relative points. * * Parameters: * * edge - whose absolute points should be updated. * points - Array of that constitute the relative points. * source - that represents the source terminal. * target - 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 * or the 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 - whose terminal points should be updated. * source - that represents the source terminal. * target - 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 - whose terminal point should be updated. * start - for the terminal on "this" side of the edge. * end - 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 - whose terminal point should be returned. * start - for the terminal on "this" side of the edge. * end - 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 that represents the source or target terminal or * port for the given edge. * * Parameters: * * state - that represents the state of the edge. * terminal - 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 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 - for the source or target terminal. * next - 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 * . * * 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 - 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 - that represents the edge. * opposite - 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 . * * Parameters: * * edge - 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 , and * . * * Parameters: * * state - whose bounds should be updated. */ mxGraphView.prototype.updateEdgeBounds = function(state) { var points = state.absolutePoints; var p0 = points[0]; var pe = points[points.length - 1]; if (p0.x != pe.x || p0.y != pe.y) { var dx = pe.x - p0.x; var dy = pe.y - p0.y; state.terminalDistance = Math.sqrt(dx * dx + dy * dy); } else { state.terminalDistance = 0; } var length = 0; var segments = []; var pt = p0; if (pt != null) { var minX = pt.x; var minY = pt.y; var maxX = minX; var maxY = minY; for (var i = 1; i < points.length; i++) { var tmp = points[i]; if (tmp != null) { var dx = pt.x - tmp.x; var dy = pt.y - tmp.y; var segment = Math.sqrt(dx * dx + dy * dy); segments.push(segment); length += segment; pt = tmp; minX = Math.min(pt.x, minX); minY = Math.min(pt.y, minY); maxX = Math.max(pt.x, maxX); maxY = Math.max(pt.y, maxY); } } state.length = length; state.segments = segments; var markerSize = 1; // TODO: include marker size state.x = minX; state.y = minY; state.width = Math.max(markerSize, maxX - minX); state.height = Math.max(markerSize, maxY - minY); } }; /** * Function: getPoint * * Returns the absolute point on the edge for the given relative * as an . The edge is represented by the given * . * * Parameters: * * state - that represents the state of the parent edge. * geometry - that represents the relative location. */ mxGraphView.prototype.getPoint = function(state, geometry) { var x = state.getCenterX(); var y = state.getCenterY(); if (state.segments != null && (geometry == null || geometry.relative)) { var gx = (geometry != null) ? geometry.x / 2 : 0; var pointCount = state.absolutePoints.length; var dist = Math.round((gx + 0.5) * state.length); var segment = state.segments[0]; var length = 0; var index = 1; while (dist >= Math.round(length + segment) && index < pointCount - 1) { length += segment; segment = state.segments[index++]; } var factor = (segment == 0) ? 0 : (dist - length) / segment; var p0 = state.absolutePoints[index-1]; var pe = state.absolutePoints[index]; if (p0 != null && pe != null) { var gy = 0; var offsetX = 0; var offsetY = 0; if (geometry != null) { gy = geometry.y; var offset = geometry.offset; if (offset != null) { offsetX = offset.x; offsetY = offset.y; } } var dx = pe.x - p0.x; var dy = pe.y - p0.y; var nx = (segment == 0) ? 0 : dy / segment; var ny = (segment == 0) ? 0 : dx / segment; x = p0.x + dx * factor + (nx * gy + offsetX) * this.scale; y = p0.y + dy * factor - (ny * gy - offsetY) * this.scale; } } else if (geometry != null) { var offset = geometry.offset; if (offset != null) { x += offset.x; y += offset.y; } } return new mxPoint(x, y); }; /** * Function: getRelativePoint * * Gets the relative point that describes the given, absolute label * position for the given edge state. * * Parameters: * * state - that represents the state of the parent edge. * x - Specifies the x-coordinate of the absolute label location. * y - Specifies the y-coordinate of the absolute label location. */ mxGraphView.prototype.getRelativePoint = function(edgeState, x, y) { var model = this.graph.getModel(); var geometry = model.getGeometry(edgeState.cell); if (geometry != null) { var pointCount = edgeState.absolutePoints.length; if (geometry.relative && pointCount > 1) { var totalLength = edgeState.length; var segments = edgeState.segments; // Works which line segment the point of the label is closest to var p0 = edgeState.absolutePoints[0]; var pe = edgeState.absolutePoints[1]; var minDist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y); var index = 0; var tmp = 0; var length = 0; for (var i = 2; i < pointCount; i++) { tmp += segments[i - 2]; pe = edgeState.absolutePoints[i]; var dist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y); if (dist <= minDist) { minDist = dist; index = i - 1; length = tmp; } p0 = pe; } var seg = segments[index]; p0 = edgeState.absolutePoints[index]; pe = edgeState.absolutePoints[index + 1]; var x2 = p0.x; var y2 = p0.y; var x1 = pe.x; var y1 = pe.y; var px = x; var py = y; var xSegment = x2 - x1; var ySegment = y2 - y1; px -= x1; py -= y1; var projlenSq = 0; px = xSegment - px; py = ySegment - py; var dotprod = px * xSegment + py * ySegment; if (dotprod <= 0.0) { projlenSq = 0; } else { projlenSq = dotprod * dotprod / (xSegment * xSegment + ySegment * ySegment); } var projlen = Math.sqrt(projlenSq); if (projlen > seg) { projlen = seg; } var yDistance = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, pe .x, pe.y, x, y)); var direction = mxUtils.relativeCcw(p0.x, p0.y, pe.x, pe.y, x, y); if (direction == -1) { yDistance = -yDistance; } // Constructs the relative point for the label return new mxPoint(((totalLength / 2 - length - projlen) / totalLength) * -2, yDistance / this.scale); } } return new mxPoint(); }; /** * Function: updateEdgeLabelOffset * * Updates for the given state. The absolute * offset is normally used for the position of the edge label. Is is * calculated from the geometry as an absolute offset from the center * between the two endpoints if the geometry is absolute, or as the * relative distance between the center along the line and the absolute * orthogonal distance if the geometry is relative. * * Parameters: * * state - whose absolute offset should be updated. */ mxGraphView.prototype.updateEdgeLabelOffset = function(state) { var points = state.absolutePoints; state.absoluteOffset.x = state.getCenterX(); state.absoluteOffset.y = state.getCenterY(); if (points != null && points.length > 0 && state.segments != null) { var geometry = this.graph.getCellGeometry(state.cell); if (geometry.relative) { var offset = this.getPoint(state, geometry); if (offset != null) { state.absoluteOffset = offset; } } else { var p0 = points[0]; var pe = points[points.length - 1]; if (p0 != null && pe != null) { var dx = pe.x - p0.x; var dy = pe.y - p0.y; var x0 = 0; var y0 = 0; var off = geometry.offset; if (off != null) { x0 = off.x; y0 = off.y; } var x = p0.x + dx / 2 + x0 * this.scale; var y = p0.y + dy / 2 + y0 * this.scale; state.absoluteOffset.x = x; state.absoluteOffset.y = y; } } } }; /** * Function: getState * * Returns the for the given cell. If create is true, then * the state is created if it does not yet exist. * * Parameters: * * cell - for which the should be returned. * create - Optional boolean indicating if a new state should be created * if it does not yet exist. Default is false. */ mxGraphView.prototype.getState = function(cell, create) { create = create || false; var state = null; if (cell != null) { state = this.states.get(cell); if (create && (state == null || this.updateStyle) && this.graph.isCellVisible(cell)) { if (state == null) { state = this.createState(cell); this.states.put(cell, state); } else { state.style = this.graph.getCellStyle(cell); } } } return state; }; /** * Function: isRendering * * Returns . */ mxGraphView.prototype.isRendering = function() { return this.rendering; }; /** * Function: setRendering * * Sets . */ mxGraphView.prototype.setRendering = function(value) { this.rendering = value; }; /** * Function: isAllowEval * * Returns . */ mxGraphView.prototype.isAllowEval = function() { return this.allowEval; }; /** * Function: setAllowEval * * Sets . */ mxGraphView.prototype.setAllowEval = function(value) { this.allowEval = value; }; /** * Function: getStates * * Returns . */ mxGraphView.prototype.getStates = function() { return this.states; }; /** * Function: setStates * * Sets . */ mxGraphView.prototype.setStates = function(value) { this.states = value; }; /** * Function: getCellStates * * Returns the for the given array of . The array * contains all states that are not null, that is, the returned array may * have less elements than the given array. If no argument is given, then * this returns . */ mxGraphView.prototype.getCellStates = function(cells) { if (cells == null) { return this.states; } else { var result = []; for (var i = 0; i < cells.length; i++) { var state = this.getState(cells[i]); if (state != null) { result.push(state); } } return result; } }; /** * Function: removeState * * Removes and returns the for the given cell. * * Parameters: * * cell - for which the should be removed. */ mxGraphView.prototype.removeState = function(cell) { var state = null; if (cell != null) { state = this.states.remove(cell); if (state != null) { this.graph.cellRenderer.destroy(state); state.invalid = true; state.destroy(); } } return state; }; /** * Function: createState * * Creates and returns an for the given cell and initializes * it using . * * Parameters: * * cell - for which a new should be created. */ mxGraphView.prototype.createState = function(cell) { return new mxCellState(this, cell, this.graph.getCellStyle(cell)); }; /** * Function: getCanvas * * Returns the DOM node that contains the background-, draw- and * overlay- and decoratorpanes. */ mxGraphView.prototype.getCanvas = function() { return this.canvas; }; /** * Function: getBackgroundPane * * Returns the DOM node that represents the background layer. */ mxGraphView.prototype.getBackgroundPane = function() { return this.backgroundPane; }; /** * Function: getDrawPane * * Returns the DOM node that represents the main drawing layer. */ mxGraphView.prototype.getDrawPane = function() { return this.drawPane; }; /** * Function: getOverlayPane * * Returns the DOM node that represents the layer above the drawing layer. */ mxGraphView.prototype.getOverlayPane = function() { return this.overlayPane; }; /** * Function: getDecoratorPane * * Returns the DOM node that represents the topmost drawing layer. */ mxGraphView.prototype.getDecoratorPane = function() { return this.decoratorPane; }; /** * Function: isContainerEvent * * Returns true if the event origin is one of the drawing panes or * containers of the view. */ mxGraphView.prototype.isContainerEvent = function(evt) { var source = mxEvent.getSource(evt); return (source == this.graph.container || source.parentNode == this.backgroundPane || (source.parentNode != null && source.parentNode.parentNode == this.backgroundPane) || source == this.canvas.parentNode || source == this.canvas || source == this.backgroundPane || source == this.drawPane || source == this.overlayPane || source == this.decoratorPane); }; /** * Function: isScrollEvent * * Returns true if the event origin is one of the scrollbars of the * container in IE. Such events are ignored. */ mxGraphView.prototype.isScrollEvent = function(evt) { var offset = mxUtils.getOffset(this.graph.container); var pt = new mxPoint(evt.clientX - offset.x, evt.clientY - offset.y); var outWidth = this.graph.container.offsetWidth; var inWidth = this.graph.container.clientWidth; if (outWidth > inWidth && pt.x > inWidth + 2 && pt.x <= outWidth) { return true; } var outHeight = this.graph.container.offsetHeight; var inHeight = this.graph.container.clientHeight; if (outHeight > inHeight && pt.y > inHeight + 2 && pt.y <= outHeight) { return true; } return false; }; /** * Function: init * * Initializes the graph event dispatch loop for the specified container * and invokes to create the required DOM nodes for the display. */ mxGraphView.prototype.init = function() { this.installListeners(); // Creates the DOM nodes for the respective display dialect var graph = this.graph; if (graph.dialect == mxConstants.DIALECT_SVG) { this.createSvg(); } else if (graph.dialect == mxConstants.DIALECT_VML) { this.createVml(); } else { this.createHtml(); } }; /** * Function: installListeners * * Installs the required listeners in the container. */ mxGraphView.prototype.installListeners = function() { var graph = this.graph; var container = graph.container; if (container != null) { // Support for touch device gestures (eg. pinch to zoom) // Double-tap handling is implemented in mxGraph.fireMouseEvent if (mxClient.IS_TOUCH) { mxEvent.addListener(container, 'gesturestart', mxUtils.bind(this, function(evt) { graph.fireGestureEvent(evt); mxEvent.consume(evt); })); mxEvent.addListener(container, 'gesturechange', mxUtils.bind(this, function(evt) { graph.fireGestureEvent(evt); mxEvent.consume(evt); })); mxEvent.addListener(container, 'gestureend', mxUtils.bind(this, function(evt) { graph.fireGestureEvent(evt); mxEvent.consume(evt); })); } // Fires event only for one pointer per gesture var pointerId = null; // Adds basic listeners for graph event dispatching mxEvent.addGestureListeners(container, mxUtils.bind(this, function(evt) { // Condition to avoid scrollbar events starting a rubberband selection if (this.isContainerEvent(evt) && ((!mxClient.IS_IE && !mxClient.IS_IE11 && !mxClient.IS_GC && !mxClient.IS_OP && !mxClient.IS_SF) || !this.isScrollEvent(evt))) { graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); pointerId = evt.pointerId; } }), mxUtils.bind(this, function(evt) { if (this.isContainerEvent(evt) && (pointerId == null || evt.pointerId == pointerId)) { graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt)); } }), mxUtils.bind(this, function(evt) { if (this.isContainerEvent(evt)) { graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt)); } pointerId = null; })); // Adds listener for double click handling on background, this does always // use native event handler, we assume that the DOM of the background // does not change during the double click mxEvent.addListener(container, 'dblclick', mxUtils.bind(this, function(evt) { if (this.isContainerEvent(evt)) { graph.dblClick(evt); } })); // Workaround for touch events which started on some DOM node // on top of the container, in which case the cells under the // mouse for the move and up events are not detected. var getState = function(evt) { var state = null; // Workaround for touch events which started on some DOM node // on top of the container, in which case the cells under the // mouse for the move and up events are not detected. if (mxClient.IS_TOUCH) { var x = mxEvent.getClientX(evt); var y = mxEvent.getClientY(evt); // Dispatches the drop event to the graph which // consumes and executes the source function var pt = mxUtils.convertPoint(container, x, y); state = graph.view.getState(graph.getCellAt(pt.x, pt.y)); } return state; }; // Adds basic listeners for graph event dispatching outside of the // container and finishing the handling of a single gesture // Implemented via graph event dispatch loop to avoid duplicate events // in Firefox and Chrome graph.addMouseListener( { mouseDown: function(sender, me) { graph.popupMenuHandler.hideMenu(); }, mouseMove: function() { }, mouseUp: function() { } }); this.moveHandler = mxUtils.bind(this, function(evt) { // Hides the tooltip if mouse is outside container if (graph.tooltipHandler != null && graph.tooltipHandler.isHideOnHover()) { graph.tooltipHandler.hide(); } if (this.captureDocumentGesture && graph.isMouseDown && graph.container != null && !this.isContainerEvent(evt) && graph.container.style.display != 'none' && graph.container.style.visibility != 'hidden' && !mxEvent.isConsumed(evt)) { graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt))); } }); this.endHandler = mxUtils.bind(this, function(evt) { if (this.captureDocumentGesture && graph.isMouseDown && graph.container != null && !this.isContainerEvent(evt) && graph.container.style.display != 'none' && graph.container.style.visibility != 'hidden') { graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt)); } }); mxEvent.addGestureListeners(document, null, this.moveHandler, this.endHandler); } }; /** * Function: createHtml * * Creates the DOM nodes for the HTML display. */ mxGraphView.prototype.createHtml = function() { var container = this.graph.container; if (container != null) { this.canvas = this.createHtmlPane('100%', '100%'); this.canvas.style.overflow = 'hidden'; // Uses minimal size for inner DIVs on Canvas. This is required // for correct event processing in IE. If we have an overlapping // DIV then the events on the cells are only fired for labels. this.backgroundPane = this.createHtmlPane('1px', '1px'); this.drawPane = this.createHtmlPane('1px', '1px'); this.overlayPane = this.createHtmlPane('1px', '1px'); this.decoratorPane = this.createHtmlPane('1px', '1px'); this.canvas.appendChild(this.backgroundPane); this.canvas.appendChild(this.drawPane); this.canvas.appendChild(this.overlayPane); this.canvas.appendChild(this.decoratorPane); container.appendChild(this.canvas); this.updateContainerStyle(container); // Implements minWidth/minHeight in quirks mode if (mxClient.IS_QUIRKS) { var onResize = mxUtils.bind(this, function(evt) { var bounds = this.getGraphBounds(); var width = bounds.x + bounds.width + this.graph.border; var height = bounds.y + bounds.height + this.graph.border; this.updateHtmlCanvasSize(width, height); }); mxEvent.addListener(window, 'resize', onResize); } } }; /** * Function: updateHtmlCanvasSize * * Updates the size of the HTML canvas. */ mxGraphView.prototype.updateHtmlCanvasSize = function(width, height) { if (this.graph.container != null) { var ow = this.graph.container.offsetWidth; var oh = this.graph.container.offsetHeight; if (ow < width) { this.canvas.style.width = width + 'px'; } else { this.canvas.style.width = '100%'; } if (oh < height) { this.canvas.style.height = height + 'px'; } else { this.canvas.style.height = '100%'; } } }; /** * Function: createHtmlPane * * Creates and returns a drawing pane in HTML (DIV). */ mxGraphView.prototype.createHtmlPane = function(width, height) { var pane = document.createElement('DIV'); if (width != null && height != null) { pane.style.position = 'absolute'; pane.style.left = '0px'; pane.style.top = '0px'; pane.style.width = width; pane.style.height = height; } else { pane.style.position = 'relative'; } return pane; }; /** * Function: createVml * * Creates the DOM nodes for the VML display. */ mxGraphView.prototype.createVml = function() { var container = this.graph.container; if (container != null) { var width = container.offsetWidth; var height = container.offsetHeight; this.canvas = this.createVmlPane(width, height); this.canvas.style.overflow = 'hidden'; this.backgroundPane = this.createVmlPane(width, height); this.drawPane = this.createVmlPane(width, height); this.overlayPane = this.createVmlPane(width, height); this.decoratorPane = this.createVmlPane(width, height); this.canvas.appendChild(this.backgroundPane); this.canvas.appendChild(this.drawPane); this.canvas.appendChild(this.overlayPane); this.canvas.appendChild(this.decoratorPane); container.appendChild(this.canvas); } }; /** * Function: createVmlPane * * Creates a drawing pane in VML (group). */ mxGraphView.prototype.createVmlPane = function(width, height) { var pane = document.createElement(mxClient.VML_PREFIX + ':group'); // At this point the width and height are potentially // uninitialized. That's OK. pane.style.position = 'absolute'; pane.style.left = '0px'; pane.style.top = '0px'; pane.style.width = width + 'px'; pane.style.height = height + 'px'; pane.setAttribute('coordsize', width + ',' + height); pane.setAttribute('coordorigin', '0,0'); return pane; }; /** * Function: createSvg * * Creates and returns the DOM nodes for the SVG display. */ mxGraphView.prototype.createSvg = function() { var container = this.graph.container; this.canvas = document.createElementNS(mxConstants.NS_SVG, 'g'); // For background image this.backgroundPane = document.createElementNS(mxConstants.NS_SVG, 'g'); this.canvas.appendChild(this.backgroundPane); // Adds two layers (background is early feature) this.drawPane = document.createElementNS(mxConstants.NS_SVG, 'g'); this.canvas.appendChild(this.drawPane); this.overlayPane = document.createElementNS(mxConstants.NS_SVG, 'g'); this.canvas.appendChild(this.overlayPane); this.decoratorPane = document.createElementNS(mxConstants.NS_SVG, 'g'); this.canvas.appendChild(this.decoratorPane); var root = document.createElementNS(mxConstants.NS_SVG, 'svg'); root.style.left = '0px'; root.style.top = '0px'; root.style.width = '100%'; root.style.height = '100%'; // NOTE: In standards mode, the SVG must have block layout // in order for the container DIV to not show scrollbars. root.style.display = 'block'; root.appendChild(this.canvas); // Workaround for scrollbars in IE11 and below if (mxClient.IS_IE || mxClient.IS_IE11) { root.style.overflow = 'hidden'; } if (container != null) { container.appendChild(root); this.updateContainerStyle(container); } }; /** * Function: updateContainerStyle * * Updates the style of the container after installing the SVG DOM elements. */ mxGraphView.prototype.updateContainerStyle = function(container) { // Workaround for offset of container var style = mxUtils.getCurrentStyle(container); if (style != null && style.position == 'static') { container.style.position = 'relative'; } // Disables built-in pan and zoom in IE10 and later if (mxClient.IS_POINTER) { container.style.touchAction = 'none'; } }; /** * Function: destroy * * Destroys the view and all its resources. */ mxGraphView.prototype.destroy = function() { var root = (this.canvas != null) ? this.canvas.ownerSVGElement : null; if (root == null) { root = this.canvas; } if (root != null && root.parentNode != null) { this.clear(this.currentRoot, true); mxEvent.removeGestureListeners(document, null, this.moveHandler, this.endHandler); mxEvent.release(this.graph.container); root.parentNode.removeChild(root); this.moveHandler = null; this.endHandler = null; this.canvas = null; this.backgroundPane = null; this.drawPane = null; this.overlayPane = null; this.decoratorPane = null; } }; /** * Class: mxCurrentRootChange * * Action to change the current root in a view. * * Constructor: mxCurrentRootChange * * Constructs a change of the current root in the given view. */ function mxCurrentRootChange(view, root) { this.view = view; this.root = root; this.previous = root; this.isUp = root == null; if (!this.isUp) { var tmp = this.view.currentRoot; var model = this.view.graph.getModel(); while (tmp != null) { if (tmp == root) { this.isUp = true; break; } tmp = model.getParent(tmp); } } }; /** * Function: execute * * Changes the current root of the view. */ mxCurrentRootChange.prototype.execute = function() { var tmp = this.view.currentRoot; this.view.currentRoot = this.previous; this.previous = tmp; var translate = this.view.graph.getTranslateForRoot(this.view.currentRoot); if (translate != null) { this.view.translate = new mxPoint(-translate.x, -translate.y); } if (this.isUp) { this.view.clear(this.view.currentRoot, true); this.view.validate(); } else { this.view.refresh(); } var name = (this.isUp) ? mxEvent.UP : mxEvent.DOWN; this.view.fireEvent(new mxEventObject(name, 'root', this.view.currentRoot, 'previous', this.previous)); this.isUp = !this.isUp; };