      1 /*
      2     Copyright 2008-2025
      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