nope/resources/ui/graph/graph.ts
2020-10-30 19:30:59 +01:00

691 lines
20 KiB
TypeScript

/**
* @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<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: 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<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: string = '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();
}
}
}