Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
*.log
coverage
.nyc_output
.nyc_output
.idea
49 changes: 45 additions & 4 deletions libs/Graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@
* @param {boolean} [options.trim] - Exclude the origin and destination nodes from the result
* @param {boolean} [options.reverse] - Return the path in reversed order
* @param {boolean} [options.cost] - Also return the cost of the path when set to true
* @param {number} [options.maxCost] - Only consider paths with total cost less than or equal to this value
* @param {number} [options.maxNodes]- Maximum number of nodes allowed in the resulting path (including start and goal)
* @param {function} [options.allowedCallback] - A predicate invoked for each potential edge expansion. Receives an object { from, to, cost, accumulatedCost, depth } and must return true to allow the move
*
* @return {array|object} Computed path between the nodes.
*
Expand Down Expand Up @@ -179,6 +182,7 @@
const explored = new Set();
const frontier = new Queue();
const previous = new Map();
const depth = new Map();

let path = [];
let totalCost = 0;
Expand All @@ -192,14 +196,28 @@
throw new Error(`Ending node (${goal}) cannot be avoided`);
}

const hasMaxCost = typeof options.maxCost === 'number' && !Number.isNaN(options.maxCost);

Check failure on line 199 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (18.x)

Replace `·typeof·options.maxCost·===·'number'` with `⏎······typeof·options.maxCost·===·"number"`

Check failure on line 199 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (16.x)

Replace `·typeof·options.maxCost·===·'number'` with `⏎······typeof·options.maxCost·===·"number"`
const maxCost = hasMaxCost ? Number(options.maxCost) : undefined;
const hasMaxNodes = typeof options.maxNodes === 'number' && !Number.isNaN(options.maxNodes);

Check failure on line 201 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (18.x)

Replace `·typeof·options.maxNodes·===·'number'` with `⏎······typeof·options.maxNodes·===·"number"`

Check failure on line 201 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (16.x)

Replace `·typeof·options.maxNodes·===·'number'` with `⏎······typeof·options.maxNodes·===·"number"`
const maxNodes = hasMaxNodes ? Math.max(1, Math.floor(options.maxNodes)) : undefined;

Check failure on line 202 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (18.x)

Replace `·?·Math.max(1,·Math.floor(options.maxNodes))` with `⏎······?·Math.max(1,·Math.floor(options.maxNodes))⏎·····`

Check failure on line 202 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (16.x)

Replace `·?·Math.max(1,·Math.floor(options.maxNodes))` with `⏎······?·Math.max(1,·Math.floor(options.maxNodes))⏎·····`
const allowedCallback = typeof options.allowedCallback === 'function' ? options.allowedCallback : null;

Check failure on line 203 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (18.x)

Replace `·typeof·options.allowedCallback·===·'function'·?·options.allowedCallback` with `⏎······typeof·options.allowedCallback·===·"function"⏎········?·options.allowedCallback⏎·······`

Check failure on line 203 in libs/Graph.js

View workflow job for this annotation

GitHub Actions / tests (16.x)

Replace `·typeof·options.allowedCallback·===·'function'·?·options.allowedCallback` with `⏎······typeof·options.allowedCallback·===·"function"⏎········?·options.allowedCallback⏎·······`

// Add the starting point to the frontier, it will be the first node visited
frontier.set(start, 0);
depth.set(start, 1);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should depth start at 0 or 1 for the root node?


