1 /* 2 Copyright 2008-2024 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,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 * 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 // attr = Type.copyAttributes(attributes, board.options, "slider", "point1"); 208 p1 = board.create("point", parents[0], attr.point1); 209 210 // end point 211 // attr = Type.copyAttributes(attributes, board.options, "slider", "point2"); 212 p2 = board.create("point", parents[1], attr.point2); 213 //g = board.create('group', [p1, p2]); 214 215 // Base line 216 // attr = Type.copyAttributes(attributes, board.options, "slider", "baseline"); 217 l1 = board.create("segment", [p1, p2], attr.baseline); 218 219 // This is required for a correct projection of the glider onto the segment below 220 l1.updateStdform(); 221 222 pos0 = p1.coords.usrCoords.slice(1); 223 pos1 = p2.coords.usrCoords.slice(1); 224 smin = parents[2][0]; 225 start = parents[2][1]; 226 smax = parents[2][2]; 227 diff = smax - smin; 228 229 sw = Type.evaluate(snapWidth); 230 s = sw === -1 ? start : 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 = Type.evaluate(this.visProp.snapwidth); 257 // snapValues, i, v; 258 259 // snapValues = Type.evaluate(this.visProp.snapvalues); 260 // if (Type.isArray(snapValues)) { 261 // for (i = 0; i < snapValues.length; i++) { 262 // v = (snapValues[i] - this._smin) / (this._smax - this._smin); 263 // if (this.position === v) { 264 // return snapValues[i]; 265 // } 266 // } 267 // } 268 269 return ev_sw === -1 270 ? this.position * d + this._smin 271 : Math.round((this.position * d + this._smin) / ev_sw) * ev_sw; 272 }; 273 274 p3.methodMap = Type.deepCopy(p3.methodMap, { 275 Value: "Value", 276 setValue: "setValue", 277 smax: "_smax", 278 // Max: "_smax", 279 smin: "_smin", 280 // Min: "_smin", 281 setMax: "setMax", 282 setMin: "setMin", 283 point1: "point1", 284 point2: "point2", 285 baseline: "baseline", 286 highline: "highline", 287 ticks: "ticks", 288 label: "label" 289 }); 290 291 /** 292 * End value of the slider range. 293 * @memberOf Slider.prototype 294 * @name _smax 295 * @type Number 296 */ 297 p3._smax = smax; 298 299 /** 300 * Start value of the slider range. 301 * @memberOf Slider.prototype 302 * @name _smin 303 * @type Number 304 */ 305 p3._smin = smin; 306 307 /** 308 * Sets the maximum value of the slider. 309 * @memberOf Slider.prototype 310 * @function 311 * @name setMax 312 * @param {Number} val New maximum value 313 * @returns {Object} this object 314 */ 315 p3.setMax = function (val) { 316 this._smax = val; 317 return this; 318 }; 319 320 /** 321 * Sets the value of the slider. This call must be followed 322 * by a board update call. 323 * @memberOf Slider.prototype 324 * @name setValue 325 * @function 326 * @param {Number} val New value 327 * @returns {Object} this object 328 */ 329 p3.setValue = function (val) { 330 var d = this._smax - this._smin; 331 332 if (Math.abs(d) > Mat.eps) { 333 this.position = (val - this._smin) / d; 334 } else { 335 this.position = 0.0; //this._smin; 336 } 337 this.position = Math.max(0.0, Math.min(1.0, this.position)); 338 return this; 339 }; 340 341 /** 342 * Sets the minimum value of the slider. 343 * @memberOf Slider.prototype 344 * @name setMin 345 * @function 346 * @param {Number} val New minimum value 347 * @returns {Object} this object 348 */ 349 p3.setMin = function (val) { 350 this._smin = val; 351 return this; 352 }; 353 354 if (withText) { 355 // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label'); 356 t = board.create('text', [ 357 function () { 358 return (p2.X() - p1.X()) * 0.05 + p2.X(); 359 }, 360 function () { 361 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 362 }, 363 function () { 364 var n, 365 d = Type.evaluate(p3.visProp.digits), 366 sl = Type.evaluate(p3.visProp.suffixlabel), 367 ul = Type.evaluate(p3.visProp.unitlabel), 368 pl = Type.evaluate(p3.visProp.postlabel); 369 370 if (d === 2 && Type.evaluate(p3.visProp.precision) !== 2) { 371 // Backwards compatibility 372 d = Type.evaluate(p3.visProp.precision); 373 } 374 375 if (sl !== null) { 376 n = sl; 377 } else if (p3.name && p3.name !== "") { 378 n = p3.name + " = "; 379 } else { 380 n = ""; 381 } 382 383 if (p3.useLocale()) { 384 n += p3.formatNumberLocale(p3.Value(), d); 385 } else { 386 n += Type.toFixed(p3.Value(), d); 387 } 388 389 if (ul !== null) { 390 n += ul; 391 } 392 if (pl !== null) { 393 n += pl; 394 } 395 396 return n; 397 } 398 ], 399 attr.label 400 ); 401 402 /** 403 * The text element to the right of the slider, indicating its current value. 404 * @memberOf Slider.prototype 405 * @name label 406 * @type JXG.Text 407 */ 408 p3.label = t; 409 410 // reset the withlabel attribute 411 p3.visProp.withlabel = true; 412 p3.hasLabel = true; 413 } 414 415 /** 416 * Start point of the base line. 417 * @memberOf Slider.prototype 418 * @name point1 419 * @type JXG.Point 420 */ 421 p3.point1 = p1; 422 423 /** 424 * End point of the base line. 425 * @memberOf Slider.prototype 426 * @name point2 427 * @type JXG.Point 428 */ 429 p3.point2 = p2; 430 431 /** 432 * The baseline the glider is bound to. 433 * @memberOf Slider.prototype 434 * @name baseline 435 * @type JXG.Line 436 */ 437 p3.baseline = l1; 438 439 /** 440 * A line on top of the baseline, indicating the slider's progress. 441 * @memberOf Slider.prototype 442 * @name highline 443 * @type JXG.Line 444 */ 445 p3.highline = l2; 446 447 if (withTicks) { 448 // Function to generate correct label texts 449 450 // attr = Type.copyAttributes(attributes, board.options, "slider", "ticks"); 451 if (!Type.exists(attr.generatelabeltext)) { 452 attr.ticks.generateLabelText = function (tick, zero, value) { 453 var labelText, 454 dFull = p3.point1.Dist(p3.point2), 455 smin = p3._smin, 456 smax = p3._smax, 457 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 458 459 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 460 // Point is zero 461 labelText = "0"; 462 } else { 463 labelText = this.formatLabelText(val); 464 } 465 return labelText; 466 }; 467 } 468 ticks = 2; 469 ti = board.create( 470 "ticks", 471 [ 472 p3.baseline, 473 p3.point1.Dist(p1) / ticks, 474 475 function (tick) { 476 var dFull = p3.point1.Dist(p3.point2), 477 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 478 479 if (dFull < Mat.eps) { 480 return 0; 481 } 482 483 return (d / dFull) * diff + smin; 484 } 485 ], 486 attr.ticks 487 ); 488 489 /** 490 * Ticks give a rough indication about the slider's current value. 491 * @memberOf Slider.prototype 492 * @name ticks 493 * @type JXG.Ticks 494 */ 495 p3.ticks = ti; 496 } 497 498 // override the point's remove method to ensure the removal of all elements 499 p3.remove = function () { 500 if (withText) { 501 board.removeObject(t); 502 } 503 504 board.removeObject(l2); 505 board.removeObject(l1); 506 board.removeObject(p2); 507 board.removeObject(p1); 508 509 Point.prototype.remove.call(p3); 510 }; 511 512 p1.dump = false; 513 p2.dump = false; 514 l1.dump = false; 515 l2.dump = false; 516 if (withText) { 517 t.dump = false; 518 } 519 520 // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER 521 p3.elType = "slider"; 522 p3.parents = parents; 523 p3.subs = { 524 point1: p1, 525 point2: p2, 526 baseLine: l1, 527 highLine: l2 528 }; 529 p3.inherits.push(p1, p2, l1, l2); 530 // Remove inherits to avoid circular inherits. 531 l1.inherits = []; 532 l2.inherits = []; 533 534 if (withTicks) { 535 ti.dump = false; 536 p3.subs.ticks = ti; 537 p3.inherits.push(ti); 538 } 539 540 p3.getParents = function () { 541 return [ 542 this.point1.coords.usrCoords.slice(1), 543 this.point2.coords.usrCoords.slice(1), 544 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 545 ]; 546 }; 547 548 p3.baseline.on("up", function (evt) { 549 var pos, c; 550 551 if (Type.evaluate(p3.visProp.moveonup) && !Type.evaluate(p3.visProp.fixed)) { 552 pos = l1.board.getMousePosition(evt, 0); 553 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 554 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 555 p3.triggerEventHandlers(['drag'], [evt]); 556 } 557 }); 558 559 // Save the visibility attribute of the sub-elements 560 // for (el in p3.subs) { 561 // p3.subs[el].status = { 562 // visible: p3.subs[el].visProp.visible 563 // }; 564 // } 565 566 // p3.hideElement = function () { 567 // var el; 568 // GeometryElement.prototype.hideElement.call(this); 569 // 570 // for (el in this.subs) { 571 // // this.subs[el].status.visible = this.subs[el].visProp.visible; 572 // this.subs[el].hideElement(); 573 // } 574 // }; 575 576 // p3.showElement = function () { 577 // var el; 578 // GeometryElement.prototype.showElement.call(this); 579 // 580 // for (el in this.subs) { 581 // // if (this.subs[el].status.visible) { 582 // this.subs[el].showElement(); 583 // // } 584 // } 585 // }; 586 587 // This is necessary to show baseline, highline and ticks 588 // when opening the board in case the visible attributes are set 589 // to 'inherit'. 590 p3.prepareUpdate().update(); 591 if (!board.isSuspendedUpdate) { 592 p3.updateVisibility().updateRenderer(); 593 p3.baseline.updateVisibility().updateRenderer(); 594 p3.highline.updateVisibility().updateRenderer(); 595 if (withTicks) { 596 p3.ticks.updateVisibility().updateRenderer(); 597 } 598 } 599 600 return p3; 601 }; 602 603 JXG.registerElement("slider", JXG.createSlider); 604 605 // export default { 606 // createSlider: JXG.createSlider 607 // }; 608