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