      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