var _ = require('underscore'); var Backbone = require('backbone'); var GRAPHICS = require('../util/constants').GRAPHICS; var VisBase = require('../visuals/visBase').VisBase; var randomHueString = function() { var hue = Math.random(); var str = 'hsb(' + String(hue) + ',0.7,1)'; return str; }; var VisBranch = VisBase.extend({ defaults: { pos: null, text: null, rect: null, arrow: null, isHead: false, flip: 1, fill: GRAPHICS.rectFill, stroke: GRAPHICS.rectStroke, 'stroke-width': GRAPHICS.rectStrokeWidth, offsetX: GRAPHICS.nodeRadius * 4.75, offsetY: 0, arrowHeight: 14, arrowInnerSkew: 0, arrowEdgeHeight: 6, arrowLength: 14, arrowOffsetFromCircleX: 10, vPad: 5, hPad: 5, animationSpeed: GRAPHICS.defaultAnimationTime, animationEasing: GRAPHICS.defaultEasing }, validateAtInit: function() { if (!this.get('branch')) { throw new Error('need a branch!'); } }, getID: function() { return this.get('branch').get('id'); }, initialize: function() { this.validateAtInit(); // shorthand notation for the main objects this.gitVisuals = this.get('gitVisuals'); this.gitEngine = this.get('gitEngine'); if (!this.gitEngine) { throw new Error('asd wtf'); } this.get('branch').set('visBranch', this); var id = this.get('branch').get('id'); if (id == 'HEAD') { // switch to a head ref this.set('isHead', true); this.set('flip', -1); this.refreshOffset(); this.set('fill', GRAPHICS.headRectFill); } else if (id !== 'master') { // we need to set our color to something random this.set('fill', randomHueString()); } }, getCommitPosition: function() { var commit = this.gitEngine.getCommitFromRef(this.get('branch')); var visNode = commit.get('visNode'); this.set('flip', this.getFlipValue(commit, visNode)); this.refreshOffset(); return visNode.getScreenCoords(); }, getFlipValue: function(commit, visNode) { var threshold = this.get('gitVisuals').getFlipPos(); var overThreshold = (visNode.get('pos').x > threshold); // easy logic first if (!this.get('isHead')) { return (overThreshold) ? -1 : 1; } // now for HEAD.... if (overThreshold) { // if by ourselves, then feel free to squeeze in. but // if other branches are here, then we need to show separate return (this.isBranchStackEmpty()) ? -1 : 1; } else { return (this.isBranchStackEmpty()) ? 1 : -1; } }, shouldOffsetY: function() { return this.gitEngine.getBranches().length > 1; }, refreshOffset: function() { var baseOffsetX = GRAPHICS.nodeRadius * 4.75; if (!this.shouldOffsetY()) { this.set('offsetY', 0); this.set('offsetX', baseOffsetX); return; } var offsetY = 33; var deltaX = 10; if (this.get('flip') === 1) { this.set('offsetY', -offsetY); this.set('offsetX', baseOffsetX - deltaX); } else { this.set('offsetY', offsetY); this.set('offsetX', baseOffsetX - deltaX); } }, getArrowTransform: function() { if (!this.shouldOffsetY()) { return ''; } else if (this.get('flip') === 1) { return 't-2,-20R-35'; } else { return 't2,20R-35'; } }, getBranchStackIndex: function() { if (this.get('isHead')) { // head is never stacked with other branches return 0; } var myArray = this.getBranchStackArray(); var index = -1; _.each(myArray, function(branch, i) { if (branch.obj == this.get('branch')) { index = i; } }, this); return index; }, getBranchStackLength: function() { if (this.get('isHead')) { // head is always by itself return 1; } return this.getBranchStackArray().length; }, isBranchStackEmpty: function() { // useful function for head when computing flip logic var arr = this.gitVisuals.branchStackMap[this.getCommitID()]; return (arr) ? arr.length === 0 : true; }, getCommitID: function() { var target = this.get('branch').get('target'); if (target.get('type') === 'branch') { // for HEAD target = target.get('target'); } return target.get('id'); }, getBranchStackArray: function() { var arr = this.gitVisuals.branchStackMap[this.getCommitID()]; if (arr === undefined) { // this only occurs when we are generating graphics inside of // a new Branch instantiation, so we need to force the update this.gitVisuals.calcBranchStacks(); return this.getBranchStackArray(); } return arr; }, getTextPosition: function() { var pos = this.getCommitPosition(); // then order yourself accordingly. we use alphabetical sorting // so everything is independent var myPos = this.getBranchStackIndex(); return { x: pos.x + this.get('flip') * this.get('offsetX'), y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY') }; }, getRectPosition: function() { var pos = this.getTextPosition(); var f = this.get('flip'); // first get text width and height var textSize = this.getTextSize(); return { x: pos.x - 0.5 * textSize.w - this.get('hPad'), y: pos.y - 0.5 * textSize.h - this.get('vPad') }; }, getArrowPath: function() { // should make these util functions... var offset2d = function(pos, x, y) { return { x: pos.x + x, y: pos.y + y }; }; var toStringCoords = function(pos) { return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y)); }; var f = this.get('flip'); var arrowTip = offset2d(this.getCommitPosition(), f * this.get('arrowOffsetFromCircleX'), 0 ); var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight')); var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight')); var arrowInnerUp = offset2d(arrowEdgeUp, f * this.get('arrowInnerSkew'), this.get('arrowEdgeHeight') ); var arrowInnerLow = offset2d(arrowEdgeLow, f * this.get('arrowInnerSkew'), -this.get('arrowEdgeHeight') ); var tailLength = 49; var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0); var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0); var pathStr = ''; pathStr += 'M' + toStringCoords(arrowStartUp) + ' '; var coords = [ arrowInnerUp, arrowEdgeUp, arrowTip, arrowEdgeLow, arrowInnerLow, arrowStartLow ]; _.each(coords, function(pos) { pathStr += 'L' + toStringCoords(pos) + ' '; }, this); pathStr += 'z'; return pathStr; }, getTextSize: function() { var getTextWidth = function(visBranch) { var textNode = (visBranch.get('text')) ? visBranch.get('text').node : null; return (textNode === null) ? 0 : textNode.clientWidth; }; var firefoxFix = function(obj) { if (!obj.w) { obj.w = 75; } if (!obj.h) { obj.h = 20; } return obj; }; var textNode = this.get('text').node; if (this.get('isHead')) { // HEAD is a special case return firefoxFix({ w: textNode.clientWidth, h: textNode.clientHeight }); } var maxWidth = 0; _.each(this.getBranchStackArray(), function(branch) { maxWidth = Math.max(maxWidth, getTextWidth( branch.obj.get('visBranch') )); }); return firefoxFix({ w: maxWidth, h: textNode.clientHeight }); }, getSingleRectSize: function() { var textSize = this.getTextSize(); var vPad = this.get('vPad'); var hPad = this.get('hPad'); return { w: textSize.w + vPad * 2, h: textSize.h + hPad * 2 }; }, getRectSize: function() { var textSize = this.getTextSize(); // enforce padding var vPad = this.get('vPad'); var hPad = this.get('hPad'); // number of other branch names we are housing var totalNum = this.getBranchStackLength(); return { w: textSize.w + vPad * 2, h: textSize.h * totalNum * 1.1 + hPad * 2 }; }, getIsRemote: function() { return this.get('branch').getIsRemote(); }, getName: function() { var name = this.get('branch').getName(); var selected = this.get('branch') === this.gitEngine.HEAD.get('target'); var isRemote = this.getIsRemote(); var after = (selected && !this.getIsInOrigin() && !isRemote) ? '*' : ''; return name + after; }, nonTextToFront: function() { this.get('arrow').toFront(); this.get('rect').toFront(); }, textToFront: function() { this.get('text').toFront(); }, textToFrontIfInStack: function() { if (this.getBranchStackIndex() !== 0) { this.get('text').toFront(); } }, getFill: function() { // in the easy case, just return your own fill if you are: // - the HEAD ref // - by yourself (length of 1) // - part of a multi branch, but your thing is hidden if (this.get('isHead') || this.getBranchStackLength() == 1 || this.getBranchStackIndex() !== 0) { return this.get('fill'); } // woof. now it's hard, we need to blend hues... return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray()); }, remove: function() { this.removeKeys(['text', 'arrow', 'rect']); // also need to remove from this.gitVisuals this.gitVisuals.removeVisBranch(this); }, genGraphics: function(paper) { var textPos = this.getTextPosition(); var name = this.getName(); var text; // when from a reload, we dont need to generate the text text = paper.text(textPos.x, textPos.y, String(name)); text.attr({ 'font-size': 14, 'font-family': 'Monaco, Courier, font-monospace', opacity: this.getTextOpacity() }); this.set('text', text); var attr = this.getAttributes(); var rectPos = this.getRectPosition(); var sizeOfRect = this.getRectSize(); var rect = paper .rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8) .attr(attr.rect); this.set('rect', rect); var arrowPath = this.getArrowPath(); var arrow = paper .path(arrowPath) .attr(attr.arrow); this.set('arrow', arrow); // set CSS var keys = ['text', 'rect', 'arrow']; _.each(keys, function(key) { $(this.get(key).node).css(attr.css); }, this); this.attachClickHandlers(); rect.toFront(); text.toFront(); }, attachClickHandlers: function() { if (this.get('gitVisuals').options.noClick) { return; } var objs = [ this.get('rect'), this.get('text'), this.get('arrow') ]; _.each(objs, function(rObj) { rObj.click(_.bind(this.onClick ,this)); }, this); }, shouldDisableClick: function() { return this.get('isHead') && !this.gitEngine.getDetachedHead(); }, onClick: function() { if (this.shouldDisableClick()) { return; } var commandStr = 'git checkout ' + this.get('branch').get('id'); var Main = require('../app'); Main.getEventBaton().trigger('commandSubmitted', commandStr); }, updateName: function() { this.get('text').attr({ text: this.getName() }); }, getNonTextOpacity: function() { if (this.get('isHead')) { return this.gitEngine.getDetachedHead() ? 1 : 0; } return this.getBranchStackIndex() === 0 ? 1 : 0.0; }, getTextOpacity: function() { if (this.get('isHead')) { return this.gitEngine.getDetachedHead() ? 1 : 0; } return 1; }, getAttributes: function() { var nonTextOpacity = this.getNonTextOpacity(); var textOpacity = this.getTextOpacity(); this.updateName(); var textPos = this.getTextPosition(); var rectPos = this.getRectPosition(); var rectSize = this.getRectSize(); var arrowPath = this.getArrowPath(); var dashArray = (this.getIsInOrigin()) ? GRAPHICS.originDash : ''; var cursorStyle = (this.shouldDisableClick()) ? 'auto' : 'pointer'; return { css: { cursor: cursorStyle }, text: { x: textPos.x, y: textPos.y, opacity: textOpacity }, rect: { x: rectPos.x, y: rectPos.y, width: rectSize.w, height: rectSize.h, opacity: nonTextOpacity, fill: this.getFill(), stroke: this.get('stroke'), //'stroke-dasharray': dashArray, 'stroke-width': this.get('stroke-width') }, arrow: { path: arrowPath, opacity: nonTextOpacity, fill: this.getFill(), stroke: this.get('stroke'), transform: this.getArrowTransform(), 'stroke-width': this.get('stroke-width') } }; }, animateUpdatedPos: function(speed, easing) { var attr = this.getAttributes(); this.animateToAttr(attr, speed, easing); }, animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) { // an animation of 0 is essentially setting the attribute directly this.animateToAttr(fromAttr, 0); this.animateToAttr(toAttr, speed, easing); }, setAttr: function(attr, instant, speed, easing) { var keys = ['text', 'rect', 'arrow']; this.setAttrBase(keys, attr, instant, speed, easing); } }); var VisBranchCollection = Backbone.Collection.extend({ model: VisBranch }); exports.VisBranchCollection = VisBranchCollection; exports.VisBranch = VisBranch; exports.randomHueString = randomHueString;