/*jslint nomen: true*/ /*global _, Color, MAPJS*/ MAPJS.defaultStyles = {}; MAPJS.layoutLinks = function (idea, visibleNodes) { 'use strict'; var result = {}; _.each(idea.links, function (link) { if (visibleNodes[link.ideaIdFrom] && visibleNodes[link.ideaIdTo]) { result[link.ideaIdFrom + '_' + link.ideaIdTo] = { ideaIdFrom: link.ideaIdFrom, ideaIdTo: link.ideaIdTo, attr: _.clone(link.attr) }; //todo - clone } }); return result; }; MAPJS.calculateFrame = function (nodes, margin) { 'use strict'; margin = margin || 0; var result = { top: _.min(nodes, function (node) { return node.y; }).y - margin, left: _.min(nodes, function (node) { return node.x; }).x - margin }; result.width = margin + _.max(_.map(nodes, function (node) { return node.x + node.width; })) - result.left; result.height = margin + _.max(_.map(nodes, function (node) { return node.y + node.height; })) - result.top; return result; }; MAPJS.contrastForeground = function (background) { 'use strict'; /*jslint newcap:true*/ var luminosity = Color(background).luminosity(); if (luminosity < 0.5) { return '#EEEEEE'; } if (luminosity < 0.9) { return '#4F4F4F'; } return '#000000'; }; MAPJS.Outline = function (topBorder, bottomBorder) { 'use strict'; var shiftBorder = function (border, deltaH) { return _.map(border, function (segment) { return { l: segment.l, h: segment.h + deltaH }; }); }; this.initialHeight = function () { return this.bottom[0].h - this.top[0].h; }; this.borders = function () { return _.pick(this, 'top', 'bottom'); }; this.spacingAbove = function (outline) { var i = 0, j = 0, result = 0, li = 0, lj = 0; while (i < this.bottom.length && j < outline.top.length) { result = Math.max(result, this.bottom[i].h - outline.top[j].h); if (li + this.bottom[i].l < lj + outline.top[j].l) { li += this.bottom[i].l; i += 1; } else if (li + this.bottom[i].l === lj + outline.top[j].l) { li += this.bottom[i].l; i += 1; lj += outline.top[j].l; j += 1; } else { lj += outline.top[j].l; j += 1; } } return result; }; this.indent = function (horizontalIndent, margin) { if (!horizontalIndent) { return this; } var top = this.top.slice(), bottom = this.bottom.slice(), vertCenter = (bottom[0].h + top[0].h) / 2; top.unshift({h: vertCenter - margin / 2, l: horizontalIndent}); bottom.unshift({h: vertCenter + margin / 2, l: horizontalIndent}); return new MAPJS.Outline(top, bottom); }; this.stackBelow = function (outline, margin) { var spacing = outline.spacingAbove(this), top = MAPJS.Outline.extendBorder(outline.top, shiftBorder(this.top, spacing + margin)), bottom = MAPJS.Outline.extendBorder(shiftBorder(this.bottom, spacing + margin), outline.bottom); return new MAPJS.Outline( top, bottom ); }; this.expand = function (initialTopHeight, initialBottomHeight) { var topAlignment = initialTopHeight - this.top[0].h, bottomAlignment = initialBottomHeight - this.bottom[0].h, top = shiftBorder(this.top, topAlignment), bottom = shiftBorder(this.bottom, bottomAlignment); return new MAPJS.Outline( top, bottom ); }; this.insertAtStart = function (dimensions, margin) { var alignment = 0, //-1 * this.top[0].h - suboutlineHeight * 0.5, topBorder = shiftBorder(this.top, alignment), bottomBorder = shiftBorder(this.bottom, alignment), easeIn = function (border) { border[0].l *= 0.5; border[1].l += border[0].l; }; topBorder[0].l += margin; bottomBorder[0].l += margin; topBorder.unshift({h: -0.5 * dimensions.height, l: dimensions.width}); bottomBorder.unshift({h: 0.5 * dimensions.height, l: dimensions.width}); if (topBorder[0].h > topBorder[1].h) { easeIn(topBorder); } if (bottomBorder[0].h < bottomBorder[1].h) { easeIn(bottomBorder); } return new MAPJS.Outline(topBorder, bottomBorder); }; this.top = topBorder.slice(); this.bottom = bottomBorder.slice(); }; MAPJS.Outline.borderLength = function (border) { 'use strict'; return _.reduce(border, function (seed, el) { return seed + el.l; }, 0); }; MAPJS.Outline.borderSegmentIndexAt = function (border, length) { 'use strict'; var l = 0, i = -1; while (l <= length) { i += 1; if (i >= border.length) { return -1; } l += border[i].l; } return i; }; MAPJS.Outline.extendBorder = function (originalBorder, extension) { 'use strict'; var result = originalBorder.slice(), origLength = MAPJS.Outline.borderLength(originalBorder), i = MAPJS.Outline.borderSegmentIndexAt(extension, origLength), lengthToCut; if (i >= 0) { lengthToCut = MAPJS.Outline.borderLength(extension.slice(0, i + 1)); result.push({h: extension[i].h, l: lengthToCut - origLength}); result = result.concat(extension.slice(i + 1)); } return result; }; MAPJS.Tree = function (options) { 'use strict'; _.extend(this, options); this.toLayout = function (x, y, parentId) { x = x || 0; y = y || 0; var result = { nodes: {}, connectors: {} }, self; self = _.pick(this, 'id', 'title', 'attr', 'width', 'height', 'level'); self.attr.data = _.extend({}, self.attr.data); if (self.level === 1) { self.x = -0.5 * this.width; self.y = -0.5 * this.height; } else { self.x = x + this.deltaX || 0; self.y = y + this.deltaY || 0; } if (!self.attr.style) { self.attr.style = {}; } result.nodes[this.id] = self; if (parentId !== undefined) { result.connectors[self.id] = { from: parentId, to: self.id }; } if (this.subtrees) { this.subtrees.forEach(function (t) { var subLayout = t.toLayout(self.x, self.y, self.id); _.extend(result.nodes, subLayout.nodes); _.extend(result.connectors, subLayout.connectors); }); } return result; }; }; MAPJS.Outline.fromDimensions = function (dimensions) { 'use strict'; return new MAPJS.Outline([{ h: -0.5 * dimensions.height, l: dimensions.width }], [{ h: 0.5 * dimensions.height, l: dimensions.width }]); }; MAPJS.calculateTree = function (content, dimensionProvider, margin, rankAndParentPredicate, level) { 'use strict'; var options = { id: content.id, title: content.title, attr: $.extend({}, content.attr, {hasChildren: !_.isEmpty(content.ideas)}), deltaY: 0, deltaX: 0, level: level || 1 }, setVerticalSpacing = function (treeArray, dy) { var i, tree, oldSpacing, newSpacing, oldPositions = _.map(treeArray, function (t) { return _.pick(t, 'deltaX', 'deltaY'); }), referenceTree, alignment; for (i = 0; i < treeArray.length; i += 1) { tree = treeArray[i]; if (tree.attr && tree.attr.position) { tree.deltaY = tree.attr.position[1]; if (referenceTree === undefined || tree.attr.position[2] > treeArray[referenceTree].attr.position[2]) { referenceTree = i; } } else { tree.deltaY += dy; } if (i > 0) { oldSpacing = oldPositions[i].deltaY - oldPositions[i - 1].deltaY; newSpacing = treeArray[i].deltaY - treeArray[i - 1].deltaY; if (newSpacing < oldSpacing) { tree.deltaY += oldSpacing - newSpacing; } } } alignment = referenceTree && (treeArray[referenceTree].attr.position[1] - treeArray[referenceTree].deltaY); if (alignment) { for (i = 0; i < treeArray.length; i += 1) { treeArray[i].deltaY += alignment; } } }, shouldIncludeSubIdeas = function () { return !(_.isEmpty(content.ideas) || (content.attr && content.attr.collapsed)); }, includedSubIdeaKeys = function () { var allRanks = _.map(_.keys(content.ideas), parseFloat), includedRanks = rankAndParentPredicate ? _.filter(allRanks, function (rank) { return rankAndParentPredicate(rank, content); }) : allRanks; return _.sortBy(includedRanks, Math.abs); }, includedSubIdeas = function () { var result = []; _.each(includedSubIdeaKeys(), function (key) { result.push(content.ideas[key]); }); return result; }, nodeDimensions = dimensionProvider(content, options.level), appendSubtrees = function (subtrees) { var suboutline, deltaHeight, subtreePosition, horizontal, treeOutline; _.each(subtrees, function (subtree) { subtree.deltaX = nodeDimensions.width + margin; subtreePosition = subtree.attr && subtree.attr.position && subtree.attr.position[0]; if (subtreePosition && subtreePosition > subtree.deltaX) { horizontal = subtreePosition - subtree.deltaX; subtree.deltaX = subtreePosition; } else { horizontal = 0; } if (!suboutline) { suboutline = subtree.outline.indent(horizontal, margin); } else { treeOutline = subtree.outline.indent(horizontal, margin); deltaHeight = treeOutline.initialHeight(); suboutline = treeOutline.stackBelow(suboutline, margin); subtree.deltaY = suboutline.initialHeight() - deltaHeight / 2 - subtree.height / 2; } }); if (subtrees && subtrees.length) { setVerticalSpacing(subtrees, 0.5 * (nodeDimensions.height - suboutline.initialHeight())); suboutline = suboutline.expand( subtrees[0].deltaY - nodeDimensions.height * 0.5, subtrees[subtrees.length - 1].deltaY + subtrees[subtrees.length - 1].height - nodeDimensions.height * 0.5 ); } options.outline = suboutline.insertAtStart(nodeDimensions, margin); }; _.extend(options, nodeDimensions); options.outline = new MAPJS.Outline.fromDimensions(nodeDimensions); if (shouldIncludeSubIdeas()) { options.subtrees = _.map(includedSubIdeas(), function (i) { return MAPJS.calculateTree(i, dimensionProvider, margin, rankAndParentPredicate, options.level + 1); }); if (!_.isEmpty(options.subtrees)) { appendSubtrees(options.subtrees); } } return new MAPJS.Tree(options); }; MAPJS.calculateLayout = function (idea, dimensionProvider, margin,layoutPredicate) { 'use strict'; var positiveTree, negativeTree, layout, negativeLayout, positive, negative, setDefaultStyles = function (nodes) { _.each(nodes, function (node) { node.attr = node.attr || {}; node.attr.style = _.extend({}, MAPJS.defaultStyles[(node.level === 1) ? 'root' : 'nonRoot'], node.attr.style); }); }; positive = layoutPredicate(idea, true); negative = layoutPredicate(idea, false); margin = margin || 20; positiveTree = MAPJS.calculateTree(idea, dimensionProvider, margin, positive); negativeTree = MAPJS.calculateTree(idea, dimensionProvider, margin, negative); layout = positiveTree.toLayout(); negativeLayout = negativeTree.toLayout(); _.each(negativeLayout.nodes, function (n) { n.x = -1 * n.x - n.width; }); _.extend(negativeLayout.nodes, layout.nodes); _.extend(negativeLayout.connectors, layout.connectors); setDefaultStyles(negativeLayout.nodes); negativeLayout.links = MAPJS.layoutLinks(idea, negativeLayout.nodes); negativeLayout.rootNodeId = idea.id; return negativeLayout; };