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