/* * 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 * options.value === options.max. * @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 options.dots. * @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} [options.dots=[]] - An array of objects describing where dots should be placed * along the circle. Members are position pos in the value range and optionally * color and class and any of the properties of options.dot. * @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 options.markers. * @property {Number} [options.marker.thickness=3] - Thickness of the marker. * @property {Number} [options.marker.margin=3] - Margin of the marker. * @property {Array} [options.markers=[]] - An array containing objects which describe where markers * are to be places. Members are the position as from and to and optionally * color, class and any of the properties of options.marker. * @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 options.labels. * @property {Integer} [options.label.margin=8] - Distance of the label from the circle of diameter * options.size. * @property {String} [options.label.align="outer"] - This option controls if labels are positioned * inside or outside of the circle with radius options.size/2 - margin. * @property {Function} [options.label.format] - Optional formatting function for the label. * Receives the label value as first argument. * @property {Array} [options.labels=[]] - An array containing objects which describe labels * to be displayed. Either a value or an object whose members are the position pos * insie the value range and optionally color, class and any of the * properties of options.label. * * @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 toolkit-circular */ 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 toolkit-base */ 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 toolkit-value */ 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 toolkit-hand */ 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 toolkit-markers */ 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 toolkit-dots */ 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 toolkit-labels */ 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);