UNPKG

webgme-engine

Version:

WebGME server and Client API without a GUI

194 lines (169 loc) 5.69 kB
/*globals define*/ /*eslint-env node, browser*/ /** * Module for finding strongly connected components in a directed graph, that is circular references. * * Based on https://gist.github.com/chadhutchins/1440602 but with constant time stack lookup. * https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm * * The example below returns the SCC v2,v3,v4 * v1 -* v2 * / * * * \ * v3 -* v4 * @example * * var t = new Tarjan(); * * t.addVertex(1); * t.addVertex(2); * t.addVertex(3); * t.addVertex(4); * * t.connectVertices(1, 2); // Order matters * t.connectVertices(2, 3); * t.connectVertices(3, 4); * t.connectVertices(4, 2); * * t.calculateSCCs(); -> [[1], [2, 3, 4]] * t.hasLoops(); -> true * * * @author pmeijer / https://github.com/pmeijer */ define([], function () { 'use strict'; function Vertex(id) { this.id = id; this.connectedVertices = []; this.index = -1; this.lowlink = -1; } Vertex.prototype.connectTo = function (vertex) { this.connectedVertices.push(vertex); }; function Graph() { // Assumption: ids are unique. this.vertices = {}; } Graph.prototype.addVertex = function (id) { if (Object.hasOwn(this.vertices, id)) { return false; } else { this.vertices[id] = new Vertex(id); return true; } }; Graph.prototype.connectVertices = function (id1, id2) { if (Object.hasOwn(this.vertices, id1) === false) { throw new Error('Vertex [' + id1 + '] was never added to graph!'); } if (Object.hasOwn(this.vertices, id2) === false) { throw new Error('Vertex [' + id2 + '] was never added to graph!'); } this.vertices[id1].connectTo(this.vertices[id2]); }; function Tarjan() { this.index = 0; this.stackLookup = {}; this.stack = []; this.graph = new Graph(); this.SCCs = []; this.didRun = false; } /** * Adds a vertex with given id. * @param {string|number} id * @returns {boolean} false if it was already added. */ Tarjan.prototype.addVertex = function (id) { if (this.didRun) { throw new Error('Cannot modify graph after algorithm ran!'); } return this.graph.addVertex(id); }; /** * Creates a connection from vertex at id1 to vertex at id2 * @param {string|number} id1 * @param {string|number} id2 */ Tarjan.prototype.connectVertices = function (id1, id2) { if (this.didRun) { throw new Error('Cannot modify graph after algorithm ran!'); } this.graph.connectVertices(id1, id2); }; /** * Checks if there are any loops in the graph. * @returns {boolean} */ Tarjan.prototype.hasLoops = function () { var i; this.calculateSCCs(); for (i = 0; i < this.SCCs.length; i += 1) { if (this.SCCs[i].length > 1) { return true; } } return false; }; /** * Returns the strongly connected components (by ids) * @returns {Array<Array<String|Number>>} An array with all SCCs. */ Tarjan.prototype.calculateSCCs = function () { var id; if (this.didRun === false) { for (id in this.graph.vertices) { if (this.graph.vertices[id].index < 0) { this._strongConnect(this.graph.vertices[id]); } } this.didRun = true; } return this.SCCs; }; Tarjan.prototype._strongConnect = function (vertex) { var i, connectedVertex, sccVertices = [], topVertex; // Set the depth index for v to the smallest unused index vertex.index = this.index; vertex.lowlink = this.index; this.index = this.index + 1; this.stack.push(vertex); this.stackLookup[vertex.id] = true; // Consider successors of vertex // aka... consider each vertex in vertex.connections for (i = 0; i < vertex.connectedVertices.length; i += 1) { connectedVertex = vertex.connectedVertices[i]; if (connectedVertex.index < 0) { // Successor connectedVertex has not yet been visited; recurse on it this._strongConnect(connectedVertex); vertex.lowlink = Math.min(vertex.lowlink, connectedVertex.lowlink); } else if (this.stackLookup[connectedVertex.id]) { // Successor connectedVertex is in stack S and hence in the current SCC vertex.lowlink = Math.min(vertex.lowlink, connectedVertex.index); } } // If vertex is a root node, pop the stack and generate an SCC. if (vertex.lowlink === vertex.index) { // start a new strongly connected component if (this.stack.length > 0) { do { topVertex = this.stack.pop(); this.stackLookup[topVertex.id] = false; // add topVertex to current strongly connected component sccVertices.push(topVertex.id); } while (vertex.id !== topVertex.id); } // output the current strongly connected component // ... i'm going to push the results to a member scc array variable if (sccVertices.length > 0) { this.SCCs.push(sccVertices); } } }; return Tarjan; });