// Run until we have visited every node in the frontier
while (!frontier.isEmpty()) {
// Get the node in the frontier with the lowest cost (`priority`)
const node = frontier.next();

// If the cheapest node already exceeds maxCost, no valid path can be found
if (hasMaxCost && node.priority > maxCost) {
// ensure path remains empty so we return null later
path = [];
break;
}

// When the node with the lowest cost in the frontier in our goal node,
// we can compute the path and exit the loop
if (node.key === goal) {
Expand All @@ -224,21 +242,44 @@
// If we already explored the node, or the node is to be avoided, skip it
if (explored.has(nNode) || avoid.includes(nNode)) return null;

const newCost = node.priority + nCost;
const currentDepth = depth.get(node.key) || 1;
const newDepth = currentDepth + 1;

// Enforce maximum number of nodes in the resulting path (including start and goal)
if (hasMaxNodes && newDepth > maxNodes) return null;

// Enforce maximum cost constraint
if (hasMaxCost && newCost > maxCost) return null;

// If provided, consult the allowedCallback to decide whether to expand this edge
if (allowedCallback) {
const allowed = allowedCallback({
from: node.key,
to: nNode,
cost: nCost,
accumulatedCost: newCost,
depth: newDepth,
});
if (!allowed) return null;
}

// If the neighboring node is not yet in the frontier, we add it with
// the correct cost
if (!frontier.has(nNode)) {
previous.set(nNode, node.key);
return frontier.set(nNode, node.priority + nCost);
depth.set(nNode, newDepth);
return frontier.set(nNode, newCost);
}

const frontierPriority = frontier.get(nNode).priority;
const nodeCost = node.priority + nCost;

// Otherwise we only update the cost of this node in the frontier when
// it's below what's currently set
if (nodeCost < frontierPriority) {
if (newCost < frontierPriority) {
previous.set(nNode, node.key);
return frontier.set(nNode, nodeCost);
depth.set(nNode, newDepth);
return frontier.set(nNode, newCost);
}

return null;
Expand Down
160 changes: 160 additions & 0 deletions libs/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
declare class Graph {
/**
* Creates a new Graph, optionally initializing it a nodes graph representation.
*
* A graph representation is an object that has as keys the name of the point and as values
* the points reachable from that node, with the cost to get there:
*
* {
* node (Number|String): {
* neighbor (Number|String): cost (Number),
* ...,
* },
* }
*
* In alternative to an object, you can pass a `Map` of `Map`. This will
* allow you to specify numbers as keys.
*
* @param [graph] - Initial graph definition
* @example
*
* const route = new Graph();
*
* // Pre-populated graph
* const route = new Graph({
* A: { B: 1 },
* B: { A: 1, C: 2, D: 4 },
* });
*
* // Passing a Map
* const g = new Map()
*
* const a = new Map()
* a.set('B', 1)
*
* const b = new Map()
* b.set('A', 1)
* b.set('C', 2)
* b.set('D', 4)
*
* g.set('A', a)
* g.set('B', b)
*
* const route = new Graph(g)
*/
constructor(nodes?: { [key: string]: { [key: string]: number } } | Map<string, Map<string, number>>);

/**
* Adds a node to the graph
*
* @param name - Name of the node
* @param neighbors - Neighboring nodes and cost to reach them
* @example
*
* const route = new Graph();
*
* route.addNode('A', { B: 1 });
*
* // It's possible to chain the calls
* route
* .addNode('B', { A: 1 })
* .addNode('C', { A: 3 });
*
* // The neighbors can be expressed in a Map
* const d = new Map()
* d.set('A', 2)
* d.set('B', 8)
*
* route.addNode('D', d)
*/
addNode(name: string, neighbors: any): Graph;

/**
* Removes a node and all of its references from the graph
*
* @param key - Key of the node to remove from the graph
* @example
*
* const route = new Graph({
* A: { B: 1, C: 5 },
* B: { A: 3 },
* C: { B: 2, A: 2 },
* });
*
* route.removeNode('C');
* // The graph now is:
* // { A: { B: 1 }, B: { A: 3 } }
*/
removeNode(name: string): Graph;

/**
* Compute the shortest path between the specified nodes
*
* @param start - Starting node
* @param goal - Node we want to reach
* @param [options] - Options
*
* @param [options.trim] - Exclude the origin and destination nodes from the result
* @param [options.reverse] - Return the path in reversed order
* @param [options.cost] - Also return the cost of the path when set to true
* @param [options.maxCost] - Only consider paths with total cost ≤ this value
* @param [options.maxNodes]- Maximum number of nodes allowed in the resulting path (including start and goal)
* @param [options.allowedCallback] - Predicate called for each potential expansion; returns true to allow the move
*
* @return Computed path between the nodes.
*
* When `option.cost` is set to true, the returned value will be an object with shape:
* - `path` *(Array)*: Computed path between the nodes
* - `cost` *(Number)*: Cost of the path
*
* @example
*
* const route = new Graph()
*
* route.addNode('A', { B: 1 })
* route.addNode('B', { A: 1, C: 2, D: 4 })
* route.addNode('C', { B: 2, D: 1 })
* route.addNode('D', { C: 1, B: 4 })
*
* route.path('A', 'D') // => ['A', 'B', 'C', 'D']
*
* // trimmed
* route.path('A', 'D', { trim: true }) // => [B', 'C']
*
* // reversed
* route.path('A', 'D', { reverse: true }) // => ['D', 'C', 'B', 'A']
*
* // include the cost
* route.path('A', 'D', { cost: true })
* // => {
* // path: [ 'A', 'B', 'C', 'D' ],
* // cost: 4
* // }
*/
path(start: any, goal: any, options?: PathOption): string[] | PathResult;
}

interface PathOption {
trim?: boolean | undefined;
reverse?: boolean | undefined;
cost?: boolean | undefined;
avoid?: any[] | undefined;
maxCost?: number | undefined;
maxNodes?: number | undefined;
allowedCallback?: ((arg: AllowedCallbackArg) => boolean) | undefined;
}

interface AllowedCallbackArg {
from: string;
to: string;
cost: number;
accumulatedCost: number;
depth: number;
}

interface PathResult {
path: string[] | null;
cost: number;
}

export = Graph;
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "A NodeJS implementation of Dijkstra's algorithm",
"author": "Alberto Restifo <alberto.restifo@gmail.com>",
"main": "libs/Graph.js",
"types": "libs/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/albertorestifo/node-dijkstra"
Expand Down
Loading