/* ----------------------------------------------------------------------------- * STARTUP */ /* TODO: Check setup variables */ /* TODO: Loading indicator */ var categoryCurrent = null; var categoryArea = document.getElementById("headers"); var xmlData = null; var gdoc = null; var gwindow = null; function init() { /* Set by frame.html */ if(!window._frameLoaded) { window.setTimeout("init();", 100); return; } setupFrame(); loadXml(makeEndpointURI(), loadedGraphData); } init(); function loadedGraphData(doc) { if(!doc) { displayError("Couldn't load graph data from server", categoryArea); return; } xmlData = doc; displayCategories(); displayCurrentPage(); } /* ----------------------------------------------------------------------------- * GRAPHS */ function getFrameDocument() { var frame = document.getElementById("main-frame"); /* COMPAT: IE and Mozilla access the content of frames differently */ return frame.contentDocument ? frame.contentDocument : frame.contentWindow.document; } function setupFrame() { gdoc = getFrameDocument(); var frame = document.getElementById("main-frame"); /* COMPAT: IE and Mozilla access the content of frames differently */ gwindow = frame.contentDocument ? frame.contentDocument.defaultView : frame.contentWindow; } function displayCurrentPage() { var area = gdoc.getElementById("graphs"); var img; /* Hide all images not from this category */ var i, images = area.getElementsByTagName("img"); for(i = 0; i < images.length; i++) { img = images.item(i); var vis = (img._category == categoryCurrent); img.style.display = vis ? "inline" : "none"; if(vis != img._visible) { /* The graph needs reloading if now turning visible */ img._visible = vis; if(vis) reloadGraph(img, false); else img._loading = false; } } var graphs = findCategoryGraphs(categoryCurrent); if(!graphs) { displayError("Graph data isn't loaded from server", area); return; } var tend = nowTime(); var tbeg = tend - 86400; /* One day by default */ for(i = 0; i < graphs.length; i++) { var child = graphs[i]; var name = child.getAttribute("name"); if(!name || !name.length) continue; /* Make sure we don't already have this graph */ var id = "graph-" + categoryCurrent + "-" + name; img = gdoc.getElementById(id); if(img != null) continue; var interval = child.getAttribute("interval"); if(!interval || isNaN(interval) || interval <= 0) interval = 0; interval = Number(interval); img = gdoc.createElement("img"); var title = child.getAttribute("title"); if(title && title.length) { img.setAttribute("title", child.getAttribute("title")); img.setAttribute("alt", child.getAttribute("title")); } else img.setAttribute("alt", "Graph"); img.className = "graph"; img.id = id; img.style.cursor = "crosshair"; area.appendChild(img); img._category = categoryCurrent; img._name = name; img._tbeg = tbeg; img._tend = tend; img._tinterval = interval; img._visible = true; img._loading = false; reloadGraph(img, false); img.onmousedown = function(evt) { actionGotoCancel(); return zoomGraphStart(evt || gwindow.event); } img.onmouseover = function(evt) { return actionsDisplay(gwindow.event ? gwindow.event : evt); } /* Setup for an auto scroll */ if(interval) { /* Bump up auto scroll interval to 5 seconds */ var interval = img._tinterval < 5 ? 5 : img._tinterval; window.setInterval(autoScroll, interval * 1000, img); } } } function reloadGraph(img, force) { img._last = nowTime(); if(!img._visible) return; /* TODO: Move these into settings */ /* TODO: We should encode colons properly */ var GRAPH_COLOR_BACK = ":252424"; var GRAPH_COLOR_FONT = ":E7E1CC"; var GRAPH_WIDTH = 450; var GRAPH_HEIGHT = 120 var uri = makeEndpointURI() + "/graph?category=" + img._category + "&name=" + img._name; /* Add date options */ uri += "&start=" + img._tbeg + "&end=" + img._tend; /* And colors */ uri += "&color=BACK" + GRAPH_COLOR_BACK + "&color=FONT" + GRAPH_COLOR_FONT + "&color=SHADEA" + GRAPH_COLOR_BACK + "&color=SHADEB" + GRAPH_COLOR_BACK; /* Size */ uri += "&height=" + GRAPH_HEIGHT + "&width=" + GRAPH_WIDTH; if(force || img.getAttribute("src") != uri) { img._loading = true; updateCursors(img); /* HACK: The onload event for IMG is called with a strange event target */ img.onload = new Function("reloadedGraph('" + img.id + "');"); img.setAttribute("src", uri); } } function reloadedGraph(id) { var img = getFrameDocument().getElementById(id); img._loading = false; updateCursors(img); } function updateCursors(img) { if(img._actions) { var i, acts = img._actions.getElementsByTagName("img"); for(i = 0; i < acts.length; i++) acts.item(i).style.cursor = img._loading ? "wait" : "pointer"; } img.style.cursor = img._loading ? "wait" : "crosshair"; } function autoScroll(img) { /* Don't auto scroll if we have loading issues */ if(img._loading) return; /* If we're displaying 'now' somewhere in the graph... */ var now = nowTime(); if((img._tend + (img._tinterval * 2)) >= now) { var diff = now - img._last; if(diff > 0) { /* ... then scroll by X seconds to scroll the graph */ img._tbeg += diff; img._tend += diff; reloadGraph(img, false); } } } /* ----------------------------------------------------------------------------- * ZOOM */ function zoomGraphStart(evt) { /* COMPAT: In IE the left button is 1, Mozilla is 0 */ var lbutton = gwindow.event ? 1 : 0; if(evt.button != lbutton) return; /* COMPAT: Target is handled differently in Internet nastiness */ var img = evt.target || evt.srcElement; if(img.className != "graph") return; var zoom = gdoc.getElementById("zoom"); zoom._img = img; zoom._zooming = true; zoom._from = evt.clientX + gdoc.body.scrollLeft; zoom._start = zoom._from - img.offsetLeft; zoom._end = zoom._start; zoom.style.width = 1; zoom.style.left = zoom._from; zoom.style.height = img.height; zoom.style.top = img.offsetTop; zoom.style.display = "block"; zoom.onmousemove = zoom._img.onmousemove = function(evt) { return zoomGraphUpdate(gwindow.event ? gwindow.event : evt); } zoom.onmouseup = zoom._img.onmouseup = function(evt) { return zoomGraphDone(gwindow.event ? gwindow.event : evt); } document.onmouseup = gdoc.onmouseup = function(evt) { return zoomGraphDone(gwindow.event ? gwindow.event : evt); } cancelEvent(evt); return false; } function zoomGraphUpdate(evt) { var zoom = gdoc.getElementById("zoom"); if(!zoom._zooming || !zoom._img) return; var to = evt.clientX + gdoc.body.scrollLeft; var width = to - zoom._from; zoom._end = to - zoom._img.offsetLeft; if(width == 0) width = 1; if(width < 0) { zoom.style.left = to; zoom.style.width = -width; } else { zoom.style.left = zoom._from; zoom.style.width = width; } zoom.style.height = zoom._img.height; zoom.style.top = zoom._img.offsetTop; zoom.style.display = "block"; cancelEvent(evt); return false; } function zoomGraphDone(evt) { var zoom = gdoc.getElementById("zoom"); if(!zoom._zooming) return; /* COMPAT: Target is handled differently in Internet nastiness */ var targ = evt.target || evt.srcElement; if(targ == zoom || targ == zoom._img) zoomGraphSelect(zoom); zoomGraphCancel(evt); } function zoomGraphSelect(zoom) { // Arbitrary values (TODO: Move these to CONFIG) var GRAPH_MARGIN_LEFT = 67; var GRAPH_MARGIN_RIGHT = 27; var left = GRAPH_MARGIN_LEFT; var right = zoom._img.width - GRAPH_MARGIN_RIGHT; var start = zoom._start > zoom._end ? zoom._end : zoom._start; var end = zoom._start > zoom._end ? zoom._start : zoom._end; /* If too small selected, then punt */ if((end - start) <= 1) return; /* If entire thing selected, then punt */ if(start < left && end > right) return; /* If only margins selected, then punt */ if(!(start < right && end > left)) return; /* Limit it to the right range */ var range = zoom._img.width - GRAPH_MARGIN_RIGHT - GRAPH_MARGIN_LEFT; start = start < left ? 0 : start - left; end = end - left > range ? range : end - left; /* Compute the ratio pixels:time */ var ratio = (zoom._img._tend - zoom._img._tbeg) / range; var obeg = zoom._img._tbeg; var oend = zoom._img._tend; zoom._img._tend = Math.floor((end * ratio) + zoom._img._tbeg); zoom._img._tbeg = Math.floor((start * ratio) + zoom._img._tbeg); reloadGraph(zoom._img, true); } function zoomGraphCancel(evt) { var zoom = gdoc.getElementById("zoom"); if(zoom._zooming) { if(zoom._img) zoom._img.onmousemove = zoom._img.onmouseup = zoom._img.onmouseleave = null; zoom._img = null; zoom.onmousemove = zoom.onmouseup = null; zoom._zooming = false; } zoom.style.display = "none"; } function zoomGraphOut(img) { var factor = Math.floor((img._tend - img._tbeg) / 2.5); var now = nowTime(); /* If near the end we have special behavior */ if(img._tend >= now - 300 || img._tend <= now) { img._tbeg -= factor; img._tend = now; } else { factor = Math.floor(factor / 2); img._tbeg -= factor img._tend += factor; } reloadGraph(img, true); } function zoomGraphMove(img, dir) { var factor = Math.floor((img._tend - img._tbeg) / 3); if(!dir) factor = -factor; img._tbeg += factor; img._tend += factor; reloadGraph(img, true); } function actionsDisplay(evt) { /* COMPAT: Target is handled differently in Internet nastiness */ var img = evt.target || evt.srcElement; if(img.className != "graph") return; var actions = gdoc.getElementById("actions"); /* Disconnect from previous image */ if(actions._img) actions._img._actions = null; /* Attach to this image */ actions._img = img; img._actions = actions; updateCursors(img); actions._display = true; actions.style.top = img.offsetTop; actions.style.left = img.offsetLeft + (img.width - 16); actions.style.display = "block"; actions.onmouseout = img.onmouseout = function(evt) { return actionsHide(gwindow.event ? gwindow.event : evt); } /* TODO: We only actually have to do this once */ var i, acts = actions.getElementsByTagName("img"); for(i = 0; i < acts.length; i++) { acts.item(i).onclick = function(evt) { return actionRun(gwindow.event ? gwindow.event : evt); } } } function actionsHide(evt) { var actions = gdoc.getElementById("actions"); if(!actions._display) return; var x = evt.clientX + gdoc.body.scrollLeft; var y = evt.clientY + gdoc.body.scrollTop; var img = actions._img; /* See if we're actually still within the image */ if(x >= img.offsetLeft && x <= img.offsetLeft + img.width && y >= img.offsetTop && y <= img.offsetTop + img.height) return; actions.style.display = "none"; img.onmouseout = actions.onmouseout = null; img._actions = null; actions._img = null; actions._display = false; } function actionRun(evt) { /* COMPAT: Target is handled differently in Internet nastiness */ var act = evt.target || evt.srcElement; if(act.className != "action") return; var actions = gdoc.getElementById("actions"); if(!actions._display) return; switch(act.getAttribute("id")) { case "action-zoom": zoomGraphOut(actions._img); break; case "action-next": zoomGraphMove(actions._img, true); break; case "action-prev": zoomGraphMove(actions._img, false); break; case "action-goto": actionGoto(actions._img, act); break; } } function actionGoto(img, act) { /* Display the selection thingy */ var got = gdoc.getElementById("goto"); /* TODO: Make numbers constants */ got.style.top = img.offsetTop + 40; got.style.left = (img.offsetLeft + img.width) - 160; got.style.width = 120; got.style.display = "block"; var sel = gdoc.getElementById("goto-select"); sel.onchange = actionGotoChange; sel._img = img; sel.value = null; sel.selectedIndex = -1; } function actionGotoCancel() { var got = gdoc.getElementById("goto"); got.style.display = "none"; } function actionGotoChange() { var sel = gdoc.getElementById("goto-select"); var img = sel._img; sel.onchange = null; sel._img = null; times = sel.value.split(":"); img._tbeg = nowTime() - times[0]; img._tend = nowTime() - times[1]; reloadGraph(img, true); actionGotoCancel(); } /* ----------------------------------------------------------------------------- * GROUP CATEGORIES */ function findCategoryGraphs(cat) { var i; if(!xmlData) return null; var ret = new Array(); var graphs = xmlData.getElementsByTagName("graph"); for(i = 0; i < graphs.length; i++) { if(graphs.item(i).getAttribute("category") == cat) ret.push(graphs.item(i)); } return ret; } function changeCategory(evt) { var header = evt.target || evt.srcElement; categoryCurrent = header.getAttribute("category"); displayCurrentPage(); } function displayCategories() { var cats = new Object(); var cat, i, name; /* Get the template and clean it up a bit */ var template = document.getElementById("header-template"); var graphs = xmlData.getElementsByTagName("graph"); for(i = 0; i < graphs.length; i++) { cat = graphs.item(i).getAttribute("category"); if(!cat) cat = "Other"; cats[cat] = true; } for(name in cats) { if(!categoryCurrent) categoryCurrent = name; var el = template.cloneNode(false); el.removeAttribute("id"); el.appendChild(document.createTextNode(name)); el.setAttribute("category", name); el.onclick = function(evt) { return changeCategory(evt || window.event); } categoryArea.insertBefore(el, template); } } /* ----------------------------------------------------------------------------- * HELPERS */ function loadXml(uri, callback) { var xmlhttp; var xmlHttpChange = function() { if(xmlhttp.readyState == 4) { if(xmlhttp.status == 200) callback(xmlhttp.responseXML); else callback(null); } } /* Mozilla code */ if(window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = xmlHttpChange; xmlhttp.open("GET", uri, true); xmlhttp.send(null); } /* And now for the Nasty */ else if (window.ActiveXObject) { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); /* TODO: need to do something when this fails (can it fail?) */ if(xmlhttp) { xmlhttp.onreadystatechange = xmlHttpChange; xmlhttp.open("GET", uri, true); xmlhttp.send() } } } function displayError(message, place) { var el = document.createElement("span"); el.className = "error"; var text = document.createTextNode(message); el.appendChild(text); if(place) place.appendChild(el); return el; } function nowTime() { return Math.floor((new Date()).getTime() / 1000); } function debug(msg) { setTimeout(function() { throw new Error("[debug] " + msg); }, 0); } function makeEndpointURI() { /* TODO: This should use the scheme of the current URI */ return "http://" + document.location.host + ENDPOINT; } function cancelEvent(evt) { /* COMPAT: Totally different for IE and mozilla */ if(evt.preventDefault) evt.preventDefault(); if(evt.stopPropagation) evt.stopPropagation(); if("cancelBubble" in evt) evt.cancelBubble = true; if("returnValue" in evt) evt.returnValue = false; }