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