1
0
Fork 0
arangodb/js/common/modules/@arangodb/graph/traversal.js

1722 lines
50 KiB
JavaScript

/* jshint strict: false, unused: true */
/* global ArangoClusterComm */
// //////////////////////////////////////////////////////////////////////////////
// / @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 generalGraph = require('@arangodb/general-graph');
var arangodb = require('@arangodb');
var BinaryHeap = require('@arangodb/heap').BinaryHeap;
var ArangoError = arangodb.ArangoError;
var db = arangodb.db;
var ArangoTraverser;
// //////////////////////////////////////////////////////////////////////////////
// / @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) {
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 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) {
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) {
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) {
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) {
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) {
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;
}
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;
}
// 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;
}
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;
}
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.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;