widgets/gauge.js

/*
 * 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
 */

import { Widget } from './widget.js';
import { Circular } from './circular.js';
import { element, addClass } from '../utils/dom.js';
import { makeSVG } from '../utils/svg.js';
import { FORMAT } from '../utils/sprintf.js';
import { objectAnd, objectSub } from '../utils/object.js';

function getCoordsSingle(deg, inner, pos) {
  deg = (deg * Math.PI) / 180;
  return {
    x: Math.cos(deg) * inner + pos,
    y: Math.sin(deg) * inner + pos,
  };
}
const formatTranslate = FORMAT('translate(%f, %f)');
const formatViewbox = FORMAT('0 0 %d %d');
/**
 * Gauge draws a single {@link Circular} into a SVG image. It inherits
 * all options of {@link Circular}.
 *
 * @class Gauge
 *
 * @extends Widget
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {Number} [options.x=0] - Displacement of the {@link Circular}
 *   in horizontal direction. This allows drawing gauges which are only
 *   represented by a segment of a circle.
 * @property {Number} [options.y=0] - Displacement of the {@link Circular}
 *   in vertical direction.
 * @property {Object} [options.label] - Optional gauge label.
 * @property {Number} [options.label.pos] - Position inside of the circle in
 *   degrees.
 * @property {String} [options.label.label] - label string.
 * @property {Number} [options.label.margin] - Margin of the label string.
 * @property {String} [options.label.align] - Alignment of the label, either
 *   <code>inner</code> or <code>outer</code>.
 */
export class Gauge extends Widget {
  static get _options() {
    return Object.assign({}, Circular.getOptionTypes(), {
      width: 'number',
      height: 'number',
      label: 'object',
    });
  }

  static get options() {
    return Object.assign({}, Circular.getDefaultOptions(), {
      width: 100, // width of the element
      height: 100, // height of the svg
      size: 100,
      label: { pos: 90, margin: 0, align: 'inner', label: '' },
    });
  }

  initialize(options) {
    super.initialize(options);

    const O = this.options;
    let S;

    if (typeof O.label === 'string') this.set('label', O.label);

    if (!this.element) this.element = element('div');

    /**
     * @member {SVGImage} Gauge#svg - The main SVG image.
     */
    this.svg = S = makeSVG('svg');

    /**
     * @member {HTMLDivElement} Gauge#element - The main DIV container.
     *   Has class <code>.aux-gauge</code>.
     */

    /**
     * @member {SVGText} Gauge#_label - The label of the gauge.
     *   Has class <code>.aux-label</code>.
     */
    this._label = makeSVG('text', { class: 'aux-label' });
    S.appendChild(this._label);

    let co = objectAnd(O, Circular.getOptionTypes());
    co = objectSub(co, Widget.getOptionTypes());
    co.container = S;

    /**
     * @member {Circular} Gauge#circular - The {@link Circular} module.
     */
    this.circular = new Circular(co);
    this.addChild(this.circular);
  }

  resize() {
    super.resize();
    this.invalid.label = true;
    this.triggerDraw();
  }

  draw(O, element) {
    addClass(element, 'aux-gauge');
    element.appendChild(this.svg);

    super.draw(O, element);
  }

  redraw() {
    const I = this.invalid,
      O = this.options;
    const S = this.svg;

    super.redraw();

    if (I.validate('width', 'height')) {
      S.setAttribute('viewBox', formatViewbox(O.width, O.height));
    }

    if (I.validate('label', 'size', 'x', 'y')) {
      const _label = this._label;
      _label.textContent = O.label.label;

      if (O.label.label) {
        S.add(
          function () {
            const t = O.label;
            const outer = O.size / 2;
            const margin = t.margin;
            const align = t.align === 'inner';
            const bb = _label.getBoundingClientRect();
            const angle = t.pos % 360;
            const outer_p = outer - margin;
            const coords = getCoordsSingle(angle, outer_p, outer);

            let mx =
              (((coords.x - outer) / outer_p) * (bb.width + bb.height / 2.5)) /
              (align ? -2 : 2);
            let my =
              (((coords.y - outer) / outer_p) * bb.height) / (align ? -2 : 2);

            mx += O.x;
            my += O.y;

            S.add(
              function () {
                _label.setAttribute(
                  'transform',
                  formatTranslate(coords.x + mx, coords.y + my)
                );
                _label.setAttribute('text-anchor', 'middle');
              }.bind(this),
              1
            );
            /**
             * Is fired when the label changed.
             *
             * @event Gauge#labeldrawn
             */
            this.emit('labeldrawn');
          }.bind(this)
        );
      }
    }
  }

  // GETTERS & SETTERS
  set(key, value) {
    if (key === 'label') {
      if (typeof value === 'string') value = { label: value };
      value = Object.assign(this.options.label, value);
    }
    // Circular does the snapping
    if (!Widget.getOptionTypes()[key] && Circular.getOptionTypes()[key])
      value = this.circular.set(key, value);
    return super.set(key, value);
  }
}