// Utility functions for ordering and stacking of items var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors /** * Order items by their start data * @param {Item[]} items */ exports.orderByStart = function(items) { items.sort(function (a, b) { return a.data.start - b.data.start; }); }; /** * Order items by their end date. If they have no end date, their start date * is used. * @param {Item[]} items */ exports.orderByEnd = function(items) { items.sort(function (a, b) { var aTime = ('end' in a.data) ? a.data.end : a.data.start, bTime = ('end' in b.data) ? b.data.end : b.data.start; return aTime - bTime; }); }; /** * Adjust vertical positions of the items such that they don't overlap each * other. * @param {Item[]} items * All visible items * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * Margins between items and between items and the axis. * @param {boolean} [force=false] * If true, all items will be repositioned. If false (default), only * items having a top===null will be re-stacked */ exports.stack = function(items, margin, force) { if (force) { // reset top position of all items for (var i = 0; i < items.length; i++) { items[i].top = null; } } // calculate new, non-overlapping positions for (var i = 0; i < items.length; i++) { var item = items[i]; if (item.stack && item.top === null) { // initialize top position item.top = margin.axis; do { // TODO: optimize checking for overlap. when there is a gap without items, // you only need to check for items from the next item on, not from zero var collidingItem = null; for (var j = 0, jj = items.length; j < jj; j++) { var other = items[j]; if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item, other.options.rtl)) { collidingItem = other; break; } } if (collidingItem != null) { // There is a collision. Reposition the items above the colliding element item.top = collidingItem.top + collidingItem.height + margin.item.vertical; } } while (collidingItem); } } }; /** * Adjust vertical positions of the items without stacking them * @param {Item[]} items * All visible items * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * Margins between items and between items and the axis. * @param {subgroups[]} subgroups * All subgroups */ exports.nostack = function(items, margin, subgroups, stackSubgroups) { for (var i = 0; i < items.length; i++) { if (items[i].data.subgroup == undefined) { items[i].top = margin.item.vertical; } else if (items[i].data.subgroup !== undefined && stackSubgroups) { var newTop = 0; for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { newTop += subgroups[subgroup].height; subgroups[items[i].data.subgroup].top = newTop; } } } items[i].top = newTop + 0.5 * margin.item.vertical; } } if (!stackSubgroups) { exports.stackSubgroups(items, margin, subgroups) } }; /** * Adjust vertical positions of the subgroups such that they don't overlap each * other. * @param {subgroups[]} subgroups * All subgroups * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * Margins between items and between items and the axis. */ exports.stackSubgroups = function(items, margin, subgroups) { for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { subgroups[subgroup].top = 0; do { // TODO: optimize checking for overlap. when there is a gap without items, // you only need to check for items from the next item on, not from zero var collidingItem = null; for (var otherSubgroup in subgroups) { if (subgroups[otherSubgroup].top !== null && otherSubgroup !== subgroup && subgroups[subgroup].index > subgroups[otherSubgroup].index && exports.collisionByTimes(subgroups[subgroup], subgroups[otherSubgroup])) { collidingItem = subgroups[otherSubgroup]; break; } } if (collidingItem != null) { // There is a collision. Reposition the subgroups above the colliding element subgroups[subgroup].top = collidingItem.top + collidingItem.height; } } while (collidingItem); } } for (var i = 0; i < items.length; i++) { if (items[i].data.subgroup !== undefined) { items[i].top = subgroups[items[i].data.subgroup].top + 0.5 * margin.item.vertical; } } } /** * Test if the two provided items collide * The items must have parameters left, width, top, and height. * @param {Item} a The first item * @param {Item} b The second item * @param {{horizontal: number, vertical: number}} margin * An object containing a horizontal and vertical * minimum required margin. * @param {boolean} rtl * @return {boolean} true if a and b collide, else false */ exports.collision = function(a, b, margin, rtl) { if (rtl) { return ((a.right - margin.horizontal + EPSILON) < (b.right + b.width) && (a.right + a.width + margin.horizontal - EPSILON) > b.right && (a.top - margin.vertical + EPSILON) < (b.top + b.height) && (a.top + a.height + margin.vertical - EPSILON) > b.top); } else { return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) && (a.left + a.width + margin.horizontal - EPSILON) > b.left && (a.top - margin.vertical + EPSILON) < (b.top + b.height) && (a.top + a.height + margin.vertical - EPSILON) > b.top); } }; /** * Test if the two provided objects collide * The objects must have parameters start, end, top, and height. * @param {Object} a The first Object * @param {Object} b The second Object * @return {boolean} true if a and b collide, else false */ exports.collisionByTimes = function(a, b) { return ( (a.start < b.start && a.end > b.start && a.top < (b.top + b.height) && (a.top + a.height) > b.top ) || (b.start < a.start && b.end > a.start && b.top < (a.top + a.height) && (b.top + b.height) > a.top ) ) }