// vim: et:ts=4:sw=4:sts=4:ft=js Ox.Calendar = function(options, self) { self = self || {}; var that = new Ox.Element({}, self) .defaults({ date: new Date(), dates: [], height: 512, range: [100, 5101], width: 512, zoom: 8 }) .options(options || {}) .addClass('OxCalendar') .css({ width: self.options.width + 'px', height: self.options.height + 'px' }); 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 new Date((i + 1) + '000'); }, name: function(i) { return Ox.formatOrdinal(i + 2) + ' millennium'; }, value: function(date) { return Math.floor(date.getFullYear() / 1000) - 1; } }, { id: 'century', seconds: 36524.25 * 86400, date: function(i) { return new Date((i + 19) + '00'); }, name: function(i) { return Ox.formatOrdinal(i + 20) + ' century'; }, value: function(date) { return Math.floor(date.getFullYear() / 100) - 19; } }, { id: 'decade', seconds: 3652.425 * 86400, date: function(i) { return (i + 197) + '0' }, name: function(i) { return (i + 197) + '0s' }, value: function(date) { return Math.floor(date.getFullYear() / 10) - 197; } }, { id: 'year', seconds: 365.2425 * 86400, date: function(i) { return (i + 1970) + ''; }, name: function(i) { return (i + 1970) + ''; }, value: function(date) { return date.getFullYear() - 1970; } }, { id: 'month', seconds: 365.2425 / 12 * 86400, date: function(i) { return (Math.floor(i / 12) + 1970) + '-' + (Ox.mod(i, 12) + 1); }, name: function(i) { return Ox.SHORT_MONTHS[Ox.mod(i, 12)] + ' ' + Math.floor(i / 12 + 1970) }, value: function(date) { return (date.getFullYear() - 1970) * 12 + date.getMonth(); } }, { 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) { // adjust for timezone difference // fixme: may still be off return i * 86400000 + Ox.TIMEZONE_OFFSET; }, name: function(i) { return Ox.formatDate(new Date(i * 86400000), '%b %e, %Y'); }, value: function(date) { return Math.floor(date / 86400000); } }, { id: 'six_hours', seconds: 21600, date: function(i) { return i * 21600000 + Ox.TIMEZONE_OFFSET; }, name: function(i) { return Ox.formatDate(new Date(i * 21600000 + Ox.TIMEZONE_OFFSET), '%b %e, %H:00'); }, value: function(date) { return Math.floor((date - Ox.TIMEZONE_OFFSET) / 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'); }, 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'); }, 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'); }, 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'); }, 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'); }, 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 > .OxDate)')) { 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 > .OxDate)')) { 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 formatDate(date) { var isFullDays = Ox.formatDate(date.start, '%H:%M:%S') == '00:00:00' && Ox.formatDate(date.stop, '%H:%M:%S') == '00:00:00', isOneDay = isFullDays && date.stop - date.start == 86400000, // fixme: wrong, DST isSameDay = Ox.formatDate(date.start, '%Y-%m-%d') == Ox.formatDate(date.stop, '%Y-%m-%d'), isSameYear = date.start.getFullYear() == date.stop.getFullYear(), timeFormat = isFullDays ? '' : ', %H:%M:%S', str = Ox.formatDate(date.start, '%a, %b %e'); if (isOneDay || isSameDay || !isSameYear) { str += Ox.formatDate(date.start, ', %Y' + timeFormat); } if (!isOneDay && !isSameDay) { str += Ox.formatDate(date.stop, ' - %a, %b %e, %Y' + timeFormat); } if (isSameDay) { str += Ox.formatDate(date.stop, ' - ' + timeFormat.replace(', ', '')); } return str; } function getCalendarDate() { var ms = self.options.width * getSecondsPerPixel() * 1000; return { start: new Date(+self.options.date - ms / 2), stop: new Date(+self.options.date + ms / 2) }; } function getDateByName(name) { var date = {}; Ox.forEach(self.options.dates, function(v) { if (v.name == name) { date = v; return false; } }); return date; } function getDateElement(date, zoom) { var left = getPosition(date.start, zoom), width = Math.max(getPosition(date.stop, zoom) - left, 1); return new Ox.Element() .addClass('OxDate') .css({ left: left + 'px', width: width + 'px' }) .data({ name: date.name }) .html(' ' + date.name); } 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( getDateElement({ name: unit.name(value + i), start: new Date(unit.date(value + i)), stop: 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), date, title; if ($target.is('.OxLine > .OxDate')) { date = getDateByName($target.data('name')); title = '' + date.name + '
' + formatDate(date); } 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(date0, date1) { return ( date0.start >= date1.start && date0.start < date1.stop ) || ( date1.start >= date0.start && date1.start < date0.stop ); } function renderCalendar() { $('.OxBackground').empty(); $('.OxDate').remove(); renderBackground(); renderTimelines(); renderDates(); self.$statusbar.html( Ox.formatDate(self.options.date, '%Y-%m-%d %H:%M:%S %s') ); } function renderBackground() { getBackgroundElements(self.options.zoom).forEach(function($element) { $element.appendTo(self.$background); }); } function renderDates() { var calendarDate = getCalendarDate(); lineDates = []; self.options.dates.filter(function(date) { // filter out dates outside the visible area return overlaps(date, calendarDate); }).sort(function(a, b) { // sort dates by duration, descending return (b.stop - b.start) - (a.stop - a.start); }).forEach(function(date, i) { var line = lineDates.length; // traverse lines Ox.forEach(lineDates, function(dates, line_) { var fits = true; // traverse dates in line Ox.forEach(dates, function(date_) { // if overlaps, check next line if (overlaps(date, date_)) { fits = false; return false; } }); if (fits) { line = line_; return false; } }); if (line == lineDates.length) { lineDates[line] = []; } lineDates[line].push(date); }); $('.OxLine').remove(); lineDates.forEach(function(dates, line) { var $line = new Ox.Element() .addClass('OxLine') .css({ top: ((line + 1) * 16) + 'px' }) .appendTo(self.$content); dates.sort(function(a, b) { // sort dates by start, ascending return a.start - b.start; }).forEach(function(date) { getDateElement(date).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 singleclick(event, e) { if ($(e.target).is(':not(.OxLine > .OxDate)')) { self.options.date = getMouseDate(e); renderCalendar(); } } self.onChange = function(key, val) { if (key == 'date') { } else if (key == 'zoom') { } }; return that; };