// Interval graph coloring algorithm, by Twal class IntervalColoration { constructor(intervals) { this.intervals = intervals; this.n = this.intervals.length; this.computeInterferenceGraph(); this.computePEO(); this.computeColoration(); } computeInterferenceGraph() { this.adj = new Array(this.n); for (let i = 0; i < this.n; ++i) { this.adj[i] = []; } for (let i = 0; i < this.n; ++i) { for (let j = 0; j < i; ++j) { let inti = this.intervals[i]; let intj = this.intervals[j]; if (inti[0] < intj[1] && intj[0] < inti[1]) { this.adj[i].push(j); this.adj[j].push(i); } } } } //Perfect elimination order using Maximum Cardinality Search //Runs in O(n^2), could be optimized in O(n log n) computePEO() { let marked = new Array(this.n); let nbMarkedNeighbor = new Array(this.n); this.perm = new Array(this.n); for (let i = 0; i < this.n; ++i) { marked[i] = false; nbMarkedNeighbor[i] = 0; } for (let k = this.n - 1; k >= 0; --k) { let maxi = -1; for (let i = 0; i < this.n; ++i) { if ( !marked[i] && (maxi == -1 || nbMarkedNeighbor[i] >= nbMarkedNeighbor[maxi]) ) { maxi = i; } } for (let i = 0; i < this.adj[maxi].length; ++i) { nbMarkedNeighbor[this.adj[maxi][i]] += 1; } this.perm[maxi] = k; marked[maxi] = true; } // console.log(this.perm); } computeColoration() { this.colors = new Array(this.n); let isColorUsed = new Array(this.n); for (let i = 0; i < this.n; ++i) { this.colors[i] = -1; isColorUsed[i] = false; } for (let i = 0; i < this.n; ++i) { let ind = this.perm[i]; for (let j = 0; j < this.adj[ind].length; ++j) { let neigh = this.adj[ind][j]; if (this.colors[neigh] >= 0) { isColorUsed[this.colors[neigh]] = true; } } for (let j = 0; j < this.n; ++j) { if (!isColorUsed[j]) { this.colors[ind] = j; break; } } for (let j = 0; j < this.adj[ind].length; ++j) { let neigh = this.adj[ind][j]; if (this.colors[neigh] >= 0) { isColorUsed[this.colors[neigh]] = false; } } } } } // Based on https://stackoverflow.com/a/15289883 function computeDateDifferenceInHours(date1, date2) { d1 = new Date( date1.getYear(), date1.getMonth(), date1.getDate(), date1.getHours(), ); d2 = new Date( date2.getYear(), date2.getMonth(), date2.getDate(), date2.getHours(), ); const msPerHour = 60 * 60 * 1000; return Math.abs(d2.getTime() - d1.getTime()) / msPerHour; } class Calendar { constructor(calendarParameters = {}) { this.containerNode = calendarParameters.containerNode !== undefined ? calendarParameters.containerNode : $("#cal-container"); this.eventContainerNode = null; this.timeSlotsContainerNode = null; this.eventDetailsContainerNode = null; this.startDate = calendarParameters.startDate !== undefined ? calendarParameters.startDate : new Date(); this.endDate = calendarParameters.endDate !== undefined ? calendarParameters.endDate : new Date(Date.now() + 24 * 60 * 60 * 1000); this.nbHoursToDisplay = 0; this.firstHourToDisplay = 0; this.endHourToDisplay = 0; this.events = []; this.onlyDisplaySubscribedEvents = calendarParameters.onlyDisplaySubscribedEvents !== undefined ? calendarParameters.onlyDisplaySubscribedEvents : false; this.groupEventsByLocation = calendarParameters.groupEventsByLocation !== undefined ? calendarParameters.groupEventsByLocation : true; this.eventDetailURLFormat = calendarParameters.eventDetailURLFormat !== undefined ? calendarParameters.eventDetailURLFormat : ""; this.subscriptionURLFormat = calendarParameters.subscriptionURLFormat !== undefined ? calendarParameters.subscriptionURLFormat : ""; this.csrfToken = calendarParameters.csrfToken !== undefined ? calendarParameters.csrfToken : ""; // Map from locations to their CSS styles this.locationStyles = new Map(); this.init(); } init() { this.updateHoursToDisplay(); this.createTimeSlotContainer(); this.createEventContainer(); this.createEventDetailsContainer(); this.updateTimeSlotContainerGridStyle(); this.updateEventContainerGridStyle(); this.createTimeSlots(); this.createEvents(); this.createLocationStyles(); this.applyLocationStylesAsCSS(); this.updateEventLocationStyleID(); //this.sortEventNodesByEndTimeAndLocation(); this.sortEventNodesByIntervalGraphColoring(); this.updateCalendarNodeHeight(); this.updateEventVisibilities(); this.initEventOverflowTooltips(); } // Date change setStartDate(newStartDate) { this.startDate = newStartDate; this.updateHoursToDisplay(); this.updateEventContainerGridStyle(); this.updateTimeSlots(); this.updateEventVisibilities(); this.updateCalendarNodeHeight(); this.sortEventNodesByIntervalGraphColoring(); this.startShowingEventOverflowTooltips(); } setEndDate(newEndDate) { this.endDate = newEndDate; this.updateHoursToDisplay(); this.updateEventContainerGridStyle(); this.updateTimeSlots(); this.updateEventVisibilities(); this.updateCalendarNodeHeight(); this.sortEventNodesByIntervalGraphColoring(); this.startShowingEventOverflowTooltips(); } updateHoursToDisplay() { this.startHourToDisplay = this.startDate.getHours(); this.endHourToDisplay = this.endDate.getHours(); this.nbHoursToDisplay = Math.floor( computeDateDifferenceInHours(this.startDate, this.endDate), ); } // Calendar container updateCalendarNodeHeight() { // Time slot hour row let timeSlotHourRowHeight = $(".cal-time-slot-hour").outerHeight(); // Event grid this.containerNode.css("height", "calc(100% )"); let eventContainerHeight = this.eventContainerNode .css("grid-template-rows") .split("px ") .reduce((heightAccumulator, currentRowHeight) => { return heightAccumulator + parseInt(currentRowHeight); }, 0); this.containerNode.css( "height", timeSlotHourRowHeight + eventContainerHeight, ); } // Time slots createTimeSlotContainer() { this.timeSlotsContainerNode = $("
") .addClass("cal-time-slot-container") .appendTo(this.containerNode); } createTimeSlots() { // Populate the container hour by hour let self = this; function getHourStringToDisplay(index, hour) { if (index === self.nbHoursToDisplay - 1 || hour === 23) { return ""; } if (hour >= 10) { return hour + 1; } else { return " " + (hour + 1); } } for (let i = 0; i < this.nbHoursToDisplay; i++) { let hour = (this.startHourToDisplay + i) % 24; // Time slot hour let timeSlotHourNode = $("
") .addClass("cal-time-slot-hour") .css({ "grid-column-start": `${i + 1}`, "grid-column-end": "span 1", "grid-row-start": "1", "grid-row-end": "1", }) .html(getHourStringToDisplay(i, hour)) .prependTo(this.timeSlotsContainerNode); // Time slot block let timeSlotBlockNode = $("
") .addClass("cal-time-slot") .css({ "grid-column-start": `${i + 1}`, "grid-column-end": "span 1", "grid-row-start": "2", "grid-row-end": "2", }) .appendTo(this.timeSlotsContainerNode); if (hour === 23) { timeSlotHourNode.addClass("cal-last-hour"); timeSlotBlockNode.addClass("cal-last-hour"); } if (hour === 0) { timeSlotHourNode.addClass("cal-first-hour"); timeSlotBlockNode.addClass("cal-first-hour"); } } } updateTimeSlotContainerGridStyle() { this.timeSlotsContainerNode.css( "grid-template-columns", `repeat(${this.nbHoursToDisplay}, ${100 / this.nbHoursToDisplay}%)`, ); } updateTimeSlots() { this.timeSlotsContainerNode.empty(); this.createTimeSlots(); this.updateTimeSlotContainerGridStyle(); } getHourSlotWidth() { return this.timeSlotsContainerNode.width() / this.nbHoursToDisplay; } // Events createEventContainer() { this.eventContainerNode = $("
") .addClass("cal-event-container") .appendTo(this.containerNode); } createEvents() { // Move all event nodes into the event container let eventElements = this.containerNode.find(".cal-event"); eventElements.appendTo(this.eventContainerNode); // Create event objects from them all for (let element of eventElements) { let newEvent = new Event($(element), this); this.events.push(newEvent); } } updateEventContainerGridStyle() { this.eventContainerNode.css( "grid-template-columns", `repeat(${this.nbHoursToDisplay}, ${100 / this.nbHoursToDisplay}%)`, ); } updateEventVisibilities() { for (let event of this.events) { event.updateVisibility(); } } // Event details createEventDetailsContainer() { this.eventDetailsContainerNode = $("
") .addClass("cal-details-container") .appendTo(this.containerNode); } // Location styles createLocationStyles() { let locationIndices = new Map(); for (let event of this.events) { if (!locationIndices.has(event.location)) { locationIndices.set(event.location, [...locationIndices.keys()].length); } } let nbUniqueLocations = [...locationIndices.keys()].length; let styleID = 0; for (let [location, index] of locationIndices.entries()) { let hue = (index / (nbUniqueLocations + 1)) * 255; styleID += 1; this.locationStyles.set(location, { id: styleID, normal: [ `background-color: hsl(${hue}, 40%, 80%);`, `border-color: hsl(${hue}, 40%, 50%);`, `color: #000;`, ], hover: [ `background-color: hsl(${hue}, 55%, 85%);`, `border-color: hsl(${hue}, 45%, 55%);`, `color: #000;`, ], subscribed: [ `background-color: hsl(${hue}, 75%, 75%);`, `border-color: hsl(${hue}, 60%, 50%);`, `color: #000;`, ], selected: [ `background-color: hsl(${hue}, 45%, 50%);`, `border-color: hsl(${hue}, 40%, 35%);`, `color: #FFF;`, ], }); } } applyLocationStylesAsCSS() { let styleNode = $("