/*jshint strict: false, unused: false */ /*global ArangoClusterComm, AQL_QUERY_IS_KILLED */ //////////////////////////////////////////////////////////////////////////////// /// @brief Traversal "classes" /// /// @file /// /// DISCLAIMER /// /// Copyright 2011-2013 triagens GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is triAGENS GmbH, Cologne, Germany /// /// @author Jan Steemann /// @author Michael Hackstein /// @author Copyright 2011-2013, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// var graph = require("@arangodb/graph-blueprint"); var generalGraph = require("@arangodb/general-graph"); var arangodb = require("@arangodb"); var BinaryHeap = require("@arangodb/heap").BinaryHeap; var ArangoError = arangodb.ArangoError; var ShapedJson = require("internal").ShapedJson; // this may be undefined/null on the client var db = arangodb.db; var ArangoTraverser; //////////////////////////////////////////////////////////////////////////////// /// @brief whether or not the query was aborted /// use the AQL_QUERY_IS_KILLED function on the server side, and a dummy /// function otherwise (ArangoShell etc.) //////////////////////////////////////////////////////////////////////////////// var throwIfAborted = function () { }; try { if (typeof AQL_QUERY_IS_KILLED === "function") { throwIfAborted = function () { if (AQL_QUERY_IS_KILLED()) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_QUERY_KILLED.code; err.errorMessage = arangodb.errors.ERROR_QUERY_KILLED.message; throw err; } }; } } catch (err) { } //////////////////////////////////////////////////////////////////////////////// /// @brief clone any object //////////////////////////////////////////////////////////////////////////////// function clone (obj) { if (obj === null || typeof obj !== "object") { return obj; } var copy; if (Array.isArray(obj)) { copy = [ ]; obj.forEach(function (i) { copy.push(clone(i)); }); } else if (obj instanceof Object) { if (ShapedJson && obj instanceof ShapedJson) { return obj; } copy = { }; Object.keys(obj).forEach(function(k) { copy[k] = clone(obj[k]); }); } return copy; } //////////////////////////////////////////////////////////////////////////////// /// @brief test if object is empty //////////////////////////////////////////////////////////////////////////////// function isEmpty(obj) { for(var key in obj) { if (obj.hasOwnProperty(key)) { return false; } } return true; } //////////////////////////////////////////////////////////////////////////////// /// @brief traversal abortion exception //////////////////////////////////////////////////////////////////////////////// var abortedException = function (message, options) { 'use strict'; this.message = message || "traversal intentionally aborted by user"; this.options = options || { }; this._intentionallyAborted = true; }; abortedException.prototype = new Error(); //////////////////////////////////////////////////////////////////////////////// /// @brief default ArangoCollection datasource /// /// This is a factory function that creates a datasource that operates on the /// specified edge collection. The vertices and edges are the documents in the /// corresponding collections. //////////////////////////////////////////////////////////////////////////////// function collectionDatasourceFactory (edgeCollection) { var c = edgeCollection; if (typeof c === 'string') { c = db._collection(c); } // we can call the "fast" version of some edge functions if we are // running server-side and are not a coordinator var useBuiltIn = (typeof ArangoClusterComm === "object"); if (useBuiltIn && require("@arangodb/cluster").isCoordinator()) { useBuiltIn = false; } return { edgeCollection: c, useBuiltIn: useBuiltIn, getVertexId: function (vertex) { return vertex._id; }, getPeerVertex: function (edge, vertex) { if (edge._from === vertex._id) { return db._document(edge._to); } if (edge._to === vertex._id) { return db._document(edge._from); } return null; }, getInVertex: function (edge) { return db._document(edge._to); }, getOutVertex: function (edge) { return db._document(edge._from); }, getEdgeId: function (edge) { return edge._id; }, getEdgeFrom: function (edge) { return edge._from; }, getEdgeTo: function (edge) { return edge._to; }, getLabel: function (edge) { return edge.$label; }, getAllEdges: function (vertex) { if (this.useBuiltIn) { return this.edgeCollection.EDGES(vertex._id); } return this.edgeCollection.edges(vertex._id); }, getInEdges: function (vertex) { if (this.useBuiltIn) { return this.edgeCollection.INEDGES(vertex._id); } return this.edgeCollection.inEdges(vertex._id); }, getOutEdges: function (vertex) { if (this.useBuiltIn) { return this.edgeCollection.OUTEDGES(vertex._id); } return this.edgeCollection.outEdges(vertex._id); } }; } //////////////////////////////////////////////////////////////////////////////// /// @brief general graph datasource /// /// This is a factory function that creates a datasource that operates on the /// specified general graph. The vertices and edges are delivered by the /// the general-graph module. //////////////////////////////////////////////////////////////////////////////// function generalGraphDatasourceFactory (graph) { var g = graph; if (typeof g === 'string') { g = generalGraph._graph(g); } return { graph: g, getVertexId: function (vertex) { return vertex._id; }, getPeerVertex: function (edge, vertex) { if (edge._from === vertex._id) { return db._document(edge._to); } if (edge._to === vertex._id) { return db._document(edge._from); } return null; }, getInVertex: function (edge) { return db._document(edge._to); }, getOutVertex: function (edge) { return db._document(edge._from); }, getEdgeId: function (edge) { return edge._id; }, getEdgeFrom: function (edge) { return edge._from; }, getEdgeTo: function (edge) { return edge._to; }, getLabel: function (edge) { return edge.$label; }, getAllEdges: function (vertex) { return this.graph._EDGES(vertex._id); }, getInEdges: function (vertex) { return this.graph._INEDGES(vertex._id); }, getOutEdges: function (vertex) { return this.graph._OUTEDGES(vertex._id); } }; } //////////////////////////////////////////////////////////////////////////////// /// @brief default Graph datasource /// /// This is a datasource that operates on the specified graph. The vertices /// are from type Vertex, the edges from type Edge. //////////////////////////////////////////////////////////////////////////////// function graphDatasourceFactory (name) { return { graph: new graph.Graph(name), getVertexId: function (vertex) { return vertex.getId(); }, getPeerVertex: function (edge, vertex) { return edge.getPeerVertex(vertex); }, getInVertex: function (edge) { return edge.getInVertex(); }, getOutVertex: function (edge) { return edge.getOutVertex(); }, getEdgeId: function (edge) { return edge.getId(); }, getEdgeFrom: function (edge) { return edge._properties._from; }, getEdgeTo: function (edge) { return edge._properties._to; }, getLabel: function (edge) { return edge.getLabel(); }, getAllEdges: function (vertex) { return vertex.edges(); }, getInEdges: function (vertex) { return vertex.inbound(); }, getOutEdges: function (vertex) { return vertex.outbound(); } }; } //////////////////////////////////////////////////////////////////////////////// /// @brief default outbound expander function //////////////////////////////////////////////////////////////////////////////// function outboundExpander (config, vertex, path) { var datasource = config.datasource; var connections = [ ]; var outEdges = datasource.getOutEdges(vertex); var edgeIterator; if (outEdges.length > 1 && config.sort) { outEdges.sort(config.sort); } if (config.buildVertices) { if (!config.expandFilter) { edgeIterator = function(edge) { try { var v = datasource.getInVertex(edge); connections.push({ edge: edge, vertex: v }); } catch (e) { // continue even in the face of non-existing documents } }; } else { edgeIterator = function(edge) { try { var v = datasource.getInVertex(edge); if (config.expandFilter(config, v, edge, path)) { connections.push({ edge: edge, vertex: v }); } } catch (e) { // continue even in the face of non-existing documents } }; } } else { if (!config.expandFilter) { edgeIterator = function(edge) { var id = datasource.getEdgeTo(edge); var v = { _id: id, _key: id.substr(id.indexOf("/") + 1)}; connections.push({ edge: edge, vertex: v }); }; } else { edgeIterator = function(edge) { var id = datasource.getEdgeTo(edge); var v = { _id: id, _key: id.substr(id.indexOf("/") + 1)}; if (config.expandFilter(config, v, edge, path)) { connections.push({ edge: edge, vertex: v }); } }; } } outEdges.forEach(edgeIterator); return connections; } //////////////////////////////////////////////////////////////////////////////// /// @brief default inbound expander function //////////////////////////////////////////////////////////////////////////////// function inboundExpander (config, vertex, path) { var datasource = config.datasource; var connections = [ ]; var inEdges = datasource.getInEdges(vertex); if (inEdges.length > 1 && config.sort) { inEdges.sort(config.sort); } var edgeIterator; if (config.buildVertices) { if (!config.expandFilter) { edgeIterator = function(edge) { try { var v = datasource.getOutVertex(edge); connections.push({ edge: edge, vertex: v }); } catch (e) { // continue even in the face of non-existing documents } }; } else { edgeIterator = function(edge) { try { var v = datasource.getOutVertex(edge); if (config.expandFilter(config, v, edge, path)) { connections.push({ edge: edge, vertex: v }); } } catch (e) { // continue even in the face of non-existing documents } }; } } else { if (!config.expandFilter) { edgeIterator = function(edge) { var id = datasource.getEdgeFrom(edge); var v = { _id: id, _key: id.substr(id.indexOf("/") + 1)}; connections.push({ edge: edge, vertex: v }); }; } else { edgeIterator = function(edge) { var id = datasource.getEdgeFrom(edge); var v = { _id: id, _key: id.substr(id.indexOf("/") + 1)}; if (config.expandFilter(config, v, edge, path)) { connections.push({ edge: edge, vertex: v }); } }; } } inEdges.forEach(edgeIterator); return connections; } //////////////////////////////////////////////////////////////////////////////// /// @brief default "any" expander function //////////////////////////////////////////////////////////////////////////////// function anyExpander (config, vertex, path) { var datasource = config.datasource; var connections = [ ]; var edges = datasource.getAllEdges(vertex); if (edges.length > 1 && config.sort) { edges.sort(config.sort); } var edgeIterator; if (config.buildVertices) { if (!config.expandFilter) { edgeIterator = function(edge) { try { var v = datasource.getPeerVertex(edge, vertex); connections.push({ edge: edge, vertex: v }); } catch (e) { // continue even in the face of non-existing documents } }; } else { edgeIterator = function(edge) { try { var v = datasource.getPeerVertex(edge, vertex); if (config.expandFilter(config, v, edge, path)) { connections.push({ edge: edge, vertex: v }); } } catch (e) { // continue even in the face of non-existing documents } }; } } else { if (!config.expandFilter) { edgeIterator = function(edge) { var id = datasource.getEdgeFrom(edge); if (id === vertex._id) { id = datasource.getEdgeTo(edge); } var v = { _id: id, _key: id.substr(id.indexOf("/") + 1)}; connections.push({ edge: edge, vertex: v }); }; } else { edgeIterator = function(edge) { var id = datasource.getEdgeFrom(edge); if (id === vertex._id) { id = datasource.getEdgeTo(edge); } var v = { _id: id, _key: id.substr(id.indexOf("/") + 1)}; if (config.expandFilter(config, v, edge, path)) { connections.push({ edge: edge, vertex: v }); } }; } } edges.forEach(edgeIterator); return connections; } /////////////////////////////////////////////////////////////////////////////////////////// /// @brief expands all outbound edges labeled with at least one label in config.labels /////////////////////////////////////////////////////////////////////////////////////////// function expandOutEdgesWithLabels (config, vertex, path) { var datasource = config.datasource; var result = [ ]; var i; if (! Array.isArray(config.labels)) { config.labels = [config.labels]; } var edgesList = datasource.getOutEdges(vertex); if (edgesList !== undefined) { for (i = 0; i < edgesList.length; ++i) { var edge = edgesList[i]; var label = datasource.getLabel(edge); if (config.labels.indexOf(label) >= 0) { result.push({ edge: edge, vertex: datasource.getInVertex(edge) }); } } } return result; } /////////////////////////////////////////////////////////////////////////////////////////// /// @brief expands all inbound edges labeled with at least one label in config.labels /////////////////////////////////////////////////////////////////////////////////////////// function expandInEdgesWithLabels (config, vertex, path) { var datasource = config.datasource; var result = [ ]; var i; if (! Array.isArray(config.labels)) { config.labels = [config.labels]; } var edgesList = config.datasource.getInEdges(vertex); if (edgesList !== undefined) { for (i = 0; i < edgesList.length; ++i) { var edge = edgesList[i]; var label = datasource.getLabel(edge); if (config.labels.indexOf(label) >= 0) { result.push({ edge: edge, vertex: datasource.getOutVertex(edge) }); } } } return result; } /////////////////////////////////////////////////////////////////////////////////////////// /// @brief expands all edges labeled with at least one label in config.labels /////////////////////////////////////////////////////////////////////////////////////////// function expandEdgesWithLabels (config, vertex, path) { var datasource = config.datasource; var result = [ ]; var i; if (! Array.isArray(config.labels)) { config.labels = [config.labels]; } var edgesList = config.datasource.getAllEdges(vertex); if (edgesList !== undefined) { for (i = 0; i < edgesList.length; ++i) { var edge = edgesList[i]; var label = datasource.getLabel(edge); if (config.labels.indexOf(label) >= 0) { result.push({ edge: edge, vertex: datasource.getPeerVertex(edge, vertex) }); } } } return result; } //////////////////////////////////////////////////////////////////////////////// /// @brief default visitor that just tracks every visit //////////////////////////////////////////////////////////////////////////////// function trackingVisitor (config, result, vertex, path) { if (! result || ! result.visited) { return; } if (result.visited.vertices) { result.visited.vertices.push(clone(vertex)); } if (result.visited.paths) { result.visited.paths.push(clone(path)); } } //////////////////////////////////////////////////////////////////////////////// /// @brief a visitor that counts the number of nodes visited //////////////////////////////////////////////////////////////////////////////// function countingVisitor (config, result, vertex, path) { if (! result) { return; } if (result.hasOwnProperty('count')) { ++result.count; } else { result.count = 1; } } //////////////////////////////////////////////////////////////////////////////// /// @brief a visitor that does nothing - can be used to quickly traverse a /// graph, e.g. for performance comparisons etc. //////////////////////////////////////////////////////////////////////////////// function doNothingVisitor () { } //////////////////////////////////////////////////////////////////////////////// /// @brief default filter to visit & expand all vertices //////////////////////////////////////////////////////////////////////////////// function visitAllFilter () { return ""; } //////////////////////////////////////////////////////////////////////////////// /// @brief filter to visit & expand all vertices up to a given depth //////////////////////////////////////////////////////////////////////////////// function maxDepthFilter (config, vertex, path) { if (path && path.vertices && path.vertices.length > config.maxDepth) { return ArangoTraverser.PRUNE; } } //////////////////////////////////////////////////////////////////////////////// /// @brief exclude all vertices up to a given depth //////////////////////////////////////////////////////////////////////////////// function minDepthFilter (config, vertex, path) { if (path && path.vertices && path.vertices.length <= config.minDepth) { return ArangoTraverser.EXCLUDE; } } //////////////////////////////////////////////////////////////////////////////// /// @brief include all vertices matching one of the given attribute sets //////////////////////////////////////////////////////////////////////////////// function includeMatchingAttributesFilter (config, vertex, path) { if (! Array.isArray(config.matchingAttributes)) { config.matchingAttributes = [config.matchingAttributes]; } var include = false; config.matchingAttributes.forEach(function(example) { var count = 0; var keys = Object.keys(example); keys.forEach(function (key) { if (vertex[key] && vertex[key] === example[key]) { count++; } }); if (count > 0 && count === keys.length) { include = true; } }); var result; if (! include) { result = "exclude"; } return result; } //////////////////////////////////////////////////////////////////////////////// /// @brief combine an array of filters //////////////////////////////////////////////////////////////////////////////// function combineFilters (filters, config, vertex, path) { var result = [ ]; filters.forEach(function (f) { var tmp = f(config, vertex, path); if (! Array.isArray(tmp)) { tmp = [ tmp ]; } result = result.concat(tmp); }); return result; } //////////////////////////////////////////////////////////////////////////////// /// @brief parse a filter result //////////////////////////////////////////////////////////////////////////////// function parseFilterResult (args) { var result = { visit: true, expand: true }; function processArgument (arg) { if (arg === undefined || arg === null) { return; } var finish = false; if (typeof(arg) === 'string') { if (arg === ArangoTraverser.EXCLUDE) { result.visit = false; finish = true; } else if (arg === ArangoTraverser.PRUNE) { result.expand = false; finish = true; } else if (arg === '') { finish = true; } } else if (Array.isArray(arg)) { var i; for (i = 0; i < arg.length; ++i) { processArgument(arg[i]); } finish = true; } if (finish) { return; } var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_GRAPH_INVALID_FILTER_RESULT.code; err.errorMessage = arangodb.errors.ERROR_GRAPH_INVALID_FILTER_RESULT.message; throw err; } processArgument(args); return result; } //////////////////////////////////////////////////////////////////////////////// /// @brief apply the uniqueness checks //////////////////////////////////////////////////////////////////////////////// function checkUniqueness (config, visited, vertex, edge) { var uniqueness = config.uniqueness; var datasource = config.datasource; var id; if (uniqueness.vertices !== ArangoTraverser.UNIQUE_NONE) { id = datasource.getVertexId(vertex); if (visited.vertices[id] === true) { return false; } visited.vertices[id] = true; } if (edge !== null && uniqueness.edges !== ArangoTraverser.UNIQUE_NONE) { id = datasource.getEdgeId(edge); if (visited.edges[id] === true) { return false; } visited.edges[id] = true; } return true; } //////////////////////////////////////////////////////////////////////////////// /// @brief check if we must process items in reverse order //////////////////////////////////////////////////////////////////////////////// function checkReverse (config) { var result = false; if (config.order === ArangoTraverser.POST_ORDER) { // post order if (config.itemOrder === ArangoTraverser.FORWARD) { result = true; } } else if (config.order === ArangoTraverser.PRE_ORDER || config.order === ArangoTraverser.PRE_ORDER_EXPANDER) { // pre order if (config.itemOrder === ArangoTraverser.BACKWARD && config.strategy === ArangoTraverser.BREADTH_FIRST) { result = true; } else if (config.itemOrder === ArangoTraverser.FORWARD && config.strategy === ArangoTraverser.DEPTH_FIRST) { result = true; } } return result; } //////////////////////////////////////////////////////////////////////////////// /// @brief implementation details for breadth-first strategy //////////////////////////////////////////////////////////////////////////////// function breadthFirstSearch () { return { requiresEndVertex: function () { return false; }, getPathItems: function (id, items) { var visited = { }; var ignore = items.length - 1; items.forEach(function (item, i) { if (i !== ignore) { visited[id(item)] = true; } }); return visited; }, createPath: function (items, idx) { var path = { edges: [ ], vertices: [ ] }; var pathItem = items[idx]; while (true) { if (pathItem.edge !== null) { path.edges.unshift(pathItem.edge); } path.vertices.unshift(pathItem.vertex); idx = pathItem.parentIndex; if (idx < 0) { break; } pathItem = items[idx]; } return path; }, run: function (config, result, startVertex) { var maxIterations = config.maxIterations, visitCounter = 0; var toVisit = [ { edge: null, vertex: startVertex, parentIndex: -1 } ]; var visited = { edges: { }, vertices: { } }; var index = 0; var step = 1; var reverse = checkReverse(config); while ((step === 1 && index < toVisit.length) || (step === -1 && index >= 0)) { var current = toVisit[index]; var vertex = current.vertex; var edge = current.edge; var path; if (visitCounter++ > maxIterations) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; throw err; } throwIfAborted(); if (current.visit === null || current.visit === undefined) { current.visit = false; path = this.createPath(toVisit, index); // first apply uniqueness check if (config.uniqueness.vertices === ArangoTraverser.UNIQUE_PATH) { visited.vertices = this.getPathItems(config.datasource.getVertexId, path.vertices); } if (config.uniqueness.edges === ArangoTraverser.UNIQUE_PATH) { visited.edges = this.getPathItems(config.datasource.getEdgeId, path.edges); } if (! checkUniqueness(config, visited, vertex, edge)) { if (index < toVisit.length - 1) { index += step; } else { step = -1; } continue; } var filterResult = parseFilterResult(config.filter(config, vertex, path)); if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) { // preorder config.visitor(config, result, vertex, path); } else { // postorder current.visit = filterResult.visit || false; } if (filterResult.expand) { var connected = config.expander(config, vertex, path), i; if (reverse) { connected.reverse(); } if (config.order === ArangoTraverser.PRE_ORDER_EXPANDER && filterResult.visit) { config.visitor(config, result, vertex, path, connected); } for (i = 0; i < connected.length; ++i) { connected[i].parentIndex = index; toVisit.push(connected[i]); } } else if (config.order === ArangoTraverser.PRE_ORDER_EXPANDER && filterResult.visit) { config.visitor(config, result, vertex, path, [ ]); } if (config.order === ArangoTraverser.POST_ORDER) { if (index < toVisit.length - 1) { index += step; } else { step = -1; } } } else { if (config.order === ArangoTraverser.POST_ORDER && current.visit) { path = this.createPath(toVisit, index); config.visitor(config, result, vertex, path); } index += step; } } } }; } //////////////////////////////////////////////////////////////////////////////// /// @brief implementation details for depth-first strategy //////////////////////////////////////////////////////////////////////////////// function depthFirstSearch () { return { requiresEndVertex: function () { return false; }, getPathItems: function (id, items) { var visited = { }; items.forEach(function (item) { visited[id(item)] = true; }); return visited; }, run: function (config, result, startVertex) { var maxIterations = config.maxIterations, visitCounter = 0; var toVisit = [ { edge: null, vertex: startVertex, visit: null } ]; var path = { edges: [ ], vertices: [ ] }; var visited = { edges: { }, vertices: { } }; var reverse = checkReverse(config); var uniqueness = config.uniqueness; var haveUniqueness = ((uniqueness.vertices !== ArangoTraverser.UNIQUE_NONE) || (uniqueness.edges !== ArangoTraverser.UNIQUE_NONE)); while (toVisit.length > 0) { if (visitCounter++ > maxIterations) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; throw err; } throwIfAborted(); // peek at the top of the stack var current = toVisit[toVisit.length - 1]; var vertex = current.vertex; var edge = current.edge; // check if we visit the element for the first time if (current.visit === null || current.visit === undefined) { current.visit = false; if (haveUniqueness) { // first apply uniqueness check if (uniqueness.vertices === ArangoTraverser.UNIQUE_PATH) { visited.vertices = this.getPathItems(config.datasource.getVertexId, path.vertices); } if (uniqueness.edges === ArangoTraverser.UNIQUE_PATH) { visited.edges = this.getPathItems(config.datasource.getEdgeId, path.edges); } if (! checkUniqueness(config, visited, vertex, edge)) { // skip element if not unique toVisit.pop(); continue; } } // push the current element onto the path stack if (edge !== null) { path.edges.push(edge); } path.vertices.push(vertex); var filterResult = parseFilterResult(config.filter(config, vertex, path)); if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) { // preorder visit config.visitor(config, result, vertex, path); } else { // postorder. mark the element visitation flag because we'll got to check it later current.visit = filterResult.visit || false; } // expand the element's children? if (filterResult.expand) { var connected = config.expander(config, vertex, path), i; if (reverse) { connected.reverse(); } if (config.order === ArangoTraverser.PRE_ORDER_EXPANDER && filterResult.visit) { config.visitor(config, result, vertex, path, connected); } for (i = 0; i < connected.length; ++i) { connected[i].visit = null; toVisit.push(connected[i]); } } else if (config.order === ArangoTraverser.PRE_ORDER_EXPANDER && filterResult.visit) { config.visitor(config, result, vertex, path, [ ]); } } else { // we have already seen this element if (config.order === ArangoTraverser.POST_ORDER && current.visit) { // postorder visitation config.visitor(config, result, vertex, path); } // pop the element from the stack toVisit.pop(); if (path.edges.length > 0) { path.edges.pop(); } path.vertices.pop(); } } } }; } //////////////////////////////////////////////////////////////////////////////// /// @brief implementation details for dijkstra shortest path strategy //////////////////////////////////////////////////////////////////////////////// function dijkstraSearch () { return { nodes: { }, requiresEndVertex: function () { return true; }, makeNode: function (vertex) { var id = vertex._id; if (! this.nodes.hasOwnProperty(id)) { this.nodes[id] = { vertex: vertex, dist: Infinity }; } return this.nodes[id]; }, vertexList: function (vertex) { var result = [ ]; while (vertex) { result.push(vertex); vertex = vertex.parent; } return result; }, buildPath: function (vertex) { var path = { vertices: [ vertex.vertex ], edges: [ ] }; var v = vertex; while (v.parent) { path.vertices.unshift(v.parent.vertex); path.edges.unshift(v.parentEdge); v = v.parent; } return path; }, run: function (config, result, startVertex, endVertex) { var maxIterations = config.maxIterations, visitCounter = 0; var heap = new BinaryHeap(function (node) { return node.dist; }); var startNode = this.makeNode(startVertex); startNode.dist = 0; heap.push(startNode); while (heap.size() > 0) { if (visitCounter++ > maxIterations) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; throw err; } throwIfAborted(); var currentNode = heap.pop(); var i, n; if (currentNode.vertex._id === endVertex._id) { var vertices = this.vertexList(currentNode).reverse(); n = vertices.length; for (i = 0; i < n; ++i) { if (! vertices[i].hide) { config.visitor(config, result, vertices[i].vertex, this.buildPath(vertices[i])); } } return; } if (currentNode.visited) { continue; } if (currentNode.dist === Infinity) { break; } currentNode.visited = true; var path = this.buildPath(currentNode); var filterResult = parseFilterResult(config.filter(config, currentNode.vertex, path)); if (! filterResult.visit) { currentNode.hide = true; } if (! filterResult.expand) { continue; } var dist = currentNode.dist; var connected = config.expander(config, currentNode.vertex, path); n = connected.length; for (i = 0; i < n; ++i) { var neighbor = this.makeNode(connected[i].vertex); if (neighbor.visited) { continue; } var edge = connected[i].edge; var weight = 1; if (config.distance) { weight = config.distance(config, currentNode.vertex, neighbor.vertex, edge); } else if (config.weight) { if (typeof edge[config.weight] === "number") { weight = edge[config.weight]; } else if (config.defaultWeight) { weight = config.defaultWeight; } else { weight = Infinity; } } var alt = dist + weight; if (alt < neighbor.dist) { neighbor.dist = alt; neighbor.parent = currentNode; neighbor.parentEdge = edge; heap.push(neighbor); } } } } }; } function dijkstraSearchMulti () { return { nodes: { }, requiresEndVertex: function () { return true; }, makeNode: function (vertex) { var id = vertex._id; if (! this.nodes.hasOwnProperty(id)) { this.nodes[id] = { vertex: vertex, dist: Infinity }; } return this.nodes[id]; }, vertexList: function (vertex) { var result = [ ]; while (vertex) { result.push(vertex); vertex = vertex.parent; } return result; }, buildPath: function (vertex) { var path = { vertices: [ vertex.vertex ], edges: [ ] }; var v = vertex; while (v.parent) { path.vertices.unshift(v.parent.vertex); path.edges.unshift(v.parentEdge); v = v.parent; } return path; }, run: function (config, result, startVertex, endVertex) { var maxIterations = config.maxIterations, visitCounter = 0; var heap = new BinaryHeap(function (node) { return node.dist; }); var startNode = this.makeNode(startVertex); startNode.dist = 0; heap.push(startNode); while (heap.size() > 0) { if (visitCounter++ > maxIterations) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; throw err; } var currentNode = heap.pop(); var i, n; if (endVertex.hasOwnProperty(currentNode.vertex._id)) { delete endVertex[currentNode.vertex._id]; config.visitor(config, result, currentNode, this.buildPath(currentNode)); if (isEmpty(endVertex)) { return; } } if (currentNode.visited) { continue; } if (currentNode.dist === Infinity) { break; } currentNode.visited = true; var path = this.buildPath(currentNode); var filterResult = parseFilterResult(config.filter(config, currentNode.vertex, path)); if (! filterResult.visit) { currentNode.hide = true; } if (! filterResult.expand) { continue; } var dist = currentNode.dist; var connected = config.expander(config, currentNode.vertex, path); n = connected.length; for (i = 0; i < n; ++i) { var neighbor = this.makeNode(connected[i].vertex); if (neighbor.visited) { continue; } var edge = connected[i].edge; var weight = 1; if (config.distance) { weight = config.distance(config, currentNode.vertex, neighbor.vertex, edge); } else if (config.weight) { if (typeof edge[config.weight] === "number") { weight = edge[config.weight]; } else if (config.defaultWeight) { weight = config.defaultWeight; } else { weight = Infinity; } } var alt = dist + weight; if (alt < neighbor.dist) { neighbor.dist = alt; neighbor.parent = currentNode; neighbor.parentEdge = edge; heap.push(neighbor); } } } } }; } //////////////////////////////////////////////////////////////////////////////// /// @brief implementation details for a* shortest path strategy //////////////////////////////////////////////////////////////////////////////// function astarSearch () { return { nodes: { }, requiresEndVertex: function () { return true; }, makeNode: function (vertex) { var id = vertex._id; if (! this.nodes.hasOwnProperty(id)) { this.nodes[id] = { vertex: vertex, f: 0, g: 0, h: 0 }; } return this.nodes[id]; }, vertexList: function (vertex) { var result = [ ]; while (vertex) { result.push(vertex); vertex = vertex.parent; } return result; }, buildPath: function (vertex) { var path = { vertices: [ vertex.vertex ], edges: [ ] }; var v = vertex; while (v.parent) { path.vertices.unshift(v.parent.vertex); path.edges.unshift(v.parentEdge); v = v.parent; } return path; }, run: function (config, result, startVertex, endVertex) { var maxIterations = config.maxIterations, visitCounter = 0; var heap = new BinaryHeap(function (node) { return node.f; }); heap.push(this.makeNode(startVertex)); while (heap.size() > 0) { if (visitCounter++ > maxIterations) { var err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; throw err; } throwIfAborted(); var currentNode = heap.pop(); var i, n; if (currentNode.vertex._id === endVertex._id) { var vertices = this.vertexList(currentNode); if (config.order !== ArangoTraverser.PRE_ORDER) { vertices.reverse(); } n = vertices.length; for (i = 0; i < n; ++i) { config.visitor(config, result, vertices[i].vertex, this.buildPath(vertices[i])); } return; } currentNode.closed = true; var path = this.buildPath(currentNode); var connected = config.expander(config, currentNode.vertex, path); n = connected.length; for (i = 0; i < n; ++i) { var neighbor = this.makeNode(connected[i].vertex); if (neighbor.closed) { continue; } var gScore = currentNode.g + 1;// + neighbor.cost; var beenVisited = neighbor.visited; if (! beenVisited || gScore < neighbor.g) { var edge = connected[i].edge; neighbor.visited = true; neighbor.parent = currentNode; neighbor.parentEdge = edge; neighbor.h = 1; if (config.distance && ! neighbor.h) { neighbor.h = config.distance(config, neighbor.vertex, endVertex, edge); } neighbor.g = gScore; neighbor.f = neighbor.g + neighbor.h; if (! beenVisited) { heap.push(neighbor); } else { heap.rescoreElement(neighbor); } } } } } }; } //////////////////////////////////////////////////////////////////////////////// /// @brief traversal constructor //////////////////////////////////////////////////////////////////////////////// ArangoTraverser = function (config) { var defaults = { order: ArangoTraverser.PRE_ORDER, itemOrder: ArangoTraverser.FORWARD, strategy: ArangoTraverser.DEPTH_FIRST, uniqueness: { vertices: ArangoTraverser.UNIQUE_NONE, edges: ArangoTraverser.UNIQUE_PATH }, visitor: trackingVisitor, filter: null, expander: outboundExpander, datasource: null, maxIterations: 10000000, minDepth: 0, maxDepth: 256, buildVertices: true }, d; var err; if (typeof config !== "object") { err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err.errorMessage = arangodb.errors.ERROR_BAD_PARAMETER.message; throw err; } // apply defaults for (d in defaults) { if (defaults.hasOwnProperty(d)) { if (! config.hasOwnProperty(d) || config[d] === undefined) { config[d] = defaults[d]; } } } function validate (value, map, param) { var m; if (value === null || value === undefined) { // use first key from map for (m in map) { if (map.hasOwnProperty(m)) { value = m; break; } } } if (typeof value === 'string') { value = value.toLowerCase().replace(/-/, ""); if (map[value] !== null && map[value] !== undefined) { return map[value]; } } for (m in map) { if (map.hasOwnProperty(m)) { if (map[m] === value) { return value; } } } err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err.errorMessage = "invalid value for " + param; throw err; } config.uniqueness = { vertices: validate(config.uniqueness && config.uniqueness.vertices, { none: ArangoTraverser.UNIQUE_NONE, global: ArangoTraverser.UNIQUE_GLOBAL, path: ArangoTraverser.UNIQUE_PATH }, "uniqueness.vertices"), edges: validate(config.uniqueness && config.uniqueness.edges, { path: ArangoTraverser.UNIQUE_PATH, none: ArangoTraverser.UNIQUE_NONE, global: ArangoTraverser.UNIQUE_GLOBAL }, "uniqueness.edges") }; config.strategy = validate(config.strategy, { depthfirst: ArangoTraverser.DEPTH_FIRST, breadthfirst: ArangoTraverser.BREADTH_FIRST, astar: ArangoTraverser.ASTAR_SEARCH, dijkstra: ArangoTraverser.DIJKSTRA_SEARCH, dijkstramulti: ArangoTraverser.DIJKSTRA_SEARCH_MULTI }, "strategy"); config.order = validate(config.order, { preorder: ArangoTraverser.PRE_ORDER, postorder: ArangoTraverser.POST_ORDER, preorderexpander: ArangoTraverser.PRE_ORDER_EXPANDER }, "order"); config.itemOrder = validate(config.itemOrder, { forward: ArangoTraverser.FORWARD, backward: ArangoTraverser.BACKWARD }, "itemOrder"); if (typeof config.visitor !== "function") { err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err.errorMessage = "invalid visitor function"; throw err; } // prepare an array of filters var filters = [ ]; if (config.minDepth !== undefined && config.minDepth !== null && config.minDepth > 0) { filters.push(minDepthFilter); } if (config.maxDepth !== undefined && config.maxDepth !== null && config.maxDepth > 0) { filters.push(maxDepthFilter); } if (! Array.isArray(config.filter)) { if (typeof config.filter === "function") { config.filter = [ config.filter ]; } else { config.filter = [ ]; } } config.filter.forEach( function (f) { if (typeof f !== "function") { err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err.errorMessage = "invalid filter function"; throw err; } filters.push(f); }); if (filters.length > 1) { // more than one filter. combine their results config.filter = function (config, vertex, path) { return combineFilters(filters, config, vertex, path); }; } else if (filters.length === 1) { // exactly one filter config.filter = filters[0]; } else { config.filter = visitAllFilter; } if (typeof config.expander !== "function") { config.expander = validate(config.expander, { outbound: outboundExpander, inbound: inboundExpander, any: anyExpander }, "expander"); } if (typeof config.expander !== "function") { err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err.errorMessage = "invalid expander function"; throw err; } if (typeof config.datasource !== "object") { err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err.errorMessage = "invalid datasource"; throw err; } this.config = config; }; //////////////////////////////////////////////////////////////////////////////// /// @brief execute the traversal //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.prototype.traverse = function (result, startVertex, endVertex) { // get the traversal strategy var strategy; if (this.config.strategy === ArangoTraverser.ASTAR_SEARCH) { strategy = astarSearch(); } else if (this.config.strategy === ArangoTraverser.DIJKSTRA_SEARCH) { strategy = dijkstraSearch(); } else if (this.config.strategy === ArangoTraverser.DIJKSTRA_SEARCH_MULTI) { strategy = dijkstraSearchMulti(); } else if (this.config.strategy === ArangoTraverser.BREADTH_FIRST) { strategy = breadthFirstSearch(); } else { strategy = depthFirstSearch(); } // check the start vertex if (startVertex === undefined || startVertex === null || typeof startVertex !== 'object') { var err1 = new ArangoError(); err1.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err1.errorMessage = arangodb.errors.ERROR_BAD_PARAMETER.message + ": invalid startVertex specified for traversal"; throw err1; } if (strategy.requiresEndVertex() && (endVertex === undefined || endVertex === null || typeof endVertex !== 'object')) { var err2 = new ArangoError(); err2.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err2.errorMessage = arangodb.errors.ERROR_BAD_PARAMETER.message + ": invalid endVertex specified for traversal"; throw err2; } // run the traversal try { strategy.run(this.config, result, startVertex, endVertex); } catch (err3) { if (typeof err3 !== "object" || ! err3._intentionallyAborted) { throw err3; } } }; //////////////////////////////////////////////////////////////////////////////// /// @brief every element can be revisited //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.UNIQUE_NONE = 0; //////////////////////////////////////////////////////////////////////////////// /// @brief element can only be revisited if not already in current path //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.UNIQUE_PATH = 1; //////////////////////////////////////////////////////////////////////////////// /// @brief element can only be revisited if not already visited //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.UNIQUE_GLOBAL = 2; //////////////////////////////////////////////////////////////////////////////// /// @brief visitation strategy breadth first //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.BREADTH_FIRST = 0; //////////////////////////////////////////////////////////////////////////////// /// @brief visitation strategy depth first //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.DEPTH_FIRST = 1; //////////////////////////////////////////////////////////////////////////////// /// @brief astar search //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.ASTAR_SEARCH = 2; //////////////////////////////////////////////////////////////////////////////// /// @brief dijkstra search //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.DIJKSTRA_SEARCH = 3; //////////////////////////////////////////////////////////////////////////////// /// @brief dijkstra search with multiple end vertices //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.DIJKSTRA_SEARCH_MULTI = 4; //////////////////////////////////////////////////////////////////////////////// /// @brief pre-order traversal, visitor called before expander //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.PRE_ORDER = 0; //////////////////////////////////////////////////////////////////////////////// /// @brief post-order traversal //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.POST_ORDER = 1; //////////////////////////////////////////////////////////////////////////////// /// @brief pre-order traversal, visitor called at expander //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.PRE_ORDER_EXPANDER = 2; //////////////////////////////////////////////////////////////////////////////// /// @brief forward item processing order //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.FORWARD = 0; //////////////////////////////////////////////////////////////////////////////// /// @brief backward item processing order //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.BACKWARD = 1; //////////////////////////////////////////////////////////////////////////////// /// @brief prune "constant" //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.PRUNE = 'prune'; //////////////////////////////////////////////////////////////////////////////// /// @brief exclude "constant" //////////////////////////////////////////////////////////////////////////////// ArangoTraverser.EXCLUDE = 'exclude'; exports.collectionDatasourceFactory = collectionDatasourceFactory; exports.generalGraphDatasourceFactory = generalGraphDatasourceFactory; exports.graphDatasourceFactory = graphDatasourceFactory; exports.outboundExpander = outboundExpander; exports.inboundExpander = inboundExpander; exports.anyExpander = anyExpander; exports.expandOutEdgesWithLabels = expandOutEdgesWithLabels; exports.expandInEdgesWithLabels = expandInEdgesWithLabels; exports.expandEdgesWithLabels = expandEdgesWithLabels; exports.trackingVisitor = trackingVisitor; exports.countingVisitor = countingVisitor; exports.doNothingVisitor = doNothingVisitor; exports.visitAllFilter = visitAllFilter; exports.maxDepthFilter = maxDepthFilter; exports.minDepthFilter = minDepthFilter; exports.includeMatchingAttributesFilter = includeMatchingAttributesFilter; exports.abortedException = abortedException; exports.Traverser = ArangoTraverser;