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