/*
* 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 clip_timeout() {
var O = this.options;
if (!O.auto_clip || O.auto_clip < 0) return false;
if (this.__cto) return;
if (O.clip)
this.__cto = window.setTimeout(this._reset_clip, O.auto_clip);
}
function peak_timeout() {
var O = this.options;
if (!O.auto_peak || O.auto_peak < 0) return false;
if (this.__pto) window.clearTimeout(this.__pto);
var value = +this.effective_value();
if (O.peak > O.base && value > O.base ||
O.peak < O.base && value < O.base)
this.__pto = window.setTimeout(this._reset_peak, O.auto_peak);
}
function label_timeout() {
var O = this.options;
var peak_label = (0 | O.peak_label);
var base = +O.base;
var label = +O.label;
var value = +this.effective_value();
if (peak_label <= 0) return false;
if (this.__lto) window.clearTimeout(this.__lto);
if (label > base && value > base ||
label < base && value < base)
this.__lto = window.setTimeout(this._reset_label, peak_label);
}
function top_timeout() {
var O = this.options;
if (!O.auto_hold || O.auto_hold < 0) return false;
if (this.__tto) window.clearTimeout(this.__tto);
if (O.top > O.base)
this.__tto = window.setTimeout(
this._reset_top,
O.auto_hold);
else
this.__tto = null;
}
function bottom_timeout() {
var O = this.options;
if (!O.auto_hold || O.auto_hold < 0) return false;
if (this.__bto) window.clearTimeout(this.__bto);
if (O.bottom < O.base)
this.__bto = window.setTimeout(this._reset_bottom, O.auto_hold);
else
this.__bto = null;
}
TK.LevelMeter = TK.class({
/**
* TK.LevelMeter is a fully functional meter bar displaying numerical values.
* TK.LevelMeter is an enhanced {@link TK.MeterBase}'s containing a clip LED,
* a peak pin with value label and hold markers.
* In addition, LevelMeter has an optional falling animation, top and bottom peak
* values and more.
*
* @class TK.LevelMeter
*
* @extends TK.MeterBase
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {Boolean} [options.show_clip=false] - If set to <code>true</code>, show the clipping LED.
* @property {Number} [options.clipping=0] - If clipping is enabled, this is the threshold for the
* clipping effect.
* @property {Integer|Boolean} [options.auto_clip=false] - This is the clipping timeout. If set to
* <code>false</code> automatic clipping is disabled. If set to <code>n</code> the clipping effect
* times out after <code>n</code> ms, if set to <code>-1</code> it remains forever.
* @property {Boolean} [options.clip=false] - If clipping is enabled, this option is set to
* <code>true</code> when clipping happens. When automatic clipping is disabled, it can be set to
* <code>true</code> to set the clipping state.
* @property {Object} [options.clip_options={}] - Additional options for the {@link TK.State} clip LED.
* @property {Boolean} [options.show_hold=false] - If set to <code>true</code>, show the hold value LED.
* @property {Integer} [options.hold_size=1] - Size of the hold value LED in the number of segments.
* @property {Number|boolean} [options.auto_hold=false] - If set to <code>false</code> the automatic
* hold LED is disabled, if set to <code>n</code> the hold value is reset after <code>n</code> ms and
* if set to <code>-1</code> the hold value is not reset automatically.
* @property {Number} [options.top=false] - The top hold value. If set to <code>false</code> it will
* equal the meter level.
* @property {Number} [options.bottom=false] - The bottom hold value. This only exists if a
* <code>base</code> value is set and the value falls below the base.
* @property {Boolean} [options.show_peak=false] - If set to <code>true</code>, show the peak label.
* @property {Integer|Boolean} [options.peak_label=false] - If set to <code>false</code> the automatic peak
* label is disabled, if set to <code>n</code> the peak label is reset after <code>n</code> ms and
* if set to <code>-1</code> it remains forever.
* @property {Function} [options.format_peak=TK.FORMAT("%.2f")] - Formatting function for the peak label.
* @property {Number} [options.falling=0] - If set to a positive number, activates the automatic falling
* animation. The meter level will fall by this amount per frame.
* @property {Number} [options.falling_fps=24] - This is the number of frames of the falling animation.
* It is not an actual frame rate, but instead is used to determine the actual speed of the falling
* animation together with the option <code>falling</code>.
* @property {Number} [options.falling_init=2] - Initial falling delay in number of frames. This option
* can be used to delay the start of the falling animation in order to avoid flickering if internal
* and external falling are combined.
*/
_class: "LevelMeter",
Extends: TK.MeterBase,
_options: Object.assign(Object.create(TK.MeterBase.prototype._options), {
falling: "number",
falling_fps: "number",
falling_init: "number",
peak: "number",
top: "number",
bottom: "number",
hold_size: "int",
show_hold: "boolean",
clipping: "number",
auto_clip: "int|boolean",
auto_peak: "int|boolean",
peak_label: "int",
auto_hold: "int|boolean",
format_peak: "function",
clip_options: "object",
}),
options: {
clip: false,
falling: 0,
falling_fps: 24,
falling_init: 2,
peak: false,
top: false,
bottom: false,
hold_size: 1,
show_hold: false,
clipping: 0,
auto_clip: false,
auto_peak: false,
peak_label: false,
auto_hold: false,
format_peak: TK.FORMAT("%.2f"),
clip_options: {}
},
static_events: {
set_label: label_timeout,
set_bottom: bottom_timeout,
set_top: top_timeout,
set_peak: peak_timeout,
set_clip: function(value) {
if (value) {
clip_timeout.call(this);
}
},
set_show_peak: peak_timeout,
set_auto_clip: function(value) {
if (this.__cto && 0|value <=0)
window.clearTimeout(this.__cto);
},
set_auto_peak: function(value) {
if (this.__pto && 0|value <=0)
window.clearTimeout(this.__pto);
},
set_peak_label: function(value) {
if (this.__lto && 0|value <=0)
window.clearTimeout(this.__lto);
},
set_auto_hold: function(value) {
if (this.__tto && 0|value <=0)
window.clearTimeout(this.__tto);
if (this.__bto && 0|value <=0)
window.clearTimeout(this.__bto);
},
},
initialize: function (options) {
/* track the age of the value option */
this.track_option("value");
TK.MeterBase.prototype.initialize.call(this, options);
this._reset_label = this.reset_label.bind(this);
this._reset_clip = this.reset_clip.bind(this);
this._reset_peak = this.reset_peak.bind(this);
this._reset_top = this.reset_top.bind(this);
this._reset_bottom = this.reset_bottom.bind(this);
/**
* @member {HTMLDivElement} TK.LevelMeter#element - The main DIV container.
* Has class <code>toolkit-level-meter</code>.
*/
TK.add_class(this.element, "toolkit-level-meter");
var O = this.options;
if (O.peak === false)
O.peak = O.value;
if (O.top === false)
O.top = O.value;
if (O.bottom === false)
O.bottom = O.value;
if (O.falling < 0)
O.falling = -O.falling;
},
redraw: function () {
var O = this.options;
var I = this.invalid;
var E = this.element;
if (I.show_hold) {
I.show_hold = false;
TK.toggle_class(E, "toolkit-has-hold", O.show_hold);
}
if (I.top || I.bottom) {
/* top and bottom require a meter redraw, so lets invalidate
* value */
I.top = I.bottom = false;
I.value = true;
}
if (I.base)
I.value = true;
TK.MeterBase.prototype.redraw.call(this);
if (I.clip) {
I.clip = false;
TK.toggle_class(E, "toolkit-clipping", O.clip);
}
},
destroy: function () {
TK.MeterBase.prototype.destroy.call(this);
},
/**
* Resets the peak label.
*
* @method TK.LevelMeter#reset_peak
*
* @emits TK.LevelMeter#resetpeak
*/
reset_peak: function () {
if (this.__pto) clearTimeout(this.__pto);
this.__pto = false;
this.set("peak", this.effective_value());
/**
* Is fired when the peak was reset.
*
* @event TK.LevelMeter#resetpeak
*/
this.fire_event("resetpeak");
},
/**
* Resets the label.
*
* @method TK.LevelMeter#reset_label
*
* @emits TK.LevelMeter#resetlabel
*/
reset_label: function () {
if (this.__lto) clearTimeout(this.__lto);
this.__lto = false;
this.set("label", this.effective_value());
/**
* Is fired when the label was reset.
*
* @event TK.LevelMeter#resetlabel
*/
this.fire_event("resetlabel");
},
/**
* Resets the clipping LED.
*
* @method TK.LevelMeter#reset_clip
*
* @emits TK.LevelMeter#resetclip
*/
reset_clip: function () {
if (this.__cto) clearTimeout(this.__cto);
this.__cto = false;
this.set("clip", false);
/**
* Is fired when the clipping LED was reset.
*
* @event TK.LevelMeter#resetclip
*/
this.fire_event("resetclip");
},
/**
* Resets the top hold.
*
* @method TK.LevelMeter#reset_top
*
* @emits TK.LevelMeter#resettop
*/
reset_top: function () {
this.set("top", this.effective_value());
/**
* Is fired when the top hold was reset.
*
* @event TK.LevelMeter#resettop
*/
this.fire_event("resettop");
},
/**
* Resets the bottom hold.
*
* @method TK.LevelMeter#reset_bottom
*
* @emits TK.LevelMeter#resetbottom
*/
reset_bottom: function () {
this.set("bottom", this.effective_value());
/**
* Is fired when the bottom hold was reset.
*
* @event TK.LevelMeter#resetbottom
*/
this.fire_event("resetbottom");
},
/**
* Resets all hold features.
*
* @method TK.LevelMeter#reset_all
*
* @emits TK.LevelMeter#resetpeak
* @emits TK.LevelMeter#resetlabel
* @emits TK.LevelMeter#resetclip
* @emits TK.LevelMeter#resettop
* @emits TK.LevelMeter#resetbottom
*/
reset_all: function () {
this.reset_label();
this.reset_peak();
this.reset_clip();
this.reset_top();
this.reset_bottom();
},
effective_value: function() {
var O = this.options;
var falling = +O.falling;
if (O.falling <= 0) return O.value;
var value = +O.value, base = +O.base;
var age = +this.value_time.value;
if (!(age > 0)) age = Date.now();
else age = +(Date.now() - age);
var frame_length = 1000.0 / +O.falling_fps;
if (age > O.falling_init * frame_length) {
if (value > base) {
value -= falling * (age / frame_length);
if (value < base) value = base;
} else {
value += falling * (age / frame_length);
if (value > base) value = base;
}
}
return value;
},
/*
* This is an _internal_ method, which calculates the non-filled regions
* in the overlaying canvas as pixel positions. The canvas is only modified
* using this information when it has _actually_ changed. This can save a lot
* of performance in cases where the segment size is > 1 or on small devices where
* the meter has a relatively small pixel size.
*/
calculate_meter: function(to, value, i) {
var O = this.options;
var falling = +O.falling;
var base = +O.base;
value = +value;
// this is a bit unelegant...
if (falling) {
value = this.effective_value();
// continue animation
if (value !== base) {
this.invalid.value = true;
// request another frame
this.trigger_draw_next();
}
}
i = TK.MeterBase.prototype.calculate_meter.call(this, to, value, i);
if (!O.show_hold) return i;
// shorten things
var hold = +O.top;
var segment = O.segment|0;
var hold_size = (O.hold_size|0) * segment;
var base = +O.base;
var pos;
if (hold > base) {
/* TODO: lets snap in set() */
pos = this.val2px(hold)|0;
if (segment !== 1) pos = segment*(Math.round(pos/segment)|0);
to[i++] = pos;
to[i++] = pos+hold_size;
}
hold = +O.bottom;
if (hold < base) {
pos = this.val2px(hold)|0;
if (segment !== 1) pos = segment*(Math.round(pos/segment)|0);
to[i++] = pos;
to[i++] = pos+hold_size;
}
return i;
},
// GETTER & SETTER
set: function (key, value) {
if (key === "value") {
var O = this.options;
var base = O.base;
// snap will enforce clipping
value = this.snap(value);
if (O.falling) {
var v = this.effective_value();
if (v >= base && value >= base && value < v ||
v <= base && value <= base && value > v) {
/* NOTE: we are doing a falling animation, but maybe its not running */
if (!this.invalid.value) {
this.invalid.value = true;
this.trigger_draw();
}
return;
}
}
if (O.auto_clip !== false && value > O.clipping && !this.has_base()) {
this.set("clip", true);
}
if (O.show_label && O.peak_label !== false &&
(value > O.label && value > base || value < O.label && value < base)) {
this.set("label", value);
}
if (O.auto_peak !== false &&
(value > O.peak && value > base || value < O.peak && value < base)) {
this.set("peak", value);
}
if (O.auto_hold !== false && O.show_hold && value > O.top) {
this.set("top", value);
}
if (O.auto_hold !== false && O.show_hold && value < O.bottom && this.has_base()) {
this.set("bottom", value);
}
} else if (key === "top" || key === "bottom") {
value = this.snap(value);
}
return TK.MeterBase.prototype.set.call(this, key, value);
}
});
/**
* @member {TK.State} TK.LevelMeter#clip - The {@link TK.State} instance for the clipping LED.
* @member {HTMLDivElement} TK.LevelMeter#clip.element - The DIV element of the clipping LED.
* Has class <code>toolkit-clip</code>.
*/
TK.ChildWidget(TK.LevelMeter, "clip", {
create: TK.State,
show: false,
map_options: {
clip: "state",
},
default_options: {
"class": "toolkit-clip"
},
toggle_class: true,
});
/**
* @member {HTMLDivElement} TK.LevelMeter#_peak - The DIV element for the peak marker.
* Has class <code>toolkit-peak</code>.
*/
TK.ChildElement(TK.LevelMeter, "peak", {
show: false,
create: function() {
var peak = TK.element("div","toolkit-peak");
peak.appendChild(TK.element("div","toolkit-peak-label"));
return peak;
},
append: function() {
this._bar.appendChild(this._peak);
},
toggle_class: true,
draw_options: [ "peak" ],
draw: function (O) {
if (!this._peak) return;
var n = this._peak.firstChild;
TK.set_text(n, O.format_peak(O.peak));
if (O.peak > O.min && O.peak < O.max && O.show_peak) {
this._peak.style.display = "block";
var pos = 0;
if (vert(O)) {
pos = O.basis - this.val2px(this.snap(O.peak));
pos = Math.min(O.basis, pos);
this._peak.style.top = pos + "px";
} else {
pos = this.val2px(this.snap(O.peak));
pos = Math.min(O.basis, pos)
this._peak.style.left = pos + "px";
}
} else {
this._peak.style.display = "none";
}
/**
* Is fired when the peak was drawn.
*
* @event TK.LevelMeter#drawpeak
*/
this.fire_event("drawpeak");
},
});
})(this, this.TK);