VulpesOS/gaia/apps/calendar/js/conflict_span.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

282 lines
7.4 KiB
JavaScript
Executable file

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