azkaban.schedule.svg.js

381 lines | 13.617 kB Blame History Raw Download
$.namespace('azkaban');

$(function() {

	var border = 20;
	var header = 30;
	var timeWidth = 60;
	var lineHeight = 40;
	var numDays = 7;
	var today = new Date();
	var totalHeight = (border * 2 + header + 24 * lineHeight);
	var monthConst = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
	var dayOfWeekConst = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
	var hourMillisConst = 3600 * 1000;
	var dayMillisConst = 24 * hourMillisConst;

	$("#svgDivCustom").svg({onLoad:
		function (svg) {

			var totalWidth = $("#svgDivCustom").width();

			$("#svgDivCustom").find("svg").eq(0).removeAttr("width");

			var dayWidth = Math.floor((totalWidth - 3 * border - timeWidth) / numDays);


			//Outer g
			var gMain = svg.group({transform: "translate(" + border + ".5," + border + ".5)", stroke : "#999", strokeWidth: 1});
			var defaultDate = new Date(today.setDate(today.getDate() - today.getDay()));
			today = new Date();
			var svgDate = defaultDate;

			//Used to filter projects or flows out
			var filterProject = new Array();
			var filterFlow = new Array();

			$(".nav-prev-week").click(function (event) {
				svgDate = new Date(svgDate.valueOf() - 7 * dayMillisConst);
				loadSvg(svgDate);
				event.stopPropagation();
			});
			$(".nav-next-week").click(function (event) {
				svgDate = new Date(svgDate.valueOf() + 7 * dayMillisConst);
				loadSvg(svgDate);
				event.stopPropagation();
			});
			$(".nav-this-week").click(function (event) {
				svgDate = defaultDate;
				loadSvg(svgDate);
				event.stopPropagation();
			});



			loadSvg(svgDate);

			function loadSvg(firstDay)
			{
				//svg.configure({viewBox: "0 0 " + totalWidth + " " + totalHeight, style: "width:100%"}, true);
				svg.remove(gMain);
				gMain = svg.group({transform: "translate(" + border + ".5," + border + ".5)", stroke : "#999", strokeWidth: 1});
				svg.text(gMain, timeWidth, header - 8, monthConst[firstDay.getMonth()], {fontSize: "20", style: "text-anchor: end;", fill : "#F60", stroke : "none"});
				//time indicator group
				var gLeft = svg.group(gMain, {transform: "translate(0," + header + ")"});
				//Draw lines and hours
				for(var i = 0; i < 24; i++)
				{
					svg.line(gLeft, 0, i * lineHeight, timeWidth, i * lineHeight);
					//Gets the hour text from an integer from 0 to 23
					var hourText = getHourText(i);
					//Move text down a bit? TODO: Is there a CSS option for top anchor?
					svg.text(gLeft, timeWidth, i * lineHeight + 15, hourText, {fontSize: "14", style: "text-anchor: end;", fill : "#333", stroke : "none"});
				}

				//var firstDay = new Date();//(new Date()).valueOf();
				firstDay = new Date(firstDay.getFullYear(), firstDay.getMonth(), firstDay.getDate()).valueOf();
				var isThisWeek = -1;
				//Draw background
				for(var deltaDay = 0; deltaDay < numDays; deltaDay++)
				{
					//Day group
					var gDay = svg.group(gMain, {transform: "translate(" + (border + timeWidth + deltaDay * dayWidth) + "," + header + ")"});

					//This is temporary.
					var date = new Date(firstDay + dayMillisConst * deltaDay);
					var day = date.getDate();

					//Draw box around
					var isToday = date.getFullYear() == today.getFullYear() && date.getMonth() == today.getMonth() && date.getDate() == today.getDate();
					if(isToday)
					{
						isThisWeek = deltaDay;
					}
					svg.rect(gDay, 0, -header, dayWidth, 24 * lineHeight + header, {fill : "none", stroke : "#F60"});
					//Draw day title
					svg.text(gDay, 6, -8, day + " " + dayOfWeekConst[date.getDay()], {fontSize: "20", fill : isToday?"#06C":"#F60", stroke : "none"});

					//Draw horizontal lines
					for(var i = 0; i < 24; i++)
					{
						svg.line(gDay, 0, i * lineHeight, dayWidth, i * lineHeight);
					}
				}

				if(isThisWeek != -1)
				{
					var date = new Date(firstDay + dayMillisConst * isThisWeek);
					var day = date.getDate();
					var gDay = svg.group(gMain, {transform: "translate(" + (border + timeWidth + isThisWeek * dayWidth) + "," + header + ")"});
					svg.rect(gDay, 0, -header, dayWidth, 24 * lineHeight + header, {fill : "none", stroke : "#06F"});
				}

				var gDayView = svg.group(gMain, {transform: "translate(" + (border + timeWidth) + "," + header + ")"});
				//A list of all items
				var itemByDay = new Array();
				for(var deltaDay = 0; deltaDay < numDays; deltaDay++) {
					itemByDay[deltaDay] = new Array();
				}

				function filterApplies(item) {
					for(var i = 0; i < filterProject.length; i++) {
						if(item.projectname == filterProject[i].projectname) {
							return true;
						}
					}
					for(var i = 0; i < filterFlow.length; i++) {
						if(item.projectname == filterFlow[i].projectname && item.flowname == filterFlow[i].flowname) {
							return true;
						}
					}
					return false;
				}

				//Function that re-renders all loaded items
				function renderDays() {
					//Clear items inside the day view
					svg.remove(gDayView);
					gDayView = svg.group(gMain, {transform: "translate(" + (border + timeWidth) + "," + header + ")"});

					//Add day groups
					for(var deltaDay = 0; deltaDay < numDays; deltaDay++) {
						var gDay = svg.group(gDayView, {transform: "translate(" + (deltaDay * dayWidth) + ")"});
						var data = itemByDay[deltaDay];
						//Sort the arrays to have a better view
						data.sort(function (a, b){
							if(a.length == dayMillisConst) return -1;
							if(b.length == dayMillisConst) return 1;
							//Smaller time in front
							var timeDiff = a.time - b.time;
							if(timeDiff == 0) {
								//Larger length in front
								return b.length - b.length;
							}
							return timeDiff;
						});
						//Sort items to columns
						var columns = new Array();
						columns.push(new Array());
						//Every item is parsed through here into columns
						for(var i = 0; i < data.length; i++) {
							//Apply filters here
							if(filterApplies(data[i])) {
								continue;
							}

							var foundColumn = false;
							//Go through every column until a place can be found
							for(var j = 0; j < columns.length; j++) {
								if(!intersectArray(data[i], columns[j])) {
									//Found a place
									columns[j].push(data[i]);
									foundColumn = true;
									break;
								}
							}
							//No place, create new column
							if(!foundColumn) {
								columns.push(new Array());
								columns[columns.length - 1].push(data[i]);
							}
						}

						//Actually drawing them
						for(var i = 0; i < columns.length; i++) {
							//Split into columns
							var gColumn = svg.group(gDay, {transform: "translate(" + (i * dayWidth / columns.length) + ")", style: "opacity: 0.8"});
							for(var j = 0; j < columns[i].length; j++) {
								//Draw item
								var item = columns[i][j];
								var startTime = new Date(item.time);
								var startY = Math.floor(startTime.getHours() * lineHeight + startTime.getMinutes() * lineHeight / 60);
								var endTime = new Date(item.time + item.length );
								var endY = Math.floor(startY + (item.length * lineHeight) / hourMillisConst);
								//var anchor = svg.a(gColumn);
								var itemUrl = contextURL + "/manager?project=" + item.projectname + "&flow=" + item.flowname;
								var gItem = svg.link(gColumn, itemUrl, {transform: "translate(0," + startY + ")"});

								//Pass the item into the DOM data store to be retrieved later on
								$(gItem).data("item", item);

								//Replace the context handler
								gItem.addEventListener('contextmenu', handleContextMenu);

								//Add a tooltip on mouse over
								gItem.addEventListener('mouseover', handleMouseOver);
								//Remove the tooltip on mouse out
								gItem.addEventListener('mouseout', handleMouseOut);

								//$(gItem).attr("style","color:red");
								var rect = svg.rect(gItem, 0, 0, Math.ceil(dayWidth / columns.length), Math.floor(endY - startY), 0, 0, {fill : "#7E7", stroke : "#444", strokeWidth : 1});
								//Draw text
								//svg.text(gItem, 6, 16, item.flowname, {fontSize: "13", fill : "#000", stroke : "none"});
							}
						}
					}
				}

				function processItem(item)
				{
					var firstTime = item.time;
					var startTime = firstDay;
					var endTime = firstDay + numDays * dayMillisConst;
					var period = item.period;

					// Shift time until we're past the start time
					if (period > 0) {
						// Calculate next execution time efficiently
						// Take into account items that ends in the date specified, but does not start on that date
						var periods = Math.floor((startTime - (firstTime + item.length)) / period);
						//Make sure we don't subtract
						if(periods < 0){
							periods = 0;
						}
						firstTime += period * periods;
						// Increment in case we haven't arrived yet. This will apply to most of the cases
						while (firstTime + item.length < startTime) {
							firstTime += period;
						}
					}

					// Bad or no period
					if (period <= 0) {
						// Single instance case
						if (firstTime >= startTime && firstTime < endTime) {
							addItem({flowname : item.flowname, projectname: item.projectname, time: firstTime, length: item.length});
						}
					}
					else {
						if(period <= hourMillisConst) {
							addItem({flowname : item.flowname, projectname: item.projectname, time: firstTime, length: endTime - firstTime});
						}
						else{
							// Repetitive schedule, firstTime is assumed to be after startTime
							while (firstTime < endTime) {
								addItem({flowname : item.flowname, projectname: item.projectname, time: firstTime, length: item.length});
								firstTime += period;
							}
						}
					}
				}

				function addItem(item)
				{
					var itemStartTime = new Date(item.time);
					var itemEndTime = new Date(item.time + item.length);
					var itemStartDate = new Date(itemStartTime.getFullYear(), itemStartTime.getMonth(), itemStartTime.getDate());
					var itemEndDate = new Date(itemEndTime.getFullYear(), itemEndTime.getMonth(), itemEndTime.getDate());

					//Cross date item, cut it to only today's portion and add another item starting tomorrow morning
					if(itemStartDate.valueOf() != itemEndDate.valueOf() && itemEndTime.valueOf() != itemStartDate + dayMillisConst)
					{
						var nextMorning = itemStartDate.valueOf() + dayMillisConst;
						var excess = item.length - (nextMorning - itemStartTime.valueOf());
						item.length = nextMorning - itemStartTime.valueOf();
						var item2 = {time: nextMorning, length: excess, projectname: item.projectname, flowname: item.flowname};
						addItem(item2);
					}

					//Now the item should be only in one day
					var index = (itemStartDate.valueOf() - firstDay) / dayMillisConst;
					if(index >= 0 && index < numDays)
					{
						//Add the item to the rendering list
						itemByDay[index].push(item);
					}
				}

				function handleContextMenu(event) {
					var requestURL = $(this).attr("href");
					var item = $(this).data("item");
					var menu = [
						{title: "Job \"" + item.flowname + "\" From Project \"" + item.projectname + "\""},
						{title: "View Job", callback: function() {window.location.href=requestURL;}},
						{title: "View Job in New Window", callback: function() {window.open(requestURL);}},
						{title: "Hide Job", callback: function() {filterFlow.push(item); renderDays();}},
						{title: "Hide All Jobs From the Same Project", callback: function() {filterProject.push(item); renderDays();}}
					];
					contextMenuView.show(event, menu);
					event.preventDefault();
					event.stopPropagation();
					return false;
				}

				function handleMouseOver(event) {
					//Create the new tooltip
					var requestURL = $(this).attr("href");
					var item = $(this).data("item");
					var offset = $("svg").offset();
					var thisOffset = $(this).offset();

					var tooltip = svg.group({transform: "translate(" + (thisOffset.left - offset.left + 2) + "," + (thisOffset.top - offset.top - 2) + ")"});
					var rect = svg.rect(tooltip, 0, -20, measureText(svg, item.flowname, {fontSize: "13"}) + 4, 20, {fill : "#FFF", stroke : "none"});
					svg.text(tooltip, 2, -5, item.flowname, {fontSize: "13", fill : "#000", stroke : "none"});


					//Store tooltip
					$(this).data("tooltip", tooltip);
				}

				function handleMouseOut(event) {
					//Clear the fade interval
					$($(this).data("tooltip")).fadeOut(750, function(){ svg.remove(this); });
				}

				//Asynchronously load data
				var requestURL = contextURL + "/schedule";
				$.ajax({
					type: "GET",
					url: requestURL,
					data: {"ajax": "loadFlow"},
					dataType: "json",
					success: function (data)
					{
						var items = data.items;

						//Sort items by day
						for(var i = 0; i < items.length; i++)
						{
							items[i].length = hourMillisConst; //TODO: parseInt(items[i].length);
							items[i].time = parseInt(items[i].time);
							items[i].period = parseInt(items[i].period);
							processItem(items[i]);
						}
						//Trigger a re-rendering of all the data
						renderDays();
					}
				});
			}
		}, settings : {
			"xmlns" : "http://www.w3.org/2000/svg", 
			"xmlns:xlink" : "http://www.w3.org/1999/xlink", 
			"shape-rendering" : "optimize-speed",
			"style" : "width:100%;height:" + totalHeight + "px"
		}});

	function dayMatch(d1, d2) {
		return d1.getDate() == d2.getDate() && d1.getFullYear() == d2.getFullYear() && d1.getMonth() == d2.getMonth();
	}

	function getHourText(hour) {
		return (hour==0 ? "12 AM" : (hour<12 ? hour + " AM" : (hour==12 ? "12 PM" : (hour-12) + " PM" )));
	}

	function intersectArray(a, arry) {
		for(var i = 0; i < arry.length; i++) {
			var b = arry[i];
			if(a.time < b.time + b.length && a.time + a.length > b.time) {
				return true;
			}
		}

		return false;
	}

	function measureText(svg, text, options) {
		var test = svg.text(0, 0, text, options);
		var width = test.getComputedTextLength();
		svg.remove(test);
		return width;
	}
});