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