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,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 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     p1 = board.create("point", parents[0], attr.point1);
208 
209     // End point
210     p2 = board.create("point", parents[1], attr.point2);
211     //g = board.create('group', [p1, p2]);
212 
213     // Base line
214     l1 = board.create("segment", [p1, p2], attr.baseline);
215 
216     // This is required for a correct projection of the glider onto the segment below
217     l1.updateStdform();
218 
219     pos0 = p1.coords.usrCoords.slice(1);
220     pos1 = p2.coords.usrCoords.slice(1);
221     smin = parents[2][0];
222     start = parents[2][1];
223     smax = parents[2][2];
224     diff = smax - smin;
225 
226     sw = Type.evaluate(snapWidth);
227     s = sw === -1 ? start : Math.round(start / sw) * sw;
228     startx = pos0[0] + ((pos1[0] - pos0[0]) * (s - smin)) / (smax - smin);
229     starty = pos0[1] + ((pos1[1] - pos0[1]) * (s - smin)) / (smax - smin);
230 
231     // glider point
232     // attr = Type.copyAttributes(attributes, board.options, "slider");
233     // overwrite this in any case; the sliders label is a special text element, not the gliders label.
234     // this will be set back to true after the text was created (and only if withlabel was true initially).
235     attr.withlabel = false;
236     // gliders set snapwidth=-1 by default (i.e. deactivate them)
237     p3 = board.create("glider", [startx, starty, l1], attr);
238     p3.setAttribute({ snapwidth: snapWidth, snapvalues: snapValues, snapvaluedistance: snapValueDistance });
239 
240     // Segment from start point to glider point: highline
241     // attr = Type.copyAttributes(attributes, board.options, "slider", "highline");
242     l2 = board.create("segment", [p1, p3], attr.highline);
243 
244     /**
245      * Returns the current slider value.
246      * @memberOf Slider.prototype
247      * @name Value
248      * @function
249      * @returns {Number}
250      */
251     p3.Value = function () {
252         var d = this._smax - this._smin,
253             ev_sw = this.evalVisProp('snapwidth');
254 
255         return ev_sw === -1
256             ? this.position * d + this._smin
257             : Math.round((this.position * d + this._smin) / ev_sw) * ev_sw;
258     };
259 
260     p3.methodMap = Type.deepCopy(p3.methodMap, {
261         Value: "Value",
262         setValue: "setValue",
263         smax: "_smax",
264         // Max: "_smax",
265         smin: "_smin",
266         // Min: "_smin",
267         setMax: "setMax",
268         setMin: "setMin",
269         point1: "point1",
270         point2: "point2",
271         baseline: "baseline",
272         highline: "highline",
273         ticks: "ticks",
274         label: "label"
275     });
276 
277     /**
278      * End value of the slider range.
279      * @memberOf Slider.prototype
280      * @name _smax
281      * @type Number
282      */
283     p3._smax = smax;
284 
285     /**
286      * Start value of the slider range.
287      * @memberOf Slider.prototype
288      * @name _smin
289      * @type Number
290      */
291     p3._smin = smin;
292 
293     /**
294      * Sets the maximum value of the slider.
295      * @memberOf Slider.prototype
296      * @function
297      * @name setMax
298      * @param {Number} val New maximum value
299      * @returns {Object} this object
300      */
301     p3.setMax = function (val) {
302         this._smax = val;
303         return this;
304     };
305 
306     /**
307      * Sets the value of the slider. This call must be followed
308      * by a board update call.
309      * @memberOf Slider.prototype
310      * @name setValue
311      * @function
312      * @param {Number} val New value
313      * @returns {Object} this object
314      */
315     p3.setValue = function (val) {
316         var d = this._smax - this._smin;
317 
318         if (Math.abs(d) > Mat.eps) {
319             this.position = (val - this._smin) / d;
320         } else {
321             this.position = 0.0; //this._smin;
322         }
323         this.position = Math.max(0.0, Math.min(1.0, this.position));
324         return this;
325     };
326 
327     /**
328      * Sets the minimum value of the slider.
329      * @memberOf Slider.prototype
330      * @name setMin
331      * @function
332      * @param {Number} val New minimum value
333      * @returns {Object} this object
334      */
335     p3.setMin = function (val) {
336         this._smin = val;
337         return this;
338     };
339 
340     if (withText) {
341         // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label');
342         t = board.create('text', [
343             function () {
344                 return (p2.X() - p1.X()) * 0.05 + p2.X();
345             },
346             function () {
347                 return (p2.Y() - p1.Y()) * 0.05 + p2.Y();
348             },
349             function () {
350                 var n,
351                     d = p3.evalVisProp('digits'),
352                     sl = p3.evalVisProp('suffixlabel'),
353                     ul = p3.evalVisProp('unitlabel'),
354                     pl = p3.evalVisProp('postlabel');
355 
356                 if (d === 2 && p3.evalVisProp('precision') !== 2) {
357                     // Backwards compatibility
358                     d = p3.evalVisProp('precision');
359                 }
360 
361                 if (sl !== null) {
362                     n = sl;
363                 } else if (p3.name && p3.name !== "") {
364                     n = p3.name + " = ";
365                 } else {
366                     n = "";
367                 }
368 
369                 if (p3.useLocale()) {
370                     n += p3.formatNumberLocale(p3.Value(), d);
371                 } else {
372                     n += Type.toFixed(p3.Value(), d);
373                 }
374 
375                 if (ul !== null) {
376                     n += ul;
377                 }
378                 if (pl !== null) {
379                     n += pl;
380                 }
381 
382                 return n;
383             }
384         ],
385             attr.label
386         );
387 
388         /**
389          * The text element to the right of the slider, indicating its current value.
390          * @memberOf Slider.prototype
391          * @name label
392          * @type JXG.Text
393          */
394         p3.label = t;
395 
396         // reset the withlabel attribute
397         p3.visProp.withlabel = true;
398         p3.hasLabel = true;
399     }
400 
401     /**
402      * Start point of the base line.
403      * @memberOf Slider.prototype
404      * @name point1
405      * @type JXG.Point
406      */
407     p3.point1 = p1;
408 
409     /**
410      * End point of the base line.
411      * @memberOf Slider.prototype
412      * @name point2
413      * @type JXG.Point
414      */
415     p3.point2 = p2;
416 
417     /**
418      * The baseline the glider is bound to.
419      * @memberOf Slider.prototype
420      * @name baseline
421      * @type JXG.Line
422      */
423     p3.baseline = l1;
424 
425     /**
426      * A line on top of the baseline, indicating the slider's progress.
427      * @memberOf Slider.prototype
428      * @name highline
429      * @type JXG.Line
430      */
431     p3.highline = l2;
432 
433     if (withTicks) {
434         // Function to generate correct label texts
435 
436         // attr = Type.copyAttributes(attributes, board.options, "slider", "ticks");
437         if (!Type.exists(attr.generatelabeltext)) {
438             attr.ticks.generateLabelText = function (tick, zero, value) {
439                 var labelText,
440                     dFull = p3.point1.Dist(p3.point2),
441                     smin = p3._smin,
442                     smax = p3._smax,
443                     val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin;
444 
445                 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) {
446                     // Point is zero
447                     labelText = "0";
448                 } else {
449                     labelText = this.formatLabelText(val);
450                 }
451                 return labelText;
452             };
453         }
454         ticks = 2;
455         ti = board.create(
456             "ticks",
457             [
458                 p3.baseline,
459                 p3.point1.Dist(p1) / ticks,
460 
461                 function (tick) {
462                     var dFull = p3.point1.Dist(p3.point2),
463                         d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick);
464 
465                     if (dFull < Mat.eps) {
466                         return 0;
467                     }
468 
469                     return (d / dFull) * diff + smin;
470                 }
471             ],
472             attr.ticks
473         );
474 
475         /**
476          * Ticks give a rough indication about the slider's current value.
477          * @memberOf Slider.prototype
478          * @name ticks
479          * @type JXG.Ticks
480          */
481         p3.ticks = ti;
482     }
483 
484     // override the point's remove method to ensure the removal of all elements
485     p3.remove = function () {
486         if (withText) {
487             board.removeObject(t);
488         }
489 
490         board.removeObject(l2);
491         board.removeObject(l1);
492         board.removeObject(p2);
493         board.removeObject(p1);
494 
495         Point.prototype.remove.call(p3);
496     };
497 
498     p1.dump = false;
499     p2.dump = false;
500     l1.dump = false;
501     l2.dump = false;
502     if (withText) {
503         t.dump = false;
504     }
505 
506     // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER
507     p3.elType = "slider";
508     p3.parents = parents;
509     p3.subs = {
510         point1: p1,
511         point2: p2,
512         baseLine: l1,
513         highLine: l2
514     };
515     p3.inherits.push(p1, p2, l1, l2);
516     // Remove inherits to avoid circular inherits.
517     l1.inherits = [];
518     l2.inherits = [];
519 
520     if (withTicks) {
521         ti.dump = false;
522         p3.subs.ticks = ti;
523         p3.inherits.push(ti);
524     }
525 
526     p3.getParents = function () {
527         return [
528             this.point1.coords.usrCoords.slice(1),
529             this.point2.coords.usrCoords.slice(1),
530             [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax]
531         ];
532     };
533 
534     p3.baseline.on("up", function (evt) {
535         var pos, c;
536 
537         if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) {
538             pos = l1.board.getMousePosition(evt, 0);
539             c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board);
540             p3.moveTo([c.usrCoords[1], c.usrCoords[2]]);
541             p3.triggerEventHandlers(['drag'], [evt]);
542         }
543     });
544 
545     // This is necessary to show baseline, highline and ticks
546     // when opening the board in case the visible attributes are set
547     // to 'inherit'.
548     p3.prepareUpdate().update();
549     if (!board.isSuspendedUpdate) {
550         p3.updateVisibility().updateRenderer();
551         p3.baseline.updateVisibility().updateRenderer();
552         p3.highline.updateVisibility().updateRenderer();
553         if (withTicks) {
554             p3.ticks.updateVisibility().updateRenderer();
555         }
556     }
557 
558     return p3;
559 };
560 
561 JXG.registerElement("slider", JXG.createSlider);
562 
563 // export default {
564 //     createSlider: JXG.createSlider
565 // };
566