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