// vim: et:ts=4:sw=4:sts=4:ft=js /*@ Ox.Calendar Basic calendar object () -> Calendar object (options) -> Calendar object (options, self) -> Calendar object options Options object date UTC Date on which the calendar is centered events <[o]|[]> Event objects to be displayed alternativeNames <[s]> Array of alternative names end End of the event (UTC Date, as string) id Id of the event name Name of the event start Start of the event (UTC Date, as string) type Type of the event (like "person") height Height in px range <[n]|[100, 5101]> Start and end year of the calendar selected Id of the selected event width Width in px zoom Initial zoom level self Shared private variable @*/ // Fixme: switch to UTC // Fixme: create a variable-resolution event type (with end that is _inclusive_) Ox.Calendar = function(options, self) { self = self || {}; var that = new Ox.Element({}, self) .defaults({ date: new Date(), events: [], height: 256, range: [1000, 3000], selected: '', width: 256, zoom: 8 }) .options(options || {}) .addClass('OxCalendar') .css({ width: self.options.width + 'px', height: self.options.height + 'px' }); self.options.events.forEach(function(event) { event.id = Ox.isUndefined(event.id) ? Ox.uid() : event.id; event.start = Ox.parseDate(event.start, true); event.end = Ox.parseDate(event.end, true); }); self.maxZoom = 32; self.minLabelWidth = 80; self.overlayWidths = [Math.round(self.options.width / 16)]; self.overlayWidths = [ Math.floor((self.options.width - self.overlayWidths[0]) / 2), self.overlayWidths[0], Math.ceil((self.options.width - self.overlayWidths[0]) / 2), ]; self.units = [ { id: 'millennium', seconds: 365242.5 * 86400, date: function(i) { return '01/01/' + (i + 1) + '000 UTC'; }, name: function(i) { return Ox.formatOrdinal(i + 2) + ' millennium'; }, value: function(date) { return Math.floor(date.getUTCFullYear() / 1000) - 1; } }, { id: 'century', seconds: 36524.25 * 86400, date: function(i) { return '01/01/' + (i + 19) + '00 UTC'; }, name: function(i) { return Ox.formatOrdinal(i + 20) + ' century'; }, value: function(date) { return Math.floor(date.getUTCFullYear() / 100) - 19; } }, { id: 'decade', seconds: 3652.425 * 86400, date: function(i) { return '01/01/' + (i + 197) + '0 UTC' }, name: function(i) { return (i + 197) + '0s' }, value: function(date) { return Math.floor(date.getUTCFullYear() / 10) - 197; } }, { id: 'year', seconds: 365.2425 * 86400, date: function(i) { return '01/01/' + (i + 1970) + ' UTC'; }, name: function(i) { return (i + 1970) + ''; }, value: function(date) { return date.getUTCFullYear() - 1970; } }, { id: 'month', seconds: 365.2425 / 12 * 86400, date: function(i) { return (Ox.mod(i, 12) + 1) + '/01/' + (Math.floor(i / 12) + 1970) + ' UTC'; }, name: function(i) { return Ox.SHORT_MONTHS[Ox.mod(i, 12)] + ' ' + Math.floor(i / 12 + 1970) }, value: function(date) { return (date.getUTCFullYear() - 1970) * 12 + date.getUTCMonth(); } }, { id: 'week', seconds: 7 * 86400, date: function(i) { return (i * 7 - 3) * 86400000; }, name: function(i) { return Ox.formatDate(new Date((i * 7 - 3) * 86400000), '%a, %b %e'); }, value: function(date) { return Math.floor((date / 86400000 + 4) / 7); } }, { id: 'day', seconds: 86400, date: function(i) { return i * 86400000; }, name: function(i) { return Ox.formatDate(new Date(i * 86400000), '%b %e, %Y', true); }, value: function(date) { return Math.floor(date / 86400000); } }, { id: 'six_hours', seconds: 21600, date: function(i) { return i * 21600000; }, name: function(i) { return Ox.formatDate(new Date(i * 21600000), '%b %e, %H:00', true); }, value: function(date) { return Math.floor(date / 21600000); } }, { id: 'hour', seconds: 3600, date: function(i) { return i * 3600000; }, name: function(i) { return Ox.formatDate(new Date(i * 3600000), '%b %e, %H:00', true); }, value: function(date) { return Math.floor(date / 3600000); } }, { id: 'five_minutes', seconds: 300, date: function(i) { return i * 300000; }, name: function(i) { return Ox.formatDate(new Date(i * 300000), '%b %e, %H:%M', true); }, value: function(date) { return Math.floor(date / 300000); } }, { id: 'minute', seconds: 60, date: function(i) { return i * 60000; }, name: function(i) { return Ox.formatDate(new Date(i * 60000), '%b %e, %H:%M', true); }, value: function(date) { return Math.floor(date / 60000); } }, { id: 'five_seconds', seconds: 5, date: function(i) { return i * 5000; }, name: function(i) { return Ox.formatDate(new Date(i * 5000), '%H:%M:%S', true); }, value: function(date) { return Math.floor(date / 5000); } }, { id: 'second', seconds: 1, date: function(i) { return i * 1000; }, name: function(i) { return Ox.formatDate(new Date(i * 1000), '%H:%M:%S', true); }, value: function(date) { return Math.floor(date / 1000); } } ]; self.$container = new Ox.Element() .addClass('OxCalendarContainer') .css({ top: '24px', bottom: '40px' }) .bind({ mouseleave: mouseleave, mousemove: mousemove, mousewheel: mousewheel }) .bindEvent({ doubleclick: doubleclick, dragstart: dragstart, drag: drag, dragpause: dragpause, dragend: dragend, singleclick: singleclick }) .appendTo(that); self.$content = new Ox.Element() .addClass('OxCalendarContent') .appendTo(self.$container); self.$background = new Ox.Element() .addClass('OxBackground') .appendTo(self.$content); self.$scalebar = new Ox.Element() .addClass('OxTimeline') .css({ posision: 'absolute', }) .appendTo(self.$content); self.$scrollbar = new Ox.Element() .addClass('OxTimeline') .css({ posision: 'absolute', bottom: '40px' }) .appendTo(that); self.$overlay = new Ox.Element() .addClass('OxOverlay') .css({ bottom: '40px' }) .append( $('
').css({ width: self.overlayWidths[0] + 'px' }) ) .append( $('
').css({ left: self.overlayWidths[0] + 'px', width: self.overlayWidths[1] + 'px' }) ) .append( $('
').css({ left: (self.overlayWidths[0] + self.overlayWidths[1]) + 'px', width: self.overlayWidths[2] + 'px' }) ) .bindEvent({ dragstart: dragstartScrollbar, drag: dragScrollbar, dragpause: dragpauseScrollbar, dragend: dragendScrollbar }) .appendTo(that); self.$zoombar = new Ox.Element() .css({ position: 'absolute', bottom: 24 + 'px', height: '16px' }) .appendTo(that); self.$zoomInput = new Ox.Range({ arrows: true, max: self.maxZoom, min: 0, size: self.options.width, thumbSize: 32, thumbValue: true, value: self.options.zoom }) .bindEvent({ change: changeZoom }) .appendTo(self.$zoombar); self.$statusbar = new Ox.Bar({ size: 24 }) .css({ // fixme: no need to set position absolute with map statusbar position: 'absolute', bottom: 0, textAlign: 'center' }) .appendTo(that); self.$tooltip = new Ox.Tooltip({ animate: false }) .css({ textAlign: 'center' }); renderCalendar(); function changeDate() { } function changeZoom(event, data) { self.options.zoom = data.value; renderCalendar(); } function doubleclick(event, e) { if ($(e.target).is(':not(.OxLine > .OxEvent)')) { if (self.options.zoom < self.maxZoom) { self.options.date = new Date( (+self.options.date + +getMouseDate(e)) / 2 ); self.options.zoom++; } renderCalendar(); } } function dragstart(event, e) { if ($(e.target).is(':not(.OxLine > .OxEvent)')) { self.drag = {x: e.clientX}; } } function drag(event, e) { if (self.drag) { ///* self.$content.css({ marginLeft: (e.clientX - self.drag.x) + 'px' }); self.$scrollbar.css({ marginLeft: Math.round((e.clientX - self.drag.x) / 16) + 'px' }); //*/ /* self.options.date = new Date( +self.options.date - e.clientDX * getSecondsPerPixel() * 1000 ); */ //self.drag = {x: e.clientX}; } } function dragpause(event, e) { if (self.drag) { dragafter(e); self.drag = {x: e.clientX}; } } function dragend(event, e) { if (self.drag) { dragafter(e); self.drag = null; } } function dragafter(e) { self.options.date = new Date( +self.options.date - (e.clientX - self.drag.x) * getSecondsPerPixel() * 1000 ); self.$content.css({ marginLeft: 0 }); self.$scrollbar.css({ marginLeft: 0 }); renderCalendar(); } function dragstartScrollbar(event, e) { self.drag = {x: e.clientX}; } function dragScrollbar(event, e) { self.$content.css({ marginLeft: ((e.clientX - self.drag.x) * 16) + 'px' }); self.$scrollbar.css({ marginLeft: (e.clientX - self.drag.x) + 'px' }); } function dragpauseScrollbar(event, e) { dragafterScrollbar(e); self.drag = {x: e.clientX}; } function dragendScrollbar(event, e) { dragafterScrollbar(e); self.drag = null; } function dragafterScrollbar(e) { self.options.date = new Date( +self.options.date + (self.drag.x - e.clientX) * getSecondsPerPixel() * 1000 * 16 ); // fixme: duplicated self.$content.css({ marginLeft: 0 }); self.$scrollbar.css({ marginLeft: 0 }); renderCalendar(); } function formatEvent(event) { var isFullDays = Ox.formatDate(event.start, '%H:%M:%S', true) == '00:00:00' && Ox.formatDate(event.end, '%H:%M:%S', true) == '00:00:00', isOneDay = isFullDays && event.end - event.start == 86400000, // fixme: wrong, DST isSameDay = Ox.formatDate(event.start, '%Y-%m-%d', true) == Ox.formatDate(event.end, '%Y-%m-%d', true), isSameYear = event.start.getUTCFullYear() == event.end.getUTCFullYear(), timeFormat = isFullDays ? '' : ', %H:%M:%S', str = Ox.formatDate(event.start, '%a, %b %e', true); if (isOneDay || isSameDay || !isSameYear) { str += Ox.formatDate(event.start, ', %Y' + timeFormat, true); } if (!isOneDay && !isSameDay) { str += Ox.formatDate(event.end, ' - %a, %b %e, %Y' + timeFormat, true); } if (isSameDay) { str += Ox.formatDate(event.end, ' - ' + timeFormat.replace(', ', ''), true); } return str; } function getCalendarEvent() { var ms = self.options.width * getSecondsPerPixel() * 1000; return { start: new Date(+self.options.date - ms / 2), end: new Date(+self.options.date + ms / 2) }; } function getEventById(id) { var event = null; Ox.forEach(self.options.events, function(v) { if (v.id == id) { event = v; return false; } }); return event; } function getEventElement(event, zoom) { var left = getPosition(event.start, zoom), width = Math.max(getPosition(event.end, zoom) - left, 1); return new Ox.Element() .addClass('OxEvent' + ( event.id == self.options.selected ? ' OxSelected' : '' )) .css({ left: left + 'px', width: width + 'px' }) .data({ id: event.id }) .html(' ' + event.name); } function getEventElementById(id) { }; function getMouseDate(e) { return new Date(+self.options.date + ( e.clientX - that.offset().left - self.options.width / 2 - 1 ) * getSecondsPerPixel() * 1000); } function getPixelsPerSecond(zoom) { return Math.pow(2, (zoom || self.options.zoom) - (self.maxZoom - 4)); } function getPosition(date, zoom) { return Math.round( self.options.width / 2 + (date - self.options.date) / 1000 * getPixelsPerSecond(zoom || self.options.zoom) ); } function getSecondsPerPixel(zoom) { return 1 / getPixelsPerSecond(zoom); } function getBackgroundElements(zoom) { // fixme: duplicated var $elements = [], units = getUnits(zoom), n, value, width; [1, 0].forEach(function(u) { var unit = units[u], value = unit.value(self.options.date), width = unit.seconds * getPixelsPerSecond(zoom), n = Math.ceil(self.options.width * 1.5/* * 16*/ / width); Ox.loop(-n, n + 1, function(i) { $elements.push( new Ox.Element() .addClass( u == 0 ? 'line' : Ox.mod(value + i, 2) == 0 ? 'even' : 'odd' ) .css({ left: getPosition(new Date(unit.date(value + i)), zoom) + 'px', width: (u == 0 ? 1 : width) + 'px' }) ); }); }); return $elements; } function getTimelineElements(zoom) { var $elements = [], unit = getUnits(zoom)[0], value = unit.value(self.options.date), width = unit.seconds * getPixelsPerSecond(zoom); n = Math.ceil(self.options.width * 1.5/* * 16*/ / width); Ox.loop(-n, n + 1, function(i) { $elements.push( getEventElement({ name: unit.name(value + i), start: new Date(unit.date(value + i)), end: new Date(unit.date(value + i + 1)) }, zoom) .addClass(Ox.mod(value + i, 2) == 0 ? 'even' : 'odd') ); }); return $elements; } function getUnits(zoom) { // returns array of 2 units // units[0] for timeline // units[1] for background var pixelsPerSecond = getPixelsPerSecond(zoom), units; self.units = self.units.reverse(); Ox.forEach(self.units, function(v, i) { width = Math.round(v.seconds * pixelsPerSecond); if (width >= self.minLabelWidth) { units = [self.units[i], self.units[i - 1]]; return false; } }); self.units = self.units.reverse(); return units; } function mouseleave() { self.$tooltip.hide(); } function mousemove(e) { var $target = $(e.target), event, title; if ($target.is('.OxLine > .OxEvent')) { event = getEventById($target.data('id')); title = '' + event.name + '
' + formatEvent(event); } else { title = Ox.formatDate(getMouseDate(e), '%a, %b %e, %Y, %H:%M:%S'); } self.$tooltip.options({ title: title }) .show(e.clientX, e.clientY); } function mousewheel(e, delta, deltaX, deltaY) { //Ox.print('mousewheel', delta, deltaX, deltaY); var deltaZ = 0; if (!self.mousewheel && deltaY && Math.abs(deltaY) > Math.abs(deltaX)) { if (deltaY < 0 && self.options.zoom > 0) { deltaZ = -1 } else if (deltaY > 0 && self.options.zoom < self.maxZoom) { deltaZ = 1 } if (deltaZ) { self.options.date = deltaZ == -1 ? new Date(2 * +self.options.date - +getMouseDate(e)) : new Date((+self.options.date + +getMouseDate(e)) / 2) self.options.zoom += deltaZ; self.$zoomInput.options({value: self.options.zoom}); renderCalendar(); } } self.mousewheel = true; setTimeout(function() { self.mousewheel = false; }, 250); } function overlaps(eventA, eventB) { return ( eventA.start >= eventB.start && eventA.start < eventB.end ) || ( eventB.start >= eventA.start && eventB.start < eventA.end ); } function renderCalendar() { $('.OxBackground').empty(); $('.OxEvent').remove(); renderBackground(); renderTimelines(); renderEvents(); self.$statusbar.html( Ox.formatDate(self.options.date, '%a, %b %e, %Y, %H:%M:%S (%s)', true) ); } function renderBackground() { getBackgroundElements(self.options.zoom).forEach(function($element) { $element.appendTo(self.$background); }); } function renderEvents() { var calendarEvent = getCalendarEvent(); lineEvents = []; self.options.events.filter(function(event) { // filter out events outside the visible area return overlaps(event, calendarEvent); }).sort(function(a, b) { // sort events by duration, descending return (b.end - b.start) - (a.end - a.start); }).forEach(function(event, i) { var line = lineEvents.length; // traverse lines Ox.forEach(lineEvents, function(events, line_) { var fits = true; // traverse events in line Ox.forEach(events, function(event_) { // if overlaps, check next line if (overlaps(event, event_)) { fits = false; return false; } }); if (fits) { line = line_; return false; } }); if (line == lineEvents.length) { lineEvents[line] = []; } lineEvents[line].push(event); }); $('.OxLine').remove(); lineEvents.forEach(function(events, line) { var $line = new Ox.Element() .addClass('OxLine') .css({ top: ((line + 1) * 16) + 'px' }) .appendTo(self.$content); events.sort(function(a, b) { // sort events by start, ascending return a.start - b.start; }).forEach(function(event) { getEventElement(event).appendTo($line); }); }); } function renderTimelines() { Ox.print(self.options.zoom, Math.max(self.options.zoom - 4, 0)) getTimelineElements(self.options.zoom).forEach(function($element) { $element.appendTo(self.$scalebar.$element); }); getTimelineElements(Math.max(self.options.zoom - 4, 0)).forEach(function($element) { $element.appendTo(self.$scrollbar.$element); }); } function selectEvent(id) { } function singleclick(event, e) { var $target = $(e.target), id = $target.data('id'); if ($target.is('.OxLine > .OxEvent')) { self.options.selected = id; self.$content.find('.OxSelected').removeClass('OxSelected'); $target.addClass('OxSelected'); // fixme: map event should also be 'select', not 'selectplace' that.triggerEvent('select', { id: id }); } else { self.options.date = getMouseDate(e); renderCalendar(); } } self.setOption = function(key, val) { if (key == 'date') { } else if (key == 'zoom') { } }; return that; };