1 /*
  2     Copyright 2008-2025
  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     }
438 
439     /**
440      * Start point of the base line.
441      * @memberOf Slider.prototype
442      * @name point1
443      * @type JXG.Point
444      */
445     p3.point1 = p1;
446 
447     /**
448      * End point of the base line.
449      * @memberOf Slider.prototype
450      * @name point2
451      * @type JXG.Point
452      */
453     p3.point2 = p2;
454 
455     /**
456      * The baseline the glider is bound to.
457      * @memberOf Slider.prototype
458      * @name baseline
459      * @type JXG.Line
460      */
461     p3.baseline = l1;
462 
463     /**
464      * A line on top of the baseline, indicating the slider's progress.
465      * @memberOf Slider.prototype
466      * @name highline
467      * @type JXG.Line
468      */
469     p3.highline = l2;
470 
471     if (withTicks) {
472         // Function to generate correct label texts
473 
474         // attr = Type.copyAttributes(attributes, board.options, "slider", "ticks");
475         if (!Type.exists(attr.generatelabeltext)) {
476             attr.ticks.generateLabelText = function (tick, zero, value) {
477                 var labelText,
478                     dFull = p3.point1.Dist(p3.point2),
479                     smin = p3._smin,
480                     smax = p3._smax,
481                     val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin;
482 
483                 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) {
484                     // Point is zero
485                     labelText = "0";
486                 } else {
487                     labelText = this.formatLabelText(val);
488                 }
489                 return labelText;
490             };
491         }
492         ticks = 2;
493         ti = board.create(
494             "ticks",
495             [
496                 p3.baseline,
497                 p3.point1.Dist(p1) / ticks,
498 
499                 function (tick) {
500                     var dFull = p3.point1.Dist(p3.point2),
501                         d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick);
502 
503                     if (dFull < Mat.eps) {
504                         return 0;
505                     }
506 
507                     return (d / dFull) * diff + smin;
508                 }
509             ],
510             attr.ticks
511         );
512 
513         /**
514          * Ticks give a rough indication about the slider's current value.
515          * @memberOf Slider.prototype
516          * @name ticks
517          * @type JXG.Ticks
518          */
519         p3.ticks = ti;
520     }
521 
522     // override the point's remove method to ensure the removal of all elements
523     p3.remove = function () {
524         if (withText) {
525             board.removeObject(t);
526         }
527 
528         board.removeObject(l2);
529         board.removeObject(l1);
530         board.removeObject(p2);
531         board.removeObject(p1);
532 
533         Point.prototype.remove.call(p3);
534     };
535 
536     p1.dump = false;
537     p2.dump = false;
538     l1.dump = false;
539     l2.dump = false;
540     if (withText) {
541         t.dump = false;
542     }
543 
544     // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER
545     p3.elType = "slider";
546     p3.parents = parents;
547     p3.subs = {
548         point1: p1,
549         point2: p2,
550         baseLine: l1,
551         highLine: l2
552     };
553     p3.inherits.push(p1, p2, l1, l2);
554     // Remove inherits to avoid circular inherits.
555     l1.inherits = [];
556     l2.inherits = [];
557 
558     if (withTicks) {
559         ti.dump = false;
560         p3.subs.ticks = ti;
561         p3.inherits.push(ti);
562     }
563 
564     p3.getParents = function () {
565         return [
566             this.point1.coords.usrCoords.slice(1),
567             this.point2.coords.usrCoords.slice(1),
568             [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax]
569         ];
570     };
571 
572     p3.baseline.on("up", function (evt) {
573         var pos, c;
574 
575         if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) {
576             pos = l1.board.getMousePosition(evt, 0);
577             c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board);
578             p3.moveTo([c.usrCoords[1], c.usrCoords[2]]);
579             p3.triggerEventHandlers(['drag'], [evt]);
580         }
581     });
582 
583     // This is necessary to show baseline, highline and ticks
584     // when opening the board in case the visible attributes are set
585     // to 'inherit'.
586     p3.prepareUpdate().update();
587     if (!board.isSuspendedUpdate) {
588         p3.updateVisibility().updateRenderer();
589         p3.baseline.updateVisibility().updateRenderer();
590         p3.highline.updateVisibility().updateRenderer();
591         if (withTicks) {
592             p3.ticks.updateVisibility().updateRenderer();
593         }
594     }
595 
596     return p3;
597 };
598 
599 JXG.registerElement("slider", JXG.createSlider);
600 
601 // export default {
602 //     createSlider: JXG.createSlider
603 // };
604