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