1 /*
  2     Copyright 2008-2021
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/>
 29     and <http://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 
 33 /*global JXG: true, define: true*/
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /* depends:
 37  jxg
 38  math/math
 39  base/constants
 40  base/point
 41  utils/type
 42   elements:
 43    point
 44    group
 45    segment
 46    ticks
 47    glider
 48    text
 49  */
 50 
 51 /**
 52  * @fileoverview The geometry object slider is defined in this file. Slider stores all
 53  * style and functional properties that are required to draw and use a slider on
 54  * a board.
 55  */
 56 
 57 define([
 58     'jxg', 'math/math', 'base/constants', 'base/coords', 'utils/type', 'base/point', 'base/group',
 59     'base/element', 'base/line', 'base/ticks', 'base/text'
 60 ], function (JXG, Mat, Const, Coords, Type, Point, Group, GeometryElement, Line, Ticks, Text) {
 61 
 62     "use strict";
 63 
 64     /**
 65      * @class A slider can be used to choose values from a given range of numbers.
 66      * @pseudo
 67      * @description
 68      * @name Slider
 69      * @augments Glider
 70      * @constructor
 71      * @type JXG.Point
 72      * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
 73      * @param {Array_Array_Array} start,end,data The first two arrays give the start and the end where the slider is drawn
 74      * on the board. The third array gives the start and the end of the range the slider operates as the first resp. the
 75      * third component of the array. The second component of the third array gives its start value.
 76      * @example
 77      * // Create a slider with values between 1 and 10, initial position is 5.
 78      * var s = board.create('slider', [[1, 2], [3, 2], [1, 5, 10]]);
 79      * </pre><div class="jxgbox" id="JXGcfb51cde-2603-4f18-9cc4-1afb452b374d" style="width: 200px; height: 200px;"></div>
 80      * <script type="text/javascript">
 81      *   (function () {
 82      *     var board = JXG.JSXGraph.initBoard('JXGcfb51cde-2603-4f18-9cc4-1afb452b374d', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false});
 83      *     var s = board.create('slider', [[1, 2], [3, 2], [1, 5, 10]]);
 84      *   })();
 85      * </script><pre>
 86      * @example
 87      * // Create a slider taking integer values between 1 and 50. Initial value is 50.
 88      * var s = board.create('slider', [[1, 3], [3, 1], [0, 10, 50]], {snapWidth: 1, ticks: { drawLabels: true }});
 89      * </pre><div class="jxgbox" id="JXGe17128e6-a25d-462a-9074-49460b0d66f4" style="width: 200px; height: 200px;"></div>
 90      * <script type="text/javascript">
 91      *   (function () {
 92      *     var board = JXG.JSXGraph.initBoard('JXGe17128e6-a25d-462a-9074-49460b0d66f4', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false});
 93      *     var s = board.create('slider', [[1, 3], [3, 1], [1, 10, 50]], {snapWidth: 1, ticks: { drawLabels: true }});
 94      *   })();
 95      * </script><pre>
 96      * @example
 97      *     // Draggable slider
 98      *     var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], {
 99      *         visible: true,
100      *         snapWidth: 2,
101      *         point1: {fixed: false},
102      *         point2: {fixed: false},
103      *         baseline: {fixed: false, needsRegularUpdate: true}
104      *     });
105      *
106      * </pre><div id="JXGbfc67817-2827-44a1-bc22-40bf312e76f8" class="jxgbox" style="width: 300px; height: 300px;"></div>
107      * <script type="text/javascript">
108      *     (function() {
109      *         var board = JXG.JSXGraph.initBoard('JXGbfc67817-2827-44a1-bc22-40bf312e76f8',
110      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
111      *         var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], {
112      *             visible: true,
113      *             snapWidth: 2,
114      *             point1: {fixed: false},
115      *             point2: {fixed: false},
116      *             baseline: {fixed: false, needsRegularUpdate: true}
117      *         });
118      *
119      *     })();
120      *
121      * </script><pre>
122      *
123      * @example
124      *     // Set the slider by clicking on the base line: attribute 'moveOnUp'
125      *     var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], {
126      *         snapWidth: 2,
127      *         moveOnUp: true // default value
128      *     });
129      *
130      * </pre><div id="JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc" class="jxgbox" style="width: 300px; height: 300px;"></div>
131      * <script type="text/javascript">
132      *     (function() {
133      *         var board = JXG.JSXGraph.initBoard('JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc',
134      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
135      *         var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], {
136      *             snapWidth: 2,
137      *             moveOnUp: true // default value
138      *         });
139      *
140      *     })();
141      *
142      * </script><pre>
143      *
144      * @example
145      * // Set colors
146      * var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], {
147      * 
148      *   baseline: { strokeColor: 'blue'},
149      *   highline: { strokeColor: 'red'},
150      *   fillColor: 'yellow',
151      *   label: {fontSize: 24, strokeColor: 'orange'},
152      *   name: 'xyz', // Not shown, if suffixLabel is set
153      *   suffixLabel: 'x = ',
154      *   postLabel: ' u'
155      * 
156      * });
157      * 
158      * </pre><div id="JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401" class="jxgbox" style="width: 300px; height: 300px;"></div>
159      * <script type="text/javascript">
160      *     (function() {
161      *         var board = JXG.JSXGraph.initBoard('JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401',
162      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
163      *     var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], {
164      *     
165      *       baseline: { strokeColor: 'blue'},
166      *       highline: { strokeColor: 'red'},
167      *       fillColor: 'yellow',
168      *       label: {fontSize: 24, strokeColor: 'orange'},
169      *       name: 'xyz', // Not shown, if suffixLabel is set
170      *       suffixLabel: 'x = ',
171      *       postLabel: ' u'
172      *     
173      *     });
174      * 
175      *     })();
176      * 
177      * </script><pre>
178      * 
179  */
180     JXG.createSlider = function (board, parents, attributes) {
181         var pos0, pos1, smin, start, smax, sdiff,
182             p1, p2, l1, ticks, ti, startx, starty, p3, l2, t,
183             withText, withTicks, snapWidth, sw, s, attr, precision;
184 
185         attr = Type.copyAttributes(attributes, board.options, 'slider');
186         withTicks = attr.withticks;
187         withText = attr.withlabel;
188         snapWidth = attr.snapwidth;
189         precision = attr.precision;
190 
191         // start point
192         attr = Type.copyAttributes(attributes, board.options, 'slider', 'point1');
193         p1 = board.create('point', parents[0],  attr);
194 
195         // end point
196         attr = Type.copyAttributes(attributes, board.options, 'slider', 'point2');
197         p2 = board.create('point', parents[1],  attr);
198         //g = board.create('group', [p1, p2]);
199 
200         // Base line
201         attr = Type.copyAttributes(attributes, board.options, 'slider', 'baseline');
202         l1 = board.create('segment', [p1, p2], attr);
203 
204         // This is required for a correct projection of the glider onto the segment below
205         l1.updateStdform();
206 
207         pos0 = p1.coords.usrCoords.slice(1);
208         pos1 = p2.coords.usrCoords.slice(1);
209         smin = parents[2][0];
210         start = parents[2][1];
211         smax = parents[2][2];
212         sdiff = smax - smin;
213 
214         sw = Type.evaluate(snapWidth);
215         s = (sw == -1) ? start : Math.round(start / sw) * sw;
216         startx = pos0[0] + (pos1[0] - pos0[0]) * (s - smin) / (smax - smin);
217         starty = pos0[1] + (pos1[1] - pos0[1]) * (s - smin) / (smax - smin);
218 
219         // glider point
220         attr = Type.copyAttributes(attributes, board.options, 'slider');
221         // overwrite this in any case; the sliders label is a special text element, not the gliders label.
222         // this will be set back to true after the text was created (and only if withlabel was true initially).
223         attr.withLabel = false;
224         // gliders set snapwidth=-1 by default (i.e. deactivate them)
225         p3 = board.create('glider', [startx, starty, l1], attr);
226         p3.setAttribute({snapwidth: snapWidth});
227 
228         // Segment from start point to glider point: highline
229         attr = Type.copyAttributes(attributes, board.options, 'slider', 'highline');
230         l2 = board.create('segment', [p1, p3],  attr);
231 
232         /**
233          * Returns the current slider value.
234          * @memberOf Slider.prototype
235          * @name Value
236          * @function
237          * @returns {Number}
238          */
239         p3.Value = function () {
240             var sdiff = this._smax - this._smin,
241                 ev_sw = Type.evaluate(this.visProp.snapwidth);
242 
243             return ev_sw === -1 ?
244                         this.position * sdiff + this._smin :
245                         Math.round((this.position * sdiff + this._smin) / ev_sw) * ev_sw;
246         };
247 
248         p3.methodMap = Type.deepCopy(p3.methodMap, {
249             Value: 'Value',
250             setValue: 'setValue',
251             smax: '_smax',
252             smin: '_smin',
253             setMax: 'setMax',
254             setMin: 'setMin'
255         });
256 
257         /**
258          * End value of the slider range.
259          * @memberOf Slider.prototype
260          * @name _smax
261          * @type Number
262          */
263         p3._smax = smax;
264 
265         /**
266          * Start value of the slider range.
267          * @memberOf Slider.prototype
268          * @name _smin
269          * @type Number
270          */
271         p3._smin = smin;
272 
273         /**
274          * Sets the maximum value of the slider.
275          * @memberOf Slider.prototype
276          * @name setMax
277          * @param {Number} val New maximum value
278          * @returns {Object} this object
279          */
280         p3.setMax = function(val) {
281             this._smax = val;
282             return this;
283         };
284 
285         /**
286          * Sets the value of the slider. This call must be followed
287          * by a board update call.
288          * @memberOf Slider.prototype
289          * @name setValue
290          * @param {Number} val New value
291          * @returns {Object} this object
292          */
293         p3.setValue = function(val) {
294             var sdiff = this._smax - this._smin;
295 
296             if (Math.abs(sdiff) > Mat.eps) {
297                 this.position = (val - this._smin) / sdiff;
298             } else {
299                 this.position = 0.0; //this._smin;
300             }
301             this.position = Math.max(0.0, Math.min(1.0, this.position));
302             return this;
303         };
304 
305         /**
306          * Sets the minimum value of the slider.
307          * @memberOf Slider.prototype
308          * @name setMin
309          * @param {Number} val New minimum value
310          * @returns {Object} this object
311          */
312         p3.setMin = function(val) {
313             this._smin = val;
314             return this;
315         };
316 
317         if (withText) {
318             attr = Type.copyAttributes(attributes, board.options, 'slider', 'label');
319             t = board.create('text', [
320                 function () {
321                     return (p2.X() - p1.X()) * 0.05 + p2.X();
322                 },
323                 function () {
324                     return (p2.Y() - p1.Y()) * 0.05 + p2.Y();
325                 },
326                 function () {
327                     var n,
328                         sl = Type.evaluate(p3.visProp.suffixlabel),
329                         ul = Type.evaluate(p3.visProp.unitlabel),
330                         pl = Type.evaluate(p3.visProp.postlabel);
331 
332                     if (sl !== null) {
333                         n = sl;
334                     } else if (p3.name && p3.name !== '') {
335                         n = p3.name + ' = ';
336                     } else {
337                         n = '';
338                     }
339 
340                     n += Type.toFixed(p3.Value(), precision);
341 
342                     if (ul !== null) {
343                         n += ul;
344                     }
345                     if (pl !== null) {
346                         n += pl;
347                     }
348 
349                     return n;
350                 }
351             ], attr);
352 
353             /**
354              * The text element to the right of the slider, indicating its current value.
355              * @memberOf Slider.prototype
356              * @name label
357              * @type JXG.Text
358              */
359             p3.label = t;
360 
361             // reset the withlabel attribute
362             p3.visProp.withlabel = true;
363             p3.hasLabel = true;
364         }
365 
366         /**
367          * Start point of the base line.
368          * @memberOf Slider.prototype
369          * @name point1
370          * @type JXG.Point
371          */
372         p3.point1 = p1;
373 
374         /**
375          * End point of the base line.
376          * @memberOf Slider.prototype
377          * @name point2
378          * @type JXG.Point
379          */
380         p3.point2 = p2;
381 
382         /**
383          * The baseline the glider is bound to.
384          * @memberOf Slider.prototype
385          * @name baseline
386          * @type JXG.Line
387          */
388         p3.baseline = l1;
389 
390         /**
391          * A line on top of the baseline, indicating the slider's progress.
392          * @memberOf Slider.prototype
393          * @name highline
394          * @type JXG.Line
395          */
396         p3.highline = l2;
397 
398         if (withTicks) {
399             // Function to generate correct label texts
400 
401             attr = Type.copyAttributes(attributes, board.options, 'slider', 'ticks');
402             if (!Type.exists(attr.generatelabeltext)) {
403                 attr.generateLabelText = function(tick, zero, value) {
404                     var labelText,
405                         dFull = p3.point1.Dist(p3.point2),
406                         smin = p3._smin, smax = p3._smax,
407                         val = this.getDistanceFromZero(zero, tick) * (smax - smin) / dFull  + smin;
408 
409                         if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { // Point is zero
410                             labelText = '0';
411                         } else {
412                             labelText = this.formatLabelText(val);
413                         }
414                         return labelText;
415                 };
416             }
417             ticks  = 2;
418             ti = board.create('ticks', [
419                 p3.baseline,
420                 p3.point1.Dist(p1) / ticks,
421 
422                 function (tick) {
423                     var dFull = p3.point1.Dist(p3.point2),
424                         d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick);
425 
426                     if (dFull < Mat.eps) {
427                         return 0;
428                     }
429 
430                     return d / dFull * sdiff + smin;
431                 }
432             ], attr);
433 
434             /**
435              * Ticks give a rough indication about the slider's current value.
436              * @memberOf Slider.prototype
437              * @name ticks
438              * @type JXG.Ticks
439              */
440             p3.ticks = ti;
441         }
442 
443         // override the point's remove method to ensure the removal of all elements
444         p3.remove = function () {
445             if (withText) {
446                 board.removeObject(t);
447             }
448 
449             board.removeObject(l2);
450             board.removeObject(l1);
451             board.removeObject(p2);
452             board.removeObject(p1);
453 
454 
455             Point.Point.prototype.remove.call(p3);
456         };
457 
458         p1.dump = false;
459         p2.dump = false;
460         l1.dump = false;
461         l2.dump = false;
462 
463         p3.elType = 'slider';
464         p3.parents = parents;
465         p3.subs = {
466             point1: p1,
467             point2: p2,
468             baseLine: l1,
469             highLine: l2
470         };
471         p3.inherits.push(p1, p2, l1, l2);
472 
473         if (withTicks) {
474             ti.dump = false;
475             p3.subs.ticks = ti;
476             p3.inherits.push(ti);
477         }
478 
479         p3.baseline.on('up', function(evt) {
480             var pos, c;
481 
482             if (Type.evaluate(p3.visProp.moveonup) && !Type.evaluate(p3.visProp.fixed) ) {
483                 pos = l1.board.getMousePosition(evt, 0);
484                 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board);
485                 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]);
486             }
487         });
488 
489         // Save the visibility attribute of the sub-elements
490         // for (el in p3.subs) {
491         //     p3.subs[el].status = {
492         //         visible: p3.subs[el].visProp.visible
493         //     };
494         // }
495 
496         // p3.hideElement = function () {
497         //     var el;
498         //     GeometryElement.prototype.hideElement.call(this);
499         //
500         //     for (el in this.subs) {
501         //         // this.subs[el].status.visible = this.subs[el].visProp.visible;
502         //         this.subs[el].hideElement();
503         //     }
504         // };
505 
506 //         p3.showElement = function () {
507 //             var el;
508 //             GeometryElement.prototype.showElement.call(this);
509 //
510 //             for (el in this.subs) {
511 // //                if (this.subs[el].status.visible) {
512 //                 this.subs[el].showElement();
513 // //                }
514 //             }
515 //         };
516 
517 
518 
519         // This is necessary to show baseline, highline and ticks
520         // when opening the board in case the visible attributes are set
521         // to 'inherit'.
522         p3.prepareUpdate().update();
523         if (!board.isSuspendedUpdate) {
524             p3.updateVisibility().updateRenderer();
525             p3.baseline.updateVisibility().updateRenderer();
526             p3.highline.updateVisibility().updateRenderer();
527             if (withTicks) {
528                 p3.ticks.updateVisibility().updateRenderer();
529             }
530         }
531 
532         return p3;
533     };
534 
535     JXG.registerElement('slider', JXG.createSlider);
536 
537     return {
538         createSlider: JXG.createSlider
539     };
540 });
541