/** 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); }); } };