/** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2018-06-08 11:29:49 * @modify date 2020-08-25 14:41:40 * @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} * @template T */ @injectable() export class BaseTree> implements ITree { protected _nodes: Set; protected _nodesByPath: Map protected _getSegmentsOfNodePath: (path: string) => string[]; /** *Creates an instance of BaseTree. * @param {interfaces.Newable} _nodeCtor * @param {IIdGenerator} _idGenerator * @memberof BaseTree */ constructor( @inject(TREE.TYPES.INodeOfTreeFactory) protected _nodeFactory: () => T, ) { const _self = this; /** Create an Array. */ this._nodes = new Set(); this._nodePathes = new Set(); this._nodesByPath = new Map(); /** 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} * @memberof BaseTree */ public get leafNodes(): Set { const _ret = new Set(); 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} * @memberof BaseTree */ public get nodes(): Set { return this._nodes; } /** * Returns a List containing all Roots. * * @readonly * @type {Array} * @memberof BaseTree */ public get rootNodes(): Set { const _ret = new Set(); Array.from(this._nodes).filter((node) => (node.parent == undefined ? node : undefined)).map((node) => _ret.add(node)); return _ret; } protected _nodePathes: Set; /** * Getter to get all Ids of the Provided Nodes. * * @readonly * @type {Array} * @memberof BaseTree */ public get NodePathes(): Set { return this._nodePathes; } public onNewNode: (_topic: T) => void = () => { }; public onInsertNode: (insertedNode: T) => void = () => { }; public onRemoveNode: (parentOfremovedNode: T) => void = () => { }; }