@progress/kendo-ui
Version:
This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.
1,314 lines (1,156 loc) • 151 kB
JavaScript
module.exports =
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ({
/***/ 0:
/***/ (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__(876);
/***/ }),
/***/ 3:
/***/ (function(module, exports) {
module.exports = function() { throw new Error("define cannot be used indirect"); };
/***/ }),
/***/ 876:
/***/ (function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function(f, define){
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [ __webpack_require__(877) ], __WEBPACK_AMD_DEFINE_FACTORY__ = (f), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
})(function(){
(function ($, undefined) {
var kendo = window.kendo,
diagram = kendo.dataviz.diagram,
Graph = diagram.Graph,
Node = diagram.Node,
Link = diagram.Link,
deepExtend = kendo.deepExtend,
Size = diagram.Size,
Rect = diagram.Rect,
Dictionary = diagram.Dictionary,
Set = diagram.Set,
HyperTree = diagram.Graph,
Utils = diagram.Utils,
Point = diagram.Point,
EPSILON = 1e-06,
DEG_TO_RAD = Math.PI / 180,
contains = Utils.contains,
grep = $.grep;
/**
* Base class for layout algorithms.
* @type {*}
*/
var LayoutBase = kendo.Class.extend({
defaultOptions: {
type: "Tree",
subtype: "Down",
roots: null,
animate: false,
//-------------------------------------------------------------------
/**
* Force-directed option: whether the motion of the nodes should be limited by the boundaries of the diagram surface.
*/
limitToView: false,
/**
* Force-directed option: the amount of friction applied to the motion of the nodes.
*/
friction: 0.9,
/**
* Force-directed option: the optimal distance between nodes (minimum energy).
*/
nodeDistance: 50,
/**
* Force-directed option: the number of time things are being calculated.
*/
iterations: 300,
//-------------------------------------------------------------------
/**
* Tree option: the separation in one direction (depends on the subtype what direction this is).
*/
horizontalSeparation: 90,
/**
* Tree option: the separation in the complementary direction (depends on the subtype what direction this is).
*/
verticalSeparation: 50,
//-------------------------------------------------------------------
/**
* Tip-over tree option: children-to-parent vertical distance.
*/
underneathVerticalTopOffset: 15,
/**
* Tip-over tree option: children-to-parent horizontal distance.
*/
underneathHorizontalOffset: 15,
/**
* Tip-over tree option: leaf-to-next-branch vertical distance.
*/
underneathVerticalSeparation: 15,
//-------------------------------------------------------------------
/**
* Settings object to organize the different components of the diagram in a grid layout structure
*/
grid: {
/**
* The width of the grid in which components are arranged. Beyond this width a component will be on the next row.
*/
width: 1500,
/**
* The left offset of the grid.
*/
offsetX: 50,
/**
* The top offset of the grid.
*/
offsetY: 50,
/**
* The horizontal padding within a cell of the grid where a single component resides.
*/
componentSpacingX: 20,
/**
* The vertical padding within a cell of the grid where a single component resides.
*/
componentSpacingY: 20
},
//-------------------------------------------------------------------
/**
* Layered option: the separation height/width between the layers.
*/
layerSeparation: 50,
/**
* Layered option: how many rounds of shifting and fine-tuning.
*/
layeredIterations: 2,
/**
* Tree-radial option: the angle at which the layout starts.
*/
startRadialAngle: 0,
/**
* Tree-radial option: the angle at which the layout starts.
*/
endRadialAngle: 360,
/**
* Tree-radial option: the separation between levels.
*/
radialSeparation: 150,
/**
* Tree-radial option: the separation between the root and the first level.
*/
radialFirstLevelSeparation: 200,
/**
* Tree-radial option: whether a virtual roots bing the components in one radial layout.
*/
keepComponentsInOneRadialLayout: false,
//-------------------------------------------------------------------
// TODO: ensure to change this to false when containers are around
ignoreContainers: true,
layoutContainerChildren: false,
ignoreInvisible: true,
animateTransitions: false
},
init: function () {
},
/**
* Organizes the components in a grid.
* Returns the final set of nodes (not the Graph).
* @param components
*/
gridLayoutComponents: function (components) {
if (!components) {
throw "No components supplied.";
}
// calculate and cache the bounds of the components
Utils.forEach(components, function (c) {
c.calcBounds();
});
// order by decreasing width
components.sort(function (a, b) {
return b.bounds.width - a.bounds.width;
});
var maxWidth = this.options.grid.width,
offsetX = this.options.grid.componentSpacingX,
offsetY = this.options.grid.componentSpacingY,
height = 0,
startX = this.options.grid.offsetX,
startY = this.options.grid.offsetY,
x = startX,
y = startY,
i,
resultLinkSet = [],
resultNodeSet = [];
while (components.length > 0) {
if (x >= maxWidth) {
// start a new row
x = startX;
y += height + offsetY;
// reset the row height
height = 0;
}
var component = components.pop();
this.moveToOffset(component, new Point(x, y));
for (i = 0; i < component.nodes.length; i++) {
resultNodeSet.push(component.nodes[i]); // to be returned in the end
}
for (i = 0; i < component.links.length; i++) {
resultLinkSet.push(component.links[i]);
}
var boundingRect = component.bounds;
var currentHeight = boundingRect.height;
if (currentHeight <= 0 || isNaN(currentHeight)) {
currentHeight = 0;
}
var currentWidth = boundingRect.width;
if (currentWidth <= 0 || isNaN(currentWidth)) {
currentWidth = 0;
}
if (currentHeight >= height) {
height = currentHeight;
}
x += currentWidth + offsetX;
}
return {
nodes: resultNodeSet,
links: resultLinkSet
};
},
moveToOffset: function (component, p) {
var i, j,
bounds = component.bounds,
deltax = p.x - bounds.x,
deltay = p.y - bounds.y;
for (i = 0; i < component.nodes.length; i++) {
var node = component.nodes[i];
var nodeBounds = node.bounds();
if (nodeBounds.width === 0 && nodeBounds.height === 0 && nodeBounds.x === 0 && nodeBounds.y === 0) {
nodeBounds = new Rect(0, 0, 0, 0);
}
nodeBounds.x += deltax;
nodeBounds.y += deltay;
node.bounds(nodeBounds);
}
for (i = 0; i < component.links.length; i++) {
var link = component.links[i];
if (link.points) {
var newpoints = [];
var points = link.points;
for (j = 0; j < points.length; j++) {
var pt = points[j];
pt.x += deltax;
pt.y += deltay;
newpoints.push(pt);
}
link.points = newpoints;
}
}
this.currentHorizontalOffset += bounds.width + this.options.grid.offsetX;
return new Point(deltax, deltay);
},
transferOptions: function (options) {
// Size options lead to stackoverflow and need special handling
this.options = kendo.deepExtend({}, this.defaultOptions);
if (Utils.isUndefined(options)) {
return;
}
this.options = kendo.deepExtend(this.options, options || {});
}
});
/**
* The data bucket a hypertree holds in its nodes. *
* @type {*}
*/
/* var ContainerGraph = kendo.Class.extend({
init: function (diagram) {
this.diagram = diagram;
this.graph = new Graph(diagram);
this.container = null;
this.containerNode = null;
}
});*/
/**
* Adapter between the diagram control and the graph representation. It converts shape and connections to nodes and edges taking into the containers and their collapsef state,
* the visibility of items and more. If the layoutContainerChildren is true a hypertree is constructed which holds the hierarchy of containers and many conditions are analyzed
* to investigate how the effective graph structure looks like and how the layout has to be performed.
* @type {*}
*/
var DiagramToHyperTreeAdapter = kendo.Class.extend({
init: function (diagram) {
/**
* The mapping to/from the original nodes.
* @type {Dictionary}
*/
this.nodeMap = new Dictionary();
/**
* Gets the mapping of a shape to a container in case the shape sits in a collapsed container.
* @type {Dictionary}
*/
this.shapeMap = new Dictionary();
/**
* The nodes being mapped.
* @type {Dictionary}
*/
this.nodes = [];
/**
* The connections being mapped.
* @type {Dictionary}
*/
this.edges = [];
// the mapping from an edge to all the connections it represents, this can be both because of multiple connections between
// two shapes or because a container holds multiple connections to another shape or container.
this.edgeMap = new Dictionary();
/**
* The resulting set of Nodes when the analysis has finished.
* @type {Array}
*/
this.finalNodes = [];
/**
* The resulting set of Links when the analysis has finished.
* @type {Array}
*/
this.finalLinks = [];
/**
* The items being omitted because of multigraph edges.
* @type {Array}
*/
this.ignoredConnections = [];
/**
* The items being omitted because of containers, visibility and other factors.
* @type {Array}
*/
this.ignoredShapes = [];
/**
* The map from a node to the partition/hypernode in which it sits. This hyperMap is null if 'options.layoutContainerChildren' is false.
* @type {Dictionary}
*/
this.hyperMap = new Dictionary();
/**
* The hypertree contains the hierarchy defined by the containers.
* It's in essence a Graph of Graphs with a tree structure defined by the hierarchy of containers.
* @type {HyperTree}
*/
this.hyperTree = new Graph();
/**
* The resulting graph after conversion. Note that this does not supply the information contained in the
* ignored connection and shape collections.
* @type {null}
*/
this.finalGraph = null;
this.diagram = diagram;
},
/**
* The hyperTree is used when the 'options.layoutContainerChildren' is true. It contains the hierarchy of containers whereby each node is a ContainerGraph.
* This type of node has a Container reference to the container which holds the Graph items. There are three possible situations during the conversion process:
* - Ignore the containers: the container are non-existent and only normal shapes are mapped. If a shape has a connection to a container it will be ignored as well
* since there is no node mapped for the container.
* - Do not ignore the containers and leave the content of the containers untouched: the top-level elements are being mapped and the children within a container are not altered.
* - Do not ignore the containers and organize the content of the containers as well: the hypertree is constructed and there is a partitioning of all nodes and connections into the hypertree.
* The only reason a connection or node is not being mapped might be due to the visibility, which includes the visibility change through a collapsed parent container.
* @param options
*/
convert: function (options) {
if (Utils.isUndefined(this.diagram)) {
throw "No diagram to convert.";
}
this.options = kendo.deepExtend({
ignoreInvisible: true,
ignoreContainers: true,
layoutContainerChildren: false
},
options || {}
);
this.clear();
// create the nodes which participate effectively in the graph analysis
this._renormalizeShapes();
// recreate the incoming and outgoing collections of each and every node
this._renormalizeConnections();
// export the resulting graph
this.finalNodes = new Dictionary(this.nodes);
this.finalLinks = new Dictionary(this.edges);
this.finalGraph = new Graph();
this.finalNodes.forEach(function (n) {
this.finalGraph.addNode(n);
}, this);
this.finalLinks.forEach(function (l) {
this.finalGraph.addExistingLink(l);
}, this);
return this.finalGraph;
},
/**
* Maps the specified connection to an edge of the graph deduced from the given diagram.
* @param connection
* @returns {*}
*/
mapConnection: function (connection) {
return this.edgeMap.get(connection.id);
},
/**
* Maps the specified shape to a node of the graph deduced from the given diagram.
* @param shape
* @returns {*}
*/
mapShape: function (shape) {
return this.nodeMap.get(shape.id);
},
/**
* Gets the edge, if any, between the given nodes.
* @param a
* @param b
*/
getEdge: function (a, b) {
return Utils.first(a.links, function (link) {
return link.getComplement(a) === b;
});
},
/**
* Clears all the collections used by the conversion process.
*/
clear: function () {
this.finalGraph = null;
this.hyperTree = (!this.options.ignoreContainers && this.options.layoutContainerChildren) ? new HyperTree() : null;
this.hyperMap = (!this.options.ignoreContainers && this.options.layoutContainerChildren) ? new Dictionary() : null;
this.nodeMap = new Dictionary();
this.shapeMap = new Dictionary();
this.nodes = [];
this.edges = [];
this.edgeMap = new Dictionary();
this.ignoredConnections = [];
this.ignoredShapes = [];
this.finalNodes = [];
this.finalLinks = [];
},
/**
* The path from a given ContainerGraph to the root (container).
* @param containerGraph
* @returns {Array}
*/
listToRoot: function (containerGraph) {
var list = [];
var s = containerGraph.container;
if (!s) {
return list;
}
list.push(s);
while (s.parentContainer) {
s = s.parentContainer;
list.push(s);
}
list.reverse();
return list;
},
firstNonIgnorableContainer: function (shape) {
if (shape.isContainer && !this._isIgnorableItem(shape)) {
return shape;
}
return !shape.parentContainer ? null : this.firstNonIgnorableContainer(shape.parentContainer);
},
isContainerConnection: function (a, b) {
if (a.isContainer && this.isDescendantOf(a, b)) {
return true;
}
return b.isContainer && this.isDescendantOf(b, a);
},
/**
* Returns true if the given shape is a direct child or a nested container child of the given container.
* If the given container and shape are the same this will return false since a shape cannot be its own child.
* @param scope
* @param a
* @returns {boolean}
*/
isDescendantOf: function (scope, a) {
if (!scope.isContainer) {
throw "Expecting a container.";
}
if (scope === a) {
return false;
}
if (contains(scope.children, a)) {
return true;
}
var containers = [];
for (var i = 0, len = scope.children.length; i < len; i++) {
var c = scope.children[i];
if (c.isContainer && this.isDescendantOf(c, a)) {
containers.push(c);
}
}
return containers.length > 0;
},
isIgnorableItem: function (shape) {
if (this.options.ignoreInvisible) {
if (shape.isCollapsed && this._isVisible(shape)) {
return false;
}
if (!shape.isCollapsed && this._isVisible(shape)) {
return false;
}
return true;
}
else {
return shape.isCollapsed && !this._isTop(shape);
}
},
/**
* Determines whether the shape is or needs to be mapped to another shape. This occurs essentially when the shape sits in
* a collapsed container hierarchy and an external connection needs a node endpoint. This node then corresponds to the mapped shape and is
* necessarily a container in the parent hierarchy of the shape.
* @param shape
*/
isShapeMapped: function (shape) {
return shape.isCollapsed && !this._isVisible(shape) && !this._isTop(shape);
},
leastCommonAncestor: function (a, b) {
if (!a) {
throw "Parameter should not be null.";
}
if (!b) {
throw "Parameter should not be null.";
}
if (!this.hyperTree) {
throw "No hypertree available.";
}
var al = this.listToRoot(a);
var bl = this.listToRoot(b);
var found = null;
if (Utils.isEmpty(al) || Utils.isEmpty(bl)) {
return this.hyperTree.root.data;
}
var xa = al[0];
var xb = bl[0];
var i = 0;
while (xa === xb) {
found = al[i];
i++;
if (i >= al.length || i >= bl.length) {
break;
}
xa = al[i];
xb = bl[i];
}
if (!found) {
return this.hyperTree.root.data;
}
else {
return grep(this.hyperTree.nodes, function (n) {
return n.data.container === found;
});
}
},
/**
* Determines whether the specified item is a top-level shape or container.
* @param item
* @returns {boolean}
* @private
*/
_isTop: function (item) {
return !item.parentContainer;
},
/**
* Determines iteratively (by walking up the container stack) whether the specified shape is visible.
* This does NOT tell whether the item is not visible due to an explicit Visibility change or due to a collapse state.
* @param shape
* @returns {*}
* @private
*/
_isVisible: function (shape) {
if (!shape.visible()) {
return false;
}
return !shape.parentContainer ? shape.visible() : this._isVisible(shape.parentContainer);
},
_isCollapsed: function (shape) {
if (shape.isContainer && shape.isCollapsed) {
return true;
}
return shape.parentContainer && this._isCollapsed(shape.parentContainer);
},
/**
* First part of the graph creation; analyzing the shapes and containers and deciding whether they should be mapped to a Node.
* @private
*/
_renormalizeShapes: function () {
// add the nodes, the adjacency structure will be reconstructed later on
if (this.options.ignoreContainers) {
for (var i = 0, len = this.diagram.shapes.length; i < len; i++) {
var shape = this.diagram.shapes[i];
// if not visible (and ignoring the invisible ones) or a container we skip
if ((this.options.ignoreInvisible && !this._isVisible(shape)) || shape.isContainer) {
this.ignoredShapes.push(shape);
continue;
}
var node = new Node(shape.id, shape);
node.isVirtual = false;
// the mapping will always contain singletons and the hyperTree will be null
this.nodeMap.add(shape.id, node);
this.nodes.push(node);
}
}
else {
throw "Containers are not supported yet, but stay tuned.";
}
},
/**
* Second part of the graph creation; analyzing the connections and deciding whether they should be mapped to an edge.
* @private
*/
_renormalizeConnections: function () {
if (this.diagram.connections.length === 0) {
return;
}
for (var i = 0, len = this.diagram.connections.length; i < len; i++) {
var conn = this.diagram.connections[i];
if (this.isIgnorableItem(conn)) {
this.ignoredConnections.push(conn);
continue;
}
var source = !conn.sourceConnector ? null : conn.sourceConnector.shape;
var sink = !conn.targetConnector ? null : conn.targetConnector.shape;
// no layout for floating connections
if (!source || !sink) {
this.ignoredConnections.push(conn);
continue;
}
if (contains(this.ignoredShapes, source) && !this.shapeMap.containsKey(source)) {
this.ignoredConnections.push(conn);
continue;
}
if (contains(this.ignoredShapes, sink) && !this.shapeMap.containsKey(sink)) {
this.ignoredConnections.push(conn);
continue;
}
// if the endpoint sits in a collapsed container we need the container rather than the shape itself
if (this.shapeMap.containsKey(source)) {
source = this.shapeMap[source];
}
if (this.shapeMap.containsKey(sink)) {
sink = this.shapeMap[sink];
}
var sourceNode = this.mapShape(source);
var sinkNode = this.mapShape(sink);
if ((sourceNode === sinkNode) || this.areConnectedAlready(sourceNode, sinkNode)) {
this.ignoredConnections.push(conn);
continue;
}
if (sourceNode === null || sinkNode === null) {
throw "A shape was not mapped to a node.";
}
if (this.options.ignoreContainers) {
// much like a floating connection here since at least one end is attached to a container
if (sourceNode.isVirtual || sinkNode.isVirtual) {
this.ignoredConnections.push(conn);
continue;
}
var newEdge = new Link(sourceNode, sinkNode, conn.id, conn);
this.edgeMap.add(conn.id, newEdge);
this.edges.push(newEdge);
}
else {
throw "Containers are not supported yet, but stay tuned.";
}
}
},
areConnectedAlready: function (n, m) {
return Utils.any(this.edges, function (l) {
return l.source === n && l.target === m || l.source === m && l.target === n;
});
}
/**
* Depth-first traversal of the given container.
* @param container
* @param action
* @param includeStart
* @private
*/
/* _visitContainer: function (container, action, includeStart) {
*//*if (container == null) throw new ArgumentNullException("container");
if (action == null) throw new ArgumentNullException("action");
if (includeStart) action(container);
if (container.children.isEmpty()) return;
foreach(
var item
in
container.children.OfType < IShape > ()
)
{
var childContainer = item
as
IContainerShape;
if (childContainer != null) this.VisitContainer(childContainer, action);
else action(item);
}*//*
}*/
});
/**
* The classic spring-embedder (aka force-directed, Fruchterman-Rheingold, barycentric) algorithm.
* http://en.wikipedia.org/wiki/Force-directed_graph_drawing
* - Chapter 12 of Tamassia et al. "Handbook of graph drawing and visualization".
* - Kobourov on preprint arXiv; http://arxiv.org/pdf/1201.3011.pdf
* - Fruchterman and Rheingold in SOFTWARE-PRACTICE AND EXPERIENCE, VOL. 21(1 1), 1129-1164 (NOVEMBER 1991)
* @type {*}
*/
var SpringLayout = LayoutBase.extend({
init: function (diagram) {
var that = this;
LayoutBase.fn.init.call(that);
if (Utils.isUndefined(diagram)) {
throw "Diagram is not specified.";
}
this.diagram = diagram;
},
layout: function (options) {
this.transferOptions(options);
var adapter = new DiagramToHyperTreeAdapter(this.diagram);
var graph = adapter.convert(options);
if (graph.isEmpty()) {
return;
}
// split into connected components
var components = graph.getConnectedComponents();
if (Utils.isEmpty(components)) {
return;
}
for (var i = 0; i < components.length; i++) {
var component = components[i];
this.layoutGraph(component, options);
}
var finalNodeSet = this.gridLayoutComponents(components);
return new diagram.LayoutState(this.diagram, finalNodeSet);
},
layoutGraph: function (graph, options) {
if (Utils.isDefined(options)) {
this.transferOptions(options);
}
this.graph = graph;
var initialTemperature = this.options.nodeDistance * 9;
this.temperature = initialTemperature;
var guessBounds = this._expectedBounds();
this.width = guessBounds.width;
this.height = guessBounds.height;
for (var step = 0; step < this.options.iterations; step++) {
this.refineStage = step >= this.options.iterations * 5 / 6;
this.tick();
// exponential cooldown
this.temperature = this.refineStage ?
initialTemperature / 30 :
initialTemperature * (1 - step / (2 * this.options.iterations ));
}
},
/**
* Single iteration of the simulation.
*/
tick: function () {
var i;
// collect the repulsive forces on each node
for (i = 0; i < this.graph.nodes.length; i++) {
this._repulsion(this.graph.nodes[i]);
}
// collect the attractive forces on each node
for (i = 0; i < this.graph.links.length; i++) {
this._attraction(this.graph.links[i]);
}
// update the positions
for (i = 0; i < this.graph.nodes.length; i++) {
var node = this.graph.nodes[i];
var offset = Math.sqrt(node.dx * node.dx + node.dy * node.dy);
if (offset === 0) {
return;
}
node.x += Math.min(offset, this.temperature) * node.dx / offset;
node.y += Math.min(offset, this.temperature) * node.dy / offset;
if (this.options.limitToView) {
node.x = Math.min(this.width, Math.max(node.width / 2, node.x));
node.y = Math.min(this.height, Math.max(node.height / 2, node.y));
}
}
},
/**
* Shakes the node away from its current position to escape the deadlock.
* @param node A Node.
* @private
*/
_shake: function (node) {
// just a simple polar neighborhood
var rho = Math.random() * this.options.nodeDistance / 4;
var alpha = Math.random() * 2 * Math.PI;
node.x += rho * Math.cos(alpha);
node.y -= rho * Math.sin(alpha);
},
/**
* The typical Coulomb-Newton force law F=k/r^2
* @remark This only works in dimensions less than three.
* @param d
* @param n A Node.
* @param m Another Node.
* @returns {number}
* @private
*/
_InverseSquareForce: function (d, n, m) {
var force;
if (!this.refineStage) {
force = Math.pow(d, 2) / Math.pow(this.options.nodeDistance, 2);
}
else {
var deltax = n.x - m.x;
var deltay = n.y - m.y;
var wn = n.width / 2;
var hn = n.height / 2;
var wm = m.width / 2;
var hm = m.height / 2;
force = (Math.pow(deltax, 2) / Math.pow(wn + wm + this.options.nodeDistance, 2)) + (Math.pow(deltay, 2) / Math.pow(hn + hm + this.options.nodeDistance, 2));
}
return force * 4 / 3;
},
/**
* The typical Hooke force law F=kr^2
* @param d
* @param n
* @param m
* @returns {number}
* @private
*/
_SquareForce: function (d, n, m) {
return 1 / this._InverseSquareForce(d, n, m);
},
_repulsion: function (n) {
n.dx = 0;
n.dy = 0;
Utils.forEach(this.graph.nodes, function (m) {
if (m === n) {
return;
}
while (n.x === m.x && n.y === m.y) {
this._shake(m);
}
var vx = n.x - m.x;
var vy = n.y - m.y;
var distance = Math.sqrt(vx * vx + vy * vy);
var r = this._SquareForce(distance, n, m) * 2;
n.dx += (vx / distance) * r;
n.dy += (vy / distance) * r;
}, this);
},
_attraction: function (link) {
var t = link.target;
var s = link.source;
if (s === t) {
// loops induce endless shakes
return;
}
while (s.x === t.x && s.y === t.y) {
this._shake(t);
}
var vx = s.x - t.x;
var vy = s.y - t.y;
var distance = Math.sqrt(vx * vx + vy * vy);
var a = this._InverseSquareForce(distance, s, t) * 5;
var dx = (vx / distance) * a;
var dy = (vy / distance) * a;
t.dx += dx;
t.dy += dy;
s.dx -= dx;
s.dy -= dy;
},
/**
* Calculates the expected bounds after layout.
* @returns {*}
* @private
*/
_expectedBounds: function () {
var size, N = this.graph.nodes.length, /*golden ration optimal?*/ ratio = 1.5, multiplier = 4;
if (N === 0) {
return size;
}
size = Utils.fold(this.graph.nodes, function (s, node) {
var area = node.width * node.height;
if (area > 0) {
s += Math.sqrt(area);
return s;
}
return 0;
}, 0, this);
var av = size / N;
var squareSize = av * Math.ceil(Math.sqrt(N));
var width = squareSize * Math.sqrt(ratio);
var height = squareSize / Math.sqrt(ratio);
return { width: width * multiplier, height: height * multiplier };
}
});
var TreeLayoutProcessor = kendo.Class.extend({
init: function (options) {
this.center = null;
this.options = options;
},
layout: function (treeGraph, root) {
this.graph = treeGraph;
if (!this.graph.nodes || this.graph.nodes.length === 0) {
return;
}
if (!contains(this.graph.nodes, root)) {
throw "The given root is not in the graph.";
}
this.center = root;
this.graph.cacheRelationships();
/* var nonull = this.graph.nodes.where(function (n) {
return n.associatedShape != null;
});*/
// transfer the rects
/*nonull.forEach(function (n) {
n.Location = n.associatedShape.Position;
n.NodeSize = n.associatedShape.ActualBounds.ToSize();
}
);*/
// caching the children
/* nonull.forEach(function (n) {
n.children = n.getChildren();
});*/
this.layoutSwitch();
// apply the layout to the actual visuals
// nonull.ForEach(n => n.associatedShape.Position = n.Location);
},
layoutLeft: function (left) {
this.setChildrenDirection(this.center, "Left", false);
this.setChildrenLayout(this.center, "Default", false);
var h = 0, w = 0, y, i, node;
for (i = 0; i < left.length; i++) {
node = left[i];
node.TreeDirection = "Left";
var s = this.measure(node, Size.Empty);
w = Math.max(w, s.Width);
h += s.height + this.options.verticalSeparation;
}
h -= this.options.verticalSeparation;
var x = this.center.x - this.options.horizontalSeparation;
y = this.center.y + ((this.center.height - h) / 2);
for (i = 0; i < left.length; i++) {
node = left[i];
var p = new Point(x - node.Size.width, y);
this.arrange(node, p);
y += node.Size.height + this.options.verticalSeparation;
}
},
layoutRight: function (right) {
this.setChildrenDirection(this.center, "Right", false);
this.setChildrenLayout(this.center, "Default", false);
var h = 0, w = 0, y, i, node;
for (i = 0; i < right.length; i++) {
node = right[i];
node.TreeDirection = "Right";
var s = this.measure(node, Size.Empty);
w = Math.max(w, s.Width);
h += s.height + this.options.verticalSeparation;
}
h -= this.options.verticalSeparation;
var x = this.center.x + this.options.horizontalSeparation + this.center.width;
y = this.center.y + ((this.center.height - h) / 2);
for (i = 0; i < right.length; i++) {
node = right[i];
var p = new Point(x, y);
this.arrange(node, p);
y += node.Size.height + this.options.verticalSeparation;
}
},
layoutUp: function (up) {
this.setChildrenDirection(this.center, "Up", false);
this.setChildrenLayout(this.center, "Default", false);
var w = 0, y, node, i;
for (i = 0; i < up.length; i++) {
node = up[i];
node.TreeDirection = "Up";
var s = this.measure(node, Size.Empty);
w += s.width + this.options.horizontalSeparation;
}
w -= this.options.horizontalSeparation;
var x = this.center.x + (this.center.width / 2) - (w / 2);
// y = this.center.y -verticalSeparation -this.center.height/2 - h;
for (i = 0; i < up.length; i++) {
node = up[i];
y = this.center.y - this.options.verticalSeparation - node.Size.height;
var p = new Point(x, y);
this.arrange(node, p);
x += node.Size.width + this.options.horizontalSeparation;
}
},
layoutDown: function (down) {
var node, i;
this.setChildrenDirection(this.center, "Down", false);
this.setChildrenLayout(this.center, "Default", false);
var w = 0, y;
for (i = 0; i < down.length; i++) {
node = down[i];
node.treeDirection = "Down";
var s = this.measure(node, Size.Empty);
w += s.width + this.options.horizontalSeparation;
}
w -= this.options.horizontalSeparation;
var x = this.center.x + (this.center.width / 2) - (w / 2);
y = this.center.y + this.options.verticalSeparation + this.center.height;
for (i = 0; i < down.length; i++) {
node = down[i];
var p = new Point(x, y);
this.arrange(node, p);
x += node.Size.width + this.options.horizontalSeparation;
}
},
layoutRadialTree: function () {
// var rmax = children.Aggregate(0D, (current, node) => Math.max(node.SectorAngle, current));
this.setChildrenDirection(this.center, "Radial", false);
this.setChildrenLayout(this.center, "Default", false);
this.previousRoot = null;
var startAngle = this.options.startRadialAngle * DEG_TO_RAD;
var endAngle = this.options.endRadialAngle * DEG_TO_RAD;
if (endAngle <= startAngle) {
throw "Final angle should not be less than the start angle.";
}
this.maxDepth = 0;
this.origin = new Point(this.center.x, this.center.y);
this.calculateAngularWidth(this.center, 0);
// perform the layout
if (this.maxDepth > 0) {
this.radialLayout(this.center, this.options.radialFirstLevelSeparation, startAngle, endAngle);
}
// update properties of the root node
this.center.Angle = endAngle - startAngle;
},
tipOverTree: function (down, startFromLevel) {
if (Utils.isUndefined(startFromLevel)) {
startFromLevel = 0;
}
this.setChildrenDirection(this.center, "Down", false);
this.setChildrenLayout(this.center, "Default", false);
this.setChildrenLayout(this.center, "Underneath", false, startFromLevel);
var w = 0, y, node, i;
for (i = 0; i < down.length; i++) {
node = down[i];
// if (node.IsSpecial) continue;
node.TreeDirection = "Down";
var s = this.measure(node, Size.Empty);
w += s.width + this.options.horizontalSeparation;
}
w -= this.options.horizontalSeparation;
// putting the root in the center with respect to the whole diagram is not a nice result, let's put it with respect to the first level only
w -= down[down.length - 1].width;
w += down[down.length - 1].associatedShape.bounds().width;
var x = this.center.x + (this.center.width / 2) - (w / 2);
y = this.center.y + this.options.verticalSeparation + this.center.height;
for (i = 0; i < down.length; i++) {
node = down[i];
// if (node.IsSpecial) continue;
var p = new Point(x, y);
this.arrange(node, p);
x += node.Size.width + this.options.horizontalSeparation;
}
/*//let's place the special node, assuming there is only one
if (down.Count(n => n.IsSpecial) > 0)
{
var special = (from n in down where n.IsSpecial select n).First();
if (special.Children.Count > 0)
throw new DiagramException("The 'special' element should not have children.");
special.Data.Location = new Point(Center.Data.Location.X + Center.AssociatedShape.BoundingRectangle.Width + this.options.HorizontalSeparation, Center.Data.Location.Y);
}*/
},
calculateAngularWidth: function (n, d) {
if (d > this.maxDepth) {
this.maxDepth = d;
}
var aw = 0, w = 1000, h = 1000, diameter = d === 0 ? 0 : Math.sqrt((w * w) + (h * h)) / d;
if (n.children.length > 0) {
// eventually with n.IsExpanded
for (var i = 0, len = n.children.length; i < len; i++) {
var child = n.children[i];
aw += this.calculateAngularWidth(child, d + 1);
}
aw = Math.max(diameter, aw);
}
else {
aw = diameter;
}
n.sectorAngle = aw;
return aw;
},
sortChildren: function (n) {
var basevalue = 0, i;
// update basevalue angle for node ordering
if (n.parents.length > 1) {
throw "Node is not part of a tree.";
}
var p = n.parents[0];
if (p) {
var pl = new Point(p.x, p.y);
var nl = new Point(n.x, n.y);
basevalue = this.normalizeAngle(Math.atan2(pl.y - nl.y, pl.x - nl.x));
}
var count = n.children.length;
if (count === 0) {
return null;
}
var angle = [];
var idx = [];
for (i = 0; i < count; ++i) {
var c = n.children[i];
var l = new Point(c.x, c.y);
idx[i] = i;
angle[i] = this.normalizeAngle(-basevalue + Math.atan2(l.y - l.y, l.x - l.x));
}
Utils.bisort(angle, idx);
var col = []; // list of nodes
var children = n.children;
for (i = 0; i < count; ++i) {
col.push(children[idx[i]]);
}
return col;
},
normalizeAngle: function (angle) {
while (angle > Math.PI * 2) {
angle -= 2 * Math.PI;
}
while (angle < 0) {
angle += Math.PI * 2;
}
return angle;
},
radialLayout: function (node, radius, startAngle, endAngle) {
var deltaTheta = endAngle - startAngle;
var deltaThetaHalf = deltaTheta / 2.0;
var parentSector = node.sectorAngle;
var fraction = 0;
var sorted = this.sortChildren(node);
for (var i = 0, len = sorted.length; i < len; i++) {
var childNode = sorted[i];
var cp = childNode;
var childAngleFraction = cp.sectorAngle / parentSector;
if (childNode.children.length > 0) {
this.radialLayout(childNode,
radius + this.options.radialSeparation,
startAngle + (fraction * deltaTheta),
startAngle + ((fraction + childAngleFraction) * deltaTheta));
}
this.setPolarLocation(childNode, radius, startAngle + (fraction * deltaTheta) + (childAngleFraction * deltaThetaHalf));
cp.angle = childAngleFraction * deltaTheta;
fraction += childAngleFraction;
}
},
setPolarLocation: function (node, radius, angle) {
node.x = this.origin.x +