traffic-traversal
Version:
Calculate the weights between each vertex node and help you find the fastest route.
517 lines (510 loc) • 15.1 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
TrafficGraph: () => TrafficGraph,
TrafficTraversal: () => TrafficTraversal
});
module.exports = __toCommonJS(src_exports);
// src/Utils/Array.ts
function ensure(array, ...items) {
for (const t of items) {
if (array.includes(t)) {
continue;
}
array.push(t);
}
}
function has(array, ...items) {
for (const t of items) {
if (!array.includes(t)) {
return false;
}
}
return true;
}
function first(array) {
return array.at(0);
}
function last(array) {
return array.at(-1);
}
function copy(array) {
return [].concat(array);
}
// src/Utils/Object.ts
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
// src/TrafficGraph.ts
var TrafficGraph = class _TrafficGraph {
/**
* It is an object that contains weight information between the vertex of the graph.
*/
_data;
/**
* Create a new graph instance. You can generate from existing data using `data` parameters.
* @param data You can restore it with existing data.This data can be obtained by `TrafficGraph.data`.
*/
constructor(data = {
vertex: {},
embedded: []
}) {
this._data = data;
}
static Create(...args) {
return new _TrafficGraph(...args);
}
/**
* Returns to an array in the form that can serialize the graph information of the current instance.
*/
get data() {
return deepCopy(this._data);
}
/**
* The current status of the instance is exported to an immutable object.
*/
get state() {
const data = Object.freeze(this.data);
const vertices = Object.freeze(this.vertices);
const timestamp = Date.now();
const state = Object.freeze({
data,
vertices,
timestamp
});
return state;
}
/**
* Returns all the vertices listed in the current instance in an array.
*/
get vertices() {
const inQueue = {};
const vertices = [];
for (const k in this._data.vertex) {
if (!(k in inQueue)) {
inQueue[k] = true;
vertices.push(k);
}
const gv = this._data.vertex[k];
for (const v in gv) {
if (!(v in inQueue)) {
inQueue[v] = true;
vertices.push(v);
}
}
}
return vertices;
}
/**
* Currently copied instance and returns to a new instance.
*/
get clone() {
return new _TrafficGraph(this._data);
}
_graphVertex(vertex) {
return vertex in this._data.vertex ? this._data.vertex[vertex] : {};
}
_embed(vertex) {
ensure(this._data.embedded, vertex);
}
_calculate(vertices, vertex, calc) {
const before = vertex in vertices ? vertices[vertex] : 0;
const symbol = calc.substring(0, 2);
const value = Number(calc.substring(2));
switch (symbol) {
case "+=": {
return before + value;
}
case "-=": {
return before - value;
}
case "*=": {
return before * value;
}
case "/=": {
return before / value;
}
default: {
throw new Error(`The string formula that can be used is as follows: +=, -=, *=, /=`);
}
}
}
/**
* Create a single direction weight route. It is possible to traverse the `source` to `dest`, but vice versa is impossible.
* If you had the same vertex before, the value is overwritten.
* @param source The starting vertex.
* @param dest This is a list of weights of each vertex. You can specify relative values. If you fill in the prior character `+=`, `-=`, `*=`, `/=`, The target value is calculated based on the current value of the property.
* @example
* graph.to('a', { b: 1 })
* graph.to('a', { b: '+=1' })
*/
to(source, dest) {
if (!(source in this._data.vertex)) {
this._data.vertex[source] = {};
this._embed(source);
}
const gv = this._graphVertex(source);
for (const v in dest) {
if (source === v) {
continue;
}
let w = dest[v];
if (typeof w === "string") {
w = this._calculate(gv, v, w);
}
gv[v] = w;
this._embed(v);
}
return this;
}
/**
* Set the weight route that leads to both directions between the two vertices. 'a' vertex and 'b' vertex can traverse to each other.
* For example, `graph.both('a', { b: 1 })` is same as `graph.to('a', { b: 1 }).to('b', { a: 1 })`
* @param a The vertex a.
* @param b This is a list of weights of each vertex. You can specify relative values. If you fill in the prior character `+=`, `-=`, `*=`, `/=`, The target value is calculated based on the current value of the property.
* @example
* graph.both('a', { b: 1 })
* graph.both('a', { b: '+=1' })
*/
both(a, b) {
this.to(a, b);
for (const v in b) {
const gv = {};
const w = b[v];
gv[a] = w;
this.to(v, gv);
}
return this;
}
/**
* Set the weight between all vertices passed by parameters.
* For example, `graph.all({ a: 1, b: 2, c: 3 })` is same as `graph.to('a', { b: 2, c: 3 }).to('b', { a: 1, c: 3 }).to('c', { a: 1, b: 2 })`
* @param dest This is a list of weights of each vertex. You can specify relative values. If you fill in the prior character `+=`, `-=`, `*=`, `/=`, The target value is calculated based on the current value of the property.
* @example
* graph.all({ a: 1, b: 2, c: 3 })
* graph.all({ a: '+=1', b: '+=1', c: '+=1' })
*/
all(dest) {
for (const v in dest) {
this.to(v, dest);
}
return this;
}
/**
* Delete the single direction weight route created by the `to` method.
* @param source The starting vertex.
* @param dest The target vertex.
*/
unlinkTo(source, dest) {
const gv = this._graphVertex(source);
delete gv[dest];
return this;
}
/**
* Delete the bidirectional weight route created by the `both` method.
* @param a The vertex a.
* @param b The vertex b.
*/
unlinkBoth(a, b) {
this.unlinkTo(a, b);
this.unlinkTo(b, a);
return this;
}
/**
* Delete certain vertices. All weight routes connected to the vertex are deleted.
* @param vertex The vertex what you want to delete.
*/
drop(vertex) {
for (const v in this._data.vertex) {
const gv = this._data.vertex[v];
delete gv[vertex];
}
delete this._data.vertex[vertex];
return this;
}
/**
* It returns whether the instance has a vertex.
* @param vertex The vertex what you want to check.
*/
has(vertex) {
return this.vertices.includes(vertex);
}
/**
* It returns whether all the vertices exist in that instance. Returns `false` if any of the vertices are missing.
* @param vertices The vertices what you want to check.
*/
hasAll(...vertices) {
return has(this.vertices, ...vertices);
}
/**
* Invert all weights in an instance. For example, when A to B has a `2` weight, it will be `-2`.
* It's useful for switching the shortest to longest routes or minimum to maximum traffic in a graph.
* @example
* const inverted = TrafficTraversal.Create(traffic.invert().state)
* const longest = invertedTraversal.routes('A', 'B')
*/
invert() {
for (const k in this._data.vertex) {
const gv = this._data.vertex[k];
for (const v in gv) {
gv[v] *= -1;
}
}
return this;
}
};
// src/Utils/Hashmap.ts
function useHashmap() {
const _map = /* @__PURE__ */ new Map();
const set = (key, data) => {
_map.set(key, data);
return data;
};
const has2 = (key) => _map.has(key);
const get = (key) => _map.get(key);
const ensure2 = (key, callback) => {
if (has2(key)) {
return get(key);
}
return set(key, callback());
};
return {
set,
has: has2,
get,
ensure: ensure2
};
}
// src/TrafficTraversal.ts
var TrafficTraversal = class _TrafficTraversal {
_trafficGraph;
_cEdge;
_cStrings;
_cNumber;
_cNumbers;
_cVertex;
static Create(...args) {
return new _TrafficTraversal(...args);
}
/**
* Create an instance that is responsible for the route and utility functions of the graph instance. It takes a `graph.state` instance as a parameter.
* @param trafficGraphState
*/
constructor(trafficGraphState) {
this._trafficGraph = trafficGraphState;
this._cEdge = useHashmap();
this._cStrings = useHashmap();
this._cNumber = useHashmap();
this._cNumbers = useHashmap();
this._cVertex = useHashmap();
}
_graphVertex(vertex) {
return this._cVertex.ensure(`vertices from '${vertex}'`, () => {
return this._trafficGraph.data.vertex[vertex] ?? {};
});
}
_getTrafficPrev(from) {
return this._cEdge.ensure(`traffic map from '${from}'`, () => {
const queue = [];
const inQueue = {};
const distance = {};
const edge = {};
for (const v of this._trafficGraph.vertices) {
distance[v] = Infinity;
edge[v] = "";
}
distance[from] = 0;
inQueue[from] = true;
queue.push(from);
while (queue.length) {
const u = queue.shift();
const vertices = this._graphVertex(u);
for (const v in vertices) {
const d_u = distance[u];
const d_v = distance[v];
const w_uv = vertices[v];
const cur = d_u + w_uv;
if (cur < d_v) {
distance[v] = cur;
edge[v] = u;
if (!inQueue[v]) {
inQueue[v] = true;
queue.push(v);
}
}
}
}
return edge;
});
}
_getRoutes(from, to) {
const routes = this._cStrings.ensure(`route ${from} to ${to}`, () => {
const inQueue = {};
const prev = this._getTrafficPrev(from);
const routes2 = [to];
let v = prev[to];
while (v) {
inQueue[v] = true;
routes2.push(v);
v = prev[v];
if (v === from) {
routes2.push(from);
break;
} else if (inQueue[v]) {
break;
}
}
routes2.reverse();
return routes2;
});
return routes;
}
_reach(routes, from, to) {
return first(routes) === from && last(routes) === to;
}
/**
* Finds the route with the lowest weight between two vertices and returns it as an array.
* @param from This is the starting vertex.
* @param to This is the target vertex.
*/
routes(from, to) {
const routes = this._getRoutes(from, to);
if (!this._reach(routes, from, to)) {
throw new Error(`It is a structure that cannot be reached from vertex '${from}' to '${to}'.`);
}
return copy(routes);
}
_addEdges(vertex, depth, curDepth, depthQueue, inQueue) {
const gv = this._graphVertex(vertex);
const done = curDepth === depth;
if (done) {
return;
}
for (const v in gv) {
depthQueue[v] = curDepth;
if (inQueue[v]) {
continue;
}
inQueue[v] = true;
this._addEdges(v, depth, curDepth + 1, depthQueue, inQueue);
}
}
/**
* Returns a list of vertices adjacent to that vertex as an array. You can set a depth limit using the `depth` parameter.
* This means that the vertex at the back of the array is deeper.
* @param vertex The vertex from which to start the search.
* @param depth Set how deep to search from the vertex. If you specify this value as a negative number, the search is unrestricted. Default is `-1`.
*/
edges(vertex, depth = -1) {
return this._cStrings.ensure(`edges from '${vertex}' with '${depth}' depth`, () => {
const depths = {};
this._addEdges(vertex, depth, 0, depths, {});
const stack = Object.entries(depths).sort((a, b) => a[1] - b[1]).flatMap((t) => t[0]);
return stack;
});
}
/**
* Returns whether the target vertex can be reached from the starting vertex.
* @param from This is the starting vertex.
* @param to This is the target vertex.
*/
reachable(from, to) {
return this._reach(this._getRoutes(from, to), from, to);
}
/**
* Returns the sum of the least weighted routes from the starting vertex to the target vertex.
* @param from This is the starting vertex.
* @param to This is the target vertex.
*/
traffic(from, to) {
return this._cNumber.ensure(`traffic from '${from}' to '${to}'`, () => {
const routes = this._getRoutes(from, to);
if (!this._reach(routes, from, to)) {
return Infinity;
}
let vertex = first(routes);
let traffic = 0;
let i = 0;
while (vertex) {
i++;
const next = routes[i];
const gv = this._graphVertex(vertex);
if (!(next in gv)) {
break;
}
traffic += gv[next];
vertex = next;
}
return traffic;
});
}
weight(vertex, mode) {
const tuple = this._cNumbers.ensure(`weight tuple from '${vertex}'`, () => {
let weight = 0;
let num = 0;
for (const k in this._trafficGraph.data.vertex) {
const gv = this._trafficGraph.data.vertex[k];
if (!(vertex in gv)) {
continue;
}
weight += gv[vertex];
num++;
}
return [weight, num];
});
switch (mode) {
case "traffic":
return tuple[0];
case "number":
return tuple[1];
case "mean": {
const mean = tuple[0] / tuple[1];
return Number.isNaN(mean) ? 0 : mean;
}
default: {
throw new Error(`The '${mode}' mode is unsupported.`);
}
}
}
/**
* Returns the shortest distance from the starting vertex to the target vertex. This is similar to the `distance` method, but takes direction into account. If unreachable, returns `Infinity`.
* @param from This is the starting vertex.
* @param to This is the target vertex.
* @returns
*/
depth(from, to) {
return this._cNumber.ensure(`depth from '${from}' to '${to}'`, () => {
const routes = this._getRoutes(from, to);
if (!this._reach(routes, from, to)) {
return Infinity;
}
return routes.length - 1;
});
}
/**
* Returns the shortest distance between two vertices. This is similar to the `depth` method, but does not take direction into account.
* @param a The vertex a.
* @param b The vertex b.
*/
distance(a, b) {
return Math.min(this.depth(a, b), this.depth(b, a));
}
};