You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
9.8 KiB
281 lines
9.8 KiB
import { doBFS, doDFS } from './utils/traverse';
|
|
const defaultFilter = () => true;
|
|
export class GraphView {
|
|
graph;
|
|
nodeFilter;
|
|
edgeFilter;
|
|
// caches
|
|
cacheEnabled;
|
|
inEdgesMap = new Map();
|
|
outEdgesMap = new Map();
|
|
bothEdgesMap = new Map();
|
|
allNodesMap = new Map();
|
|
allEdgesMap = new Map();
|
|
constructor(options) {
|
|
this.graph = options.graph;
|
|
const nodeFilter = options.nodeFilter || defaultFilter;
|
|
const edgeFilter = options.edgeFilter || defaultFilter;
|
|
this.nodeFilter = nodeFilter;
|
|
this.edgeFilter = (edge) => {
|
|
const { source, target } = this.graph.getEdgeDetail(edge.id);
|
|
if (!nodeFilter(source) || !nodeFilter(target)) {
|
|
return false;
|
|
}
|
|
return edgeFilter(edge, source, target);
|
|
};
|
|
if (options.cache === 'auto') {
|
|
this.cacheEnabled = true;
|
|
this.startAutoCache();
|
|
}
|
|
else if (options.cache === 'manual') {
|
|
this.cacheEnabled = true;
|
|
}
|
|
else {
|
|
this.cacheEnabled = false;
|
|
}
|
|
}
|
|
/**
|
|
* Clear all cache data. Therefore `getAllNodes()` will return `[]`.
|
|
* If you want to disable caching, use `graphView.cacheEnabled = false` instead.
|
|
*/
|
|
clearCache = () => {
|
|
this.inEdgesMap.clear();
|
|
this.outEdgesMap.clear();
|
|
this.bothEdgesMap.clear();
|
|
this.allNodesMap.clear();
|
|
this.allEdgesMap.clear();
|
|
};
|
|
/**
|
|
* Fully refresh all cache data to the current graph state.
|
|
*/
|
|
refreshCache = () => {
|
|
this.clearCache();
|
|
this.updateCache(this.graph.getAllNodes().map((node) => node.id));
|
|
};
|
|
/**
|
|
* Instead of a fully refreshment, this method partially update the cache data by specifying
|
|
* involved(added, removed, updated) nodes. It's more efficient when handling small changes
|
|
* on a large graph.
|
|
*/
|
|
updateCache = (involvedNodeIds) => {
|
|
const involvedEdgeIds = new Set();
|
|
involvedNodeIds.forEach((id) => {
|
|
// Collect all involved old edges.
|
|
const oldEdgesSet = this.bothEdgesMap.get(id);
|
|
if (oldEdgesSet) {
|
|
oldEdgesSet.forEach((edge) => involvedEdgeIds.add(edge.id));
|
|
}
|
|
if (!this.hasNode(id)) {
|
|
// When an involved node becomes unvisitable:
|
|
// 1. Delete its related edges cache.
|
|
this.inEdgesMap.delete(id);
|
|
this.outEdgesMap.delete(id);
|
|
this.bothEdgesMap.delete(id);
|
|
// 2. Delete it from the allNodesMap.
|
|
this.allNodesMap.delete(id);
|
|
}
|
|
else {
|
|
// When an involved node becomes or stays visitable:
|
|
// 1. Collect its new edges.
|
|
const inEdges = this.graph
|
|
.getRelatedEdges(id, 'in')
|
|
.filter(this.edgeFilter);
|
|
const outEdges = this.graph
|
|
.getRelatedEdges(id, 'out')
|
|
.filter(this.edgeFilter);
|
|
const bothEdges = Array.from(new Set([...inEdges, ...outEdges]));
|
|
bothEdges.forEach((edge) => involvedEdgeIds.add(edge.id));
|
|
// 2. Update its related edges cache.
|
|
this.inEdgesMap.set(id, inEdges);
|
|
this.outEdgesMap.set(id, outEdges);
|
|
this.bothEdgesMap.set(id, bothEdges);
|
|
// 3. Add to allNodesMap.
|
|
this.allNodesMap.set(id, this.graph.getNode(id));
|
|
}
|
|
});
|
|
// Update allEdgesMap.
|
|
involvedEdgeIds.forEach((id) => {
|
|
if (this.hasEdge(id)) {
|
|
this.allEdgesMap.set(id, this.graph.getEdge(id));
|
|
}
|
|
else {
|
|
this.allEdgesMap.delete(id);
|
|
}
|
|
});
|
|
};
|
|
startAutoCache() {
|
|
this.refreshCache();
|
|
this.graph.on('changed', this.handleGraphChanged);
|
|
}
|
|
stopAutoCache() {
|
|
this.graph.off('changed', this.handleGraphChanged);
|
|
}
|
|
handleGraphChanged = (event) => {
|
|
// Collect all involved nodes.
|
|
const involvedNodeIds = new Set();
|
|
event.changes.forEach((change) => {
|
|
switch (change.type) {
|
|
case 'NodeAdded':
|
|
involvedNodeIds.add(change.value.id);
|
|
break;
|
|
case 'NodeDataUpdated':
|
|
involvedNodeIds.add(change.id);
|
|
break;
|
|
case 'EdgeAdded':
|
|
involvedNodeIds.add(change.value.source);
|
|
involvedNodeIds.add(change.value.target);
|
|
break;
|
|
case 'EdgeUpdated':
|
|
if (change.propertyName === 'source' ||
|
|
change.propertyName === 'target') {
|
|
involvedNodeIds.add(change.oldValue);
|
|
involvedNodeIds.add(change.newValue);
|
|
}
|
|
break;
|
|
case 'EdgeDataUpdated':
|
|
if (event.graph.hasEdge(change.id)) {
|
|
const edge = event.graph.getEdge(change.id);
|
|
involvedNodeIds.add(edge.source);
|
|
involvedNodeIds.add(edge.target);
|
|
}
|
|
break;
|
|
case 'EdgeRemoved':
|
|
involvedNodeIds.add(change.value.source);
|
|
involvedNodeIds.add(change.value.target);
|
|
break;
|
|
case 'NodeRemoved':
|
|
involvedNodeIds.add(change.value.id);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
// Update their caches.
|
|
this.updateCache(involvedNodeIds);
|
|
};
|
|
// ================= Node =================
|
|
checkNodeExistence(id) {
|
|
this.getNode(id);
|
|
}
|
|
hasNode(id) {
|
|
if (!this.graph.hasNode(id))
|
|
return false;
|
|
const node = this.graph.getNode(id);
|
|
return this.nodeFilter(node);
|
|
}
|
|
areNeighbors(firstNodeId, secondNodeId) {
|
|
this.checkNodeExistence(firstNodeId);
|
|
return this.getNeighbors(secondNodeId).some((neighbor) => neighbor.id === firstNodeId);
|
|
}
|
|
getNode(id) {
|
|
const node = this.graph.getNode(id);
|
|
if (!this.nodeFilter(node)) {
|
|
throw new Error('Node not found for id: ' + id);
|
|
}
|
|
return node;
|
|
}
|
|
getRelatedEdges(id, direction) {
|
|
this.checkNodeExistence(id);
|
|
if (this.cacheEnabled) {
|
|
if (direction === 'in') {
|
|
return this.inEdgesMap.get(id);
|
|
}
|
|
else if (direction === 'out') {
|
|
return this.outEdgesMap.get(id);
|
|
}
|
|
else {
|
|
return this.bothEdgesMap.get(id);
|
|
}
|
|
}
|
|
const edges = this.graph.getRelatedEdges(id, direction);
|
|
return edges.filter(this.edgeFilter);
|
|
}
|
|
getDegree(id, direction) {
|
|
return this.getRelatedEdges(id, direction).length;
|
|
}
|
|
getSuccessors(id) {
|
|
const outEdges = this.getRelatedEdges(id, 'out');
|
|
const targets = outEdges.map((edge) => this.getNode(edge.target));
|
|
return Array.from(new Set(targets));
|
|
}
|
|
getPredecessors(id) {
|
|
const inEdges = this.getRelatedEdges(id, 'in');
|
|
const sources = inEdges.map((edge) => this.getNode(edge.source));
|
|
return Array.from(new Set(sources));
|
|
}
|
|
getNeighbors(id) {
|
|
const predecessors = this.getPredecessors(id);
|
|
const successors = this.getSuccessors(id);
|
|
return Array.from(new Set([...predecessors, ...successors]));
|
|
}
|
|
// ================= Edge =================
|
|
hasEdge(id) {
|
|
if (!this.graph.hasEdge(id))
|
|
return false;
|
|
const edge = this.graph.getEdge(id);
|
|
return this.edgeFilter(edge);
|
|
}
|
|
getEdge(id) {
|
|
const edge = this.graph.getEdge(id);
|
|
if (!this.edgeFilter(edge)) {
|
|
throw new Error('Edge not found for id: ' + id);
|
|
}
|
|
return edge;
|
|
}
|
|
getEdgeDetail(id) {
|
|
const edge = this.getEdge(id);
|
|
return {
|
|
edge,
|
|
source: this.getNode(edge.source),
|
|
target: this.getNode(edge.target),
|
|
};
|
|
}
|
|
// ================= Tree =================
|
|
hasTreeStructure(treeKey) {
|
|
return this.graph.hasTreeStructure(treeKey);
|
|
}
|
|
getRoots(treeKey) {
|
|
return this.graph.getRoots(treeKey).filter(this.nodeFilter);
|
|
}
|
|
getChildren(id, treeKey) {
|
|
this.checkNodeExistence(id);
|
|
return this.graph.getChildren(id, treeKey).filter(this.nodeFilter);
|
|
}
|
|
getParent(id, treeKey) {
|
|
this.checkNodeExistence(id);
|
|
const parent = this.graph.getParent(id, treeKey);
|
|
if (!parent || !this.nodeFilter(parent))
|
|
return null;
|
|
return parent;
|
|
}
|
|
// ================= Graph =================
|
|
getAllNodes() {
|
|
if (this.cacheEnabled) {
|
|
return Array.from(this.allNodesMap.values());
|
|
}
|
|
return this.graph.getAllNodes().filter(this.nodeFilter);
|
|
}
|
|
getAllEdges() {
|
|
if (this.cacheEnabled) {
|
|
return Array.from(this.allEdgesMap.values());
|
|
}
|
|
return this.graph.getAllEdges().filter(this.edgeFilter);
|
|
}
|
|
bfs(id, fn, direction = 'out') {
|
|
const navigator = {
|
|
in: this.getPredecessors.bind(this),
|
|
out: this.getSuccessors.bind(this),
|
|
both: this.getNeighbors.bind(this),
|
|
}[direction];
|
|
doBFS([this.getNode(id)], new Set(), fn, navigator);
|
|
}
|
|
dfs(id, fn, direction = 'out') {
|
|
const navigator = {
|
|
in: this.getPredecessors.bind(this),
|
|
out: this.getSuccessors.bind(this),
|
|
both: this.getNeighbors.bind(this),
|
|
}[direction];
|
|
doDFS(this.getNode(id), new Set(), fn, navigator);
|
|
}
|
|
}
|
|
//# sourceMappingURL=graphView.js.map
|