diff --git a/mindplot/src/main/javascript/nlayout/ChangeEvent.js b/mindplot/src/main/javascript/nlayout/ChangeEvent.js new file mode 100644 index 00000000..f7b1f81d --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/ChangeEvent.js @@ -0,0 +1,36 @@ +mindplot.nlayout.ChangeEvent = new Class({ + initialize:function(id) { + $assert(!isNaN(id), "id can not be null"); + this._id = id; + this._position = null; + this._order = null; + }, + + getId:function() { + return this._id; + }, + + getOrder: function() { + return this._order; + }, + + getPosition: function() { + return this._position; + }, + + setOrder: function(value) { + $assert(!isNaN(value), "value can not be null"); + this._order = value; + }, + + setPosition: function(value) { + $assert(value, "value can not be null"); + this._position = value; + }, + + toString: function() { + return "[order:" + this.getOrder() + ", position: {" + this.getPosition().x + "," + this.getPosition().y + "}]"; + } +}); + + diff --git a/mindplot/src/main/javascript/nlayout/ChildrenSorterStrategy.js b/mindplot/src/main/javascript/nlayout/ChildrenSorterStrategy.js new file mode 100644 index 00000000..37c362c0 --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/ChildrenSorterStrategy.js @@ -0,0 +1,23 @@ +mindplot.nlayout.ChildrenSorterStrategy = new Class({ + initialize:function() { + + }, + + predict:function(treeSet, parent, position) { + throw "Method must be implemented"; + }, + + sorter: function(treeSet, parent, child, order) { + throw "Method must be implemented"; + }, + + computeChildrenIdByHeights: function(treeSet, node) { + throw "Method must be implemented"; + }, + + computeOffsets:function(treeSet, node) { + throw "Method must be implemented"; + + } +}); + diff --git a/mindplot/src/main/javascript/nlayout/GridSorter.js b/mindplot/src/main/javascript/nlayout/GridSorter.js new file mode 100644 index 00000000..7aa406ae --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/GridSorter.js @@ -0,0 +1,8 @@ +mindplot.nlayout.GridSorter = new Class({ + Extends: mindplot.nlayout.SymetricSorder + +}); + +mindplot.nlayout.GridSorter.GRID_HORIZONTAR_SIZE = 50; +mindplot.nlayout.GridSorter.INTER_NODE_VERTICAL_DISTANCE = 50; + diff --git a/mindplot/src/main/javascript/nlayout/LayoutManager.js b/mindplot/src/main/javascript/nlayout/LayoutManager.js new file mode 100644 index 00000000..ad7c2ba3 --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/LayoutManager.js @@ -0,0 +1,115 @@ +mindplot.nlayout.LayoutManager = new Class({ + Extends: Events, + initialize: function(rootNodeId, rootSize) { + $assert($defined(rootNodeId), "rootNodeId can not be null"); + $assert(rootSize, "rootSize can not be null"); + + this._treeSet = new mindplot.nlayout.RootedTreeSet(); + this._layout = new mindplot.nlayout.OriginalLayout(this._treeSet); + + var rootNode = this._layout.createNode(rootNodeId, rootSize, {x:0,y:0}, 'root'); + this._treeSet.setRoot(rootNode); + this._events = []; + }, + + updateNodeSize: function(id, size) { + var node = this._treeSet.find(id); + node.setSize(size); + }, + + updateShirkState: function(id, isShrink) { + + }, + + connectNode: function(parentId, childId, order) { + $assert($defined(parentId), "parentId can not be null"); + $assert($defined(childId), "childId can not be null"); + $assert($defined(order), "order can not be null"); + + this._layout.connectNode(parentId, childId, order); + }, + + disconnectNode: function(sourceId) { + + }, + + deleteNode : function(id) { + + }, + + addNode:function(id, size, position) { + $assert($defined(id), "id can not be null"); + var result = this._layout.createNode(id, size, position, 'topic'); + this._treeSet.add(result); + }, + + predict: function(parentId, childId, position) { + $assert($defined(parentId), "parentId can not be null"); + $assert($defined(childId), "childId can not be null"); + $assert(position, "childId can not be null"); + + var parent = this._treeSet.find(parentId); + var sorter = parent.getSorter(); + var result = sorter.predict(parent, this._treeSet, position); + }, + + dump: function() { + console.log(this._treeSet.dump()); + }, + + layout: function(fireEvents) { + // File repositioning ... + this._layout.layout(); + + // Collect changes ... + this._collectChanges(); + + if (fireEvents) { + this.flushEvents(); + } + + }, + + flushEvents: function() { + this._events.forEach(function(event) { + this.fireEvent('change', event); + }, this); + this._events = []; + }, + + _collectChanges: function(nodes) { + if (!nodes) + nodes = this._treeSet.getTreeRoots(); + + nodes.forEach(function(node) { + if (node.hasOrderChanged() || node.hasPositionChanged()) { + + // Find or create a event ... + var id = node.getId(); + var event = this._events.some(function(event) { + return event.id == id; + }); + if (!event) { + event = new mindplot.nlayout.ChangeEvent(id); + } + + // Update nodes ... + if (node.hasOrderChanged()) { + event.setOrder(node.getOrder()); + node.resetOrderState(); + + } + + if (node.hasPositionChanged()) { + event.setPosition(node.getPosition()); + node.resetPositionState(); + } + this._events.push(event); + } + this._collectChanges(this._treeSet.getChildren(node)); + }, this); + + } + +}); + diff --git a/mindplot/src/main/javascript/nlayout/Node.js b/mindplot/src/main/javascript/nlayout/Node.js new file mode 100644 index 00000000..619d5a05 --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/Node.js @@ -0,0 +1,117 @@ +mindplot.nlayout.Node = new Class({ + initialize:function(id, size, position, sorter) { + $assert(!isNaN(id), "id can not be null"); + $assert(size, "size can not be null"); + $assert(position, "position can not be null"); + $assert(sorter, "sorter can not be null"); + + this._id = id; + this._sorter = sorter; + this._properties = {}; + + this.setSize(size); + this.setPosition(position); + }, + + getId:function() { + return this._id; + }, + + setOrder: function(order) { + $assert(!isNaN(order), "Order can not be null"); + this._setProperty('order', order, false); + }, + + resetPositionState : function() { + var prop = this._properties['position']; + if (prop) { + prop.hasChanded = false; + } + }, + + resetOrderState : function() { + var prop = this._properties['order']; + if (prop) { + prop.hasChanded = false; + } + }, + + getOrder: function() { + return this._getProperty('order'); + }, + + hasOrderChanged: function() { + return this._isPropertyChanged('order'); + }, + + hasPositionChanged: function() { + return this._isPropertyChanged('position'); + + }, + + getPosition: function() { + return this._getProperty('position'); + }, + + setSize : function(size) { + $assert($defined(size), "Size can not be null"); + this._setProperty('size', Object.clone(size)); + }, + + getSize: function() { + return this._getProperty('size'); + }, + + setPosition : function(position) { + $assert($defined(position), "Position can not be null"); + $assert(!isNaN(position.x), "x can not be null"); + $assert(!isNaN(position.y), "y can not be null"); + + this._setProperty('position', Object.clone(position)); + }, + + _setProperty: function(key, value) { + var prop = this._properties[key]; + if (!prop) { + prop = { + hasChanded:false, + value: null, + oldValue : null + }; + } + + prop.oldValue = prop.value; + prop.value = value; + prop.hasChanded = true; + + this._properties[key] = prop; + }, + + _getProperty: function(key) { + var prop = this._properties[key]; + return $defined(prop) ? prop.value : null; + }, + + _isPropertyChanged: function(key) { + var prop = this._properties[key]; + return prop ? prop.hasChanded : false; + }, + + _setPropertyUpdated : function(key) { + var prop = this._properties[key]; + if (prop) { + this._properties[key] = true; + } + }, + + getSorter: function() { + return this._sorter; + }, + + + toString: function() { + return "[order:" + this.getOrder() + ", position: {" + this.getPosition().x + "," + this.getPosition().y + "}]"; + } + +}); + diff --git a/mindplot/src/main/javascript/nlayout/OriginalLayout.js b/mindplot/src/main/javascript/nlayout/OriginalLayout.js new file mode 100644 index 00000000..dc5d1c84 --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/OriginalLayout.js @@ -0,0 +1,90 @@ +mindplot.nlayout.OriginalLayout = new Class({ + initialize: function(treeSet) { + this._treeSet = treeSet; + + this._heightByNode = {}; + }, + + createNode:function(id, size, position, type) { + $assert($defined(id), "id can not be null"); + $assert(size, "size can not be null"); + $assert(position, "position can not be null"); + $assert(type, "type can not be null"); + + var strategy = type === 'root' ? mindplot.nlayout.OriginalLayout.GRID_SORTER : mindplot.nlayout.OriginalLayout.SYMETRIC_SORTER; + return new mindplot.nlayout.Node(id, size, position, strategy); + }, + + connectNode: function(parentId, childId, order) { + + var parent = this._treeSet.find(parentId); + var child = this._treeSet.find(childId); + + // Connect the new node ... + this._treeSet.connect(parentId, childId); + + // Insert the new node ... + var sorter = parent.getSorter(); + sorter.insert(this._treeSet, parent, child, order); + + // Fire a basic validation ... + sorter.verify(this._treeSet, parent); + }, + + layout: function() { + var roots = this._treeSet.getTreeRoots(); + roots.forEach(function(node) { + + // Calculate all node heights ... + var sorter = node.getSorter(); + + // @Todo: This must not be implemented in this way.Each sorter could have different notion of heights ... + var heightById = sorter.computeChildrenIdByHeights(this._treeSet, node); + + this._layoutChildren(node, heightById); + }.bind(this)); + + // Finally, return the list of nodes and properties that has been changed during the layout ... + + + + }, + + _layoutChildren: function(node, heightById) { + + var nodeId = node.getId(); + var children = this._treeSet.getChildren(node); + var childrenOrderMoved = children.some(function(child) { + return child.hasOrderChanged(); + }); + var heightChanged = this._heightByNode[nodeId] != heightById[nodeId]; + throw "Esto no esta bien:"+ this._heightByNode; + + // If ether any of the nodes has been changed of position or the height of the children is not + // the same, children nodes must be repositioned .... + if (childrenOrderMoved || heightChanged) { + + var sorter = node.getSorter(); + var offsetById = sorter.computeOffsets(this._treeSet, node); + var parentPosition = node.getPosition(); + + children.forEach(function(child) { + var offset = offsetById[child.getId()]; + var newPos = {x:parentPosition.x + offset.x,y:parentPosition.y + offset.y}; + this._treeSet.updateBranchPosition(child, newPos); + }.bind(this)); + } + + // Continue reordering the children nodes ... + children.forEach(function(child) { + this._layoutChildren(child, heightById); + }.bind(this)); + } + +}); + +mindplot.nlayout.OriginalLayout.SYMETRIC_SORTER = new mindplot.nlayout.SymetricSorder(); +mindplot.nlayout.OriginalLayout.GRID_SORTER = new mindplot.nlayout.SymetricSorder(); + + + diff --git a/mindplot/src/main/javascript/nlayout/RootedTreeSet.js b/mindplot/src/main/javascript/nlayout/RootedTreeSet.js new file mode 100644 index 00000000..d039fb59 --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/RootedTreeSet.js @@ -0,0 +1,141 @@ +mindplot.nlayout.RootedTreeSet = new Class({ + initialize:function() { + this._rootNodes = []; + }, + + setRoot:function(root) { + $assert(root, 'root can not be null'); + this._rootNodes.push(this._decodate(root)); + }, + + getTreeRoots:function() { + return this._rootNodes; + }, + + _decodate:function(node) { + node._children = []; + return node; + }, + + add: function(node) { + $assert(node, 'node can not be null'); + $assert(!node._children, 'node already added'); + this._rootNodes.push(this._decodate(node)); + }, + + remove: function(node) { + throw "Must be implemted"; + }, + + 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, true); + 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); + }, + + disconnect: function(nodeId) { + $assert(node, 'node can not be null'); + $assert(node._parent, 'child node not connected connected'); + + node._parent._children.erase(node); + this._isolated.push(node); + + node._parent = null; + }, + + 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; + } + } + $assert(validate ? result : true, 'node could not be found id:' + id); + 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; + }, + + getChildren:function(node) { + $assert(node, 'node can not be null'); + return node._children; + }, + + 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; + }, + + 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); + children.forEach(function(child) { + this._shiftBranchPosition(child, xOffset, yOffset); + }.bind(this)); + + }, + + _shiftBranchPosition : function(node, xOffset, yOffset) { + var position = node.getPosition(); + node.setPosition({x:position.x + xOffset, y:position.y + yOffset}); + + var children = this.getChildren(node); + children.forEach(function(child) { + this._shiftBranchPosition(child, xOffset, yOffset); + }.bind(this)); + } + +}); + diff --git a/mindplot/src/main/javascript/nlayout/SymetricSorter.js b/mindplot/src/main/javascript/nlayout/SymetricSorter.js new file mode 100644 index 00000000..11f635c8 --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/SymetricSorter.js @@ -0,0 +1,139 @@ +mindplot.nlayout.SymetricSorder = new Class({ + Extends: mindplot.nlayout.ChildrenSorterStrategy, + initialize:function() { + + }, + + computeChildrenIdByHeights: function(treeSet, node) { + var result = {}; + this._computeChildrenHeight(treeSet, node, result); + return result; + }, + + + _computeChildrenHeight : function(treeSet, node, heightCache) { + var height = node.getSize().height + (mindplot.nlayout.SymetricSorder.INTERNODE_VERTICAL_PADDING * 2); // 2* Top and down padding; + + var result; + var children = treeSet.getChildren(node); + if (children.length == 0) { + result = height; + } else { + var childrenHeight = 0; + children.forEach(function(child) { + childrenHeight += this._computeChildrenHeight(treeSet, child, heightCache); + }, this); + + result = Math.max(height, childrenHeight); + } + + if (heightCache) { + heightCache[node.getId()] = result; + } + + return result; + }, + + predict : function(parent, graph, position) { + + // No children... + var children = graph.getChildren(parent); + if (children.length == 0) { + return [0,parent.getPosition()]; + } + + // Try to fit within ... + // + // - Order is change if the position top position is changed ... + // - Suggested position is the middle bitween the two topics... + // + var result = null; + children.forEach(function(child) { + var cpos = child.getPosition(); + if (position.y > cpos.y) { + result = [child.getOrder(),{x:cpos.x,y:cpos.y + child.getSize().height}]; + } + }); + + // Ok, no overlap. Suggest a new order. + if (result) { + var last = children.getLast(); + result = [last.getOrder() + 1,{x:cpos.x,y:cpos.y - (mindplot.nlayout.SymetricSorder.INTERNODE_VERTICAL_PADDING * 4)}]; + } + + return result; + }, + + insert: function(treeSet, parent, child, order) { + var children = treeSet.getChildren(parent); + $assert(order <= children.length, "Order must be continues and can not have holes. Order:" + order); + + // Sort array list .. + children.sort(function(a, b) { + return a.getOrder() - b.getOrder() + }); + + // Shift all the elements in one . + for (var i = order; i < children.length; i++) { + var node = children[i]; + node.setOrder(node.getOrder() + 1); + } + child.setOrder(order); + }, + + verify:function(treeSet, node) { + // Check that all is consistent ... + var children = treeSet.getChildren(node); + children.sort(function(a, b) { + return a.getOrder() - b.getOrder() + }); + + for (var i = 0; i < children.length; i++) { + $assert(children[i].getOrder() == i, "missing order elements"); + } + }, + + computeOffsets:function(treeSet, node) { + $assert(treeSet, "treeSet can no be null."); + $assert(node, "node can no be null."); + $assert("order can no be null."); + + var children = treeSet.getChildren(node); + children.sort(function(a, b) { + return a.getOrder() - b.getOrder() + }); + + // Compute heights ... + var heights = children.map(function(child) { + return {id:child.getId(),height:this._computeChildrenHeight(treeSet, child)}; + }.bind(this)); + + // Compute the center of the branch ... + var totalHeight = 0; + heights.forEach(function(elem) { + totalHeight += elem.height; + }); + var ysum = totalHeight / 2; + + // Calculate the offsets ... + var result = {}; + for (var i = 0; i < heights.length; i++) { + ysum = ysum - heights[i].height; + + var yOffset = ysum + mindplot.nlayout.SymetricSorder.INTERNODE_VERTICAL_PADDING; + var xOffset = mindplot.nlayout.SymetricSorder.INTERNODE_HORIZONTAL_PADDING; + + $assert(!isNaN(xOffset), "xOffset can not be null"); + $assert(!isNaN(yOffset), "yOffset can not be null"); + + result[heights[i].id] = {x:xOffset,y:yOffset}; + + } + return result; + } +}); + +mindplot.nlayout.SymetricSorder.INTERNODE_VERTICAL_PADDING = 5; +mindplot.nlayout.SymetricSorder.INTERNODE_HORIZONTAL_PADDING = 5; + + diff --git a/mindplot/src/main/javascript/nlayout/TestSuite.js b/mindplot/src/main/javascript/nlayout/TestSuite.js new file mode 100644 index 00000000..7bb0e937 --- /dev/null +++ b/mindplot/src/main/javascript/nlayout/TestSuite.js @@ -0,0 +1,70 @@ +mindplot.nlayout.TestSuite = new Class({ + Extends: mindplot.nlayout.ChildrenSorterStrategy, + initialize:function() { + +// this.testAligned(); + this.testEvents(); + + // @ Agregar tests que garantice que no se reposicional cosan inecesariamente 2 veces... + }, + + testAligned: function() { + + var size = {width:30,height:30}; + var position = {x:0,y:0}; + var manager = new mindplot.nlayout.LayoutManager(0, size); + + manager.addNode(1, size, position); + manager.connectNode(0, 1, 0); + + manager.layout(); + manager.dump(); + }, + + testEvents: function() { + var size = {width:10,height:10}; + var position = {x:0,y:0}; + var manager = new mindplot.nlayout.LayoutManager(0, size); + + // Add 3 nodes... + manager.addNode(1, size, position); + manager.addNode(2, size, position); + manager.addNode(3, size, position); + manager.addNode(4, size, position); + + // Now connect one with two.... + manager.connectNode(0, 1, 0); + manager.connectNode(0, 2, 0); + manager.connectNode(1, 3, 0); + + // Reposition ... + manager.layout(); + console.log("Updated tree:"); + manager.dump(); + + // Listen for changes ... + console.log("Updated nodes ..."); + var events = []; + manager.addEvent('change', function(event) { + console.log("Updated nodes: {id:" + event.getId() + ", order: " + event.getOrder() + ",position: {" + event.getPosition().x + "," + event.getPosition().y + "}"); + events.push(event); + }); + manager.flushEvents(); + + // Second flush must not fire events ... + console.log("---- Test Flush ---"); + + events.empty(); + manager.flushEvents(); + $assert(events.length == 0, "Event should not be fire twice."); + + // Ok, if a new node is added, this an event should be fired ... + console.log("---- Layout without changes should not affect the tree ---"); + events.empty(); + manager.layout(true); + + $assert(events.length == 0, "Unnecessary tree updated."); + } + +}); + diff --git a/mindplot/src/test/javascript/static/layout.html b/mindplot/src/test/javascript/static/layout.html new file mode 100644 index 00000000..943322db --- /dev/null +++ b/mindplot/src/test/javascript/static/layout.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + +
+ The button +
+ + + + \ No newline at end of file