/*
* This file is part of AUX.
*
* AUX 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.
*
* AUX 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
* 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
*/
/* jshint -W018 */
import { defineChildWidget } from '../child_widget.js';
import { Meter } from './meter.js';
import { State } from './state.js';
import { addClass, toggleClass } from '../utils/dom.js';
import { effectiveValue } from '../modules/range.js';
function clearTimeout(to) {
if (to >= 0) window.clearTimeout(to);
}
function clipTimeout() {
const O = this.options;
if (!O.auto_clip || O.auto_clip < 0) return false;
clearTimeout(this.__cto);
this.__cto = window.setTimeout(this._reset_clip, O.auto_clip);
}
function valueTimeout() {
const peak_value = 0 | this.options.peak_value;
if (peak_value <= 0) return false;
clearTimeout(this.__lto);
this.__lto = window.setTimeout(this._reset_value, peak_value);
}
function topTimeout() {
const O = this.options;
if (!O.auto_hold || O.auto_hold < 0) return false;
clearTimeout(this.__tto);
this.__tto = window.setTimeout(this._reset_top, O.auto_hold);
}
function bottomTimeout() {
const O = this.options;
if (!O.auto_hold || O.auto_hold < 0) return false;
clearTimeout(this.__bto);
this.__bto = window.setTimeout(this._reset_bottom, O.auto_hold);
}
/**
* LevelMeter is a fully functional meter bar displaying numerical values.
* LevelMeter is an enhanced {@link Meter} containing a clip LED and hold markers.
* In addition, LevelMeter has an optional falling animation, top and bottom peak
* values and more.
*
* @class LevelMeter
*
* @extends Meter
*
* @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 {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 {Integer|Boolean} [options.peak_value=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 {Number} [options.falling=0] - If set to a positive number, activates the automatic falling
* animation. The meter level will fall by this amount over the time set via `options.falling_duration`.
* @property {Number} [options.falling_duration=1000] - This is the time in milliseconds for the falling
* animation. The level falls by `options.falling` in this period of time.
* @property {Number} [options.falling_init=50] - Initial falling delay in milliseconds. 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.
*/
export class LevelMeter extends Meter {
static get _options() {
return Object.assign({}, Meter.getOptionTypes(), {
falling: 'number',
falling_duration: 'int',
falling_init: 'number',
top: 'number',
bottom: 'number',
hold_size: 'int',
show_hold: 'boolean',
clipping: 'number',
auto_clip: 'int|boolean',
auto_hold: 'int|boolean',
peak_value: 'int|boolean',
});
}
static get options() {
return {
clip: false,
falling: 0,
falling_duration: 1000,
falling_init: 50,
top: false,
bottom: false,
hold_size: 1,
show_hold: false,
clipping: 0,
auto_clip: false,
auto_hold: false,
peak_value: false,
};
}
static get static_events() {
return {
set_auto_clip: function (value) {
if (this.__cto >= 0 && 0 | (value <= 0))
window.clearTimeout(this.__cto);
},
set_peak_value: function (value) {
if (this.__lto >= 0 && 0 | (value <= 0))
window.clearTimeout(this.__lto);
if (value === false) this.set('sync_value', this.options._sync_value);
else this.set('sync_value', false);
},
set_sync_value: function (v) {
this.set('_sync_value', v);
},
set_auto_hold: function (value) {
if (this.__tto >= 0 && value === -1) window.clearTimeout(this.__tto);
if (this.__bto >= 0 && value === -1) window.clearTimeout(this.__bto);
},
};
}
initialize(options) {
/* track the age of the value option */
this.trackOption('value');
super.initialize(options);
this._reset_value = this.resetValue.bind(this);
this._reset_clip = this.resetClip.bind(this);
this._reset_top = this.resetTop.bind(this);
this._reset_bottom = this.resetBottom.bind(this);
/**
* @member {HTMLDivElement} LevelMeter#element - The main DIV container.
* Has class <code>.aux-levelmeter</code>.
*/
const O = this.options;
if (O.top === false) O.top = O.value;
if (O.bottom === false) O.bottom = O.value;
if (O.falling < 0) O.falling = -O.falling;
}
draw(O, element) {
addClass(element, 'aux-levelmeter');
super.draw(O, element);
}
redraw() {
const O = this.options;
const I = this.invalid;
const E = this.element;
if (I.show_hold) {
I.show_hold = false;
toggleClass(E, 'aux-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;
super.redraw();
if (I.clip) {
I.clip = false;
toggleClass(E, 'aux-clipping', O.clip);
}
}
destroy() {
super.destroy();
}
effectiveValue() {
const O = this.options;
return effectiveValue(
+O.value,
+O.base,
+O.falling,
+O.falling_duration,
+O.falling_init,
+this.value_time.value
);
}
/**
* Resets the value.
*
* @method LevelMeter#resetValue
*
* @emits LevelMeter#resetvalue
*/
resetValue() {
clearTimeout(this.__lto);
this.set('value_label', this.effectiveValue());
/**
* Is fired when the value label was reset.
*
* @event LevelMeter#resetvalue
*/
this.emit('resetvalue');
}
/**
* Resets the clipping LED.
*
* @method LevelMeter#resetClip
*
* @emits LevelMeter#resetclip
*/
resetClip() {
clearTimeout(this.__cto);
this.set('clip', false);
/**
* Is fired when the clipping LED was reset.
*
* @event LevelMeter#resetclip
*/
this.emit('resetclip');
}
/**
* Resets the top hold.
*
* @method LevelMeter#resetTop
*
* @emits LevelMeter#resettop
*/
resetTop() {
this.set('top', this.effectiveValue());
/**
* Is fired when the top hold was reset.
*
* @event LevelMeter#resettop
*/
this.emit('resettop');
}
/**
* Resets the bottom hold.
*
* @method LevelMeter#resetBottom
*
* @emits LevelMeter#resetbottom
*/
resetBottom() {
this.set('bottom', this.effectiveValue());
/**
* Is fired when the bottom hold was reset.
*
* @event LevelMeter#resetbottom
*/
this.emit('resetbottom');
}
/**
* Resets all hold features.
*
* @method LevelMeter#resetAll
*
* @emits LevelMeter#resetvalue
* @emits LevelMeter#resetclip
* @emits LevelMeter#resettop
* @emits LevelMeter#resetbottom
*/
resetAll() {
this.resetValue();
this.resetClip();
this.resetTop();
this.resetBottom();
}
/*
* 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.
*/
calculateMeter(to, value, i) {
const O = this.options;
const falling = +O.falling;
const base = +O.base;
const transformation = O.transformation;
value = +value;
// this is a bit unelegant...
if (falling) {
value = this.effectiveValue();
// continue animation
if (value !== base) {
this.invalid.value = true;
// request another frame
this.triggerDrawNext();
}
}
i = super.calculateMeter(to, value, i);
if (!O.show_hold) return i;
// shorten things
let hold = +O.top;
const segment = O.segment | 0;
const hold_size = O.hold_size * segment;
let pos;
if (!(hold_size > 0)) return i;
const pos_base = +transformation.valueToPixel(base);
if (hold > base) {
/* TODO: lets snap in set() */
pos = transformation.valueToPixel(hold) | 0;
if (segment !== 1) pos = segment * (Math.round(pos / segment) | 0);
if (pos > pos_base) {
to[i++] = pos - hold_size;
to[i++] = pos;
} else {
to[i++] = pos;
to[i++] = pos + hold_size;
}
}
hold = +O.bottom;
if (hold < base) {
pos = transformation.valueToPixel(hold) | 0;
if (segment !== 1) pos = segment * (Math.round(pos / segment) | 0);
if (pos > pos_base) {
to[i++] = pos - hold_size;
to[i++] = pos;
} else {
to[i++] = pos;
to[i++] = pos + hold_size;
}
}
return i;
}
// GETTER & SETTER
set(key, value) {
if (key === 'value') {
const O = this.options;
const base = O.base;
// snap will enforce clipping
value = O.snap_module.snap(value);
if (O.falling) {
const v = this.effectiveValue();
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.triggerDraw();
}
return;
}
}
if (O.auto_clip !== false && value >= O.clipping && !this.hasBase()) {
this.set('clip', true);
clipTimeout.call(this);
}
if (
O.show_value &&
O.peak_value !== false &&
((value > O.value_label && value > base) ||
(value < O.value_label && value < base))
) {
clearTimeout(this.__lto);
this.set('value_label', value);
}
if (
O.show_value &&
O.peak_value !== false &&
((value < O.value_label && value > base) ||
(value > O.value_label && value < base))
) {
valueTimeout.call(this);
}
if (O.auto_hold !== false && O.show_hold && value > O.top) {
clearTimeout(this.__tto);
this.set('top', value);
}
if (O.auto_hold !== false && O.show_hold && value < O.top) {
topTimeout.call(this);
}
if (
O.auto_hold !== false &&
O.show_hold &&
value < O.bottom &&
this.hasBase()
) {
clearTimeout(this.__bto);
this.set('bottom', value);
}
if (
O.auto_hold !== false &&
O.show_hold &&
value > O.bottom &&
this.hasBase()
) {
bottomTimeout.call(this);
}
} else if (key === 'top' || key === 'bottom') {
value = this.options.snap_module.snap(value);
}
return super.set(key, value);
}
}
/**
* @member {State} LevelMeter#clip - The {@link State} instance for the clipping LED.
* @member {HTMLDivElement} LevelMeter#clip.element - The DIV element of the clipping LED.
* Has class <code>.aux-clip</code>.
*/
defineChildWidget(LevelMeter, 'clip', {
create: State,
show: false,
map_options: {
clip: 'state',
},
default_options: {
class: 'aux-clip',
},
toggle_class: true,
});