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@4/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     /**
299      * End value of the slider range.
300      * @memberOf Slider.prototype
301      * @name _smax
302      * @type Number
303      */
304     p3._smax = smax;
305 
306     /**
307      * Start value of the slider range.
308      * @memberOf Slider.prototype
309      * @name _smin
310      * @type Number
311      */
312     p3._smin = smin;
313 
314     /**
315      * Sets the maximum value of the slider.
316      * @memberOf Slider.prototype
317      * @function
318      * @name setMax
319      * @param {Number} val New maximum value
320      * @returns {Object} this object
321      */
322     p3.setMax = function (val) {
323         this._smax = val;
324         return this;
325     };
326 
327     /**
328      * Sets the value of the slider. This call must be followed
329      * by a board update call.
330      * @memberOf Slider.prototype
331      * @name setValue
332      * @function
333      * @param {Number} val New value
334      * @returns {Object} this object
335      */
336     p3.setValue = function (val) {
337         var d = this._smax - this._smin;
338 
339         if (Math.abs(d) > Mat.eps) {
340             this.position = (val - this._smin) / d;
341         } else {
342             this.position = 0.0; //this._smin;
343         }
344         this.position = Math.max(0.0, Math.min(1.0, this.position));
345         return this;
346     };
347 
348     /**
349      * Sets the minimum value of the slider.
350      * @memberOf Slider.prototype
351      * @name setMin
352      * @function
353      * @param {Number} val New minimum value
354      * @returns {Object} this object
355      */
356     p3.setMin = function (val) {
357         this._smin = val;
358         return this;
359     };
360 
361     if (withText) {
362         // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label');
363         t = board.create('text', [
364             function () {
365                 return (p2.X() - p1.X()) * 0.05 + p2.X();
366             },
367             function () {
368                 return (p2.Y() - p1.Y()) * 0.05 + p2.Y();
369             },
370             function () {
371                 var n,
372                     d = p3.evalVisProp('digits'),
373                     sl = p3.evalVisProp('suffixlabel'),
374                     ul = p3.evalVisProp('unitlabel'),
375                     pl = p3.evalVisProp('postlabel');
376 
377                 if (d === 2 && p3.evalVisProp('precision') !== 2) {
378                     // Backwards compatibility
379                     d = p3.evalVisProp('precision');
380                 }
381 
382                 if (sl !== null) {
383                     n = sl;
384                 } else if (p3.name && p3.name !== "") {
385                     n = p3.name + " = ";
386                 } else {
387                     n = "";
388                 }
389 
390                 if (p3.useLocale()) {
391                     n += p3.formatNumberLocale(p3.Value(), d);
392                 } else {
393                     n += Type.toFixed(p3.Value(), d);
394                 }
395 
396                 if (ul !== null) {
397                     n += ul;
398                 }
399                 if (pl !== null) {
400                     n += pl;
401                 }
402 
403                 return n;
404             }
405         ],
406             attr.label
407         );
408 
409         /**
410          * The text element to the right of the slider, indicating its current value.
411          * @memberOf Slider.prototype
412          * @name label
413          * @type JXG.Text
414          */
415         p3.label = t;
416 
417         // reset the withlabel attribute
418         p3.visProp.withlabel = true;
419         p3.hasLabel = true;
420         p3.inherits.push(t);
421         t.addParents(p3);
422         p3.addChild(t);
423     }
424 
425     /**
426      * Start point of the base line.
427      * @memberOf Slider.prototype
428      * @name point1
429      * @type JXG.Point
430      */
431     p3.point1 = p1;
432 
433     /**
434      * End point of the base line.
435      * @memberOf Slider.prototype
436      * @name point2
437      * @type JXG.Point
438      */
439     p3.point2 = p2;
440 
441     /**
442      * The baseline the glider is bound to.
443      * @memberOf Slider.prototype
444      * @name baseline
445      * @type JXG.Line
446      */
447     p3.baseline = l1;
448 
449     /**
450      * A line on top of the baseline, indicating the slider's progress.
451      * @memberOf Slider.prototype
452      * @name highline
453      * @type JXG.Line
454      */
455     p3.highline = l2;
456 
457     if (withTicks) {
458         // Function to generate correct label texts
459 
460         // attr = Type.copyAttributes(attributes, board.options, "slider", 'ticks');
461         if (!Type.exists(attr.generatelabeltext)) {
462             attr.ticks.generateLabelText = function (tick, zero, value) {
463                 var labelText,
464                     dFull = p3.point1.Dist(p3.point2),
465                     smin = p3._smin,
466                     smax = p3._smax,
467                     val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin;
468 
469                 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) {
470                     // Point is zero
471                     labelText = '0';
472                 } else {
473                     labelText = this.formatLabelText(val);
474                 }
475                 return labelText;
476             };
477         }
478         ticks = 2;
479         ti = board.create(
480             "ticks",
481             [
482                 p3.baseline,
483                 p3.point1.Dist(p1) / ticks,
484 
485                 function (tick) {
486                     var dFull = p3.point1.Dist(p3.point2),
487                         d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick);
488 
489                     if (dFull < Mat.eps) {
490                         return 0;
491                     }
492 
493                     return (d / dFull) * diff + smin;
494                 }
495             ],
496             attr.ticks
497         );
498 
499         /**
500          * Ticks give a rough indication about the slider's current value.
501          * @memberOf Slider.prototype
502          * @name ticks
503          * @type JXG.Ticks
504          */
505         p3.ticks = ti;
506     }
507 
508     // override the point's remove method to ensure the removal of all elements
509     p3.remove = function () {
510         if (withText) {
511             board.removeObject(t);
512         }
513 
514         board.removeObject(l2);
515         board.removeObject(l1);
516         board.removeObject(p2);
517         board.removeObject(p1);
518 
519         Point.prototype.remove.call(p3);
520     };
521 
522     p1.dump = false;
523     p2.dump = false;
524     l1.dump = false;
525     l2.dump = false;
526     if (withText) {
527         t.dump = false;
528     }
529 
530     // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER
531     p3.elType = 'slider';
532     p3.parents = parents;
533     p3.subs = {
534         point1: p1,
535         point2: p2,
536         baseLine: l1,
537         highLine: l2
538     };
539     p3.inherits.push(p1, p2, l1, l2);
540     // Remove inherits to avoid circular inherits.
541     l1.inherits = [];
542     l2.inherits = [];
543 
544     if (withTicks) {
545         ti.dump = false;
546         p3.subs.ticks = ti;
547         p3.inherits.push(ti);
548     }
549 
550     p3.getParents = function () {
551         return [
552             this.point1.coords.usrCoords.slice(1),
553             this.point2.coords.usrCoords.slice(1),
554             [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax]
555         ];
556     };
557 
558     p3.baseline.on("up", function (evt) {
559         var pos, c;
560 
561         if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) {
562             pos = l1.board.getMousePosition(evt, 0);
563             c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board);
564             p3.moveTo([c.usrCoords[1], c.usrCoords[2]]);
565             p3.triggerEventHandlers(['drag'], [evt]);
566         }
567     });
568 
569     // This is necessary to show baseline, highline and ticks
570     // when opening the board in case the visible attributes are set
571     // to 'inherit'.
572     p3.prepareUpdate().update();
573     if (!board.isSuspendedUpdate) {
574         p3.updateVisibility().updateRenderer();
575         p3.baseline.prepareUpdate().updateVisibility().updateRenderer(); // prepareUpdate needed because needsRegularUpdate==false
576         p3.highline.updateVisibility().updateRenderer();
577         if (withTicks) {
578             p3.prepareUpdate().ticks.updateVisibility().updateRenderer();
579         }
580     }
581 
582     Type.extendInstanceMethodMap(p3, {
583         Value: "Value",
584         setValue: "setValue",
585         smax: "_smax",
586         // Max: "_smax",
587         smin: "_smin",
588         // Min: "_smin",
589         setMax: "setMax",
590         setMin: "setMin",
591         point1: "point1",
592         point2: "point2",
593         baseline: "baseline",
594         highline: "highline",
595         ticks: "ticks",
596         label: "label"
597     });
598 
599     return p3;
600 };
601 
602 JXG.registerElement("slider", JXG.createSlider);
603 
604 // export default {
605 //     createSlider: JXG.createSlider
606 // };
607