import { Injectable } from '@angular/core'; import { Displayable } from 'app/site/base/displayable'; import { Identifiable } from 'app/shared/models/base/identifiable'; /** * A basic representation of a tree node. This node does not stores any data. */ export interface OSTreeNodeWithoutItem { name: string; id: number; children?: OSTreeNodeWithoutItem[]; } /** * A representation of nodes with the item atached. */ export interface OSTreeNode extends OSTreeNodeWithoutItem { item: T; children?: OSTreeNode[]; } /** * This services handles all operations belonging to trees. It can build trees of plain lists (giving the weight * and parentId property) and traverse the trees in pre-order. */ @Injectable({ providedIn: 'root' }) export class TreeService { /** * Returns the weight casted to a number from a given model. * * @param item The model to get the weight from. * @param key * @returns the weight of the model */ private getAttributeAsNumber(item: T, key: keyof T): number { return (item[key]) as number; } /** * Build our representation of a tree node given the model and optional children * to append to this node. * * @param item The model to create a node of. * @param children Optional children to append to this node. * @returns The created node. */ private buildTreeNode(item: T, children?: OSTreeNode[]): OSTreeNode { return { name: item.getTitle(), id: item.id, item: item, children: children }; } /** * Builds a tree from the given items on the relations between items with weight and parentId * * @param items All items to traverse * @param weightKey The key giving access to the weight property * @param parentIdKey The key giving access to the parentId property * @returns An iterator for all items in the right order. */ public makeTree( items: T[], weightKey: keyof T, parentIdKey: keyof T ): OSTreeNode[] { // Sort items after their weight items.sort((a, b) => this.getAttributeAsNumber(a, weightKey) - this.getAttributeAsNumber(b, weightKey)); // Build a dict with all children (dict-value) to a specific // item id (dict-key). const children: { [parendId: number]: T[] } = {}; items.forEach(model => { if (model[parentIdKey]) { const parentId = this.getAttributeAsNumber(model, parentIdKey); if (children[parentId]) { children[parentId].push(model); } else { children[parentId] = [model]; } } }); // Recursive function that generates a nested list with all // items with there children const getChildren: (_models?: T[]) => OSTreeNode[] = _models => { if (!_models) { return; } const nodes: OSTreeNode[] = []; _models.forEach(_model => { nodes.push(this.buildTreeNode(_model, getChildren(children[_model.id]))); }); return nodes; }; // Generates the list of root items (with no parents) const parentItems = items.filter(model => !this.getAttributeAsNumber(model, parentIdKey)); return getChildren(parentItems); } /** * Traverses the given tree in pre order. * * @param tree The tree to traverse * @returns An iterator for all items in the right order. */ public *traverseTree(tree: OSTreeNode[]): Iterator { const nodesToVisit = tree.reverse(); while (nodesToVisit.length > 0) { const node = nodesToVisit.pop(); if (node.children) { node.children.reverse().forEach(n => { nodesToVisit.push(n); }); } yield node.item; } } /** * Removes `item` from the tree. * * @param tree The tree with items * @returns The tree without items */ public stripTree(tree: OSTreeNode[]): OSTreeNodeWithoutItem[] { return tree.map(node => { const nodeWithoutItem: OSTreeNodeWithoutItem = { name: node.name, id: node.id }; if (node.children) { nodeWithoutItem.children = this.stripTree(node.children); } return nodeWithoutItem; }); } /** * Traverses items in pre-order givem (implicit) by the weight and parentId. * * Just builds the tree with `makeTree` and get the iterator from `traverseTree`. * * @param items All items to traverse * @param weightKey The key giving access to the weight property * @param parentIdKey The key giving access to the parentId property * @returns An iterator for all items in the right order. */ public traverseItems( items: T[], weightKey: keyof T, parentIdKey: keyof T ): Iterator { const tree = this.makeTree(items, weightKey, parentIdKey); return this.traverseTree(tree); } /** * Reduce a list of items to nodes independent from each other in a given * branch of a tree * * @param branch the tree to traverse * @param items the items to check * @returns the selection of items that belong to different branches */ private getTopItemsFromBranch(branch: OSTreeNode, items: T[]): T[] { const item = items.find(i => branch.item.id === i.id); if (item) { return [item]; } else if (!branch.children) { return []; } else { return [].concat(...branch.children.map(child => this.getTopItemsFromBranch(child, items))); } } /** * Reduce a list of items to nodes independent from each other in a given tree * * @param tree the tree to traverse * @param items the items to check * @returns the selection of items that belong to different branches */ public getTopItemsFromTree(tree: OSTreeNode[], items: T[]): T[] { let results: T[] = []; tree.forEach(branch => { const i = this.getTopItemsFromBranch(branch, items); if (i.length) { results = results.concat(i); } }); return results; } /** * Return all items not being hierarchically dependant on the items in the input arrray * * @param tree * @param items * @returns all items that are neither in the input nor dependants of items in the input */ public getTreeWithoutSelection(tree: OSTreeNode[], items: T[]): T[] { let result: T[] = []; tree.forEach(branch => { if (!items.find(i => i.id === branch.item.id)) { result.push(branch.item); if (branch.children) { result = result.concat(this.getTreeWithoutSelection(branch.children, items)); } } }); return result; } }