diff --git a/js/common/modules/org/arangodb/graph/traversal.js b/js/common/modules/org/arangodb/graph/traversal.js index 2a6f83b421..98bbf50fec 100644 --- a/js/common/modules/org/arangodb/graph/traversal.js +++ b/js/common/modules/org/arangodb/graph/traversal.js @@ -72,7 +72,20 @@ function ArangoTraverser (config) { if (typeof config.visitor !== "function") { throw "invalid visitor"; } - + + if (Array.isArray(config.filter)) { + config.filter.forEach( function (f) { + if (typeof f !== "function") { + throw "invalid filter"; + } + }); + var innerFilters = config.filter.slice() + var combinedFilter = function(config, vertex, path) { + return CombineFilters(innerFilters, config, vertex, path); + }; + config.filter = combinedFilter; + } + if (typeof config.filter !== "function") { throw "invalid filter"; } @@ -492,6 +505,40 @@ function VisitAllFilter () { return ""; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief default filter to visit & expand all vertices up to a given depth +//////////////////////////////////////////////////////////////////////////////// +function MaxDepthFilter (config, vertex, path) { + if (path.vertices.length > config.maxDepth) { + return "prune"; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief default filter to exclude all vertices up to a given depth +//////////////////////////////////////////////////////////////////////////////// +function MinDepthFilter (config, vertex, path) { + if (path.vertices.length <= config.minDepth) { + return "exclude"; + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +/// @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; +} + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// @@ -589,6 +636,8 @@ exports.CollectionOutboundExpander = CollectionOutboundExpander; exports.CollectionInboundExpander = CollectionInboundExpander; exports.VisitAllFilter = VisitAllFilter; exports.TrackingVisitor = TrackingVisitor; +exports.MinDepthFilter = MinDepthFilter; +exports.MaxDepthFilter = MaxDepthFilter; //////////////////////////////////////////////////////////////////////////////// /// @} diff --git a/js/common/tests/shell-graph-traversal.js b/js/common/tests/shell-graph-traversal.js index 15db439021..3dfcb848e6 100644 --- a/js/common/tests/shell-graph-traversal.js +++ b/js/common/tests/shell-graph-traversal.js @@ -640,8 +640,425 @@ function GraphTreeTraversalSuite () { ]; assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); - } + }, + //////////////////////////////////////////////////////////////////////////////// + /// @brief test minimal depth filter with depth 0 + //////////////////////////////////////////////////////////////////////////////// + + testMinDepthFilterWithDepth0 : function () { + var config = { + expander: expander, + filter: traversal.MinDepthFilter, + minDepth: 0 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + "vertices/Europe", + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/Asia", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/AU", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"], + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test minimal depth filter with depth 1 + //////////////////////////////////////////////////////////////////////////////// + + testMinDepthFilterWithDepth1 : function () { + var config = { + expander: expander, + filter: traversal.MinDepthFilter, + minDepth: 1 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/Europe", + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/Asia", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/AU", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test minimal depth filter with depth 2 + //////////////////////////////////////////////////////////////////////////////// + + testMinDepthFilterWithDepth2 : function () { + var config = { + expander: expander, + filter: traversal.MinDepthFilter, + minDepth: 2 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/US", + "vertices/MX", + "vertices/AU", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test maximal depth filter with depth 0 + //////////////////////////////////////////////////////////////////////////////// + + testMaxDepthFilterWithDepth0 : function () { + var config = { + expander: expander, + filter: traversal.MaxDepthFilter, + maxDepth: 0 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test maximal depth filter with depth 1 + //////////////////////////////////////////////////////////////////////////////// + + testMaxDepthFilterWithDepth1 : function () { + var config = { + expander: expander, + filter: traversal.MaxDepthFilter, + maxDepth: 1 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + "vertices/Europe", + "vertices/Asia", + "vertices/America", + "vertices/Australia", + "vertices/Africa", + "vertices/Antarctica", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"], + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test maximal depth filter with depth 2 + //////////////////////////////////////////////////////////////////////////////// + + testMaxDepthFilterWithDepth2 : function () { + var config = { + expander: expander, + filter: traversal.MaxDepthFilter, + maxDepth: 2 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + "vertices/Europe", + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/Asia", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/AU", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"], + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + // ----------------------------------------------------------------------------- + // --SECTION-- combineFilters + // ----------------------------------------------------------------------------- + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test combination of filters + //////////////////////////////////////////////////////////////////////////////// + + testCombineFilters : function () { + + var excluder1 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.exclude1) return "exclude"; + }; + + var excluder2 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.exclude2) return "exclude"; + }; + + var excluder3 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.exclude3) return "exclude"; + }; + + var pruner1 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.prune1) return "prune"; + }; + + var pruner2 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.prune2) return "prune"; + }; + + var config = { + expander: expander, + filter: [ + excluder1, + pruner1, + excluder2, + pruner2, + excluder3 + ], + exclude1: "Europe", + exclude2: "AU", + exclude3: "World", + prune1: "Asia", + prune2: "Europe" + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/Asia", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + }, + + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test if exclude or prune can be overridden in combined filters + //////////////////////////////////////////////////////////////////////////////// + + testOverrideExcludeAndPruneOfCombinedFilters : function () { + + var excludeAndPrune = function(config, vertex, path) { + if (vertex.name && vertex.name === config.excludeAndPrune) return ["prune", "exclude"]; + }; + + var config = { + expander: expander, + filter: [ + excludeAndPrune, + traversal.VisitAllFilter + ], + excludeAndPrune: "World" + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertices["vertices/World"]); + + var expectedVisits = []; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = []; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + } }; } @@ -930,10 +1347,10 @@ function CollectionTraversalSuite () { assertEqual(expectedVisits, getIds(result.visited.vertices)); } - }; } + // ----------------------------------------------------------------------------- // --SECTION-- main // -----------------------------------------------------------------------------