2022-02-09 22:03:03 -08:00

445 lines
13 KiB
TypeScript

/*
* Copyright [2021] [wisemapping]
*
* Licensed under WiseMapping Public License, Version 1.0 (the "License").
* It is basically the Apache License, Version 2.0 (the "License") plus the
* "powered by wisemapping" text requirement on every single page;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the license at
*
* http://www.wisemapping.org/license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { $assert, $defined } from '@wisemapping/core-js';
import PositionType from '../PositionType';
import Node from './Node';
class RootedTreeSet {
private _rootNodes: Node[];
protected _children: Node[];
constructor() {
this._rootNodes = [];
}
/**
* @param root
* @throws will throw an error if root is null or undefined
*/
setRoot(root: Node) {
$assert(root, 'root can not be null');
this._rootNodes.push(this._decodate(root));
}
/** getter */
getTreeRoots() {
return this._rootNodes;
}
_decodate(node: Node) {
node._children = [];
return node;
}
/**
* @param {mindplot.model.NodeModel} node
* @throws will throw an error if node is null or undefined
* @throws will throw an error if node with id already exists
* @throws will throw an error if node has been added already
*/
add(node: Node) {
$assert(node, 'node can not be null');
$assert(
!this.find(node.getId(), false),
`node already exits with this id. Id:${node.getId()}: ${this.dump()}`,
);
$assert(!node._children, 'node already added');
this._rootNodes.push(this._decodate(node));
}
/**
* @param nodeId
* @throws will throw an error if nodeId is null or undefined
*/
remove(nodeId: number) {
$assert($defined(nodeId), 'nodeId can not be null');
const node = this.find(nodeId);
this._rootNodes = this._rootNodes.filter((n) => n !== node);
}
/**
* @param parentId
* @param childId
* @throws will throw an error if parentId is null or undefined
* @throws will throw an error if childId is null or undefined
* @throws will throw an error if node with id childId is already a child of parent
*/
connect(parentId: number, childId: number) {
$assert($defined(parentId), 'parent can not be null');
$assert($defined(childId), 'child can not be null');
const parent = this.find(parentId);
const child = this.find(childId, true);
$assert(
!child._parent,
`node already connected. Id:${child.getId()},previous:${child._parent}`,
);
parent._children.push(child);
child._parent = parent;
this._rootNodes = this._rootNodes.filter((c) => c !== child);
}
/**
* @param nodeId
* @throws will throw an error if nodeId is null or undefined
* @throws will throw an error if node is not connected
*/
disconnect(nodeId: number) {
$assert($defined(nodeId), 'nodeId can not be null');
const node = this.find(nodeId);
$assert(node._parent, 'Node is not connected');
node._parent._children = node._parent._children.filter((n) => node !== n);
this._rootNodes.push(node);
node._parent = null;
}
/**
* @param id
* @param validate
* @throws will throw an error if id is null or undefined
* @throws will throw an error if node cannot be found
* @return node
*/
find(id: number, validate = true): Node {
$assert($defined(id), 'id can not be null');
const graphs = this._rootNodes;
let result = null;
for (let i = 0; i < graphs.length; i++) {
const node = graphs[i];
result = this._find(id, node);
if (result) {
break;
}
}
$assert(
validate ? result : true,
`node could not be found id:${id}\n,RootedTreeSet${this.dump()}`,
);
return result;
}
private _find(id: number, parent: Node): Node {
if (parent.getId() === id) {
return parent;
}
let result = null;
const children = parent._children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
result = this._find(id, child);
if (result) break;
}
return result;
}
/**
* @param node
* @throws will throw an error if nodeId is null or undefined
* @return children
*/
getChildren(node: Node): Node[] {
$assert(node, 'node cannot be null');
return node._children;
}
/**
* @param node
* @throws will throw an error if node is null or undefined
* @return root node or the provided node, if it has no parent
*/
getRootNode(node: Node) {
$assert(node, 'node cannot be null');
const parent = this.getParent(node);
if ($defined(parent)) {
return this.getRootNode(parent);
}
return node;
}
/**
* @param node
* @throws will throw an error if node is null or undefined
* @return {Array} ancestors */
getAncestors(node: Node): Node[] {
$assert(node, 'node cannot be null');
return this._getAncestors(this.getParent(node), []);
}
_getAncestors(node: Node, ancestors: Node[]) {
const result = ancestors;
if (node) {
result.push(node);
this._getAncestors(this.getParent(node), result);
}
return result;
}
/**
* @param node
* @throws will throw an error if node is null or undefined
* @return {Array} siblings
*/
getSiblings(node: Node): Node[] {
$assert(node, 'node cannot be null');
if (!$defined(node._parent)) {
return [];
}
const siblings = node._parent._children.filter((child) => child !== node);
return siblings;
}
/**
* @param node
* @throws will throw an error if node is null or undefined
* @return {Boolean} whether the node has a single path to a single leaf (no branching)
*/
hasSinglePathToSingleLeaf(node: Node): boolean {
$assert(node, 'node cannot be null');
return this._hasSinglePathToSingleLeaf(node);
}
private _hasSinglePathToSingleLeaf(node: Node): boolean {
const children = this.getChildren(node);
if (children.length === 1) {
return this._hasSinglePathToSingleLeaf(children[0]);
}
return children.length === 0;
}
/**
* @param node
* @return {Boolean} whether the node is the start of a subbranch */
isStartOfSubBranch(node: Node): boolean {
return this.getSiblings(node).length > 0 && this.getChildren(node).length === 1;
}
/**
* @param node
* @throws will throw an error if node is null or undefined
* @return {Boolean} whether the node is a leaf
*/
isLeaf(node: Node): boolean {
$assert(node, 'node cannot be null');
return this.getChildren(node).length === 0;
}
/**
* @param node
* @throws will throw an error if node is null or undefined
* @return parent
*/
getParent(node: Node): Node {
$assert(node, 'node cannot be null');
return node._parent;
}
/**
* @return result
*/
dump() {
const branches = this._rootNodes;
let result = '';
for (let i = 0; i < branches.length; i++) {
const branch = branches[i];
result += this._dump(branch, '');
}
return result;
}
_dump(node: Node, indent: string) {
let result = `${indent + node}\n`;
const children = this.getChildren(node);
for (let i = 0; i < children.length; i++) {
const child = children[i];
result += this._dump(child, `${indent} `);
}
return result;
}
/**
* @param canvas
*/
plot(canvas) {
const branches = this._rootNodes;
for (let i = 0; i < branches.length; i++) {
const branch = branches[i];
this._plot(canvas, branch);
}
}
_plot(canvas, node: Node, root?) {
const children = this.getChildren(node);
const cx = node.getPosition().x + canvas.width / 2 - node.getSize().width / 2;
const cy = node.getPosition().y + canvas.height / 2 - node.getSize().height / 2;
const rect = canvas.rect(cx, cy, node.getSize().width, node.getSize().height);
const order = node.getOrder() == null ? 'r' : node.getOrder();
const text = canvas.text(
node.getPosition().x + canvas.width / 2,
node.getPosition().y + canvas.height / 2,
`${node.getId()}[${order}]`,
);
text.attr('fill', '#FFF');
let fillColor;
if (this._rootNodes.includes(node)) {
fillColor = '#000';
} else {
fillColor = node.isFree()
? '#abc'
: '#c00';
}
rect.attr('fill', fillColor);
const rectPosition = {
x: rect.attr('x') - canvas.width / 2 + rect.attr('width') / 2,
y: rect.attr('y') - canvas.height / 2 + rect.attr('height') / 2,
};
const rectSize = { width: rect.attr('width'), height: rect.attr('height') };
rect.click(() => {
console.log(
`[id:${node.getId()
}, order:${node.getOrder()
}, position:(${rectPosition.x
}, ${rectPosition.y
}), size:${rectSize.width
},${rectSize.height
}, freeDisplacement:(${node.getFreeDisplacement().x
},${node.getFreeDisplacement().y
})]`,
);
});
text.click(() => {
console.log(
`[id:${node.getId()
}, order:${node.getOrder()
}, position:(${rectPosition.x
},${rectPosition.y
}), size:${rectSize.width
}x${rectSize.height
}, freeDisplacement:(${node.getFreeDisplacement().x
},${node.getFreeDisplacement().y
})]`,
);
});
for (let i = 0; i < children.length; i++) {
const child = children[i];
this._plot(canvas, child);
}
}
/**
* @param node
* @param position
*/
updateBranchPosition(node: Node, position: PositionType): void {
const oldPos = node.getPosition();
node.setPosition(position);
const xOffset = oldPos.x - position.x;
const yOffset = oldPos.y - position.y;
const children = this.getChildren(node);
const me = this;
children.forEach((child) => {
me.shiftBranchPosition(child, xOffset, yOffset);
});
}
/**
* @param node
* @param xOffset
* @param yOffset
*/
shiftBranchPosition(node: Node, xOffset: number, yOffset: number): void {
const position = node.getPosition();
node.setPosition({ x: position.x + xOffset, y: position.y + yOffset });
const children = this.getChildren(node);
const me = this;
children.forEach((child) => {
me.shiftBranchPosition(child, xOffset, yOffset);
});
}
/**
* @param node
* @param yOffset
* @return siblings in the offset (vertical) direction, i.e. with lower or higher order
*/
getSiblingsInVerticalDirection(node: Node, yOffset: number): Node[] {
// siblings with lower or higher order
// (depending on the direction of the offset and on the same side as their parent)
const parent = this.getParent(node);
const siblings = this.getSiblings(node).filter((sibling) => {
const sameSide = node.getPosition().x > parent.getPosition().x
? sibling.getPosition().x > parent.getPosition().x
: sibling.getPosition().x < parent.getPosition().x;
const orderOK = yOffset < 0
? sibling.getOrder() < node.getOrder()
: sibling.getOrder() > node.getOrder();
return orderOK && sameSide;
});
if (yOffset < 0) {
siblings.reverse();
}
return siblings;
}
/**
* @param node
* @param yOffset
* @return branches of the root node on the same side as the given node's, in the given
* vertical direction
*/
getBranchesInVerticalDirection(node: Node, yOffset: number): Node[] {
// direct descendants of the root that do not contain the node and are on the same side
// and on the direction of the offset
const rootNode = this.getRootNode(node);
const branches = this.getChildren(rootNode)
.filter(((child) => this._find(node.getId(), child)));
const branch = branches[0];
const result = this.getSiblings(branch).filter((sibling) => {
const sameSide = node.getPosition().x > rootNode.getPosition().x
? sibling.getPosition().x > rootNode.getPosition().x
: sibling.getPosition().x < rootNode.getPosition().x;
const sameDirection = yOffset < 0
? sibling.getOrder() < branch.getOrder()
: sibling.getOrder() > branch.getOrder();
return sameSide && sameDirection;
}, this);
return result;
}
}
export default RootedTreeSet;