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