/*
* This file is part of Toolkit.
*
* Toolkit is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* Toolkit is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU General
* Public License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*/
"use strict";
(function(w, TK){
function vert(O) {
return O.layout === "left" || O.layout === "right";
}
function fill_interval(ctx, w, h, a, is_vertical) {
var i;
if (is_vertical) {
for (i = 0; i < a.length; i+= 2) {
ctx.fillRect(0, h - a[i+1], w, a[i+1]-a[i]);
}
} else {
for (i = 0; i < a.length; i+= 2) {
ctx.fillRect(a[i], 0, a[i+1]-a[i], h);
}
}
}
function clear_interval(ctx, w, h, a, is_vertical) {
var i;
if (is_vertical) {
for (i = 0; i < a.length; i+= 2) {
ctx.clearRect(0, h - a[i+1], w, a[i+1]-a[i]);
}
} else {
for (i = 0; i < a.length; i+= 2) {
ctx.clearRect(a[i], 0, a[i+1]-a[i], h);
}
}
}
function draw_full(ctx, w, h, a, is_vertical) {
ctx.fillRect(0, 0, w, h);
clear_interval(ctx, w, h, a, is_vertical);
}
function make_interval(a) {
var i, tmp, again, j;
do {
again = false;
for (i = 0; i < a.length-2; i+=2) {
if (a[i] > a[i+2]) {
tmp = a[i];
a[i] = a[i+2];
a[i+2] = tmp;
tmp = a[i+1];
a[i+1] = a[i+3];
a[i+3] = tmp;
again = true;
}
}
} while (again);
for (i = 0; i < a.length-2; i+= 2) {
if (a[i+1] > a[i+2]) {
if (a[i+3] > a[i+1]) {
a[i+1] = a[i+3];
}
for (j = i+3; j < a.length; j++) a[j-1] = a[j];
a.length = j-2;
i -=2;
continue;
}
}
}
function cmp_intervals(a, b) {
var ret = 0;
var i;
for (i = 0; i < a.length; i+=2) {
if (a[i] === b[i]) {
if (a[i+1] === b[i+1]) continue;
ret |= (a[i+1] < b[i+1]) ? 1 : 2;
} else if (a[i+1] === b[i+1]) {
ret |= (a[i] > b[i]) ? 1 : 2;
} else return 4;
}
return ret;
}
function subtract_intervals(a, b) {
var i;
var ret = [];
for (i = 0; i < a.length; i+=2) {
if (a[i] === b[i]) {
if (a[i+1] <= b[i+1]) continue;
ret.push(b[i+1], a[i+1]);
} else {
if (a[i] > b[i]) continue;
ret.push(a[i], b[i]);
}
}
return ret;
}
TK.MeterBase = TK.class({
/**
* TK.MeterBase is a base class to build different meters such as {@link TK.LevelMeter} from.
* TK.MeterBase uses {@link TK.Gradient} and contains a {@link TK.Scale} widget.
* TK.MeterBase inherits all options from {@link TK.Scale}.
*
* Note that the two options <code>format_labels</code> and
* <code>scale_base</code> have different names here.
*
* Note that level meters with high update frequencies can be very demanding when it comes
* to rendering performance. These performance requirements can be reduced by increasing the
* segment size using the <code>segment</code> option. Using a segment, the different level
* meter positions are reduced. This widget will take advantage of that by avoiding rendering those
* changes to the meter level, which fall into the same segment.
*
* @class TK.MeterBase
*
* @extends TK.Widget
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {String} [options.layout="left"] - A string describing the layout of the meter.
* Possible values are <code>"left"</code>, <code>"right"</code>, <code>"top"</code> and
* <code>"bottom"</code>. <code>"left"</code> and <code>"right"</code> are vertical
* layouts, where the meter is on the left or right of the scale, respectively. Similarly,
* <code>"top"</code> and <code>"bottom"</code> are horizontal layouts in which the meter
* is at the top or the bottom, respectively.
* @property {Integer} [options.segment=1] - Segment size. Pixel positions of the meter level are
* rounded to multiples of this size. This can be used to give the level meter a LED effect and to
* reduce processor load.
* @property {Number} [options.value=0] - Level value.
* @property {Number} [options.base=false] - The base value of the meter. If set to <code>false</code>,
* the base will coincide with the minimum value <code>options.min</code>. The meter level is drawn
* starting from the base to the value.
* @property {Number} [options.label=0] - Value to be displayed on the label.
* @property {Function} [options.format_label=TK.FORMAT("%.2f")] - Function for formatting the
* label.
* @property {Boolean} [options.show_label=false] - If set to <code>true</code> a label is displayed.
* @property {Number} [options.title=false] - The title of the TK.MeterBase. Set to `false` to hide it.
* @property {Boolean} [options.show_scale=true] - Set to <code>false</code> to hide the scale.
* @property {Number|Boolean} [options.scale_base=false] - Base of the meter scale, see {@link TK.Scale} for more information.
* @property {Boolean} [options.show_labels=true] - If <code>true</code>, display labels on the
* scale.
* @property {Function} [options.format_labels=TK.FORMAT("%.2f")] - Function for formatting the
* scale labels. This is passed to TK.Scale as option <code>labels</code>.
*
*/
_class: "MeterBase",
Extends: TK.Widget,
Implements: [TK.Gradient],
_options: Object.assign(Object.create(TK.Widget.prototype._options),
TK.Gradient.prototype._options, TK.Scale.prototype._options, {
layout: "string",
segment: "number",
value: "number",
base: "number|boolean",
min: "number",
max: "number",
label: "number",
title: "string|boolean",
show_labels: "boolean",
format_label: "function",
scale_base: "number|boolean",
format_labels: "function",
background: "string|boolean",
gradient: "object|boolean"
}),
options: {
layout: "left",
segment: 1,
value: 0,
base: false,
label: 0,
title: false,
show_labels: true,
format_label: TK.FORMAT("%.2f"),
levels: [1, 5, 10], // array of steps where to draw labels
scale_base: false,
format_labels: TK.FORMAT("%.2f"),
background: false,
gradient: false
},
static_events: {
set_label: function(value) {
/**
* Is fired when the label changed.
* The argument is the actual label value.
*
* @event TK.MeterBase#labelchanged
*
* @param {string} label - The label of the {@link TK.MeterBase}.
*/
this.fire_event("labelchanged", value);
},
set_title: function(value) {
/**
* Is fired when the title changed.
* The argument is the actual title.
*
* @event TK.MeterBase#titlechanged
*
* @param {string} title - The title of the {@link TK.MeterBase}.
*/
this.fire_event("titlechanged", value);
},
set_segment: function(value) {
// what is this supposed to do?
// -> probably invalidate the value to force a redraw
this.set("value", this.options.value);
},
set_value: function(value) {
/**
* Is fired when the value changed.
* The argument is the actual value.
*
* @event TK.MeterBase#valuechanged
*
* @param {number} value - The value of the {@link TK.MeterBase}.
*/
this.fire_event("valuechanged", value);
},
set_base: function(value) {
if (value === false) {
var O = this.options;
O.base = value = O.min;
}
/**
* Is fired when the base value changed.
* The argument is the actual base value.
*
* @event TK.MeterBase#basechanged
*
* @param {number} base - The value of the base.
*/
this.fire_event("basechanged", value);
},
set_layout: function () {
var O = this.options;
this.set("value", O.value);
this.set("min", O.min);
this.trigger_resize();
},
rangedchanged: function() {
var gradient = this.options.gradient;
if (gradient) {
this.set("gradient", gradient);
}
},
},
initialize: function (options) {
var E;
TK.Widget.prototype.initialize.call(this, options);
var O = this.options;
/**
* @member {HTMLDivElement} TK.MeterBase#element - The main DIV container.
* Has class <code>toolkit-meter-base</code>.
*/
if (!(E = this.element)) this.element = E = TK.element("div");
TK.add_class(E, "toolkit-meter-base");
this.widgetize(E, false, true, true);
this._bar = TK.element("div", "toolkit-bar");
/**
* @member {HTMLCanvas} TK.MeterBase#_canvas - The canvas element drawing the mask.
* Has class <code>toolkit-mask</code>.
*/
this._canvas = document.createElement("canvas");
TK.add_class(this._canvas, "toolkit-mask");
this._fillstyle = false;
E.appendChild(this._bar);
this._bar.appendChild(this._canvas);
/**
* @member {HTMLDivElement} TK.MeterBase#_bar - The DIV element containing the masks
* and drawing the background. Has class <code>toolkit-bar</code>.
*/
this.delegate(this._bar);
this._last_meters = [];
this._current_meters = [];
this.set("label", O.value);
this.set("base", O.base);
},
destroy: function () {
this._bar.remove();
TK.Widget.prototype.destroy.call(this);
},
redraw: function () {
var I = this.invalid;
var O = this.options;
var E = this.element;
if (this._fillstyle === false) {
this._canvas.style.removeProperty("background-color");
TK.S.add(function() {
this._fillstyle = TK.get_style(this._canvas, "background-color");
TK.S.add(function() {
this._canvas.getContext("2d").fillStyle = this._fillstyle;
this._canvas.style.setProperty("background-color", "transparent", "important");
this.trigger_draw();
}.bind(this), 3);
}.bind(this), 2);
}
if (I.reverse) {
I.reverse = false;
TK.toggle_class(E, "toolkit-reverse", O.reverse);
}
if (I.gradient || I.background) {
I.gradient = I.background = false;
this.draw_gradient(this._bar, O.gradient, O.background);
}
TK.Widget.prototype.redraw.call(this);
if (I.layout) {
I.layout = false;
TK.remove_class(E, "toolkit-vertical",
"toolkit-horizontal", "toolkit-left",
"toolkit-right", "toolkit-top", "toolkit-bottom");
var scale = this.scale ? this.scale.element : null;
var bar = this._bar;
switch (O.layout) {
case "left":
TK.add_class(E, "toolkit-vertical", "toolkit-left");
if (scale) TK.insert_after(scale, bar);
break;
case "right":
TK.add_class(E, "toolkit-vertical", "toolkit-right");
if (scale) TK.insert_after(bar, scale);
break;
case "top":
TK.add_class(E, "toolkit-horizontal", "toolkit-top");
if (scale) TK.insert_after(scale, bar);
break;
case "bottom":
TK.add_class(E, "toolkit-horizontal", "toolkit-bottom");
if (scale) TK.insert_after(bar, scale);
break;
default:
throw("unsupported layout");
}
}
if (this._fillstyle === false) return;
if (I.basis && O._height > 0 && O._width > 0) {
this._canvas.setAttribute("height", Math.round(O._height));
this._canvas.setAttribute("width", Math.round(O._width));
/* FIXME: I am not sure why this is even necessary */
this._canvas.style.width = O._width + "px";
this._canvas.style.height = O._height + "px";
this._canvas.getContext("2d").fillStyle = this._fillstyle;
}
if (I.value && O.show_label) {
this.label.set("label", O.format_label(O.value));
}
if (I.value || I.basis || I.min || I.max) {
I.basis = I.value = I.min = I.max = false;
this.draw_meter();
}
},
resize: function() {
var O = this.options;
TK.Widget.prototype.resize.call(this);
var w = TK.inner_width(this._bar);
var h = TK.inner_height(this._bar);
this.set("_width", w);
this.set("_height", h);
var i = vert(O) ? h : w;
this.set("basis", i);
this._last_meters.length = 0;
this._fillstyle = false;
},
calculate_meter: function(to, value, i) {
var O = this.options;
// Set the mask elements according to options.value to show a value in
// the meter bar
var base = O.base;
var segment = O.segment|0;
var reverse = !!O.reverse;
var size = O.basis|0;
/* At this point the whole meter bar is filled. We now want
* to clear the area between base and value.
*/
/* canvas coordinates are reversed */
var v1 = this.val2px(base)|0;
var v2 = this.val2px(value)|0;
if (segment !== 1) v2 = segment*(Math.round(v2/segment)|0);
if (v2 < v1) {
to[i++] = v2;
to[i++] = v1;
} else {
to[i++] = v1;
to[i++] = v2;
}
return i;
},
draw_meter: function () {
var O = this.options;
var w = Math.round(O._width);
var h = Math.round(O._height);
var i, j;
if (!(w > 0 && h > 0)) return;
var a = this._current_meters;
var tmp = this._last_meters;
var i = this.calculate_meter(a, O.value, 0);
if (i < a.length) a.length = i;
make_interval(a);
this._last_meters = a;
this._current_meters = tmp;
var diff;
if (tmp.length === a.length) {
diff = cmp_intervals(tmp, a)|0;
} else diff = 4;
if (!diff) return;
// FIXME: this is currently broken for some reason
if (diff == 1)
diff = 4;
var ctx = this._canvas.getContext("2d");
ctx.fillStyle = this._fillstyle;
var is_vertical = vert(O);
if (diff === 1) {
/* a - tmp is non-empty */
clear_interval(ctx, w, h, subtract_intervals(a, tmp), is_vertical);
return;
}
if (diff === 2) {
/* tmp - a is non-empty */
fill_interval(ctx, w, h, subtract_intervals(tmp, a), is_vertical);
return;
}
draw_full(ctx, w, h, a, is_vertical);
},
// HELPERS & STUFF
_val2seg: function (val) {
// rounds values to fit in the segments size
// always returns values without taking options.reverse into account
var s = +this.val2px(this.snap(val));
s -= s % +this.options.segment;
if (this.options.reverse)
s = +this.options.basis - s;
return s;
},
has_base: function() {
var O = this.options;
return O.base > O.min;
},
});
/**
* @member {TK.Scale} TK.MeterBase#scale - The {@link TK.Scale} of the meter.
*/
TK.ChildWidget(TK.MeterBase, "scale", {
create: TK.Scale,
map_options: {
format_labels: "labels",
scale_base: "base",
},
inherit_options: true,
show: true,
toggle_class: true,
static_events: {
set: function(key, value) {
var p = this.parent;
if (p)
p.fire_event("scalechanged", key, value);
},
},
});
/**
* @member {TK.Label} TK.MeterBase#title - The {@link TK.Label} displaying the title.
* Has class <code>toolkit-title</code>.
*/
TK.ChildWidget(TK.MeterBase, "title", {
create: TK.Label,
show: false,
option: "title",
default_options: { "class" : "toolkit-title" },
map_options: { "title" : "label" },
toggle_class: true,
});
/**
* @member {TK.Label} TK.MeterBase#label - The {@link TK.Label} displaying the label.
*/
TK.ChildWidget(TK.MeterBase, "label", {
create: TK.Label,
show: false,
default_options: { "class" : "toolkit-value" },
toggle_class: true,
});
})(this, this.TK);