mrtg/htdocs/js/rrdGraphPng.js
2016-02-10 12:53:09 +01:00

480 lines
17 KiB
JavaScript

/* *********************************************************************
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></canvas>');
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<width;x+=xIncr){
ctx.fillStyle = gridStyleA,
ctx.fillRect(x,0,xWidth,height);
ctx.fillStyle = gridStyleB,
ctx.fillRect(x+xWidth,0,xWidth,height);
};
},
__clearGrid: function(img){
var ctx = img.__ctx;
if (!ctx){
return;
}
var width = img.getWidth();
var height = img.getHeight();
ctx.clearRect(0,0,width,height);
},
__addTrack: function(img){
var qxDocument = q(document);
var initialStart = this.__start;
var initialRange = this.__range;
var xOrigin;
var pointerOrigin;
var imgWidth = img.getWidth() - this.getConfig('canvasPadding');
var active = false;
var trackLock = false;
var vertical;
var onPointerMove = function(e){
if (!active) return;
if (e.pointerType == 'touch' && e._original.touches.length > 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});
}
});