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 
531     if (withTicks) {
532         ti.dump = false;
533         p3.subs.ticks = ti;
534         p3.inherits.push(ti);
535     }
536 
537     p3.getParents = function () {
538         return [
539             this.point1.coords.usrCoords.slice(1),
540             this.point2.coords.usrCoords.slice(1),
541             [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax]
542         ];
543     };
544 
545     p3.baseline.on("up", function (evt) {
546         var pos, c;
547 
548         if (Type.evaluate(p3.visProp.moveonup) && !Type.evaluate(p3.visProp.fixed)) {
549             pos = l1.board.getMousePosition(evt, 0);
550             c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board);
551             p3.moveTo([c.usrCoords[1], c.usrCoords[2]]);
552             p3.triggerEventHandlers(['drag'], [evt]);
553         }
554     });
555 
556     // Save the visibility attribute of the sub-elements
557     // for (el in p3.subs) {
558     //     p3.subs[el].status = {
559     //         visible: p3.subs[el].visProp.visible
560     //     };
561     // }
562 
563     // p3.hideElement = function () {
564     //     var el;
565     //     GeometryElement.prototype.hideElement.call(this);
566     //
567     //     for (el in this.subs) {
568     //         // this.subs[el].status.visible = this.subs[el].visProp.visible;
569     //         this.subs[el].hideElement();
570     //     }
571     // };
572 
573     //         p3.showElement = function () {
574     //             var el;
575     //             GeometryElement.prototype.showElement.call(this);
576     //
577     //             for (el in this.subs) {
578     // //                if (this.subs[el].status.visible) {
579     //                 this.subs[el].showElement();
580     // //                }
581     //             }
582     //         };
583 
584     // This is necessary to show baseline, highline and ticks
585     // when opening the board in case the visible attributes are set
586     // to 'inherit'.
587     p3.prepareUpdate().update();
588     if (!board.isSuspendedUpdate) {
589         p3.updateVisibility().updateRenderer();
590         p3.baseline.updateVisibility().updateRenderer();
591         p3.highline.updateVisibility().updateRenderer();
592         if (withTicks) {
593             p3.ticks.updateVisibility().updateRenderer();
594         }
595     }
596 
597     return p3;
598 };
599 
600 JXG.registerElement("slider", JXG.createSlider);
601 
602 // export default {
603 //     createSlider: JXG.createSlider
604 // };
605