Home Reference Source

lib/BaseGauge.js

require('./polyfill');

const SmartCanvas = require('./SmartCanvas');
const Animation = require('./Animation');
const Collection = require('./Collection');
const DomObserver = require('./DomObserver');

let gauges = new Collection();

/**
 * Basic abstract BaseGauge class implementing common functionality
 * for different type of gauges.
 *
 * It should not be instantiated directly but must be extended by a final
 * gauge implementation.
 *
 * @abstract
 * @example
 *
 * class MyCoolGauge extends BaseGauge {
 *
 *     // theses methods below MUST be implemented:
 *
 *     constructor(options) {
 *        // ... do something with options
 *        super(options);
 *        // ... implement anything else
 *     }
 *
 *     draw() {
 *         // ... some implementation here
 *         return this;
 *     }
 * }
 */
export default class BaseGauge {

    /**
     * @constructor
     * @abstract
     * @param {GenericOptions} options
     */
    constructor(options) {
        let className = this.constructor.name;

        if (className === 'BaseGauge') {
            throw new TypeError('Attempt to instantiate abstract class!');
        }

        gauges.push(this);

        /**
         * Gauge type class
         *
         * @type {BaseGauge} type
         */
        this.type = ns[className];

        options.minValue = parseFloat(options.minValue);
        options.maxValue = parseFloat(options.maxValue);

        if (!options.borders) {
            options.borderInnerWidth = options.borderMiddleWidth =
                options.borderOuterWidth = 0;
        }

        if (!options.renderTo) {
            throw TypeError('Canvas element was not specified when creating ' +
                'the Gauge object!');
        }

        let canvas = options.renderTo.tagName ?
            options.renderTo :
            /* istanbul ignore next: to be tested with e2e tests */
            document.getElementById(options.renderTo);

        if (!(canvas instanceof HTMLCanvasElement)) {
            throw TypeError('Given gauge canvas element is invalid!');
        }

        if (!options.width || !options.height) {
            if (!options.width) options.width = canvas.parentNode ?
                canvas.parentNode.offsetWidth : canvas.offsetWidth;
            if (!options.height) options.height = canvas.parentNode ?
                canvas.parentNode.offsetHeight : canvas.offsetHeight;
        }

        /**
         * Gauge options
         *
         * @type {GenericOptions} options
         */
        this.options = options || {};

        /**
         * @type {SmartCanvas} canvas
         */
        this.canvas = new SmartCanvas(canvas, options.width, options.height);
        this.canvas.onRedraw = this.draw.bind(this);

        /**
         * @type {Animation} animation
         */
        this.animation = new Animation(
            options.animationRule,
            options.animationDuration);
    }

    /**
     * Sets new value for this gauge.
     * If gauge is animated by configuration it will trigger a proper animation.
     * Upsetting a value triggers gauge redraw.
     *
     * @param {number} value
     */
    set value(value) {
        if (value === this.options.value) return;

        if (this.options.animation) {
            let fromValue = this.options.value;

            /**
             * @type {number}
             * @access private
             */
            this._value = value;

            this.animation.animate(percent => {
                this.options.value = fromValue + (value - fromValue) * percent;

                this.draw();
            }, () => {
                this.options.value = value;
                delete this._value;
                this.draw();
            });
        }

        else {
            this.options.value = value;
            this.draw();
        }
    }

    /**
     * Returns current value of the gauge
     *
     * @return {number}
     */
    get value() {
        return typeof this._value === 'undefined' ?
            this.options.value : this._value;
    }

    /**
     * Updates gauge configuration options at runtime and redraws the gauge
     *
     * @param {RadialGaugeOptions} options
     * @returns {BaseGauge}
     */
    update(options) {
        Object.assign(this.options, options || {});

        this.canvas.width = this.options.width;
        this.canvas.height = this.options.height;

        this.animation.rule = this.options.animationRule;
        this.animation.duration = this.options.animationDuration;

        this.canvas.redraw();

        return this;
    }

    /**
     * Performs destruction of this object properly
     */
    destroy() {
        let index = gauges.indexOf(this);

        /* istanbul ignore else */
        if (~index) {
            //noinspection JSUnresolvedFunction
            gauges.splice(index, 1);
        }

        this.canvas.destroy();
        this.canvas = null;

        this.animation.destroy();
        this.animation = null;
    }

    /**
     * Triggering gauge render on a canvas.
     *
     * @abstract
     * @returns {BaseGauge}
     */
    draw() {
        throw new TypeError('draw() must be implemented by a child class!');
    }

    /**
     * Inject given gauge object into DOM
     *
     * @param {string} Type
     * @param {GenericOptions} options
     */
    static initialize(Type, options) {
        new DomObserver(options, 'canvas', Type);
    }
}


/**
 * @typedef {object} ns
 */
(ns.document || ns)['gauges'] = gauges;

module.exports = BaseGauge;