import { Queue } from "./Queue.mjs";
import { PriorityQueue } from "./PriorityQueue.mjs";
/**
* Represents an Node in the Graph.
*/
class Node {
/**
* @param {any} [val] - The value of the element
*/
constructor(value = null) {
this.value = value;
this.edges = new Map();
}
}
/**
* Graph Class
*/
class Graph {
constructor(directed = false) {
this.directed = directed;
this.nodes = new Map();
this.vertices = 0;
this.edgesNumber = 0;
}
_nodeExists(id) {
return this.nodes.has(id);
}
/**
* Adds a new node to the graph
* @param {number|string} id
* @param {any=} val
*/
addNode(id, value = null) {
if (typeof id === "string" || typeof id === "number") {
if (this._nodeExists(id)) throw new Error("Node already exists");
this.nodes.set(id, new Node(value));
this.vertices++;
} else throw new Error("Invalid node id type");
}
/**
* Updates value for an existing node
* @param {number|string} id
* @param {any=} val
* @returns {boolean}
*/
updateNode(id, value) {
if (!this._nodeExists(id)) throw new Error("Node does not exists");
this.nodes.set(id).value = value;
return true;
}
/**
* Remove the node, and its edges across all the nodes
* @param {number|string} id
* @returns {boolean}
*/
removeNode(id) {
if (!this._nodeExists(id)) throw new Error("Node does not exists");
if (!this.directed) {
// Remode edges based on edges at index element
this.nodes.get(id).edges.forEach((value, key) => {
this.nodes.get(key).edges.delete(id);
});
} else {
// Remode edges checking within each element
this.nodes.forEach((value) => {
if (value.edges.has(id)) value.edges.delete(id);
});
}
this.nodes.delete(id);
return true;
}
/**
* Returns true/false if edge exists between two nodes
* @param {number|string} nodeA
* @param {number|string} nodeB
* @returns {boolean} - True/False if connection exists between nodes
*/
nodesConnected(nodeA, nodeB) {
if (!this._nodeExists(nodeA) || !this._nodeExists(nodeB))
throw new Error("Node does not exists");
return this.nodes.get(nodeA).edges.has(nodeB);
}
/**
* Adds a new edge between two nodes
* @param {number|string} nodeA
* @param {number|string} nodeB
* @param {any=} weight - Optional, default 1 if not provided
*/
addEdge(nodeA, nodeB, weight = 1) {
if (!this._nodeExists(nodeA) || !this._nodeExists(nodeB))
throw new Error("Node does not exists");
if (this.nodes.get(nodeA).edges.has(nodeB))
throw new Error(`Edge(${nodeA}-${nodeB}) already exists`);
this.nodes.get(nodeA).edges.set(nodeB, weight);
if (!this.directed) this.nodes.get(nodeB).edges.set(nodeA, weight);
this.edgesNumber++;
}
/**
* Updates the weight of an existing edge between two nodes
* @param {number|string} nodeA
* @param {number|string} nodeB
* @param {weight=} weight - Optional, default 1 if not provided
* @returns {boolean}
*/
updateEdge(nodeA, nodeB, weight = 1) {
if (!this._nodeExists(nodeA) || !this._nodeExists(nodeB))
throw new Error("Node does not exists");
if (!this.nodes.get(nodeA).edges.has(nodeB))
throw new Error("Edge does not exists ");
this.nodes.get(nodeA).edges.set(nodeB, weight);
if (!this.directed) this.nodes.get(nodeB).edges.set(nodeA, weight);
this.edgesNumber++;
return true;
}
/**
* Removes an existing edge between two nodes
* @param {number|string} nodeA
* @param {number|string} nodeB
* @returns {boolean}
*/
removeEdge(nodeA, nodeB) {
if (!this._nodeExists(nodeA) || !this._nodeExists(nodeB))
throw new Error("Node does not exists");
if (!this.nodes.get(nodeA).edges.has(nodeB))
throw new Error("Edge does not exists ");
this.nodes.get(nodeA).edges.delete(nodeB);
if (!this.directed) this.nodes.get(nodeB).edges.delete(nodeA);
this.edgesNumber--;
return true;
}
/**
* Returns value of node
* @param {number|string} id
* @returns {any} - Value
*/
getNode(id) {
if (!this._nodeExists(id)) throw new Error("Node does not exists");
return this.nodes.get(id).value;
}
/**
* Returns array of the shortest weighted path between two nodes (Dijkstra algorithm)
* Based on the shortest number of intermediate nodes
* @param {number|string} nodeA
* @param {number|string} nodeB
* @returns {Array[]} - [{nodes: [], distance: number}]
*/
getPathWeighted(nodeA, nodeB) {
const distances = new Map();
const pq = new PriorityQueue("custom", (a, b) => a.distance - b.distance); // Using distance (min)
pq.add({ key: nodeA, distance: 0, nodes: [nodeA] });
for (let [key] of this.nodes)
distances.set(key, { nodes: [], distance: Infinity });
while (!pq.isEmpty) {
const element = pq.poll();
const edges = this.nodes.get(element.key).edges;
const currentNodeDistance = distances.get(element.key);
if (currentNodeDistance.distance > element.distance) {
// If distance is smaller than current
distances.set(element.key, {
nodes: element.nodes,
distance: element.distance,
});
edges.forEach((value, key) => {
pq.add({
key,
distance:
element.distance == Infinity ? value : element.distance + value,
nodes: [...element.nodes, key],
});
});
}
}
if (distances.get(nodeB).distance !== Infinity) return distances.get(nodeB);
throw new Error(`Nodes ${nodeA} and ${nodeB} are not connected`);
}
/**
* Returns array of the shortest path between two nodes
* Based on the shortest number of intermediate nodes
* @param {number|string} nodeA
* @param {number|string} nodeB
* @returns {Array[]} - [{node, weight}]
*/
getPath(nodeA, nodeB) {
if (!this._nodeExists(nodeA) || !this._nodeExists(nodeB))
throw new Error("Node does not exists");
if (nodeA === nodeB) return false;
// BFS
const visited = new Set();
const queue = new Queue();
queue.add({
edges: this.nodes.get(nodeA).edges,
nodePath: [{ node: nodeA, weight: 0 }],
});
visited.add(nodeA);
while (!queue.isEmpty) {
const { edges, nodePath } = queue.poll();
for (let [key, value] of edges) {
if (key === nodeB) return [...nodePath, { node: key, weight: value }];
if (!visited.has(key)) {
queue.add({
edges: this.nodes.get(key).edges,
nodePath: [...nodePath, { node: key, weight: value }],
});
visited.add(key);
}
}
}
throw new Error(`Nodes ${nodeA} and ${nodeB} are not connected`);
}
/**
* Returns number of nodes in the graph
* @returns {number} - Number of nodes
*/
get size() {
return this.vertices;
}
/**
* Returns number of edges in the graph
* @returns {number} - Number of edges
*/
get edges() {
return this.edgesNumber;
}
/**
* Returns the sum of weights between two nodes, including intermediate ones.
* Based on the shortest number of intermediate nodes
* @param {number|string} nodeA
* @param {number|string} nodeB
* @returns {number}
*/
getWeight(nodeA, nodeB) {
if (!this._nodeExists(nodeA) || !this._nodeExists(nodeB))
throw new Error("Node does not exists");
if (nodeA === nodeB) return 0;
// BFS
const visited = new Set();
const queue = new Queue();
queue.add({ edges: this.nodes.get(nodeA).edges, weight: 0 });
visited.add(nodeA);
while (!queue.isEmpty) {
const { edges, weight } = queue.poll();
for (let [key, value] of edges) {
if (key === nodeB) return weight + value;
if (!visited.has(key)) {
queue.add({
edges: this.nodes.get(key).edges,
weight: weight + value,
});
visited.add(key);
}
}
}
throw new Error(`Nodes ${nodeA} and ${nodeB} are not connected`);
}
}
export { Graph };