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