diff --git a/js/ui/mxgraph/src/js/view/mxGraphView.js b/js/ui/mxgraph/src/js/view/mxGraphView.js new file mode 100644 index 0000000..6bd893c --- /dev/null +++ b/js/ui/mxgraph/src/js/view/mxGraphView.js @@ -0,0 +1,2943 @@ +/** + * 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: 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, false); + 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.revalidate(); + this.graph.sizeDidChange(); + } + } + + 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.revalidate(); + this.graph.sizeDidChange(); + } + } + + 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.revalidate(); + this.graph.sizeDidChange(); + } + } + + this.fireEvent(new mxEventObject(mxEvent.TRANSLATE, + 'translate', this.translate, 'previousTranslate', previousTranslate)); +}; + +/** + * 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.style = this.graph.getCellStyle(state.cell); + } + + 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()); + } + } + + 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 || 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.width / this.scale + offset.x; + state.origin.y += geo.y * pState.height / this.scale + 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; + + 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); + } + + 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) +{ + if (state != null && pt != null) + { + var orig = state.origin; + + return new mxPoint(this.scale * (pt.x + this.translate.x + orig.x), + this.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 + */ +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 (!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 = perimeter(bounds, terminal, next, orthogonal); + } + } + + 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 not a layer + if (model.getParent(best) == model.getRoot()) + { + 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); + })); + } + + // 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)); + } + }), + mxUtils.bind(this, function(evt) + { + if (this.isContainerEvent(evt)) + { + 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)); + } + })); + + // 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: create + * + * 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%'); + + // 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: create + * + * 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.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: create + * + * 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.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); + + 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; +};