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