422 lines
15 KiB
TypeScript
422 lines
15 KiB
TypeScript
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<T> extends TreeNodeWithoutItem {
|
|
item: T;
|
|
children?: OSTreeNode<T>[];
|
|
}
|
|
|
|
/**
|
|
* 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<T> {
|
|
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<T extends Identifiable & Displayable>(item: T, key: keyof T): number {
|
|
return (<any>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<T extends Identifiable & Displayable>(item: T, children?: OSTreeNode<T>[]): OSTreeNode<T> {
|
|
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<T extends Identifiable & Displayable>(
|
|
items: T[],
|
|
weightKey: keyof T,
|
|
parentKey: keyof T
|
|
): FlatNode<T>[] {
|
|
const tree = this.makeTree(items, weightKey, parentKey);
|
|
const flatNodes: FlatNode<T>[] = [];
|
|
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<T extends Identifiable & Displayable>(nodes: FlatNode<T>[]): 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<T extends Identifiable & Displayable>(
|
|
items: T[],
|
|
weightKey: keyof T,
|
|
parentIdKey: keyof T
|
|
): OSTreeNode<T>[] {
|
|
// 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<T>[] = _models => {
|
|
if (!_models) {
|
|
return;
|
|
}
|
|
const nodes: OSTreeNode<T>[] = [];
|
|
_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<T>(tree: OSTreeNode<T>[]): 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<T extends Identifiable & Displayable>(branch: OSTreeNode<T>, 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<T extends Identifiable & Displayable>(
|
|
tree: OSTreeNode<T>[],
|
|
items: T[]
|
|
): OSTreeNode<T>[] {
|
|
let results: OSTreeNode<T>[] = [];
|
|
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<T extends Identifiable & Displayable>(
|
|
tree: OSTreeNode<T>[],
|
|
branches: OSTreeNode<T>[],
|
|
parentId: number,
|
|
olderSibling?: number
|
|
): OSTreeNode<T>[] {
|
|
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<T extends Identifiable & Displayable>(
|
|
tree: OSTreeNode<T>[],
|
|
items: T[]
|
|
): OSTreeNode<T>[] {
|
|
const result: OSTreeNode<T>[] = [];
|
|
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<T extends Identifiable & Displayable>(tree: OSTreeNode<T>[]): 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<T extends Identifiable & Displayable>(
|
|
item: OSTreeNode<T>,
|
|
level: number
|
|
): FlatNode<T>[] {
|
|
const children = item.children;
|
|
const node: FlatNode<T> = {
|
|
id: item.id,
|
|
item: item.item,
|
|
expandable: !!children,
|
|
isExpanded: !!children,
|
|
level: level,
|
|
isSeen: true
|
|
};
|
|
const flatNodes: FlatNode<T>[] = [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<T extends Identifiable & Displayable>(
|
|
node: FlatNode<T>,
|
|
nodes: FlatNode<T>[],
|
|
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 };
|
|
}
|
|
}
|