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 /*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 * @description 52 * @name Slider 53 * @augments Glider 54 * @constructor 55 * @type JXG.Point 56 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 57 * @param {Array_Array_Array} start,end,data The first two arrays give the start and the end where the slider is drawn 58 * on the board. The third array gives the start and the end of the range the slider operates as the first resp. the 59 * third component of the array. The second component of the third array gives its start value. 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 */ 164 JXG.createSlider = function (board, parents, attributes) { 165 var pos0, 166 pos1, 167 smin, 168 start, 169 smax, 170 sdiff, 171 p1, 172 p2, 173 l1, 174 ticks, 175 ti, 176 startx, 177 starty, 178 p3, 179 l2, 180 t, 181 withText, 182 withTicks, 183 snapWidth, 184 sw, 185 s, 186 attr, 187 digits; 188 189 attr = Type.copyAttributes(attributes, board.options, "slider"); 190 withTicks = attr.withticks; 191 withText = attr.withlabel; 192 snapWidth = attr.snapwidth; 193 194 // start point 195 attr = Type.copyAttributes(attributes, board.options, "slider", "point1"); 196 p1 = board.create("point", parents[0], attr); 197 198 // end point 199 attr = Type.copyAttributes(attributes, board.options, "slider", "point2"); 200 p2 = board.create("point", parents[1], attr); 201 //g = board.create('group', [p1, p2]); 202 203 // Base line 204 attr = Type.copyAttributes(attributes, board.options, "slider", "baseline"); 205 l1 = board.create("segment", [p1, p2], attr); 206 207 // This is required for a correct projection of the glider onto the segment below 208 l1.updateStdform(); 209 210 pos0 = p1.coords.usrCoords.slice(1); 211 pos1 = p2.coords.usrCoords.slice(1); 212 smin = parents[2][0]; 213 start = parents[2][1]; 214 smax = parents[2][2]; 215 sdiff = smax - smin; 216 217 sw = Type.evaluate(snapWidth); 218 s = sw === -1 ? start : Math.round(start / sw) * sw; 219 startx = pos0[0] + ((pos1[0] - pos0[0]) * (s - smin)) / (smax - smin); 220 starty = pos0[1] + ((pos1[1] - pos0[1]) * (s - smin)) / (smax - smin); 221 222 // glider point 223 attr = Type.copyAttributes(attributes, board.options, "slider"); 224 // overwrite this in any case; the sliders label is a special text element, not the gliders label. 225 // this will be set back to true after the text was created (and only if withlabel was true initially). 226 attr.withLabel = false; 227 // gliders set snapwidth=-1 by default (i.e. deactivate them) 228 p3 = board.create("glider", [startx, starty, l1], attr); 229 p3.setAttribute({ snapwidth: snapWidth }); 230 231 // Segment from start point to glider point: highline 232 attr = Type.copyAttributes(attributes, board.options, "slider", "highline"); 233 l2 = board.create("segment", [p1, p3], attr); 234 235 /** 236 * Returns the current slider value. 237 * @memberOf Slider.prototype 238 * @name Value 239 * @function 240 * @returns {Number} 241 */ 242 p3.Value = function () { 243 var sdiff = this._smax - this._smin, 244 ev_sw = Type.evaluate(this.visProp.snapwidth); 245 246 return ev_sw === -1 247 ? this.position * sdiff + this._smin 248 : Math.round((this.position * sdiff + this._smin) / ev_sw) * ev_sw; 249 }; 250 251 p3.methodMap = Type.deepCopy(p3.methodMap, { 252 Value: "Value", 253 setValue: "setValue", 254 smax: "_smax", 255 smin: "_smin", 256 setMax: "setMax", 257 setMin: "setMin" 258 }); 259 260 /** 261 * End value of the slider range. 262 * @memberOf Slider.prototype 263 * @name _smax 264 * @type Number 265 */ 266 p3._smax = smax; 267 268 /** 269 * Start value of the slider range. 270 * @memberOf Slider.prototype 271 * @name _smin 272 * @type Number 273 */ 274 p3._smin = smin; 275 276 /** 277 * Sets the maximum value of the slider. 278 * @memberOf Slider.prototype 279 * @name setMax 280 * @param {Number} val New maximum value 281 * @returns {Object} this object 282 */ 283 p3.setMax = function (val) { 284 this._smax = val; 285 return this; 286 }; 287 288 /** 289 * Sets the value of the slider. This call must be followed 290 * by a board update call. 291 * @memberOf Slider.prototype 292 * @name setValue 293 * @param {Number} val New value 294 * @returns {Object} this object 295 */ 296 p3.setValue = function (val) { 297 var sdiff = this._smax - this._smin; 298 299 if (Math.abs(sdiff) > Mat.eps) { 300 this.position = (val - this._smin) / sdiff; 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 * @param {Number} val New minimum value 313 * @returns {Object} this object 314 */ 315 p3.setMin = function (val) { 316 this._smin = val; 317 return this; 318 }; 319 320 if (withText) { 321 attr = Type.copyAttributes(attributes, board.options, "slider", "label"); 322 t = board.create( 323 "text", 324 [ 325 function () { 326 return (p2.X() - p1.X()) * 0.05 + p2.X(); 327 }, 328 function () { 329 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 330 }, 331 function () { 332 var n, 333 d = Type.evaluate(p3.visProp.digits), 334 sl = Type.evaluate(p3.visProp.suffixlabel), 335 ul = Type.evaluate(p3.visProp.unitlabel), 336 pl = Type.evaluate(p3.visProp.postlabel); 337 338 if (d === 2 && Type.evaluate(p3.visProp.precision) !== 2) { 339 // Backwards compatibility 340 d = Type.evaluate(p3.visProp.precision); 341 } 342 343 if (sl !== null) { 344 n = sl; 345 } else if (p3.name && p3.name !== "") { 346 n = p3.name + " = "; 347 } else { 348 n = ""; 349 } 350 351 n += Type.toFixed(p3.Value(), d); 352 353 if (ul !== null) { 354 n += ul; 355 } 356 if (pl !== null) { 357 n += pl; 358 } 359 360 return n; 361 } 362 ], 363 attr 364 ); 365 366 /** 367 * The text element to the right of the slider, indicating its current value. 368 * @memberOf Slider.prototype 369 * @name label 370 * @type JXG.Text 371 */ 372 p3.label = t; 373 374 // reset the withlabel attribute 375 p3.visProp.withlabel = true; 376 p3.hasLabel = true; 377 } 378 379 /** 380 * Start point of the base line. 381 * @memberOf Slider.prototype 382 * @name point1 383 * @type JXG.Point 384 */ 385 p3.point1 = p1; 386 387 /** 388 * End point of the base line. 389 * @memberOf Slider.prototype 390 * @name point2 391 * @type JXG.Point 392 */ 393 p3.point2 = p2; 394 395 /** 396 * The baseline the glider is bound to. 397 * @memberOf Slider.prototype 398 * @name baseline 399 * @type JXG.Line 400 */ 401 p3.baseline = l1; 402 403 /** 404 * A line on top of the baseline, indicating the slider's progress. 405 * @memberOf Slider.prototype 406 * @name highline 407 * @type JXG.Line 408 */ 409 p3.highline = l2; 410 411 if (withTicks) { 412 // Function to generate correct label texts 413 414 attr = Type.copyAttributes(attributes, board.options, "slider", "ticks"); 415 if (!Type.exists(attr.generatelabeltext)) { 416 attr.generateLabelText = function (tick, zero, value) { 417 var labelText, 418 dFull = p3.point1.Dist(p3.point2), 419 smin = p3._smin, 420 smax = p3._smax, 421 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 422 423 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 424 // Point is zero 425 labelText = "0"; 426 } else { 427 labelText = this.formatLabelText(val); 428 } 429 return labelText; 430 }; 431 } 432 ticks = 2; 433 ti = board.create( 434 "ticks", 435 [ 436 p3.baseline, 437 p3.point1.Dist(p1) / ticks, 438 439 function (tick) { 440 var dFull = p3.point1.Dist(p3.point2), 441 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 442 443 if (dFull < Mat.eps) { 444 return 0; 445 } 446 447 return (d / dFull) * sdiff + smin; 448 } 449 ], 450 attr 451 ); 452 453 /** 454 * Ticks give a rough indication about the slider's current value. 455 * @memberOf Slider.prototype 456 * @name ticks 457 * @type JXG.Ticks 458 */ 459 p3.ticks = ti; 460 } 461 462 // override the point's remove method to ensure the removal of all elements 463 p3.remove = function () { 464 if (withText) { 465 board.removeObject(t); 466 } 467 468 board.removeObject(l2); 469 board.removeObject(l1); 470 board.removeObject(p2); 471 board.removeObject(p1); 472 473 Point.Point.prototype.remove.call(p3); 474 }; 475 476 p1.dump = false; 477 p2.dump = false; 478 l1.dump = false; 479 l2.dump = false; 480 if (withText) { 481 t.dump = false; 482 } 483 484 p3.elType = "slider"; 485 p3.parents = parents; 486 p3.subs = { 487 point1: p1, 488 point2: p2, 489 baseLine: l1, 490 highLine: l2 491 }; 492 p3.inherits.push(p1, p2, l1, l2); 493 494 if (withTicks) { 495 ti.dump = false; 496 p3.subs.ticks = ti; 497 p3.inherits.push(ti); 498 } 499 500 p3.getParents = function () { 501 return [ 502 this.point1.coords.usrCoords.slice(1), 503 this.point2.coords.usrCoords.slice(1), 504 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 505 ]; 506 }; 507 508 p3.baseline.on("up", function (evt) { 509 var pos, c; 510 511 if (Type.evaluate(p3.visProp.moveonup) && !Type.evaluate(p3.visProp.fixed)) { 512 pos = l1.board.getMousePosition(evt, 0); 513 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 514 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 515 p3.triggerEventHandlers(['drag'], [evt]); 516 } 517 }); 518 519 // Save the visibility attribute of the sub-elements 520 // for (el in p3.subs) { 521 // p3.subs[el].status = { 522 // visible: p3.subs[el].visProp.visible 523 // }; 524 // } 525 526 // p3.hideElement = function () { 527 // var el; 528 // GeometryElement.prototype.hideElement.call(this); 529 // 530 // for (el in this.subs) { 531 // // this.subs[el].status.visible = this.subs[el].visProp.visible; 532 // this.subs[el].hideElement(); 533 // } 534 // }; 535 536 // p3.showElement = function () { 537 // var el; 538 // GeometryElement.prototype.showElement.call(this); 539 // 540 // for (el in this.subs) { 541 // // if (this.subs[el].status.visible) { 542 // this.subs[el].showElement(); 543 // // } 544 // } 545 // }; 546 547 // This is necessary to show baseline, highline and ticks 548 // when opening the board in case the visible attributes are set 549 // to 'inherit'. 550 p3.prepareUpdate().update(); 551 if (!board.isSuspendedUpdate) { 552 p3.updateVisibility().updateRenderer(); 553 p3.baseline.updateVisibility().updateRenderer(); 554 p3.highline.updateVisibility().updateRenderer(); 555 if (withTicks) { 556 p3.ticks.updateVisibility().updateRenderer(); 557 } 558 } 559 560 return p3; 561 }; 562 563 JXG.registerElement("slider", JXG.createSlider); 564 565 export default { 566 createSlider: JXG.createSlider 567 }; 568