Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
282
gaia/apps/calendar/js/conflict_span.js
Executable file
282
gaia/apps/calendar/js/conflict_span.js
Executable file
|
|
@ -0,0 +1,282 @@
|
|||
/**
|
||||
* Representation of conflicts over a span of time, organized into
|
||||
* non-overlapping columns tracked by IntervalTree instances.
|
||||
*/
|
||||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var IntervalTree = require('interval_tree');
|
||||
var Timespan = require('common/timespan');
|
||||
|
||||
// Smallest gap interval to use in splitting conflict spans
|
||||
var MIN_SPLIT_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Auto-increment ID for instances
|
||||
var _id = 0;
|
||||
|
||||
function ConflictSpan(parent) {
|
||||
this.id = (_id++);
|
||||
this.parent = parent;
|
||||
this.startTime = null;
|
||||
this.endTime = null;
|
||||
this.all = new IntervalTree();
|
||||
this.columnsByID = {};
|
||||
this.columns = [];
|
||||
this.addColumn();
|
||||
}
|
||||
module.exports = ConflictSpan;
|
||||
|
||||
ConflictSpan.prototype = {
|
||||
/**
|
||||
* Get a list of all the busytime IDs in this span.
|
||||
*
|
||||
* @return {Array} List of all the busytime IDs.
|
||||
*/
|
||||
getIDs: function() {
|
||||
return Object.keys(this.all.byId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new column tracked by an IntervalTree
|
||||
*
|
||||
* @return {Object} IntervalTree tracking the column.
|
||||
*/
|
||||
addColumn: function() {
|
||||
var tree = new IntervalTree();
|
||||
this.columns.push(tree);
|
||||
return tree;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a column where the given busytime fits without conflict, adding a
|
||||
* new column if necessary.
|
||||
*
|
||||
* @param {Object} busytime full busytime object.
|
||||
* @return {Object} IntervalTree column that can accept the busytime.
|
||||
*/
|
||||
findColumn: function(busytime, skipAdd) {
|
||||
var column = null;
|
||||
var span = new Timespan(busytime._startDateMS, busytime._endDateMS);
|
||||
for (var i = 0; i < this.columns.length; i++) {
|
||||
var curr = this.columns[i];
|
||||
if (!curr.query(span).length) {
|
||||
column = curr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!column && !skipAdd) {
|
||||
column = this.addColumn();
|
||||
}
|
||||
return column;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a busytime to the conflict span
|
||||
*
|
||||
* @param {Object} busytime full busytime object.
|
||||
*/
|
||||
add: function(busytime) {
|
||||
var id = busytime._id;
|
||||
|
||||
this.parent.conflicts[id] = this;
|
||||
this.all.add(busytime);
|
||||
|
||||
var column = this.findColumn(busytime);
|
||||
column.add(busytime);
|
||||
this.columnsByID[id] = column;
|
||||
|
||||
this._updateTimes(busytime);
|
||||
this._updateLayout();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a busytime from the conflict span
|
||||
*
|
||||
* @param {Object} busytime full busytime object.
|
||||
* @param {Boolean} skipMaintenance skip post-removal maintenance.
|
||||
*/
|
||||
remove: function(busytime, skipMaintenance) {
|
||||
var id = busytime._id;
|
||||
|
||||
this.all.remove(busytime);
|
||||
var column = this.columnsByID[id];
|
||||
if (!column) { return; }
|
||||
|
||||
column.remove(busytime);
|
||||
delete this.columnsByID[id];
|
||||
delete this.parent.conflicts[id];
|
||||
|
||||
// Removing a single item requires maintenance after. But, this can be
|
||||
// skipped during a split, which does its own cleanup after multiple
|
||||
// removes & adds between spans.
|
||||
if (skipMaintenance) { return this; }
|
||||
|
||||
this._splitIfNecessary();
|
||||
var boom = this._selfDestructIfNecessary();
|
||||
if (!boom) {
|
||||
this._resetTimes();
|
||||
this._purgeEmptyColumns();
|
||||
this._updateLayout();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Absorb the given conflict span into this one
|
||||
*
|
||||
* @param {Object} ConflictSpan to be absorbed.
|
||||
*/
|
||||
absorb: function(otherCS) {
|
||||
var self = this;
|
||||
var otherIDs = otherCS.getIDs();
|
||||
otherIDs.forEach(function(otherID) {
|
||||
var otherBusytime = self.parent.tree.byId[otherID];
|
||||
self.add(otherBusytime);
|
||||
// Cheat: skip removing from the other span, since references go away.
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the start/end times for this span from a new busytime.
|
||||
*
|
||||
* @param {Object} busytime full busytime object.
|
||||
*/
|
||||
_updateTimes: function(busytime) {
|
||||
var start = busytime._startDateMS;
|
||||
if (null === this.startTime || start < this.startTime) {
|
||||
this.startTime = start;
|
||||
}
|
||||
var end = busytime._endDateMS;
|
||||
if (null === this.endTime || end > this.endTime) {
|
||||
this.endTime = end;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset times with a complete re-scan of all events in the span.
|
||||
*/
|
||||
_resetTimes: function() {
|
||||
this.startTime = this.endTime = null;
|
||||
var byId = this.all.byId;
|
||||
for (var k in byId) {
|
||||
this._updateTimes(byId[k]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Scan through the events in this span. If a significant gap is found,
|
||||
* presumably after a removal, split this span in two.
|
||||
*
|
||||
* @param {Object} busytime full busytime object.
|
||||
*/
|
||||
_splitIfNecessary: function() {
|
||||
var start = this.startTime;
|
||||
var end = this.endTime;
|
||||
|
||||
// Scan for the end of the first gap, if any.
|
||||
var splitAt = false;
|
||||
var prevHits = null;
|
||||
for (var top = start; top < end; top += MIN_SPLIT_INTERVAL) {
|
||||
var span = new Timespan(top, top + MIN_SPLIT_INTERVAL);
|
||||
var hits = this.all.query(span).length;
|
||||
if (0 === prevHits && hits > 0) {
|
||||
// Transition from empty to non-empty is where we split.
|
||||
splitAt = top; break;
|
||||
}
|
||||
prevHits = hits;
|
||||
}
|
||||
|
||||
// Bail if we never found a gap.
|
||||
if (splitAt === false) { return; }
|
||||
|
||||
// Remove & collect the post-gap items for new split.
|
||||
var newItems = [];
|
||||
var splitSpan = new Timespan(splitAt, Infinity);
|
||||
var splitItems = this.all.query(splitSpan);
|
||||
var self = this;
|
||||
splitItems.forEach(function(item) {
|
||||
self.remove(item, true);
|
||||
newItems.push(item);
|
||||
});
|
||||
|
||||
// Perform partial post-removal maintenance
|
||||
var boom = this._selfDestructIfNecessary();
|
||||
if (!boom) {
|
||||
this._resetTimes();
|
||||
this._purgeEmptyColumns();
|
||||
this._updateLayout();
|
||||
}
|
||||
|
||||
// Bail if there's just one item for new split - no conflict.
|
||||
if (newItems.length == 1) {
|
||||
this.parent._clearLayout(newItems[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, populate a new span with the conflicting items.
|
||||
var newCS = new ConflictSpan(this.parent);
|
||||
newItems.forEach(function(item) {
|
||||
newCS.add(item);
|
||||
});
|
||||
|
||||
// Finally, recurse into the new span and split further, if necessary.
|
||||
newCS._splitIfNecessary();
|
||||
},
|
||||
|
||||
/**
|
||||
* If this span has only one event left, then self-destruct because there's
|
||||
* no longer a conflict.
|
||||
*/
|
||||
_selfDestructIfNecessary: function() {
|
||||
var keys = this.getIDs();
|
||||
if (keys.length > 1) {
|
||||
// There's still a conflict, so bail.
|
||||
return false;
|
||||
}
|
||||
if (keys.length == 1) {
|
||||
// Exactly one left, so clean up.
|
||||
var busytime = this.all.byId[keys[0]];
|
||||
this.remove(busytime, true);
|
||||
this.parent._clearLayout(busytime);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Purge empty columns from the conflict span.
|
||||
*/
|
||||
_purgeEmptyColumns: function() {
|
||||
var newColumns = [];
|
||||
for (var i = 0; i < this.columns.length; i++) {
|
||||
var column = this.columns[i];
|
||||
if (Object.keys(column.byId).length > 0) {
|
||||
newColumns.push(column);
|
||||
}
|
||||
}
|
||||
this.columns = newColumns;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update layout for all events participating in this conflict span.
|
||||
*/
|
||||
_updateLayout: function() {
|
||||
var numCols = this.columns.length;
|
||||
var width = (100 / numCols);
|
||||
for (var cIdx = 0; cIdx < numCols; cIdx++) {
|
||||
var column = this.columns[cIdx];
|
||||
for (var k in column.byId) {
|
||||
var busytime = column.byId[k];
|
||||
var el = this.parent.getElement(busytime);
|
||||
el.style.width = width + '%';
|
||||
el.style.left = (width * cIdx) + '%';
|
||||
// we toggle the style based on amount of overlaps
|
||||
el.classList.toggle('many-overlaps', numCols > 4);
|
||||
el.classList.toggle('has-overlaps', numCols > 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue