2020-08-30 07:45:44 +00:00
|
|
|
/**
|
|
|
|
* @author Martin Karkowski
|
|
|
|
* @email m.karkowski@zema.de
|
|
|
|
* @create date 2018-06-08 11:29:49
|
2020-11-15 19:11:25 +00:00
|
|
|
* @modify date 2020-11-13 16:12:16
|
2020-08-30 07:45:44 +00:00
|
|
|
* @desc [description]
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { inject, injectable, interfaces } from 'inversify';
|
|
|
|
import { memoize } from 'lodash';
|
|
|
|
import { generateId } from '../../../lib/helpers/idMethods';
|
|
|
|
import { SPLITCHAR } from '../../../lib/helpers/objectMethods';
|
|
|
|
import { INodeOfTree, ITree } from '../type/interfaces';
|
|
|
|
import * as TREE from '../type/types';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A Tree containing all Nodes
|
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @class BaseTree
|
|
|
|
* @implements {ITree<T>}
|
|
|
|
* @template T
|
|
|
|
*/
|
|
|
|
@injectable()
|
|
|
|
export class BaseTree<T extends INodeOfTree<T>> implements ITree<T> {
|
|
|
|
|
|
|
|
protected _nodes: Set<T>;
|
|
|
|
|
|
|
|
protected _nodesByPath: Map<string, T>
|
|
|
|
|
|
|
|
protected _getSegmentsOfNodePath: (path: string) => string[];
|
|
|
|
|
|
|
|
/**
|
|
|
|
*Creates an instance of BaseTree.
|
|
|
|
* @param {interfaces.Newable<T>} _nodeCtor
|
|
|
|
* @param {IIdGenerator} _idGenerator
|
|
|
|
* @memberof BaseTree
|
|
|
|
*/
|
|
|
|
constructor(
|
|
|
|
@inject(TREE.TYPES.INodeOfTreeFactory) protected _nodeFactory: () => T,
|
|
|
|
) {
|
|
|
|
const _self = this;
|
|
|
|
|
|
|
|
/** Create an Array. */
|
|
|
|
this._nodes = new Set<T>();
|
|
|
|
this._nodePathes = new Set<string>();
|
|
|
|
this._nodesByPath = new Map<string, T>();
|
|
|
|
|
|
|
|
/** Disable Memoize in the Beginning */
|
|
|
|
this._getSegmentsOfNodePath = memoize((_path: string) => {
|
|
|
|
return _path.split('.');
|
|
|
|
});
|
|
|
|
|
|
|
|
/** Create a Memoized Function for generate a Child */
|
|
|
|
this.lazyGetNode = memoize((_path: string) => _self._lazyGetNode(_path));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
protected updateMap(): void {
|
|
|
|
/** Search the Node */
|
|
|
|
this._nodesByPath.clear()
|
|
|
|
|
|
|
|
for (const _node of this._nodes) {
|
|
|
|
/** Check whether the required Path Matches the Path */
|
|
|
|
this._nodesByPath.set(_node.path, _node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate A Child.
|
|
|
|
*
|
|
|
|
* @param {string} path
|
|
|
|
* @returns {T}
|
|
|
|
* @memberof BaseNodeTree
|
|
|
|
*/
|
|
|
|
protected _lazyGetNode(path: string): T {
|
|
|
|
|
|
|
|
|
|
|
|
if (path.length === 0) {
|
|
|
|
throw Error('A Path must be given');
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Search the Node */
|
|
|
|
let _ret: T | null = this._nodesByPath.has(path) ? this._nodesByPath.get(path) as T : null;
|
|
|
|
|
|
|
|
if (_ret === null) {
|
|
|
|
/** The Node wasn't found ==> Extract the Effected Elements */
|
|
|
|
const _segmentsOfPath = this._getSegmentsOfNodePath(path);
|
|
|
|
|
|
|
|
/** Create the Structure. */
|
|
|
|
for (const [_idx, _segment] of _segmentsOfPath.entries()) {
|
|
|
|
|
|
|
|
const _pathToNode = _segmentsOfPath.slice(0, _idx + 1).join(SPLITCHAR);
|
|
|
|
|
|
|
|
/** Try to Find the Node */
|
|
|
|
let _node = this._nodesByPath.get(_pathToNode);
|
|
|
|
|
|
|
|
if (_node === undefined) {
|
|
|
|
/** Node is not defined */
|
|
|
|
|
|
|
|
/** Create a Node */
|
|
|
|
_node = this._nodeFactory();
|
|
|
|
|
|
|
|
_node.id = generateId();
|
|
|
|
_node.segment = _segment;
|
|
|
|
|
|
|
|
/** Set its Parent */
|
|
|
|
_node.parent = _ret;
|
|
|
|
|
|
|
|
if (_ret) {
|
|
|
|
/** Mark as Child on the Parent */
|
|
|
|
_ret.addDirectSubNode(_node);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Add the Node */
|
|
|
|
this._addNode(_node);
|
|
|
|
}
|
|
|
|
|
|
|
|
_ret = _node;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
/** Return the Node */
|
|
|
|
return _ret as T;
|
|
|
|
}
|
|
|
|
|
|
|
|
public lazyGetNode: (path: string) => T;
|
|
|
|
|
|
|
|
|
|
|
|
public insertNode(path: string, segment: string) {
|
|
|
|
if (path.length > 0) {
|
|
|
|
|
|
|
|
const _toAdapt = this.lazyGetNode(path);
|
|
|
|
const _childsToAdapt = Array.from(_toAdapt.getDirectSubNodes());
|
|
|
|
|
|
|
|
/** Delete All Childs. */
|
|
|
|
_toAdapt.getDirectSubNodes().clear();
|
|
|
|
const _insertion = this.lazyGetNode(path + SPLITCHAR + segment);
|
|
|
|
|
|
|
|
/** Change the Parent and Add the Childs to the Insertion */
|
|
|
|
for (const _child of _childsToAdapt) {
|
|
|
|
_child.parent = _insertion;
|
|
|
|
_child.path = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Delete the Memoized Cache */
|
|
|
|
(this.lazyGetNode as any).cache.clear();
|
|
|
|
|
|
|
|
if (typeof this.onInsertNode === 'function') {
|
|
|
|
this.onInsertNode(_insertion);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
/** A common root should be entered. */
|
|
|
|
const _insertion = this.lazyGetNode(segment);
|
|
|
|
|
|
|
|
const _roots = Array.from(this.rootNodes);
|
|
|
|
|
|
|
|
for (const _root of _roots) {
|
|
|
|
/** Prevent Endless Loops by checking the Identity */
|
|
|
|
if (_root != _insertion) {
|
|
|
|
_root.parent = _insertion;
|
|
|
|
_insertion.addDirectSubNode(_root);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Update All Pathes */
|
|
|
|
for (const _node of this._nodes) {
|
|
|
|
_node.path = '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public removeNode(path: string, count: number | 'all' = 'all') {
|
|
|
|
/** Get the path of the parent */
|
|
|
|
if (path != '') {
|
|
|
|
|
|
|
|
const _segments = this._getSegmentsOfNodePath(path);
|
|
|
|
|
|
|
|
/** Define the number of segments, that should be removed */
|
|
|
|
let counter = (count == 'all' ? _segments.length : (typeof count === 'number' ? count : 1));
|
|
|
|
|
|
|
|
let _affectParentNode: T | null = null;
|
|
|
|
|
|
|
|
const _affectedChildNodes = Array.from(this.lazyGetNode(path).getDirectSubNodes());
|
|
|
|
|
|
|
|
/** Generate the Path of the Parent */
|
|
|
|
let _parentPath = '';
|
|
|
|
|
|
|
|
while (counter > 0) {
|
|
|
|
/** Drop the Last Element */
|
|
|
|
_segments.pop();
|
|
|
|
/** Get the node which should be Removed.*/
|
|
|
|
const _nodeToRemove = this.lazyGetNode(path);
|
|
|
|
|
|
|
|
/** Generate the Path of the Parent */
|
|
|
|
_parentPath = _segments.join(SPLITCHAR);
|
|
|
|
/** Adapt the Counter */
|
|
|
|
counter -= 1;
|
|
|
|
|
|
|
|
/** Determine the Parent Node */
|
|
|
|
_affectParentNode = _parentPath.length > 0 ? this.lazyGetNode(_parentPath) : null;
|
|
|
|
|
|
|
|
if (_affectParentNode !== null) {
|
|
|
|
_affectParentNode.removeDirectSubNode(_nodeToRemove);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Delete the Memoized Cache */
|
|
|
|
(this.lazyGetNode as any).cache.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Change the Parent and Add the Childs to the _affectParentNode */
|
|
|
|
for (const _child of _affectedChildNodes) {
|
|
|
|
_child.parent = _affectParentNode;
|
|
|
|
if (_affectParentNode)
|
|
|
|
_affectParentNode.addDirectSubNode(_child);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Delete the Memoized Cache */
|
|
|
|
(this.lazyGetNode as any).cache.clear();
|
|
|
|
|
|
|
|
if (typeof this.onRemoveNode === 'function') {
|
|
|
|
this.onRemoveNode(_affectParentNode as T);
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
console.log((new Date()).toISOString(), '- error - Path to remove a Node of a Tree must be setted.');
|
|
|
|
throw Error('Path to remove a Node of a Tree must be setted!');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _addNode(_node: T) {
|
|
|
|
/** Add the Node */
|
|
|
|
this._nodes.add(_node);
|
|
|
|
this._nodePathes.add(_node.path);
|
|
|
|
|
|
|
|
this._nodesByPath.set(_node.path, _node);
|
|
|
|
|
|
|
|
/** Fire an Event, that the new Node has been created. */
|
|
|
|
if (typeof this.onNewNode === 'function') {
|
|
|
|
this.onNewNode(_node);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _removeNode(_node: T) {
|
|
|
|
/** Add the Node */
|
|
|
|
this._nodes.delete(_node);
|
|
|
|
this._nodePathes.delete(_node.path);
|
|
|
|
this._nodesByPath.delete(_node.path);
|
|
|
|
|
|
|
|
// (this._lazyFindNode as any).cache.delete(_node.path);
|
|
|
|
|
|
|
|
if (typeof this.onRemoveNode === 'function' && _node.parent) {
|
|
|
|
this.onRemoveNode(_node.parent as T);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List Returning all Leafs
|
|
|
|
*
|
|
|
|
* @readonly
|
|
|
|
* @type {Array<T>}
|
|
|
|
* @memberof BaseTree
|
|
|
|
*/
|
|
|
|
public get leafNodes(): Set<T> {
|
|
|
|
const _ret = new Set<T>();
|
|
|
|
Array.from(this._nodes).filter((node) => (node.getDirectSubNodes().size === 0 ? node : undefined)).map((node) => _ret.add(node));
|
|
|
|
return _ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all contained Nodes
|
|
|
|
*
|
|
|
|
* @readonly
|
|
|
|
* @type {Array<T>}
|
|
|
|
* @memberof BaseTree
|
|
|
|
*/
|
|
|
|
public get nodes(): Set<T> {
|
|
|
|
return this._nodes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a List containing all Roots.
|
|
|
|
*
|
|
|
|
* @readonly
|
|
|
|
* @type {Array<T>}
|
|
|
|
* @memberof BaseTree
|
|
|
|
*/
|
|
|
|
public get rootNodes(): Set<T> {
|
|
|
|
const _ret = new Set<T>();
|
|
|
|
Array.from(this._nodes).filter((node) => (node.parent == undefined ? node : undefined)).map((node) => _ret.add(node));
|
|
|
|
return _ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _nodePathes: Set<string>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Getter to get all Ids of the Provided Nodes.
|
|
|
|
*
|
|
|
|
* @readonly
|
|
|
|
* @type {Array<string>}
|
|
|
|
* @memberof BaseTree
|
|
|
|
*/
|
|
|
|
public get NodePathes(): Set<string> {
|
|
|
|
return this._nodePathes;
|
|
|
|
}
|
|
|
|
|
|
|
|
public onNewNode: (_topic: T) => void = () => { };
|
|
|
|
|
|
|
|
public onInsertNode: (insertedNode: T) => void = () => { };
|
|
|
|
|
|
|
|
public onRemoveNode: (parentOfremovedNode: T) => void = () => { };
|
|
|
|
}
|
|
|
|
|