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