UNPKG

@joint/core

Version:

JavaScript diagramming library

1,221 lines (952 loc) 41.4 kB
import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; import { Model } from '../mvc/Model.mjs'; import { Collection } from '../mvc/Collection.mjs'; import { wrappers, wrapWith } from '../util/wrappers.mjs'; import { cloneCells } from '../util/index.mjs'; const GraphCells = Collection.extend({ initialize: function(models, opt) { // Set the optional namespace where all model classes are defined. if (opt.cellNamespace) { this.cellNamespace = opt.cellNamespace; } else { /* eslint-disable no-undef */ this.cellNamespace = typeof joint !== 'undefined' && util.has(joint, 'shapes') ? joint.shapes : null; /* eslint-enable no-undef */ } this.graph = opt.graph; }, model: function(attrs, opt) { const collection = opt.collection; const namespace = collection.cellNamespace; const { type } = attrs; // Find the model class based on the `type` attribute in the cell namespace const ModelClass = util.getByPath(namespace, type, '.'); if (!ModelClass) { throw new Error(`dia.Graph: Could not find cell constructor for type: '${type}'. Make sure to add the constructor to 'cellNamespace'.`); } return new ModelClass(attrs, opt); }, _addReference: function(model, options) { Collection.prototype._addReference.apply(this, arguments); // If not in `dry` mode and the model does not have a graph reference yet, // set the reference. if (!options.dry && !model.graph) { model.graph = this.graph; } }, _removeReference: function(model, options) { Collection.prototype._removeReference.apply(this, arguments); // If not in `dry` mode and the model has a reference to this exact graph, // remove the reference. if (!options.dry && model.graph === this.graph) { model.graph = null; } }, // `comparator` makes it easy to sort cells based on their `z` index. comparator: function(model) { return model.get('z') || 0; } }); export const Graph = Model.extend({ initialize: function(attrs, opt) { opt = opt || {}; // Passing `cellModel` function in the options object to graph allows for // setting models based on attribute objects. This is especially handy // when processing JSON graphs that are in a different than JointJS format. var cells = new GraphCells([], { model: opt.cellModel, cellNamespace: opt.cellNamespace, graph: this }); Model.prototype.set.call(this, 'cells', cells); // Make all the events fired in the `cells` collection available. // to the outside world. cells.on('all', this.trigger, this); // JointJS automatically doesn't trigger re-sort if models attributes are changed later when // they're already in the collection. Therefore, we're triggering sort manually here. this.on('change:z', this._sortOnChangeZ, this); // `joint.dia.Graph` keeps an internal data structure (an adjacency list) // for fast graph queries. All changes that affect the structure of the graph // must be reflected in the `al` object. This object provides fast answers to // questions such as "what are the neighbours of this node" or "what // are the sibling links of this link". // Outgoing edges per node. Note that we use a hash-table for the list // of outgoing edges for a faster lookup. // [nodeId] -> Object [edgeId] -> true this._out = {}; // Ingoing edges per node. // [nodeId] -> Object [edgeId] -> true this._in = {}; // `_nodes` is useful for quick lookup of all the elements in the graph, without // having to go through the whole cells array. // [node ID] -> true this._nodes = {}; // `_edges` is useful for quick lookup of all the links in the graph, without // having to go through the whole cells array. // [edgeId] -> true this._edges = {}; this._batches = {}; cells.on('add', this._restructureOnAdd, this); cells.on('remove', this._restructureOnRemove, this); cells.on('reset', this._restructureOnReset, this); cells.on('change:source', this._restructureOnChangeSource, this); cells.on('change:target', this._restructureOnChangeTarget, this); cells.on('remove', this._removeCell, this); }, _sortOnChangeZ: function() { this.get('cells').sort(); }, _restructureOnAdd: function(cell) { if (cell.isLink()) { this._edges[cell.id] = true; var { source, target } = cell.attributes; if (source.id) { (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; } if (target.id) { (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; } } else { this._nodes[cell.id] = true; } }, _restructureOnRemove: function(cell) { if (cell.isLink()) { delete this._edges[cell.id]; var { source, target } = cell.attributes; if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { delete this._out[source.id][cell.id]; } if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { delete this._in[target.id][cell.id]; } } else { delete this._nodes[cell.id]; } }, _restructureOnReset: function(cells) { // Normalize into an array of cells. The original `cells` is GraphCells mvc collection. cells = cells.models; this._out = {}; this._in = {}; this._nodes = {}; this._edges = {}; cells.forEach(this._restructureOnAdd, this); }, _restructureOnChangeSource: function(link) { var prevSource = link.previous('source'); if (prevSource.id && this._out[prevSource.id]) { delete this._out[prevSource.id][link.id]; } var source = link.attributes.source; if (source.id) { (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; } }, _restructureOnChangeTarget: function(link) { var prevTarget = link.previous('target'); if (prevTarget.id && this._in[prevTarget.id]) { delete this._in[prevTarget.id][link.id]; } var target = link.get('target'); if (target.id) { (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; } }, // Return all outbound edges for the node. Return value is an object // of the form: [edgeId] -> true getOutboundEdges: function(node) { return (this._out && this._out[node]) || {}; }, // Return all inbound edges for the node. Return value is an object // of the form: [edgeId] -> true getInboundEdges: function(node) { return (this._in && this._in[node]) || {}; }, toJSON: function(opt = {}) { // JointJS does not recursively call `toJSON()` on attributes that are themselves models/collections. // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitly. var json = Model.prototype.toJSON.apply(this, arguments); json.cells = this.get('cells').toJSON(opt.cellAttributes); return json; }, fromJSON: function(json, opt) { if (!json.cells) { throw new Error('Graph JSON must contain cells array.'); } return this.set(json, opt); }, set: function(key, val, opt) { var attrs; // Handle both `key`, value and {key: value} style arguments. if (typeof key === 'object') { attrs = key; opt = val; } else { (attrs = {})[key] = val; } // Make sure that `cells` attribute is handled separately via resetCells(). if (attrs.hasOwnProperty('cells')) { this.resetCells(attrs.cells, opt); attrs = util.omit(attrs, 'cells'); } // The rest of the attributes are applied via original set method. return Model.prototype.set.call(this, attrs, opt); }, clear: function(opt) { opt = util.assign({}, opt, { clear: true }); var collection = this.get('cells'); if (collection.length === 0) return this; this.startBatch('clear', opt); // The elements come after the links. var cells = collection.sortBy(function(cell) { return cell.isLink() ? 1 : 2; }); do { // Remove all the cells one by one. // Note that all the links are removed first, so it's // safe to remove the elements without removing the connected // links first. cells.shift().remove(opt); } while (cells.length > 0); this.stopBatch('clear'); return this; }, _prepareCell: function(cell) { let attrs; if (cell instanceof Model) { attrs = cell.attributes; } else { attrs = cell; } if (!util.isString(attrs.type)) { throw new TypeError('dia.Graph: cell type must be a string.'); } return cell; }, minZIndex: function() { var firstCell = this.get('cells').first(); return firstCell ? (firstCell.get('z') || 0) : 0; }, maxZIndex: function() { var lastCell = this.get('cells').last(); return lastCell ? (lastCell.get('z') || 0) : 0; }, addCell: function(cell, opt) { if (Array.isArray(cell)) { return this.addCells(cell, opt); } if (cell instanceof Model) { if (!cell.has('z')) { cell.set('z', this.maxZIndex() + 1); } } else if (cell.z === undefined) { cell.z = this.maxZIndex() + 1; } this.get('cells').add(this._prepareCell(cell, opt), opt || {}); return this; }, addCells: function(cells, opt) { if (cells.length === 0) return this; cells = util.flattenDeep(cells); opt.maxPosition = opt.position = cells.length - 1; this.startBatch('add', opt); cells.forEach(function(cell) { this.addCell(cell, opt); opt.position--; }, this); this.stopBatch('add', opt); return this; }, // When adding a lot of cells, it is much more efficient to // reset the entire cells collection in one go. // Useful for bulk operations and optimizations. resetCells: function(cells, opt) { var preparedCells = util.toArray(cells).map(function(cell) { return this._prepareCell(cell, opt); }, this); this.get('cells').reset(preparedCells, opt); return this; }, removeCells: function(cells, opt) { if (cells.length) { this.startBatch('remove'); util.invoke(cells, 'remove', opt); this.stopBatch('remove'); } return this; }, _removeCell: function(cell, collection, options) { options = options || {}; if (!options.clear) { // Applications might provide a `disconnectLinks` option set to `true` in order to // disconnect links when a cell is removed rather then removing them. The default // is to remove all the associated links. if (options.disconnectLinks) { this.disconnectLinks(cell, options); } else { this.removeLinks(cell, options); } } // Silently remove the cell from the cells collection. Silently, because // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events // would be triggered on the graph model. this.get('cells').remove(cell, { silent: true }); }, transferCellEmbeds: function(sourceCell, targetCell, opt = {}) { const batchName = 'transfer-embeds'; this.startBatch(batchName); // Embed children of the source cell in the target cell. const children = sourceCell.getEmbeddedCells(); targetCell.embed(children, { ...opt, reparent: true }); this.stopBatch(batchName); }, transferCellConnectedLinks: function(sourceCell, targetCell, opt = {}) { const batchName = 'transfer-connected-links'; this.startBatch(batchName); // Reconnect all the links connected to the old cell to the new cell. const connectedLinks = this.getConnectedLinks(sourceCell, opt); connectedLinks.forEach((link) => { if (link.getSourceCell() === sourceCell) { link.prop(['source', 'id'], targetCell.id, opt); } if (link.getTargetCell() === sourceCell) { link.prop(['target', 'id'], targetCell.id, opt); } }); this.stopBatch(batchName); }, // Get a cell by `id`. getCell: function(id) { return this.get('cells').get(id); }, getCells: function() { return this.get('cells').toArray(); }, getElements: function() { return this.get('cells').toArray().filter(cell => cell.isElement()); }, getLinks: function() { return this.get('cells').toArray().filter(cell => cell.isLink()); }, getFirstCell: function() { return this.get('cells').first(); }, getLastCell: function() { return this.get('cells').last(); }, // Get all inbound and outbound links connected to the cell `model`. getConnectedLinks: function(model, opt) { opt = opt || {}; var indirect = opt.indirect; var inbound = opt.inbound; var outbound = opt.outbound; if ((inbound === undefined) && (outbound === undefined)) { inbound = outbound = true; } // the final array of connected link models var links = []; // a hash table of connected edges of the form: [edgeId] -> true // used for quick lookups to check if we already added a link var edges = {}; if (outbound) { addOutbounds(this, model); } if (inbound) { addInbounds(this, model); } function addOutbounds(graph, model) { util.forIn(graph.getOutboundEdges(model.id), function(_, edge) { // skip links that were already added // (those must be self-loop links) // (because they are inbound and outbound edges of the same two elements) if (edges[edge]) return; var link = graph.getCell(edge); links.push(link); edges[edge] = true; if (indirect) { if (inbound) addInbounds(graph, link); if (outbound) addOutbounds(graph, link); } }.bind(graph)); if (indirect && model.isLink()) { var outCell = model.getTargetCell(); if (outCell && outCell.isLink()) { if (!edges[outCell.id]) { links.push(outCell); addOutbounds(graph, outCell); } } } } function addInbounds(graph, model) { util.forIn(graph.getInboundEdges(model.id), function(_, edge) { // skip links that were already added // (those must be self-loop links) // (because they are inbound and outbound edges of the same two elements) if (edges[edge]) return; var link = graph.getCell(edge); links.push(link); edges[edge] = true; if (indirect) { if (inbound) addInbounds(graph, link); if (outbound) addOutbounds(graph, link); } }.bind(graph)); if (indirect && model.isLink()) { var inCell = model.getSourceCell(); if (inCell && inCell.isLink()) { if (!edges[inCell.id]) { links.push(inCell); addInbounds(graph, inCell); } } } } // if `deep` option is `true`, check also all the links that are connected to any of the descendant cells if (opt.deep) { var embeddedCells = model.getEmbeddedCells({ deep: true }); // in the first round, we collect all the embedded elements var embeddedElements = {}; embeddedCells.forEach(function(cell) { if (cell.isElement()) { embeddedElements[cell.id] = true; } }); embeddedCells.forEach(function(cell) { if (cell.isLink()) return; if (outbound) { util.forIn(this.getOutboundEdges(cell.id), function(exists, edge) { if (!edges[edge]) { var edgeCell = this.getCell(edge); var { source, target } = edgeCell.attributes; var sourceId = source.id; var targetId = target.id; // if `includeEnclosed` option is falsy, skip enclosed links if (!opt.includeEnclosed && (sourceId && embeddedElements[sourceId]) && (targetId && embeddedElements[targetId])) { return; } links.push(this.getCell(edge)); edges[edge] = true; } }.bind(this)); } if (inbound) { util.forIn(this.getInboundEdges(cell.id), function(exists, edge) { if (!edges[edge]) { var edgeCell = this.getCell(edge); var { source, target } = edgeCell.attributes; var sourceId = source.id; var targetId = target.id; // if `includeEnclosed` option is falsy, skip enclosed links if (!opt.includeEnclosed && (sourceId && embeddedElements[sourceId]) && (targetId && embeddedElements[targetId])) { return; } links.push(this.getCell(edge)); edges[edge] = true; } }.bind(this)); } }, this); } return links; }, getNeighbors: function(model, opt) { opt || (opt = {}); var inbound = opt.inbound; var outbound = opt.outbound; if (inbound === undefined && outbound === undefined) { inbound = outbound = true; } var neighbors = this.getConnectedLinks(model, opt).reduce(function(res, link) { var { source, target } = link.attributes; var loop = link.hasLoop(opt); // Discard if it is a point, or if the neighbor was already added. if (inbound && util.has(source, 'id') && !res[source.id]) { var sourceElement = this.getCell(source.id); if (sourceElement.isElement()) { if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { res[source.id] = sourceElement; } } } // Discard if it is a point, or if the neighbor was already added. if (outbound && util.has(target, 'id') && !res[target.id]) { var targetElement = this.getCell(target.id); if (targetElement.isElement()) { if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { res[target.id] = targetElement; } } } return res; }.bind(this), {}); if (model.isLink()) { if (inbound) { var sourceCell = model.getSourceCell(); if (sourceCell && sourceCell.isElement() && !neighbors[sourceCell.id]) { neighbors[sourceCell.id] = sourceCell; } } if (outbound) { var targetCell = model.getTargetCell(); if (targetCell && targetCell.isElement() && !neighbors[targetCell.id]) { neighbors[targetCell.id] = targetCell; } } } return util.toArray(neighbors); }, getCommonAncestor: function(/* cells */) { var cellsAncestors = Array.from(arguments).map(function(cell) { var ancestors = []; var parentId = cell.get('parent'); while (parentId) { ancestors.push(parentId); parentId = this.getCell(parentId).get('parent'); } return ancestors; }, this); cellsAncestors = cellsAncestors.sort(function(a, b) { return a.length - b.length; }); var commonAncestor = util.toArray(cellsAncestors.shift()).find(function(ancestor) { return cellsAncestors.every(function(cellAncestors) { return cellAncestors.includes(ancestor); }); }); return this.getCell(commonAncestor); }, // Find the whole branch starting at `element`. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. getSuccessors: function(element, opt) { opt = opt || {}; var res = []; // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. this.search(element, function(el) { if (el !== element) { res.push(el); } }, util.assign({}, opt, { outbound: true })); return res; }, cloneCells: cloneCells, // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. // Return a map of the form: [original cell ID] -> [clone]. cloneSubgraph: function(cells, opt) { var subgraph = this.getSubgraph(cells, opt); return this.cloneCells(subgraph); }, // Return `cells` and all the connected links that connect cells in the `cells` array. // If `opt.deep` is `true`, return all the cells including all their embedded cells // and all the links that connect any of the returned cells. // For example, for a single shallow element, the result is that very same element. // For two elements connected with a link: `A --- L ---> B`, the result for // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. getSubgraph: function(cells, opt) { opt = opt || {}; var subgraph = []; // `cellMap` is used for a quick lookup of existence of a cell in the `cells` array. var cellMap = {}; var elements = []; var links = []; util.toArray(cells).forEach(function(cell) { if (!cellMap[cell.id]) { subgraph.push(cell); cellMap[cell.id] = cell; if (cell.isLink()) { links.push(cell); } else { elements.push(cell); } } if (opt.deep) { var embeds = cell.getEmbeddedCells({ deep: true }); embeds.forEach(function(embed) { if (!cellMap[embed.id]) { subgraph.push(embed); cellMap[embed.id] = embed; if (embed.isLink()) { links.push(embed); } else { elements.push(embed); } } }); } }); links.forEach(function(link) { // For links, return their source & target (if they are elements - not points). var { source, target } = link.attributes; if (source.id && !cellMap[source.id]) { var sourceElement = this.getCell(source.id); subgraph.push(sourceElement); cellMap[sourceElement.id] = sourceElement; elements.push(sourceElement); } if (target.id && !cellMap[target.id]) { var targetElement = this.getCell(target.id); subgraph.push(this.getCell(target.id)); cellMap[targetElement.id] = targetElement; elements.push(targetElement); } }, this); elements.forEach(function(element) { // For elements, include their connected links if their source/target is in the subgraph; var links = this.getConnectedLinks(element, opt); links.forEach(function(link) { var { source, target } = link.attributes; if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { subgraph.push(link); cellMap[link.id] = link; } }); }, this); return subgraph; }, // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. getPredecessors: function(element, opt) { opt = opt || {}; var res = []; // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. this.search(element, function(el) { if (el !== element) { res.push(el); } }, util.assign({}, opt, { inbound: true })); return res; }, // Perform search on the graph. // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. // By setting `opt.inbound` to `true`, you can reverse the direction of the search. // If `opt.deep` is `true`, take into account embedded elements too. // `iteratee` is a function of the form `function(element) {}`. // If `iteratee` explicitly returns `false`, the searching stops. search: function(element, iteratee, opt) { opt = opt || {}; if (opt.breadthFirst) { this.bfs(element, iteratee, opt); } else { this.dfs(element, iteratee, opt); } }, // Breadth-first search. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). // `iteratee` is a function of the form `function(element, distance) {}`. // where `element` is the currently visited element and `distance` is the distance of that element // from the root `element` passed the `bfs()`, i.e. the element we started the search from. // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. // If `iteratee` explicitly returns `false`, the searching stops. bfs: function(element, iteratee, opt = {}) { const visited = {}; const distance = {}; const queue = []; queue.push(element); distance[element.id] = 0; while (queue.length > 0) { var next = queue.shift(); if (visited[next.id]) continue; visited[next.id] = true; if (iteratee.call(this, next, distance[next.id]) === false) continue; const neighbors = this.getNeighbors(next, opt); for (let i = 0, n = neighbors.length; i < n; i++) { const neighbor = neighbors[i]; distance[neighbor.id] = distance[next.id] + 1; queue.push(neighbor); } } }, // Depth-first search. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). // `iteratee` is a function of the form `function(element, distance) {}`. // If `iteratee` explicitly returns `false`, the search stops. dfs: function(element, iteratee, opt = {}) { const visited = {}; const distance = {}; const queue = []; queue.push(element); distance[element.id] = 0; while (queue.length > 0) { const next = queue.pop(); if (visited[next.id]) continue; visited[next.id] = true; if (iteratee.call(this, next, distance[next.id]) === false) continue; const neighbors = this.getNeighbors(next, opt); const lastIndex = queue.length; for (let i = 0, n = neighbors.length; i < n; i++) { const neighbor = neighbors[i]; distance[neighbor.id] = distance[next.id] + 1; queue.splice(lastIndex, 0, neighbor); } } }, // Get all the roots of the graph. Time complexity: O(|V|). getSources: function() { var sources = []; util.forIn(this._nodes, function(exists, node) { if (!this._in[node] || util.isEmpty(this._in[node])) { sources.push(this.getCell(node)); } }.bind(this)); return sources; }, // Get all the leafs of the graph. Time complexity: O(|V|). getSinks: function() { var sinks = []; util.forIn(this._nodes, function(exists, node) { if (!this._out[node] || util.isEmpty(this._out[node])) { sinks.push(this.getCell(node)); } }.bind(this)); return sinks; }, // Return `true` if `element` is a root. Time complexity: O(1). isSource: function(element) { return !this._in[element.id] || util.isEmpty(this._in[element.id]); }, // Return `true` if `element` is a leaf. Time complexity: O(1). isSink: function(element) { return !this._out[element.id] || util.isEmpty(this._out[element.id]); }, // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. isSuccessor: function(elementA, elementB) { var isSuccessor = false; this.search(elementA, function(element) { if (element === elementB && element !== elementA) { isSuccessor = true; return false; } }, { outbound: true }); return isSuccessor; }, // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. isPredecessor: function(elementA, elementB) { var isPredecessor = false; this.search(elementA, function(element) { if (element === elementB && element !== elementA) { isPredecessor = true; return false; } }, { inbound: true }); return isPredecessor; }, // Return `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` // for more details. // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. isNeighbor: function(elementA, elementB, opt) { opt = opt || {}; var inbound = opt.inbound; var outbound = opt.outbound; if ((inbound === undefined) && (outbound === undefined)) { inbound = outbound = true; } var isNeighbor = false; this.getConnectedLinks(elementA, opt).forEach(function(link) { var { source, target } = link.attributes; // Discard if it is a point. if (inbound && util.has(source, 'id') && (source.id === elementB.id)) { isNeighbor = true; return false; } // Discard if it is a point, or if the neighbor was already added. if (outbound && util.has(target, 'id') && (target.id === elementB.id)) { isNeighbor = true; return false; } }); return isNeighbor; }, // Disconnect links connected to the cell `model`. disconnectLinks: function(model, opt) { this.getConnectedLinks(model).forEach(function(link) { link.set((link.attributes.source.id === model.id ? 'source' : 'target'), { x: 0, y: 0 }, opt); }); }, // Remove links connected to the cell `model` completely. removeLinks: function(model, opt) { util.invoke(this.getConnectedLinks(model), 'remove', opt); }, // Find all cells at given point findElementsAtPoint: function(point, opt) { return this._filterAtPoint(this.getElements(), point, opt); }, findLinksAtPoint: function(point, opt) { return this._filterAtPoint(this.getLinks(), point, opt); }, findCellsAtPoint: function(point, opt) { return this._filterAtPoint(this.getCells(), point, opt); }, _filterAtPoint: function(cells, point, opt = {}) { return cells.filter(el => el.getBBox({ rotate: true }).containsPoint(point, opt)); }, // Find all cells in given area findElementsInArea: function(area, opt = {}) { return this._filterInArea(this.getElements(), area, opt); }, findLinksInArea: function(area, opt = {}) { return this._filterInArea(this.getLinks(), area, opt); }, findCellsInArea: function(area, opt = {}) { return this._filterInArea(this.getCells(), area, opt); }, _filterInArea: function(cells, area, opt = {}) { const r = new g.Rect(area); const { strict = false } = opt; const method = strict ? 'containsRect' : 'intersect'; return cells.filter(el => r[method](el.getBBox({ rotate: true }))); }, // Find all cells under the given element. findElementsUnderElement: function(element, opt) { return this._filterCellsUnderElement(this.getElements(), element, opt); }, findLinksUnderElement: function(element, opt) { return this._filterCellsUnderElement(this.getLinks(), element, opt); }, findCellsUnderElement: function(element, opt) { return this._filterCellsUnderElement(this.getCells(), element, opt); }, _isValidElementUnderElement: function(el1, el2) { return el1.id !== el2.id && !el1.isEmbeddedIn(el2); }, _isValidLinkUnderElement: function(link, el) { return ( link.source().id !== el.id && link.target().id !== el.id && !link.isEmbeddedIn(el) ); }, _validateCellsUnderElement: function(cells, element) { return cells.filter(cell => { return cell.isLink() ? this._isValidLinkUnderElement(cell, element) : this._isValidElementUnderElement(cell, element); }); }, _getFindUnderElementGeometry: function(element, searchBy = 'bbox') { const bbox = element.getBBox({ rotate: true }); return (searchBy !== 'bbox') ? util.getRectPoint(bbox, searchBy) : bbox; }, _filterCellsUnderElement: function(cells, element, opt = {}) { const geometry = this._getFindUnderElementGeometry(element, opt.searchBy); const filteredCells = (geometry.type === g.types.Point) ? this._filterAtPoint(cells, geometry) : this._filterInArea(cells, geometry, opt); return this._validateCellsUnderElement(filteredCells, element); }, // @deprecated use `findElementsInArea` instead findModelsInArea: function(area, opt) { return this.findElementsInArea(area, opt); }, // @deprecated use `findElementsAtPoint` instead findModelsFromPoint: function(point) { return this.findElementsAtPoint(point); }, // @deprecated use `findModelsUnderElement` instead findModelsUnderElement: function(element, opt) { return this.findElementsUnderElement(element, opt); }, // Return bounding box of all elements. getBBox: function() { return this.getCellsBBox(this.getCells()); }, // Return the bounding box of all cells in array provided. getCellsBBox: function(cells, opt = {}) { const { rotate = true } = opt; return util.toArray(cells).reduce(function(memo, cell) { const rect = cell.getBBox({ rotate }); if (!rect) return memo; if (memo) { return memo.union(rect); } return rect; }, null); }, translate: function(dx, dy, opt) { // Don't translate cells that are embedded in any other cell. var cells = this.getCells().filter(function(cell) { return !cell.isEmbedded(); }); util.invoke(cells, 'translate', dx, dy, opt); return this; }, resize: function(width, height, opt) { return this.resizeCells(width, height, this.getCells(), opt); }, resizeCells: function(width, height, cells, opt) { // `getBBox` method returns `null` if no elements provided. // i.e. cells can be an array of links var bbox = this.getCellsBBox(cells); if (bbox) { var sx = Math.max(width / bbox.width, 0); var sy = Math.max(height / bbox.height, 0); util.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); } return this; }, startBatch: function(name, data) { data = data || {}; this._batches[name] = (this._batches[name] || 0) + 1; return this.trigger('batch:start', util.assign({}, data, { batchName: name })); }, stopBatch: function(name, data) { data = data || {}; this._batches[name] = (this._batches[name] || 0) - 1; return this.trigger('batch:stop', util.assign({}, data, { batchName: name })); }, hasActiveBatch: function(name) { const batches = this._batches; let names; if (arguments.length === 0) { names = Object.keys(batches); } else if (Array.isArray(name)) { names = name; } else { names = [name]; } return names.some((batch) => batches[batch] > 0); } }, { validations: { multiLinks: function(graph, link) { // Do not allow multiple links to have the same source and target. var { source, target } = link.attributes; if (source.id && target.id) { var sourceModel = link.getSourceCell(); if (sourceModel) { var connectedLinks = graph.getConnectedLinks(sourceModel, { outbound: true }); var sameLinks = connectedLinks.filter(function(_link) { var { source: _source, target: _target } = _link.attributes; return _source && _source.id === source.id && (!_source.port || (_source.port === source.port)) && _target && _target.id === target.id && (!_target.port || (_target.port === target.port)); }); if (sameLinks.length > 1) { return false; } } } return true; }, linkPinning: function(_graph, link) { var { source, target } = link.attributes; return source.id && target.id; } } }); wrapWith(Graph.prototype, ['resetCells', 'addCells', 'removeCells'], wrappers.cells);