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 TreeIdNode { id: number; children?: TreeIdNode[]; } /** * Extends the TreeIdNode with a name to display. */ export interface TreeNodeWithoutItem extends TreeIdNode { name: string; children?: TreeNodeWithoutItem[]; } /** * A representation of nodes with the item atached. */ export interface OSTreeNode extends TreeNodeWithoutItem { item: T; children?: OSTreeNode[]; } /** * Interface which defines the nodes for the sorting trees. * * Contains information like * item: The base item the node is created from. * level: The level of the node. The higher, the deeper the level. * position: The position in the array of the node. * isExpanded: Boolean if the node is expanded. * expandable: Boolean if the node is expandable. * id: The id of the node. * filtered: Optional boolean to check, if the node is filtered. */ export interface FlatNode { item: T; level: number; position?: number; isExpanded?: boolean; isSeen: boolean; expandable: boolean; id: number; filtered?: boolean; } /** * 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 }; } /** * Function to build flat nodes from `OSTreeNode`s. * Iterates recursively through the list of nodes. * * @param items * @param weightKey * @param parentKey * * @returns An array containing flat nodes. */ public makeFlatTree( items: T[], weightKey: keyof T, parentKey: keyof T ): FlatNode[] { const tree = this.makeTree(items, weightKey, parentKey); const flatNodes: FlatNode[] = []; for (const node of tree) { flatNodes.push(...this.makePartialFlatTree(node, 0)); } for (let i = 0; i < flatNodes.length; ++i) { flatNodes[i].position = i; } return flatNodes; } /** * Function to convert a flat tree to a nested tree built from `OSTreeNodeWithOutItem`. * * @param nodes The array of flat nodes, which should be converted. * * @returns The tree with nested information. */ public makeTreeFromFlatTree(nodes: FlatNode[]): TreeIdNode[] { const basicTree: TreeIdNode[] = []; for (let i = 0; i < nodes.length; ) { // build the next node inclusive its children const nextNode = this.buildBranchFromFlatTree(nodes[i], nodes, 0); // append this node to the tree basicTree.push(nextNode.node); // step to the next related item in the array i += nextNode.length; } return basicTree; } /** * 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); } /** * Removes `item` from the tree. * * @param tree The tree with items * @returns The tree without items */ public stripTree(tree: OSTreeNode[]): TreeNodeWithoutItem[] { return tree.map(node => { const nodeWithoutItem: TreeNodeWithoutItem = { name: node.name, id: node.id }; if (node.children) { nodeWithoutItem.children = this.stripTree(node.children); } return nodeWithoutItem; }); } /** * 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))); } } /** * Searches a tree for a list of given items and fetches all branches that include * these items and their dependants * * @param tree an array of OsTreeNode branches * @param items the items that need to be included * * @returns an array of OsTreeNodes with the top-most item being included * in the input list */ public getBranchesFromTree( tree: OSTreeNode[], items: T[] ): OSTreeNode[] { let results: OSTreeNode[] = []; tree.forEach(branch => { if (items.some(item => item.id === branch.item.id)) { results.push(branch); } else if (branch.children && branch.children.length) { results = results.concat(this.getBranchesFromTree(branch.children, items)); } }); return results; } /** * Inserts OSTreeNode branches into another tree at the position specified * * @param tree A (partial) tree the branches need to be inserted into. It * is assumed that this tree does not contain the branches to be inserted. * See also {@link getTreeWithoutSelection} * @param branches OsTreeNodes to be inserted. See also {@link getBranchesFromTree} * @param parentId the id of a parent node under which the branches should be inserted * @param olderSibling (optional) the id of the item on the same level * the tree is to be inserted behind * @returns the re-arranged tree containing the branches */ public insertBranchesIntoTree( tree: OSTreeNode[], branches: OSTreeNode[], parentId: number, olderSibling?: number ): OSTreeNode[] { if (!parentId && olderSibling) { const older = tree.findIndex(branch => branch.id === olderSibling); if (older >= 0) { return [...tree.slice(0, older + 1), ...branches, ...tree.slice(older + 1)]; } else { for (const branch of tree) { if (branch.children && branch.children.length) { branch.children = this.insertBranchesIntoTree(branch.children, branches, null, olderSibling); } } return tree; } } else if (parentId) { for (const branch of tree) { if (branch.id !== parentId) { if (branch.children && branch.children.length) { branch.children = this.insertBranchesIntoTree( branch.children, branches, parentId, olderSibling ); } } else { if (!branch.children) { branch.children = branches; } else { if (olderSibling) { const older = branch.children.findIndex(child => child.id === olderSibling); if (older >= 0) { branch.children = [ ...branch.children.slice(0, older + 1), ...branches, ...branch.children.slice(older + 1) ]; } } else { branch.children = [...branch.children, ...branches]; } } } } return tree; } else { throw new Error('This should not happen. Invalid sorting items given'); } } /** * Return the part of a tree not including or being hierarchically dependant * on the items in the input arrray * * @param tree * @param items * @returns all the branch without the given items or their dependants */ public getTreeWithoutSelection( tree: OSTreeNode[], items: T[] ): OSTreeNode[] { const result: OSTreeNode[] = []; tree.forEach(branch => { if (!items.find(i => i.id === branch.item.id)) { if (branch.children) { branch.children = this.getTreeWithoutSelection(branch.children, items); } result.push(branch); } }); return result; } /** * Helper to turn a tree into an array of items * * @param tree * @returns the items contained in the tree. */ public getFlatItemsFromTree(tree: OSTreeNode[]): T[] { let result = []; for (const branch of tree) { result.push(branch.item); if (branch.children && branch.children.length) { result = result.concat(this.getFlatItemsFromTree(branch.children)); } } return result; } /** * Helper function to go recursively through the children of given node. * * @param item The current item from which the flat node will be created. * @param level The level the flat node will be. * @param additionalTag Optional: A key of the items. If this parameter is set, the nodes will have a tag for filtering them. * * @returns An array containing the parent node with all its children. */ private makePartialFlatTree( item: OSTreeNode, level: number ): FlatNode[] { const children = item.children; const node: FlatNode = { id: item.id, item: item.item, expandable: !!children, isExpanded: !!children, level: level, isSeen: true }; const flatNodes: FlatNode[] = [node]; if (children) { for (const child of children) { flatNodes.push(...this.makePartialFlatTree(child, level + 1)); } } return flatNodes; } /** * Function, that returns a node containing information like id, name and children. * Children only, if available. * * @param node The node which is converted. * @param nodes The array with all nodes to convert. * @param length The number of converted nodes related to the parent node. * * @returns `OSTreeNodeWithOutItem` */ private buildBranchFromFlatTree( node: FlatNode, nodes: FlatNode[], length: number ): { node: TreeIdNode; length: number } { const children = []; // Begins at the position of the node in the array. // Ends if the next node has the same or higher level than the given node. for (let i = node.position + 1; !!nodes[i] && nodes[i].level >= node.level + 1; ++i) { const nextNode = nodes[i]; // The next node is a child if the level is one higher than the given node. if (nextNode.level === node.level + 1) { // Makes the child nodes recursively. const child = this.buildBranchFromFlatTree(nextNode, nodes, 0); length += child.length; children.push(child.node); } } // Makes the node with child nodes. const osNode: TreeIdNode = { id: node.id, children: children.length > 0 ? children : undefined }; // Returns the built node and increase the length by one. return { node: osNode, length: ++length }; } }