wisemapping-open-source/mindplot/src/main/javascript/layout/RootedTreeSet.js

396 lines
13 KiB
JavaScript

/*
* Copyright [2015] [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.
*/
mindplot.layout.RootedTreeSet = new Class(/** @lends RootedTreeSet */{
/** @constructs */
initialize:function () {
this._rootNodes = [];
},
/**
* @param root
* @throws will throw an error if root is null or undefined
*/
setRoot:function (root) {
$assert(root, 'root can not be null');
this._rootNodes.push(this._decodate(root));
},
/** getter */
getTreeRoots:function () {
return this._rootNodes;
},
_decodate:function (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:function (node) {
$assert(node, 'node can not be null');
$assert(!this.find(node.getId(), false), 'node already exits with this id. Id:' + node.getId());
$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:function (nodeId) {
$assert($defined(nodeId), 'nodeId can not be null');
var node = this.find(nodeId);
this._rootNodes.erase(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:function (parentId, childId) {
$assert($defined(parentId), 'parent can not be null');
$assert($defined(childId), 'child can not be null');
var parent = this.find(parentId);
var 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.erase(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:function (nodeId) {
$assert($defined(nodeId), 'nodeId can not be null');
var node = this.find(nodeId);
$assert(node._parent, "Node is not connected");
node._parent._children.erase(node);
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:function (id, validate) {
$assert($defined(id), 'id can not be null');
var graphs = this._rootNodes;
var result = null;
for (var i = 0; i < graphs.length; i++) {
var node = graphs[i];
result = this._find(id, node);
if (result) {
break;
}
}
validate = !$defined(validate) ? true : validate;
$assert(validate ? result : true, 'node could not be found id:' + id + "\n,RootedTreeSet" + this.dump());
return result;
},
_find:function (id, parent) {
if (parent.getId() == id) {
return parent;
}
var result = null;
var children = parent._children;
for (var i = 0; i < children.length; i++) {
var 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:function (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:function (node) {
$assert(node, "node cannot be null");
var 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:function (node) {
$assert(node, 'node cannot be null');
return this._getAncestors(this.getParent(node), []);
},
_getAncestors:function (node, ancestors) {
var 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:function (node) {
$assert(node, 'node cannot be null');
if (!$defined(node._parent)) {
return [];
}
var siblings = node._parent._children.filter(function (child) {
return 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:function (node) {
$assert(node, 'node cannot be null');
return this._hasSinglePathToSingleLeaf(node);
},
_hasSinglePathToSingleLeaf:function (node) {
var 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:function (node) {
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:function (node) {
$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:function (node) {
$assert(node, 'node cannot be null');
return node._parent;
},
/**
* @return result
*/
dump:function () {
var branches = this._rootNodes;
var result = "";
for (var i = 0; i < branches.length; i++) {
var branch = branches[i];
result += this._dump(branch, "");
}
return result;
},
_dump:function (node, indent) {
var result = indent + node + "\n";
var children = this.getChildren(node);
for (var i = 0; i < children.length; i++) {
var child = children[i];
result += this._dump(child, indent + " ");
}
return result;
},
/**
* @param canvas
*/
plot:function (canvas) {
var branches = this._rootNodes;
for (var i = 0; i < branches.length; i++) {
var branch = branches[i];
this._plot(canvas, branch);
}
},
_plot:function (canvas, node, root) {
var children = this.getChildren(node);
var cx = node.getPosition().x + canvas.width / 2 - node.getSize().width / 2;
var cy = node.getPosition().y + canvas.height / 2 - node.getSize().height / 2;
var rect = canvas.rect(cx, cy, node.getSize().width, node.getSize().height);
var order = node.getOrder() == null ? "r" : node.getOrder();
var text = canvas.text(node.getPosition().x + canvas.width / 2, node.getPosition().y + canvas.height / 2, node.getId() + "[" + order + "]");
text.attr('fill', '#FFF');
var fillColor = this._rootNodes.contains(node) ? "#000" : (node.isFree() ? "#abc" : "#c00");
rect.attr('fill', fillColor);
var rectPosition = {x:rect.attr("x") - canvas.width / 2 + rect.attr("width") / 2, y:rect.attr("y") - canvas.height / 2 + rect.attr("height") / 2};
var rectSize = {width:rect.attr("width"), height:rect.attr("height")};
rect.click(function () {
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 + ")]");
});
text.click(function () {
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 (var i = 0; i < children.length; i++) {
var child = children[i];
this._plot(canvas, child);
}
},
/**
* @param node
* @param position
*/
updateBranchPosition:function (node, position) {
var oldPos = node.getPosition();
node.setPosition(position);
var xOffset = oldPos.x - position.x;
var yOffset = oldPos.y - position.y;
var children = this.getChildren(node);
var me = this;
_.each(children, function (child) {
me.shiftBranchPosition(child, xOffset, yOffset);
});
},
/**
* @param node
* @param xOffset
* @param yOffset
*/
shiftBranchPosition:function (node, xOffset, yOffset) {
var position = node.getPosition();
node.setPosition({x:position.x + xOffset, y:position.y + yOffset});
var children = this.getChildren(node);
var me = this;
_.each(children, function (child) {
me.shiftBranchPosition(child, xOffset, yOffset);
});
},
/**
* @param node
* @param yOffset
* @return siblings in the offset (vertical) direction, i.e. with lower or higher order, respectively
*/
getSiblingsInVerticalDirection:function (node, yOffset) {
// siblings with lower or higher order, depending on the direction of the offset and on the same side as their parent
var parent = this.getParent(node);
var siblings = this.getSiblings(node).filter(function (sibling) {
var sameSide = node.getPosition().x > parent.getPosition().x ? sibling.getPosition().x > parent.getPosition().x : sibling.getPosition().x < parent.getPosition().x;
var 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:function (node, yOffset) {
// direct descendants of the root that do not contain the node and are on the same side
// and on the direction of the offset
var rootNode = this.getRootNode(node);
var branches = this.getChildren(rootNode).filter(function (child) {
return this._find(node.getId(), child);
}, this);
var branch = branches[0];
var rootDescendants = this.getSiblings(branch).filter(function (sibling) {
var sameSide = node.getPosition().x > rootNode.getPosition().x ? sibling.getPosition().x > rootNode.getPosition().x : sibling.getPosition().x < rootNode.getPosition().x;
var sameDirection = yOffset < 0 ? sibling.getOrder() < branch.getOrder() : sibling.getOrder() > branch.getOrder();
return sameSide && sameDirection;
}, this);
return rootDescendants;
}
});