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