/** A diagram
* @constructor
*/
hui.ui.Diagram = function(options) {
this.options = hui.override({layout: 'D3'}, options);
this.name = options.name;
this.nodes = [];
this.lines = [];
this.data = {};
this.translation = {x:0,y:0};
this.element = hui.get(options.element);
this.width = this.element.clientWidth;
this.height = this.element.clientHeight;
this.layout = hui.ui.Diagram[this.options.layout];
this.layout.diagram = this;
hui.ui.extend(this);
if (options.source) {
options.source.listen(this);
}
this._init();
};
hui.ui.Diagram.create = function(options) {
options = hui.override({width: null, height: null}, options);
options.element = hui.build('div', {
'class': 'hui_diagram',
parent: options.parent,
style: {height: options.height + 'px'}
});
return new hui.ui.Diagram(options);
};
hui.ui.Diagram.prototype = {
_init : function() {
this.background = hui.ui.Drawing.create({
width: this.width || 0,
height: this.height || 0
});
this.element.appendChild(this.background.element);
this.fire('added');
},
$$layout : function() {
var newWidth = this.element.clientWidth;
var newHeight = this.element.clientHeight;
if (newWidth === this.width && newHeight === this.height) {
// Only re-layout if size actually changed
return;
}
this.width = newWidth;
this.height = newHeight;
this.background.setSize(this.width,this.height);
this.layout.resize();
this.layout.resume();
},
_getMagnet : function(from,to,node) {
var margin = 1;
var size = node.getSize();
var center = node.getCenter();
var topLeft = {
x : center.x - size.width/2 - margin,
y : center.y - size.height/2 - margin
},
bottomRight = {
x : topLeft.x + size.width + margin * 2,
y : topLeft.y + size.height + margin * 2
};
var hits = [];
hits = hui.geometry.intersectLineRectangle(from,to,topLeft,bottomRight);
if (hits.length>0) {
return hits[0];
}
return to;
},
// Data ...
/** @private */
$objectsLoaded : function(data) {
this.setData(data);
},
setData : function(data) {
this.data = data;
this.clear();
var nodes = data.nodes,
lines = data.lines || data.edges;
if (!nodes || !lines) {
return;
}
var i;
for (i = 0; i < nodes.length; i++) {
if (nodes[i].type=='icon') {
this.addIcon(nodes[i]);
} else {
this.addBox(nodes[i]);
}
}
for (i = 0; i < lines.length; i++) {
this.addLine(lines[i]);
}
if (this.layout.loaded) {
this.layout.populate();
} else {
this.play();
}
},
/** Deprecated */
play : function() {
this.layout.start();
},
resume : function() {
if (this.layout.resume) { this.layout.resume(); }
},
expand : function() {
if (this.layout.expand) { this.layout.expand(); }
},
contract : function() {
if (this.layout.contract) { this.layout.contract(); }
},
/** @private */
$sourceShouldRefresh : function() {
return hui.dom.isVisible(this.element);
},
/** @private */
$visibilityChanged : function() {
if (hui.dom.isVisible(this.element)) {
this.width = this.element.clientWidth;
this.height = this.element.clientHeight;
this.background.setSize(this.width,this.height);
if (this.options.source) {
this.options.source.refreshFirst();
}
}
},
clear : function() {
this.layout.clear();
this.selection = null;
this.background.clear();
this.lines = [];
for (var i = this.nodes.length - 1; i >= 0; i--) {
hui.dom.remove(this.nodes[i].element);
}
this.nodes = [];
var lines = hui.get.byClass(this.element,'hui_diagram_line_label');
for (var j = lines.length - 1; j >= 0; j--) {
hui.dom.remove(lines[j]);
}
},
addBox : function(options) {
var box = hui.ui.Diagram.Box.create(options,this);
this.add(box);
},
addIcon : function(options) {
var box = hui.ui.Diagram.Icon.create(options,this);
this.add(box);
},
add : function(widget) {
var e = widget.element;
this.element.appendChild(e);
widget.setCenter({x: this.width / 2, y : this.height / 2});
this.nodes.push(widget);
},
addLine : function(options) {
var from = this.getNode(options.from),
to = this.getNode(options.to);
if (from === null || to === null) {
hui.log('Unable to build line...');
hui.log(options);
return;
}
var fromCenter = this._getCenter(from),
toCenter = this._getCenter(to);
var lineNode = this.background.addLine({
from: fromCenter,
to: toCenter,
color: options.color || '#999' ,end:{}
}),
line = {
from: options.from,
fromNode : from,
to: options.to,
toNode : to,
node: lineNode
};
if (options.label) {
line.label = hui.build('span',{
parent: this.element,
'class': 'hui_diagram_line_label',
text: options.label
});
this._updateLine(line);
}
this.lines.push(line);
},
_getCenter : function(widget) {
return widget.getCenter();
},
getNode : function(id) {
return this._getNode(id,this.nodes);
},
getDataNode : function(id) {
return this._getNode(id,this.data.nodes);
},
_getNode : function(id,nodes) {
if (nodes) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id == id) {
return nodes[i];
}
}
}
return null;
},
// Drawing...
_updateLine : function(line) {
if (!line.label) {
return;
}
var from = line.node.getFrom(),
to = line.node.getTo(),
label = line.label;
var middle = { x : from.x+(to.x-from.x)/2, y : from.y+(to.y-from.y)/2 };
//var deg = Math.atan((from.y-to.y) / (from.x-to.x)) * 180/Math.PI;
line.label.style.webkitTransform='rotate('+line.node.getDegree()+'deg)';
//line.label.innerHTML = Math.round(hui.geometry.distance(from,to));
var width = Math.round(hui.geometry.distance(from,to)-30);
// TODO: cache width + height
var w = label.huiWidth = label.huiWidth || label.clientWidth;
var h = label.huiHeight = label.huiHeight || label.clientHeight;
w = Math.min(w,width);
hui.style.set(line.label,{
left : (middle.x - w / 2) + 'px',
top : (middle.y - h / 2) + 'px',
maxWidth : Math.max(0, width) + 'px',
visibility : width > 10 ? '' : 'hidden'
});
},
__nodeMoved : function(widget) {
var center = this._getCenter(widget);
for (var i=0; i < this.lines.length; i++) {
var line = this.lines[i];
var magnet, magnet2;
if (line.from == widget.id) {
magnet = this._getMagnet(line.node.getTo(),center,widget);
line.node.setFrom(magnet);
magnet2 = this._getMagnet(center,this._getCenter(line.toNode),line.toNode);
line.node.setTo(magnet2);
this._updateLine(line);
}
else if (line.to == widget.id) {
magnet = this._getMagnet(line.node.getFrom(),center,widget);
line.node.setTo(magnet);
magnet2 = this._getMagnet(center,this._getCenter(line.fromNode),line.fromNode);
line.node.setFrom(magnet2);
this._updateLine(line);
}
}
},
__select : function(widget) {
if (this.selection) {
this.selection.setSelected(false);
}
this.selection = widget;
this.selection.setSelected(true);
},
__nodeOpen : function(widget) {
this.fire('open',this.getDataNode(widget.id));
}
};
hui.ui.Diagram.D3 = {
loaded : false,
diagram : null,
_load : function() {
hui.require(hui.ui.getURL('lib/d3.v3/d3.v3.min.js'),function() {
this.loaded = true;
this.start();
}.bind(this));
},
resize : function() {
if (this.layout) {
this.layout.size([this.diagram.width,this.diagram.height]);
}
},
start : function() {
if (!this.loaded) {
this._load();
return;
}
var diagram = this.diagram,
nodes = diagram.nodes,
lines = diagram.lines,
width = diagram.element.clientWidth,
height = diagram.element.clientHeight;
for (var i=0; i < lines.length; i++) {
lines[i].source = this._findById(nodes,lines[i].from);
lines[i].target = this._findById(nodes,lines[i].to);
}
var force = this.layout = d3.layout.force()
.linkDistance(100)
.friction(0.9)
.gravity(0.1)
.theta(0.3)
.linkStrength(0.2)
.charge(-1000)
.distance(100)
.nodes(this.diagram.nodes)
.links(this.diagram.lines)
.size([width, height]);
var ticker = function() {
var sel = diagram.selection ? diagram.selection.id : null;
var nodes = force.nodes(),
links = force.links();
for (var i=0; i < nodes.length; i++) {
var node = diagram.nodes[nodes[i].index];
if (node.id!=sel) {
node.setCenter(nodes[i]);
}
}
for (var j=0; j < links.length; j++) {
var link = links[j];
var source = link.source,
sourceCenter = link.source.center;
var target = link.target,
targetCenter = link.target;
if (source==diagram.selection) {
sourceCenter = diagram._getCenter(diagram.selection);
}
if (target==diagram.selection) {
targetCenter = diagram._getCenter(diagram.selection);
}
var from = diagram._getMagnet(sourceCenter,targetCenter,source);
var to = diagram._getMagnet(targetCenter,sourceCenter,target);
link.node.setFrom(from);
link.node.setTo(to);
diagram._updateLine(link);
}
};
force.start();
force.gravity(0.5);
for (var k=0; k < 10000; k++) {
force.tick();
}
force.gravity(0.1);
force.on("tick", ticker);
force.start();
},
resume : function() {
if (this.layout) { this.layout.start(); }
},
expand : function() {
if (this.layout) {
this.layout.linkDistance(this.layout.linkDistance() * 1.3);
this.layout.charge(this.layout.charge() * 1.3);
this.layout.start();
}
},
contract : function() {
if (this.layout) {
this.layout.linkDistance(Math.max(0,this.layout.linkDistance() * 0.9));
this.layout.charge(Math.min(0,this.layout.charge() * 0.9));
this.layout.start();
}
},
_findById : function(nodes,id) {
for (var i = nodes.length - 1; i >= 0; i--){
if (nodes[i].id===id) {
return i;
}
}
return null;
},
_convert : function(data) {
var nodes = data.nodes;
data.links = data.edges;
for (var i = data.links.length - 1; i >= 0; i--){
var link = data.links[i];
link.source = this._findById(nodes,link.from);
link.target = this._findById(nodes,link.to);
}
return data;
},
populate : function() {
this.start();
},
clear : function() {
if (this.layout) {
this.layout.stop();
}
}
};
/** A box in a diagram
* @constructor
*/
hui.ui.Diagram.Box = function(options) {
this.options = options;
this.id = options.id;
this.name = options.name;
this.element = hui.get(options.element);
this.center = {};
this.size = null;
hui.ui.extend(this);
hui.ui.Diagram.util.enableDragging(this);
};
hui.ui.Diagram.Box.create = function(options,diagram) {
options = hui.override({title: 'Untitled', diagram: diagram}, options);
var e = options.element = hui.build('div', {'class': 'hui_diagram_box'});
hui.build('h1',{text: options.title, parent: e});
if (options.properties) {
var table = hui.build('table', {parent: e});
for (var i=0; i < options.properties.length; i++) {
var p = options.properties[i];
var tr = hui.build('tr',{parent: table});
hui.build('th',{parent: tr,text: p.label});
var td = hui.build('td',{parent: tr,text: p.value || ''});
if (p.hint) {
hui.build('em',{parent: td, text: p.hint});
}
}
}
return new hui.ui.Diagram.Box(options);
};
hui.ui.Diagram.Box.prototype = {
_syncSize : function() {
if (this.size) {
return;
}
this.size = {
width : this.element.offsetWidth,
height : this.element.offsetHeight
};
},
getSize : function() {
this._syncSize();
return this.size;
},
getCenter : function() {
return this.center;
},
setCenter : function(point) {
this._syncSize();
this.center = {x : point.x, y : point.y};
this._updateCenter();
},
_updateCenter : function() {
this.element.style.top = Math.round(this.center.y - this.size.height / 2) + 'px';
this.element.style.left = Math.round(this.center.x - this.size.width / 2) + 'px';
},
setSelected : function(selected) {
hui.cls.set(this.element,'hui_diagram_box_selected', selected);
}
};
if (hui.browser.webkit) {
hui.ui.Diagram.Box.prototype._updateCenter = function() {
this.element.style.WebkitTransform = 'translate3d(' + Math.round(this.center.x - this.size.width/2) + 'px,' + Math.round(this.center.y - this.size.height/2) + 'px,0)';
};
}
/** A box in a diagram
* @constructor
*/
hui.ui.Diagram.Icon = function(options) {
this.options = options;
this.id = options.id;
this.name = options.name;
this.element = hui.get(options.element);
this.center = {};
hui.ui.extend(this);
hui.ui.Diagram.util.enableDragging(this);
};
hui.ui.Diagram.Icon.create = function(options,diagram) {
options = hui.override({icon:'common/folder',diagram:diagram},options);
var e = options.element = hui.build('div',{'class':'hui_diagram_icon'});
e.appendChild(hui.ui.createIcon(options.icon,32));
if (options.title) {
hui.build('strong',{parent: e, text: options.title});
}
return new hui.ui.Diagram.Icon(options);
};
hui.ui.Diagram.Icon.prototype = {
_syncSize : function() {
if (this.size) {
return;
}
this.size = {
width : this.element.offsetWidth,
height : this.element.offsetHeight
};
},
getSize : function() {
this._syncSize();
return this.size;
},
getCenter : function() {
return this.center;
},
setCenter : function(point) {
var e = this.element;
e.style.top = Math.round(point.y - e.clientHeight / 2) + 'px';
e.style.left = Math.round(point.x - e.clientWidth / 2) + 'px';
this.center = {x : point.x, y : point.y};
},
setSelected : function(selected) {
hui.cls.set(this.element, 'hui_diagram_icon_selected', selected);
}
};
/** Utilities **/
hui.ui.Diagram.util = {
enableDragging : function(obj) {
var diagram = obj.options.diagram;
hui.cls.add(obj.element, 'hui_diagram_dragable');
var dragState = null;
hui.drag.register({
touch : true,
element : obj.element,
onStart : function() {
hui.cls.add(obj.element,'hui_diagram_dragging');
obj.fixed = true;
},
onNotMoved : function() {
diagram.__select(obj);
diagram.fire('select', obj.id);
},
onBeforeMove : function(e) {
diagram.__nodeMoved(obj);
e = hui.event(e);
obj.element.style.zIndex = hui.ui.nextPanelIndex();
var pos = obj.getCenter();
var size = obj.getSize();
pos = {left: pos.x - size.width / 2, top: pos.y - size.height / 2};
var diagramPosition = hui.position.get(diagram.element);
dragState = {
left : e.getLeft() - pos.left,
top : e.getTop()-pos.top
};
obj.element.style.right = 'auto';
},
onMove : function(e) {
var top = (e.getTop()-dragState.top);
var left = (e.getLeft()-dragState.left);
var size = obj.getSize();
top += size.height/2;
left += size.width/2;
obj.setCenter({x:left,y:top});
obj.px = left;
obj.py = top;
diagram.__nodeMoved(obj);
},
onEnd : function() {
hui.cls.remove(obj.element,'hui_diagram_dragging');
obj.fixed = false;
hui.log('end');
}
});
hui.listen(obj.element,'dblclick',function(e) {
diagram.__nodeOpen(obj);
});
}
};