690 lines
20 KiB
TypeScript
690 lines
20 KiB
TypeScript
/**
|
|
* @author Martin Karkowski
|
|
* @email m.karkowski@zema.de
|
|
* @create date 2019-04-24 09:31:13
|
|
* @modify date 2020-11-25 12:06:48
|
|
* @desc [description]
|
|
*/
|
|
|
|
import { EventEmitter } from "events";
|
|
/** Import Undo-Stack */
|
|
import * as SimpleUndo from "simple-undo";
|
|
import { dynamicSort } from "../../../lib/helpers/arrayMethods";
|
|
import { generateId } from "../../../lib/helpers/idMethods";
|
|
import { parse, stringify } from "../../../lib/helpers/jsonMethods";
|
|
import { rsetattr } from "../../../lib/helpers/objectMethods";
|
|
import * as vis from "../../visjs/vis";
|
|
import { flattenData } from "./helpers/data.handlers";
|
|
import { sortNodes } from "./helpers/sort.nodes";
|
|
import { IBaseEdgeOptions } from "./interfaces/IBaseEdgeOptions";
|
|
import { IBaseNodeOptions } from "./interfaces/IBaseNodeOptions";
|
|
import { IClusters } from "./interfaces/IClusters";
|
|
import { IEvent } from "./interfaces/IEvents";
|
|
import { IBaseGraph, IUndoRedoGraph } from "./interfaces/IGraph";
|
|
import { INetwork } from "./interfaces/INetwork";
|
|
import { IVisjsOptions } from "./interfaces/IVisjsOptions";
|
|
|
|
|
|
export class BaseGraph<N, E> implements IBaseGraph<N, E> {
|
|
|
|
public visNetwork: any;
|
|
private _data: { nodes: any, edges: any };
|
|
private _emitter: EventEmitter;
|
|
|
|
/**
|
|
* Containing the SVG-Drawing
|
|
*/
|
|
public drawing: any;
|
|
|
|
public get network() {
|
|
return this.visNetwork;
|
|
}
|
|
|
|
public get edges(): Array<IBaseEdgeOptions<E>> {
|
|
return this._data.edges.get();
|
|
}
|
|
|
|
public get nodes(): Array<IBaseNodeOptions<N>> {
|
|
return this._data.nodes.get();
|
|
}
|
|
|
|
public get data() {
|
|
return this._data;
|
|
}
|
|
|
|
public get emitter() {
|
|
return this._emitter;
|
|
}
|
|
|
|
constructor(
|
|
/** The Reference, where the Graph should be rednered in */
|
|
protected _reference: React.RefObject<any>,
|
|
/** The SVG Options. See Options on http://visjs.org/docs/network/ */
|
|
options: IVisjsOptions = {
|
|
edges: { smooth: false },
|
|
physics: {
|
|
enabled: false,
|
|
},
|
|
}
|
|
) {
|
|
/** Create the corresponding Data Sets */
|
|
this._data = {
|
|
nodes: new vis.DataSet(),
|
|
edges: new vis.DataSet()
|
|
};
|
|
|
|
/** If no custom method for adding an edge, is provided => generate one */
|
|
if (options.manipulation === undefined || options.manipulation === false) {
|
|
options.manipulation = {};
|
|
rsetattr(options, "manipulation.enabled", false);
|
|
}
|
|
|
|
options.autoResize = false;
|
|
|
|
// Element Height = Viewport height - element.offset.top - desired bottom margin
|
|
// options.height = _parent.current
|
|
options.height = (_reference.current.parentElement.clientHeight - 48).toString() + "px";
|
|
|
|
/** Create the VIS-Network */
|
|
this.visNetwork = new vis.Network(_reference.current, this._data, options);
|
|
|
|
/** Create an extra Emitter */
|
|
this._emitter = new EventEmitter();
|
|
this._emitter.setMaxListeners(Number.MAX_SAFE_INTEGER - 1);
|
|
}
|
|
|
|
public addNode(element: Array<IBaseNodeOptions<N>> | IBaseNodeOptions<N>) {
|
|
/** Add the Node Data. */
|
|
if (Array.isArray(element)) {
|
|
/** Make shure Flattend Data is used. */
|
|
this._data.nodes.add(flattenData(element));
|
|
} else {
|
|
|
|
this._data.nodes.add(element);
|
|
}
|
|
}
|
|
|
|
public loadJSON(data: string, overwrite = true, unsafe = true) {
|
|
/** Read In the JSON File */
|
|
const content = parse(data, unsafe);
|
|
/** Load the Data itself */
|
|
this.loadData(content, overwrite);
|
|
}
|
|
|
|
public getJSON(unsafe = true){
|
|
const content = this.getData();
|
|
return stringify(unsafe);
|
|
}
|
|
|
|
public userAddNode(){
|
|
const _this = this;
|
|
|
|
return new Promise<IBaseNodeOptions<any>>((resolve,reject) => {
|
|
_this.network.addNodeMode();
|
|
_this.once("addNode", resolve);
|
|
_this.once("mode", (mode)=> {
|
|
if (mode !== "none" && mode !== "add-node"){
|
|
reject(mode);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
public userAddEdge(){
|
|
const _this = this;
|
|
|
|
return new Promise<IBaseEdgeOptions<any>>((resolve,reject) => {
|
|
_this.network.addEdgeMode();
|
|
_this.once("addEdge", resolve);
|
|
_this.once("mode", (mode)=> {
|
|
if (mode !== "none" && mode !== "add-edge"){
|
|
reject(mode);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
public disableEditMode(){
|
|
this.network.disableEditMode();
|
|
}
|
|
|
|
public removeNode(id: string | number) {
|
|
try {
|
|
/** Try removing the Node */
|
|
this.visNetwork.body.data;
|
|
|
|
this._data.nodes.remove({ id });
|
|
} catch (err) {
|
|
console.error("Removing Node" + err);
|
|
}
|
|
}
|
|
|
|
public updateNode(options: Array<IBaseNodeOptions<N>> | IBaseNodeOptions<N>) {
|
|
try {
|
|
/** Try updateing the Node. */
|
|
this._data.nodes.update(options);
|
|
} catch (err) {
|
|
console.error("Upadting Node" + err);
|
|
}
|
|
}
|
|
|
|
public getNode<T=N>(id) {
|
|
return this._data.nodes.get(id) as IBaseNodeOptions<T>;
|
|
}
|
|
|
|
public getEdge<T=E>(id) {
|
|
return this._data.edges.get(id) as IBaseEdgeOptions<T>;
|
|
}
|
|
|
|
public clearNodes() {
|
|
this._data.nodes.clear();
|
|
}
|
|
|
|
public clearEdges() {
|
|
this._data.edges.clear();
|
|
}
|
|
|
|
public addEdge(values: IBaseEdgeOptions<E> | Array<IBaseEdgeOptions<E>>) {
|
|
/** Add the Node Data. */
|
|
this._data.edges.add(values);
|
|
}
|
|
|
|
public updateEdge(values: IBaseEdgeOptions<E> | Array<IBaseEdgeOptions<E>>) {
|
|
try {
|
|
/** Try to Update the Edge-Data */
|
|
this._data.edges.update(values);
|
|
} catch (err) {
|
|
console.error("Upadting Edge" + err);
|
|
}
|
|
}
|
|
|
|
public removeEdge(id: string | number) {
|
|
try {
|
|
/** Try to remove the Edge */
|
|
this._data.edges.remove({ id });
|
|
} catch (err) {
|
|
console.error("Removing Edge" + err);
|
|
}
|
|
}
|
|
|
|
public clear() {
|
|
/** Clearsout existing Nodes and Edges */
|
|
this.clearNodes();
|
|
this.clearEdges();
|
|
}
|
|
|
|
public destroy() {
|
|
/** Destroys the complete Rendered Graph */
|
|
this.visNetwork.destroy();
|
|
}
|
|
|
|
/** Function to fully fit the Graph */
|
|
public fit(options: {
|
|
nodes?: Array<string>,
|
|
animation?: boolean | {
|
|
duration?: number,
|
|
easingFunction?: string
|
|
}
|
|
} = {
|
|
animation: true
|
|
}) {
|
|
/**
|
|
* Afterwards use this Elements to Perform the
|
|
* Animation and zoom, that all Nodes are Viewable.
|
|
*/
|
|
this.network.fit(options);
|
|
}
|
|
|
|
public hasNode(id: string): boolean {
|
|
return this._data.nodes.get(id) !== null;
|
|
}
|
|
|
|
public on(eventName: IEvent, callback: (...args) => void): () => void {
|
|
|
|
const _self = this;
|
|
|
|
switch (eventName) {
|
|
case "click":
|
|
/** Make a difference between a Click event and e.g. a select Event. */
|
|
let counter = 0;
|
|
let timer: any = null;
|
|
|
|
const func = (...args) => {
|
|
counter++;
|
|
if (counter === 1) {
|
|
timer = setTimeout(() => {
|
|
callback(...args);
|
|
counter = 0;
|
|
}, 200);
|
|
} else {
|
|
counter = 0;
|
|
clearTimeout(timer);
|
|
}
|
|
};
|
|
|
|
return this.network.subscribe(eventName, func);
|
|
case "addEdge":
|
|
case "removeEdge":
|
|
case "updateEdge":
|
|
this._data.edges.on(eventName.split("Edge")[0], callback);
|
|
return () => {
|
|
_self._data.edges.off(eventName.split("Edge")[0], callback);
|
|
};
|
|
case "addNode":
|
|
case "removeNode":
|
|
case "updateNode":
|
|
this._data.nodes.on(eventName.split("Node")[0], callback);
|
|
return () => {
|
|
_self._data.nodes.off(eventName.split("Node")[0], callback);
|
|
};
|
|
case "dataUpdate":
|
|
const events = [
|
|
"add",
|
|
"remove",
|
|
"update"
|
|
];
|
|
|
|
for (const type of ["edges", "nodes"]) {
|
|
for (const event of events) {
|
|
this._data[type].on(event, callback);
|
|
}
|
|
}
|
|
|
|
return () => {
|
|
for (const type of ["edges", "nodes"]) {
|
|
for (const event of events) {
|
|
this._data[type].off(event, callback);
|
|
}
|
|
}
|
|
};
|
|
default:
|
|
return this.visNetwork.subscribe(eventName, (...args) => {
|
|
callback(...args);
|
|
});
|
|
}
|
|
}
|
|
|
|
public getClusters() {
|
|
|
|
const clusters: {
|
|
[index: string]: {
|
|
nodes: Array<string>,
|
|
clusters: Array<string>,
|
|
options: IBaseNodeOptions,
|
|
}
|
|
} = {};
|
|
|
|
// Iterate over all Nodes of the Graph.
|
|
// Find their clusters. Based on their order
|
|
// Define an arry containing all array etc.
|
|
for (const node of this.nodes) {
|
|
// Get all Nodes in Cluster
|
|
const res = this.visNetwork.findNode(node.id) as Array<string>;
|
|
// Remove the First-Element
|
|
res.pop();
|
|
|
|
let first = true;
|
|
let lastCluster = "";
|
|
|
|
// Iterate over the used Clusters.
|
|
// If its the first cluster, add the node
|
|
// Otherwise add the last cluster.
|
|
for (const cluster of res.reverse()) {
|
|
|
|
// If the Cluster isnt know => create an obect, describing the Cluster.
|
|
if (clusters[cluster] === undefined) {
|
|
|
|
clusters[cluster] = {
|
|
nodes: new Array<string>(),
|
|
clusters: new Array<string>(),
|
|
options: {
|
|
id: cluster
|
|
}
|
|
};
|
|
|
|
// Test if the Cluster has special Ids.
|
|
if (this.visNetwork.clustering.body.nodes[cluster]) {
|
|
clusters[cluster].options = this.visNetwork.clustering.body.nodes[cluster].options;
|
|
clusters[cluster].options.id = cluster;
|
|
}
|
|
}
|
|
|
|
// Add the Node the the first Cluster only
|
|
if (first) {
|
|
clusters[cluster].nodes.push(node.id);
|
|
} else {
|
|
clusters[cluster].clusters.push(lastCluster);
|
|
}
|
|
|
|
lastCluster = cluster;
|
|
first = false;
|
|
}
|
|
}
|
|
|
|
// Define the Return type.
|
|
const ret: IClusters = [];
|
|
|
|
// Add all clsuters.
|
|
for (const key of Object.getOwnPropertyNames(clusters)) {
|
|
ret.push(Object.assign(clusters[key], { id: key }));
|
|
}
|
|
|
|
return ret.sort(dynamicSort("clusters.length"));
|
|
}
|
|
|
|
public readinClusters(clusters: Array<{
|
|
nodes: Array<string>,
|
|
clusters: Array<string>,
|
|
id: string,
|
|
options: IBaseNodeOptions,
|
|
}>) {
|
|
|
|
for (const cluster of clusters) {
|
|
// Automatically create a cluster.
|
|
this.network.cluster({
|
|
joinCondition(node: IBaseNodeOptions) {
|
|
return cluster.nodes.indexOf(node.id) !== -1 || cluster.clusters.indexOf(node.id) !== -1;
|
|
},
|
|
clusterNodeProperties: cluster.options,
|
|
});
|
|
}
|
|
}
|
|
|
|
public once(eventName: IEvent, callback: (...args) => void): () => void {
|
|
|
|
let func = (...args) => { };
|
|
const _self = this;
|
|
|
|
switch (eventName) {
|
|
case "click":
|
|
/** Make a difference between a Click event and e.g. a select Event. */
|
|
let counter = 0;
|
|
let timer: any = null;
|
|
|
|
func = (...args) => {
|
|
counter++;
|
|
if (counter === 1) {
|
|
timer = setTimeout(() => {
|
|
callback(...args);
|
|
counter = 0;
|
|
}, 200);
|
|
} else {
|
|
counter = 0;
|
|
clearTimeout(timer);
|
|
}
|
|
};
|
|
|
|
return this.network.once(eventName, func);
|
|
case "addEdge":
|
|
case "removeEdge":
|
|
case "updateEdge":
|
|
func = (...args) => {
|
|
callback(...args);
|
|
_self._data.edges.off(eventName.split("Edge")[0], func);
|
|
};
|
|
|
|
this._data.edges.on(eventName.split("Edge")[0], func);
|
|
break;
|
|
case "addNode":
|
|
case "removeNode":
|
|
case "updateNode":
|
|
func = (...args) => {
|
|
callback(...args);
|
|
_self._data.nodes.off(eventName.split("Node")[0], func);
|
|
};
|
|
|
|
this._data.nodes.on(eventName.split("Node")[0], func);
|
|
break;
|
|
default:
|
|
return this.visNetwork.once(eventName, callback);
|
|
}
|
|
}
|
|
|
|
public useVersionControl = true;
|
|
public version = "20200713";
|
|
public name: string;
|
|
public id: string = generateId();
|
|
|
|
public getData(name: string = this.name, id: string = this.id, version: string = this.version): INetwork<N, E> {
|
|
|
|
const position = this.network.getViewPosition();
|
|
const scale = this.network.getScale();
|
|
|
|
return {
|
|
nodes: this.nodes,
|
|
edges: this.edges,
|
|
clusters: this.getClusters(),
|
|
version,
|
|
name,
|
|
id,
|
|
view: {
|
|
position,
|
|
scale
|
|
}
|
|
};
|
|
}
|
|
|
|
public resize(){
|
|
this.network.setSize(undefined, this._reference.current.parentElement.offsetHeight.toString() + "px");
|
|
this.network.redraw();
|
|
}
|
|
|
|
/**
|
|
* Function to load a Cluster.
|
|
* @param data
|
|
* @param clear
|
|
* @param cluster
|
|
*/
|
|
public loadData(data: INetwork<N, E>,
|
|
clear = true,
|
|
cluster = true) {
|
|
let somethingLoaded = false;
|
|
|
|
if (this.useVersionControl && ((data.version && data.version != this.version) || !data.version)) {
|
|
throw Error("File Version not compatible");
|
|
}
|
|
|
|
if (clear) {
|
|
this.clear();
|
|
}
|
|
|
|
let adaptViewport = false;
|
|
|
|
if (data?.nodes) {
|
|
somethingLoaded = true;
|
|
adaptViewport = true;
|
|
|
|
this.addNode(sortNodes(data.nodes));
|
|
if (data.edges) {
|
|
this.addEdge(data.edges);
|
|
}
|
|
if (cluster && data.clusters) {
|
|
this.readinClusters(data.clusters);
|
|
}
|
|
|
|
// Get the View.
|
|
if (data.view) {
|
|
this.network.moveTo({
|
|
position: data.view.position,
|
|
scale: data.view.scale,
|
|
animation: false
|
|
});
|
|
|
|
adaptViewport = false;
|
|
}
|
|
}
|
|
|
|
if (somethingLoaded && adaptViewport) {
|
|
this.fit({
|
|
animation: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export class UndoRedoGraph<N, E> extends BaseGraph<N, E> implements IUndoRedoGraph<N, E> {
|
|
|
|
/**
|
|
* Element for the Undo / Redo Queue
|
|
*/
|
|
private _stack: SimpleUndo;
|
|
/**
|
|
* Flag to Toggle Loading and disabling
|
|
* Elements
|
|
*/
|
|
public disableUndoRedo = false;
|
|
private _stateCounter = 0;
|
|
private _currentCounter = 0;
|
|
|
|
public undoEnabled = false;
|
|
public redoEnabled = false;
|
|
|
|
constructor(
|
|
/** The Reference, where the Graph should be rednered in */
|
|
reference: React.RefObject<any>,
|
|
/** The SVG Options. See Options on http://visjs.org/docs/network/ */
|
|
options: IVisjsOptions = {
|
|
edges: { smooth: false },
|
|
physics: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
maxStackLength = 100
|
|
) {
|
|
super(reference,options);
|
|
|
|
const _self = this;
|
|
|
|
if (maxStackLength > 0) {
|
|
/** Stack containing the Steps */
|
|
this._stack = new SimpleUndo({
|
|
maxLength: maxStackLength,
|
|
provider: (callback: (data: any) => void) => {
|
|
|
|
callback(_self.getData());
|
|
|
|
// Raise the State Counters
|
|
_self._stateCounter = _self._stack.count();
|
|
_self._currentCounter = _self._stack.count();
|
|
|
|
|
|
// Update the Undo / Redo Flags
|
|
_self._updateFlags();
|
|
},
|
|
});
|
|
// Save the Initial State
|
|
this.save();
|
|
|
|
this.on("dragEnd", (params) => {
|
|
// Make shure the Position is only stored,
|
|
// nodes are dragged and not the View.
|
|
if (!_self.disableUndoRedo && params.nodes.length > 0) {
|
|
_self.network.storePositions();
|
|
}
|
|
});
|
|
|
|
// Link the Emitter with the Updates.
|
|
const events = [
|
|
"addNode",
|
|
"removeNode",
|
|
"updateNode",
|
|
"addEdge",
|
|
"removeEdge",
|
|
"updateEdge"
|
|
];
|
|
|
|
// Function to Update the Stack.
|
|
const _updateStack = () => {
|
|
_self.save();
|
|
};
|
|
|
|
for (const event of events) {
|
|
this.on(event as any, _updateStack);
|
|
}
|
|
}
|
|
}
|
|
|
|
public save() {
|
|
if (!this.disableUndoRedo && this._stack) {
|
|
this._stack.save();
|
|
}
|
|
}
|
|
|
|
public undo() {
|
|
if (this._stack) {
|
|
const _self = this;
|
|
|
|
this._stack.undo((data) => {
|
|
|
|
// Prevent Storing the Content
|
|
_self.disableUndoRedo = true;
|
|
|
|
// Update the Current Counter;
|
|
_self._currentCounter--;
|
|
|
|
// Remove the current Graph.
|
|
_self.clear();
|
|
|
|
// Only if Data are available => Load them
|
|
if (data !== null) {
|
|
// Add the Edges and Nodes
|
|
_self.loadData(data);
|
|
}
|
|
|
|
// Update the Flags
|
|
_self._updateFlags();
|
|
|
|
// Enable storing updates again.
|
|
_self.disableUndoRedo = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
private _updateFlags() {
|
|
this.redoEnabled = (this._currentCounter < this._stateCounter);
|
|
this.undoEnabled = (this._currentCounter > 0);
|
|
}
|
|
|
|
public redo() {
|
|
if (this._stack) {
|
|
const _self = this;
|
|
this._stack.redo((data) => {
|
|
// Only if Data are available => Load them
|
|
if (data != null) {
|
|
|
|
// Prevent Storing the Content during REDO operation
|
|
_self.disableUndoRedo = true;
|
|
|
|
_self.loadData(data);
|
|
|
|
// Update the Flags
|
|
_self._updateFlags();
|
|
|
|
// Enable storing updates again.
|
|
_self.disableUndoRedo = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public resetHistory() {
|
|
if (this._stack) {
|
|
// Define the State
|
|
const state = {
|
|
nodes: this.nodes,
|
|
edges: this.edges,
|
|
};
|
|
|
|
this._stack.initialize(state);
|
|
this._stack.clear();
|
|
|
|
// Raise the State Counters
|
|
this._stateCounter = this._stack.count();
|
|
this._currentCounter = this._stack.count();
|
|
|
|
// Update the Undo / Redo Flags
|
|
this._updateFlags();
|
|
}
|
|
}
|
|
}
|