/* ********************************************************************* rrdGraphPng - make rrdcharts interactive Copyright: 2015 OETIKER+PARTNER AG http://www.oetiker.ch License: Gnu GPL Version 2 Version: #VERSION#, #DATE# Authors: * Tobias Oetiker (oetiker) * **********************************************************************/ /** * The rrdGraphPng control turns a static RRD chart into an interactive one * all that is required for this to work, is the ability to configure the * start and end and possible with and height parameters in the chart URL. * * See index.html in this directory for inspiration on how to use this library. * * */ qxWeb.define('rrdGraphPng',{ extend: qxWeb.$$qx.ui.website.Widget, statics: { _config : { canvasPadding: 100, initialStart : (new Date()).getTime() / 1000 - 24*3600, initialRange: 24*3600, moveZoom: 1, autoUpdate: true, gridFillStyleA: 'rgba(0,0,0,0.08)', gridFillStyleB: 'rgba(255,255,255,0.08)' }, rrdGraphPng: function(cfg){ var png = new rrdGraphPng(this); png.init(cfg); return png; } }, construct : function(selector, context) { this.base(arguments, selector, context); }, members : { __start: null, __range: null, __syncJob: null, init: function(cfg){ if (!this.base(arguments)) { return false; }; var that = this; try { throw new Error(''); } catch (e){ // this is voodoo, but it does often detect where the javascript file // lives and thus we can hope to find the cursor files there too if (e.stack){ that.setConfig('cursorUrl',e.stack.replace(/[^$]*http/,'http').replace(/[^\/]*\.js[^$]*/,'')); } }; // update the grid no more then 30 times a second this.__paintGrid = qxWeb.func.throttle(this.__paintGridReal,32,{trailing: false}); if (cfg){ for (var key in cfg){ this.setConfig(key,cfg[key]) } } var qxWindow = qxWeb(window); this.__start = parseInt(this.getConfig('initialStart')); this.__range = parseInt(this.getConfig('initialRange')); qxWindow.on('resize',this.update,this); this._forEachElementWrapped(function(img,idx) { img.setStyle('display','inline-block'); img.setAttributes({ unselectable: true, draggable: false }); this.__addLoader(img); this.__addCanvas(img); this.__addTrack(img); this.__addRoll(img); img.emit('update'); },this); if (this.getConfig('autoUpdate')){ this.__addSyncCharts(); } return true; }, setStart: function(start){ this.__start = start; this.update(); }, getStart: function(){ return this.__start; }, getRange: function(){ return this.__range; }, setRange: function(range){ this.__range = range; this.update(); }, setStartRange: function(start,range){ this.__start = start; this.__range = range; this.update(); }, update: function(){ this._forEachElementWrapped(function(img,idx) { img.emit('update'); },this); }, __addSyncCharts: function(){ var lastEnd = this.__start + this.__range; var lastNow = false; var that = this; var syncCharts = function(){ var currentEnd = that.__start + that.__range; var now = Math.round((new Date).getTime()/1000); if (now < currentEnd && now > that.__start){ if (!lastNow) { lastNow = now; lastEnd = currentEnd; return; } var increment = now - lastNow; var go = false; that._forEachElementWrapped(function(img,idx) { if (that.__range / img.getWidth() < increment){ go = true; } }); if (go){ lastNow = now; that.__start += increment; lastEnd = that.__start + that.__range; that.update(); that.emit('change',that.__start,that.__range); } } else { lastNow = false; } }; if (this.getConfig('autoUpdate')){ this.__syncJob = window.setInterval(syncCharts,1000); } }, __buildUrl: function(img,zoom){ var template = img.getData('src-template'); var start = this.__start; if (start == null || isNaN(start)) return ''; return qxWeb.template.render(template,{ width: img.getWidth(), height: img.getHeight(), start: start, end: start + this.__range, zoom: zoom ? zoom : 1, random: Math.round(Math.random()*1000000000).toString(36) }); }, __addCanvas: function(img){ var offset = img.getOffset(); var pos = img.getPosition(); // console.log(img,offset,pos); var canvas = qxWeb.create(''); canvas.setStyles({ position: 'absolute' }) .setAttributes({ draggable: "false", unselectable: "true" }) .insertBefore(img); canvas.setStyle('cursor','url(' + this.getConfig('cursorUrl') + '/MoveCursor.cur), move'); var resize = function(){ var width = img.getWidth(); var height = img.getHeight(); canvas.setStyles({width: width+'px', height: height+'px'}); canvas.setProperties({width: width.toString(), height: height.toString()}); //canvas.width = width; //canvas.height = height; }; qxWeb(window).on('resize',resize); img.__canvas = canvas; if (canvas[0].getContext){ img.__ctx = canvas[0].getContext("2d"); } resize(); }, __rangeCap: function(range){ return Math.round(Math.min(Math.max(10,range),24*3600*366*20)); }, __addLoader: function(img){ var loading = false; var skipped = false; var lastSrc = null; var start; var retry = 0; var onError = function(){ loading = false; //if (retry < 3){ // retry++; // img.emit('update'); //} }; img.on('error',onError,this); var onLoad = function(){ loading = false; if (skipped){ skipped = false; img.emit('update'); } retry = 0; }; img.on('load',onLoad,this); var onUpdate = function(zoom){ var url = this.__buildUrl(img,zoom); if (!url) return; if (! loading){ loading = true; img.setProperty('src',url); } else { skipped = true; } }; var onUpdateThrottled = qxWeb.func.throttle(onUpdate,120); img.on('update',onUpdateThrottled,this); img.once('qxRrdDispose',function(){ img.off('load',onLoad,this); img.off('update',onUpdateThrottled,this); img.off('error',onError,this); },this); }, __addRoll: function(img){ var that = this; var syncUp = qxWeb.func.debounce(function(){ that.update(); that.emit('changeStartRange',{start:that.__start,range:that._range}); },200); var xPos = img.getWidth()/2; var onMove = function(e){ var newXPos = e.pageX - img.getOffset().left; if (! isNaN(newXPos)){ xPos = newXPos; } }; var initialDotRange; var initialDotStart; var dotOff = true; var killerId; var onRoll = function(e){ if (e.pointerType != "wheel" || !e._original.ctrlKey ) return; e.preventDefault(); e.stopPropagation(); var delta = e.delta.y; var initialRange = this.__range; var xOrigin = xPos / img.getWidth(); if (dotOff){ initialDotRange = this.__range; initialDotStart = this.__start; dotOff = false; } this.__paintGrid(img,initialDotRange,initialDotStart); this.__range = this.__rangeCap(this.__range*(1+(delta/10000))); this.__start = Math.round(this.__start + (initialRange - this.__range)*xOrigin); var that = this; killerId = window.setTimeout(function(){ window.clearTimeout(killerId); that.__clearGrid(img); dotOff=true },1000); img.emit('update',this.getConfig('moveZoom')); syncUp(); }; img.__canvas.on('pointermove',onMove,this); img.__canvas.on('roll',onRoll,this); //img.once('qxRrdDispose',function(){ // img.__canvas.off('pointermove',onMove,this); // img.__canvas.off('roll',onRoll,this); //},this); }, __paintGridReal: function(img,initialRange,initialStart){ var ctx = img.__ctx; if (!ctx){ return; } var width = img.getWidth(); var height = img.getHeight(); var skip = 100; var xIncr = Math.round(initialRange / this.__range * skip); var xOff = Math.round((width / this.__range * (initialStart - this.__start)) % xIncr); var xWidth = Math.round(xIncr/2); var gridStyleA = this.getConfig('gridFillStyleA'); var gridStyleB = this.getConfig('gridFillStyleB'); ctx.clearRect(0,0,width,height); for (var x=-xIncr+xOff;x 1) return; var delta = { x: e.pageX - pointerOrigin.x, y: e.pageY - pointerOrigin.y }; if (!trackLock){ if (Math.abs(delta.x) > 10 || Math.abs(delta.y) > 10){ vertical = Math.abs(delta.x) < Math.abs(delta.y) trackLock = true; } else { return; } } if (vertical){ if (e.pointerType == 'touch') return; if (! isNaN(xOrigin)){ this.__range = this.__rangeCap(initialRange*Math.pow(1.02,delta.y)); this.__start = Math.round(initialStart + (initialRange - this.__range)*xOrigin); } } else { this.__start = initialStart-Math.round(this.__range/imgWidth*delta.x); } this.__paintGrid(img,initialRange,initialStart); e.preventDefault(); e.stopPropagation(); img.emit('update',this.getConfig('moveZoom')); }; var onPinch = function(e){ if (!active) return; var scale = e.getScale(); if (!scale) return; e.preventDefault(); e.stopPropagation(); this.__range = this.__rangeCap(initialRange/scale); this.__start = Math.round(initialStart + (initialRange - this.__range)/2); this.__paintGrid(img,initialRange,initialStart); img.emit('update',this.getConfig('moveZoom')); }; var canvas = img.__canvas; var qxDoc = qxWeb(window); var onPointerUp = function(e){ if (!active) return; //e.stopPropagation(); //e.preventDefault(); active = false; this.__clearGrid(img); this.update(); if (initialRange != this.__range || initialStart != this.__start){ this.emit('changeStartRange',{start:this.__start,range:this.__range}); } trackLock = false; canvas.setStyle('cursor','url(' + this.getConfig('cursorUrl') + '/MoveCursor.cur), move'); qxDoc.off("pointermove",onPointerMove,this); }; var onPointerDown = function(e){ //e.preventDefault(); //e.stopPropagation(); if (active) return; active = true; initialStart = this.__start; initialRange = this.__range; imgWidth = img.getWidth() - this.getConfig('canvasPadding'); canvas.setStyle('cursor','url(' + this.getConfig('cursorUrl') + '/DragCursor.cur), move'); var newXPos = e.pageX - img.getOffset().left; pointerOrigin = { x: e.pageX, y: e.pageY }; if (! isNaN(newXPos)){ xOrigin = newXPos / img.getWidth(); } qxDoc.on("pointermove",onPointerMove,this); // on mobile devices we do not kill 'touch' because this could // spell the start of a vertical scroll if (e.pointerType != 'touch') { e.preventDefault(); e.stopPropagation(); } }; var onPointerOut = function(e){ if (!active) return; e.preventDefault(); e.stopPropagation(); }; qxDoc.on("pointerup",onPointerUp,this,true); qxDoc.on('pointerout',onPointerOut,this,true); canvas.on('pinch',onPinch,this); canvas.on('pointerdown',onPointerDown,this); img.once('qxRrdDispose',function(){ canvas.allOff(); qxDoc.off("pointerup",onPointerUp,this,true); qxDoc.off('pointerout',onPointerOut,this,true); qxDoc.off('pointermove',onPointerMove,this); canvas.remove(); }); }, dispose: function(){ if (this.__syncJob){ window.clearInterval(this.__syncJob); } this._forEachElementWrapped(function(img) { img.emit('qxRrdDispose'); img.removeAttribute('unselectable'); img.removeAttribute('draggable'); }); return this.base(arguments); } }, defer : function(statics) { qxWeb.$attach({rrdGraphPng : statics.rrdGraphPng}); } });