Source: modules/circular.js

/*
 * 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 interpret_label(x) {
    if (typeof x === "object") return x;
    if (typeof x === "number") return { pos: x };
    TK.error("Unsupported label type ", x);
}
var __rad = Math.PI / 180;
function _get_coords(deg, inner, outer, pos) {
    deg = +deg;
    inner = +inner;
    outer = +outer;
    pos = +pos;
    deg = deg * __rad;
    return {
        x1: Math.cos(deg) * outer + pos,
        y1: Math.sin(deg) * outer + pos,
        x2: Math.cos(deg) * inner + pos,
        y2: Math.sin(deg) * inner + pos
    }
}
function _get_coords_single(deg, inner, pos) {
    deg = +deg;
    inner = +inner;
    pos = +pos;
    deg = deg * __rad;
    return {
        x: Math.cos(deg) * inner + pos,
        y: Math.sin(deg) * inner + pos
    }
}
var format_path = TK.FORMAT("M %f,%f " +
                            "A %f,%f 0 %d,%d %f,%f " +
                            "L %f,%f " +
                            "A %f,%f 0 %d,%d %f,%f z");
var format_translate = TK.FORMAT("translate(%f, %f)");
var format_translate_rotate = TK.FORMAT("translate(%f %f) rotate(%f %f %f)");
var format_rotate = TK.FORMAT("rotate(%f %f %f)");

function draw_dots() {
    // depends on dots, dot, min, max, size
    var _dots = this._dots;
    var O = this.options;
    var dots = O.dots;
    var dot = O.dot;
    var angle = O.angle;
    TK.empty(_dots);
    for (var i = 0; i < dots.length; i++) {
        var m = dots[i];
        var r = TK.make_svg("rect", {"class": "toolkit-dot"});
        
        var length = m.length === void(0)
                   ? dot.length : m.length;
        var width  = m.width === void(0)
                   ? dot.width : m.width;
        var margin = m.margin === void(0)
                   ? dot.margin : m.margin;
        var pos    = Math.min(O.max, Math.max(O.min, m.pos));
        // TODO: consider adding them all at once
        _dots.appendChild(r);
        if (m["class"]) TK.add_class(r, m["class"]);
        if (m.color) r.style.fill = m.color;
                 
        r.setAttribute("x", O.size - length - margin);
        r.setAttribute("y", O.size / 2 - width / 2);
        
        r.setAttribute("width",  length);
        r.setAttribute("height", width);
        
        r.setAttribute("transform", "rotate("
            + (this.val2coef(this.snap(pos)) * angle) + " "
            + (O.size / 2) + " " + (this.options.size / 2) + ")");
    }
    /**
     * Is fired when dots are (re)drawn.
     * @event TK.Circular#dotsdrawn
     */
    this.fire_event("dotsdrawn");
}
function draw_markers() {
    // depends on size, markers, marker, min, max
    var I = this.invalid;
    var O = this.options;
    var markers = O.markers;
    var marker = O.marker;
    TK.empty(this._markers);
    
    var stroke  = this._get_stroke();
    var outer   = O.size / 2;
    var angle = O.angle;
    
    for (var i = 0; i < markers.length; i++) {
        var m       = markers[i];
        var thick   = m.thickness === void(0)
                    ? marker.thickness : m.thickness;
        var margin  = m.margin === void(0)
                    ? marker.margin : m.margin;
        var inner   = outer - thick;
        var outer_p = outer - margin - stroke / 2;
        var inner_p = inner - margin - stroke / 2;
        var from, to;
        
        if (m.from === void(0))
            from = O.min;
        else
            from = Math.min(O.max, Math.max(O.min, m.from));
        
        if (m.to === void(0))
            to = O.max;
        else
            to = Math.min(O.max, Math.max(O.min, m.to));
        
        var s = TK.make_svg("path", {"class": "toolkit-marker"});
        this._markers.appendChild(s);
        
        if (m["class"]) TK.add_class(s, m["class"]);
        if (m.color) s.style.fill = m.color;
        if (!m.nosnap) {
            from = this.snap(from);
            to = this.snap(to);
        }
        from = this.val2coef(from) * angle;
        to = this.val2coef(to) * angle;
        
        draw_slice.call(this, from, to, inner_p, outer_p, outer, s);
    }
    /**
     * Is fired when markers are (re)drawn.
     * @event TK.Circular#markersdrawn
     */
    this.fire_event("markersdrawn");
}
function draw_labels() {
    // depends on size, labels, label, min, max, start
    var _labels = this._labels;
    var O = this.options;
    var labels = O.labels;
    TK.empty(this._labels);

    if (!labels.length) return;
    
    var outer   = O.size / 2;
    var a = new Array(labels.length);
    var i;

    var l, p, positions = new Array(labels.length);

    for (i = 0; i < labels.length; i++) {
        l = labels[i];
        p = TK.make_svg("text", {"class": "toolkit-label",
                                 style: "dominant-baseline: central;"
        });
        
        if (l["class"]) TK.add_class(p, l["class"]);
        if (l.color) p.style.fill = l.color;

                 
        if (l.label !== void(0))
            p.textContent = l.label;
        else
            p.textContent = O.label.format(l.pos);

        p.setAttribute("text-anchor", "middle");
                 
        _labels.appendChild(p);
        a[i] = p;
    }
    /* FORCE_RELAYOUT */

    TK.S.add(function() {
        var i, p;
        for (i = 0; i < labels.length; i++) {
            l = labels[i];
            p = a[i];

            var margin  = l.margin !== void(0) ? l.margin : O.label.margin;
            var align   = (l.align !== void(0) ? l.align : O.label.align) === "inner";
            var pos     = Math.min(O.max, Math.max(O.min, l.pos));
            var bb      = p.getBBox();
            var angle   = (this.val2coef(this.snap(pos)) * O.angle + O.start) % 360;
            var outer_p = outer - margin;
            var coords  = _get_coords_single(angle, outer_p, outer);
            
            var mx = ((coords.x - outer) / outer_p) * (bb.width + bb.height / 2.5) / (align ? -2 : 2);
            var my = ((coords.y - outer) / outer_p) * bb.height / (align ? -2 : 2);

            positions[i] = format_translate(coords.x + mx, coords.y + my);
        }

        TK.S.add(function() {
            for (i = 0; i < labels.length; i++) {
                p = a[i];
                p.setAttribute("transform", positions[i]);
            }
            /**
             * Is fired when labels are (re)drawn.
             * @event TK.Circular#labelsdrawn
             */
            this.fire_event("labelsdrawn");
        }.bind(this), 1);
    }.bind(this));
}
function draw_slice(a_from, a_to, r_inner, r_outer, pos, slice) {
    a_from = +a_from;
    a_to = +a_to;
    r_inner = +r_inner;
    r_outer = +r_outer;
    pos = +pos;
    // ensure from !== to
    if(a_from % 360 === a_to % 360) a_from += 0.001;
    // ensure from and to in bounds
    while (a_from < 0) a_from += 360;
    while (a_to < 0) a_to += 360;
    if (a_from > 360) a_from %= 360;
    if (a_to > 360) a_to   %= 360;
    // get drawing direction (sweep = clock-wise)
    if (this.options.reverse && a_to <= a_from
    || !this.options.reverse && a_to > a_from)
        var sweep = 1;
    else
        var sweep = 0;
    // get large flag
    if (Math.abs(a_from - a_to) >= 180)
        var large = 1;
    else
        var large = 0;
    // draw this slice
    var from = _get_coords(a_from, r_inner, r_outer, pos);
    var to = _get_coords(a_to, r_inner, r_outer, pos);

    var path = format_path(from.x1, from.y1,
                           r_outer, r_outer, large, sweep, to.x1, to.y1,
                           to.x2, to.y2,
                           r_inner, r_inner, large, !sweep, from.x2, from.y2);
    slice.setAttribute("d", path);
}
TK.Circular = TK.class({
    /**
     * TK.Circular is a SVG group element containing two paths for displaying
     * numerical values in a circular manner. TK.Circular is able to draw labels,
     * dots and markers and can show a hand. TK.Circular e.g. is implemented by
     * {@link TK.Clock} to draw hours, minutes and seconds.
     * 
     * @class TK.Circular
     * 
     * @param {Object} [options={ }] - An object containing initial options.
     * 
     * @property {Number} [options.value=0] - Sets the value on the hand and on the
     *   ring at the same time.
     * @property {Number} [options.value_hand=0] - Sets the value on the hand.
     * @property {Number} [options.value_ring=0] - Sets the value on the ring.
     * @property {Number} [options.size=100] - The diameter of the circle. This
     *   is the base value for all following layout-related parameters. Keeping
     *   it set to 100 offers percentual lenghts. Set the final size of the widget
     *   via CSS.
     * @property {Number} [options.thickness=3] - The thickness of the circle.
     * @property {Number} [options.margin=0] - The margin between base and value circles.
     * @property {Boolean} [options.show_hand=true] - Draw the hand.
     * @property {Object} [options.hand] - Dimensions of the hand.
     * @property {Number} [options.hand.width=2] - Width of the hand.
     * @property {Number} [options.hand.length=30] - Length of the hand.
     * @property {Number} [options.hand.margin=10] - Margin of the hand.
     * @property {Number} [options.start=135] - The starting point in degrees.
     * @property {Number} [options.angle=270] - The maximum degree of the rotation when
     *   <code>options.value === options.max</code>.
     * @property {Number|Boolean} [options.base=false] - If a base value is set in degrees,
     *   circular starts drawing elements from this position.
     * @property {Boolean} [options.show_base=true] - Draw the base ring.
     * @property {Boolean} [options.show_value=true] - Draw the value ring.
     * @property {Number} [options.x=0] - Horizontal displacement of the circle.
     * @property {Number} [options.y=0] - Vertical displacement of the circle.
     * @property {Boolean} [options.show_dots=true] - Show/hide all dots.
     * @property {Object} [options.dot] - This option acts as default values for the individual dots
     *   specified in <code>options.dots</code>.
     * @property {Number} [options.dot.width=2] - Width of the dots.
     * @property {Number} [options.dot.length=2] - Length of the dots.
     * @property {Number} [options.dot.margin=5] - Margin of the dots.
     * @property {Array<Object>} [options.dots=[]] - An array of objects describing where dots should be placed
     *   along the circle. Members are position <code>pos</code> in the value range and optionally
     *   <code>color</code> and <code>class</code> and any of the properties of <code>options.dot</code>.
     * @property {Boolean} [options.show_markers=true] - Show/hide all markers.
     * @property {Object} [options.marker] - This option acts as default values of the individual markers
     *   specified in <code>options.markers</code>.
     * @property {Number} [options.marker.thickness=3] - Thickness of the marker.
     * @property {Number} [options.marker.margin=3] - Margin of the marker.
     * @property {Array<Object>} [options.markers=[]] - An array containing objects which describe where markers
     *   are to be places. Members are the position as <code>from</code> and <code>to</code> and optionally
     *   <code>color</code>, <code>class</code> and any of the properties of <code>options.marker</code>.
     * @property {Boolean} [options.show_labels=true] - Show/hide all labels.
     * @property {Object} [options.label] - This option acts as default values for the individual labels
     *   specified in <code>options.labels</code>.
     * @property {Integer} [options.label.margin=8] - Distance of the label from the circle of diameter
     *   <code>options.size</code>.
     * @property {String} [options.label.align="outer"] - This option controls if labels are positioned
     *   inside or outside of the circle with radius <code>options.size/2 - margin</code>.
     * @property {Function} [options.label.format] - Optional formatting function for the label.
     *   Receives the label value as first argument.
     * @property {Array<Object>} [options.labels=[]] - An array containing objects which describe labels
     *   to be displayed. Either a value or an object whose members are the position <code>pos</code>
     *   insie the value range and optionally <code>color</code>, <code>class</code> and any of the
     *   properties of <code>options.label</code>.
     * 
     * @extends TK.Widget
     * 
     * @mixes TK.Warning
     * 
     * @mixes TK.Ranged
     */
    _class: "Circular",
    Extends: TK.Widget,
    Implements: [TK.Warning, TK.Ranged],
    _options: Object.assign(Object.create(TK.Widget.prototype._options), TK.Ranged.prototype._options, {
        value: "number",
        value_hand: "number",
        value_ring: "number",
        size: "number",
        thickness: "number",
        margin: "number",
        hand: "object",
        start: "number",
        angle: "number",
        base: "number|boolean",
        show_base: "boolean",
        show_value: "boolean",
        show_hand: "boolean",
        x: "number",
        y: "number",
        dot: "object",
        dots: "array",
        marker: "object",
        markers: "array",
        label: "object",
        labels: "array"
    }),
    static_events: {
        set_value: function(value) {
            this.set("value_hand", value);
            this.set("value_ring", value);
        },
        initialized: function() {
            // calculate the stroke here once. this happens before
            // the initial redraw
            TK.S.after_frame(this._get_stroke.bind(this));
            this.set("value", this.options.value);
        },
    },
    options: {
        value:      0,
        value_hand: 0,
        value_ring: 0,
        size:       100,
        thickness:  3,
        margin:     0,
        hand:       {width: 2, length: 30, margin: 10},
        start:      135,
        angle:      270,
        base:       false,
        show_base:  true,
        show_value: true,
        show_hand:  true,
        x:          0,
        y:          0,
        dot:        {width: 2, length: 2, margin: 5},
        dots:       [],
        marker:     {thickness: 3, margin: 0},
        markers:    [],
        label:      {margin: 8, align: "inner", format: function(val){return val;}},
        labels:     []
    },
    
    initialize: function (options) {
        TK.Widget.prototype.initialize.call(this, options);
        var E;
        
        /**
         * @member {SVGImage} TK.Circular#element - The main SVG element.
         *      Has class <code>toolkit-circular</code> 
         */
        this.element = E = TK.make_svg("g", {"class": "toolkit-circular"});
        this.widgetize(E, true, true, true);
        
        /**
         * @member {SVGPath} TK.Circular#_base - The base of the ring.
         *      Has class <code>toolkit-base</code> 
         */
        this._base = TK.make_svg("path", {"class": "toolkit-base"});
        E.appendChild(this._base);
        
        /**
         * @member {SVGPath} TK.Circular#_value - The ring showing the value.
         *      Has class <code>toolkit-value</code> 
         */
        this._value = TK.make_svg("path", {"class": "toolkit-value"});
        E.appendChild(this._value);
        
        /**
         * @member {SVGRect} TK.Circular#_hand - The hand of the knob.
         *      Has class <code>toolkit-hand</code> 
         */
        this._hand = TK.make_svg("rect", {"class": "toolkit-hand"});
        E.appendChild(this._hand);

        if (this.options.labels)
            this.set("labels", this.options.labels);
    },

    resize: function () {
        this.invalid.labels = true;
        this.trigger_draw();
        TK.Widget.prototype.resize.call(this);
    },
    
    redraw: function () {
        TK.Widget.prototype.redraw.call(this);
        var I = this.invalid;
        var O = this.options;
        var E = this.element;
        var outer   = O.size / 2;
        var tmp;

        if (I.validate("x", "y") || I.start || I.size) {
            E.setAttribute("transform", format_translate_rotate(O.x, O.y, O.start, outer, outer));
            this._labels.setAttribute("transform", format_rotate(-O.start, outer, outer));
        }

        if (O.show_labels && (I.validate("show_labels", "labels", "label") ||
                              I.size || I.min || I.max || I.start)) {
            draw_labels.call(this);
        }

        if (O.show_dots && (I.validate("show_dots", "dots", "dot") ||
                            I.min || I.max || I.size)) {
            draw_dots.call(this);
        }

        if (O.show_markers && (I.validate("show_markers", "markers", "marker") ||
                               I.size || I.min || I.max)) {
            draw_markers.call(this);
        }

        var stroke  = this._get_stroke();
        var inner   = outer - O.thickness;
        var outer_p = outer - stroke / 2 - O.margin;
        var inner_p = inner - stroke / 2 - O.margin;
        
        if (I.show_value || I.value_ring) {
            I.show_value = false;
            if (O.show_value) {
                draw_slice.call(this, this.val2coef(this.snap(O.base)) * O.angle, this.val2coef(this.snap(O.value_ring)) * O.angle, inner_p, outer_p, outer,
                                this._value);
            } else {
                this._value.removeAttribute("d");
            }
        }

        if (I.show_base) {
            I.show_base = false;
            if (O.show_base) {
                draw_slice.call(this, 0, O.angle, inner_p, outer_p, outer, this._base);
            } else {
                /* TODO: make this a child element */
                this._base.removeAttribute("d");
            }
        }
        if (I.show_hand) {
            I.show_hand = false;
            if (O.show_hand) {
                this._hand.style.display = "block";
            } else {
                this._hand.style.display = "none";
            }
        }
        if (I.validate("size", "value_hand", "hand", "min", "max", "start")) {
            tmp = this._hand;
            tmp.setAttribute("x", O.size - O.hand.length - O.hand.margin);
            tmp.setAttribute("y", (O.size - O.hand.width) / 2.0);
            tmp.setAttribute("width", O.hand.length);
            tmp.setAttribute("height",O.hand.width);
            tmp.setAttribute("transform",
                             format_rotate(this.val2coef(this.snap(O.value_hand)) * O.angle, O.size / 2, O.size / 2));
        }
    },
    
    destroy: function () {
        this._dots.remove();
        this._markers.remove();
        this._base.remove();
        this._value.remove();
        TK.Widget.prototype.destroy.call(this);
    },
    _get_stroke: function () {
        if (this.hasOwnProperty("_stroke")) return this._stroke;
        var strokeb = parseInt(TK.get_style(this._base, "stroke-width")) || 0;
        var strokev = parseInt(TK.get_style(this._value, "stroke-width")) || 0;
        this._stroke = Math.max(strokeb, strokev);
        return this._stroke;
    },

    /**
     * Adds a label.
     *
     * @method TK.Circular#add_label
     * @param {Object|Number} label - The label. Please refer to the initial options
     *   to learn more about possible values.
     * @returns {Object} label - The interpreted object to build the label from.
     */
    add_label: function(label) {
        var O = this.options;

        if (!O.labels) {
            O.labels = [];
        }

        label = interpret_label(label);
        
        if (label) {
            O.labels.push(label);
            this.invalid.labels = true;
            this.trigger_draw();
            return label;
        }
    },

    /**
     * Removes a label.
     *
     * @method TK.Circular#remove_label
     * @param {Object} label - The label object as returned from `add_label`.
     * @returns {Object} label - The removed label options.
     */
    remove_label: function(label) {
        var O = this.options;

        if (!O.labels) return;

        var i = O.labels.indexOf(label);

        if (i === -1) return;

        O.labels.splice(i);
        this.invalid.labels = true;
        this.trigger_draw();
    },
    
    // GETTERS & SETTERS
    set: function (key, value) {
        switch (key) {
        case "dot":
        case "marker":
        case "label":
            value = Object.assign(this.options[key], value);
            break;
        case "base":
            if (value === false) value = this.options.min;
            break;
        case "value":
            if (value > this.options.max || value < this.options.min)
                this.warning(this.element);
            value = this.snap(value);
            break;
        case "labels":
            if (value)
                for (var i = 0; i < value.length; i++) {
                    value[i] = interpret_label(value[i]);
                }
            break;
        }

        return TK.Widget.prototype.set.call(this, key, value);
    }
});
/**
 * @member {SVGGroup} TK.Circular#_markers - A SVG group containing all markers.
 *      Has class <code>toolkit-markers</code> 
 */
TK.ChildElement(TK.Circular, "markers", {
    //option: "markers",
    //display_check: function(v) { return !!v.length; },
    show: true,
    create: function() {
        return TK.make_svg("g", {"class": "toolkit-markers"});
    },
});
/** 
 * @member {SVGGroup} TK.Circular#_dots - A SVG group containing all dots.
 *      Has class <code>toolkit-dots</code> 
 */
TK.ChildElement(TK.Circular, "dots", {
    //option: "dots",
    //display_check: function(v) { return !!v.length; },
    show: true,
    create: function() {
        return TK.make_svg("g", {"class": "toolkit-dots"});
    },
});
/**
 * @member {SVGGroup} TK.Circular#_labels - A SVG group containing all labels.
 *      Has class <code>toolkit-labels</code> 
 */
TK.ChildElement(TK.Circular, "labels", {
    //option: "labels",
    //display_check: function(v) { return !!v.length; },
    show: true,
    create: function() {
        return TK.make_svg("g", {"class": "toolkit-labels"});
    },
});
})(this, this.TK);