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