/** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2019-04-24 09:31:13 * @modify date 2020-10-29 17:44:41 * @desc [description] */ import { EventEmitter } from 'events'; /** Import Undo-Stack */ import * as SimpleUndo from 'simple-undo'; import { dynamicSort } from '../../../lib/helpers/arrayMethods'; import { deepClone, 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 { IBasicTemplate } from './interfaces/IBasicTemplate'; 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'; import { stringify, parse } from '../../../lib/helpers/jsonMethods' import { generateId } from '../../../lib/helpers/idMethods'; export class BaseGraph implements IBaseGraph { 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> { return this._data.edges.get(); } public get nodes(): Array> { 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, /** 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) { /** 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>((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>((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) { try { /** Try updateing the Node. */ this._data.nodes.update(options); } catch (err) { console.error('Upadting Node' + err); } } public getNode(id) { return this._data.nodes.get(id) as IBaseNodeOptions; } public getEdge(id) { return this._data.edges.get(id) as IBaseEdgeOptions; } public clearNodes() { this._data.nodes.clear(); } public clearEdges() { this._data.edges.clear(); } public addEdge(values: IBaseEdgeOptions | Array>) { /** Add the Node Data. */ this._data.edges.add(values); } public updateEdge(values: IBaseEdgeOptions | Array>) { 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, 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, clusters: Array, 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; // Remove the First-Element res.pop() let first = true; let lastCluster: string = ''; // 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(), clusters: new Array(), 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, clusters: Array, 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: string = '20200713'; public name: string; public id: string = generateId(); public getData(name: string = this.name, id: string = this.id, version: string = this.version): INetwork { 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, 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 extends BaseGraph implements IUndoRedoGraph { /** * 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, /** 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(); } } }