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 * @name setMax 265 * @param {Number} val New maximum value 266 * @returns {Object} this object 267 */ 268 p3.setMax = function (val) { 269 this._smax = val; 270 return this; 271 }; 272 273 /** 274 * Sets the value of the slider. This call must be followed 275 * by a board update call. 276 * @memberOf Slider.prototype 277 * @name setValue 278 * @param {Number} val New value 279 * @returns {Object} this object 280 */ 281 p3.setValue = function (val) { 282 var sdiff = this._smax - this._smin; 283 284 if (Math.abs(sdiff) > Mat.eps) { 285 this.position = (val - this._smin) / sdiff; 286 } else { 287 this.position = 0.0; //this._smin; 288 } 289 this.position = Math.max(0.0, Math.min(1.0, this.position)); 290 return this; 291 }; 292 293 /** 294 * Sets the minimum value of the slider. 295 * @memberOf Slider.prototype 296 * @name setMin 297 * @param {Number} val New minimum value 298 * @returns {Object} this object 299 */ 300 p3.setMin = function (val) { 301 this._smin = val; 302 return this; 303 }; 304 305 if (withText) { 306 attr = Type.copyAttributes(attributes, board.options, "slider", "label"); 307 t = board.create( 308 "text", 309 [ 310 function () { 311 return (p2.X() - p1.X()) * 0.05 + p2.X(); 312 }, 313 function () { 314 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 315 }, 316 function () { 317 var n, 318 d = Type.evaluate(p3.visProp.digits), 319 sl = Type.evaluate(p3.visProp.suffixlabel), 320 ul = Type.evaluate(p3.visProp.unitlabel), 321 pl = Type.evaluate(p3.visProp.postlabel); 322 323 if (d === 2 && Type.evaluate(p3.visProp.precision) !== 2) { 324 // Backwards compatibility 325 d = Type.evaluate(p3.visProp.precision); 326 } 327 328 if (sl !== null) { 329 n = sl; 330 } else if (p3.name && p3.name !== "") { 331 n = p3.name + " = "; 332 } else { 333 n = ""; 334 } 335 336 n += Type.toFixed(p3.Value(), d); 337 338 if (ul !== null) { 339 n += ul; 340 } 341 if (pl !== null) { 342 n += pl; 343 } 344 345 return n; 346 } 347 ], 348 attr 349 ); 350 351 /** 352 * The text element to the right of the slider, indicating its current value. 353 * @memberOf Slider.prototype 354 * @name label 355 * @type JXG.Text 356 */ 357 p3.label = t; 358 359 // reset the withlabel attribute 360 p3.visProp.withlabel = true; 361 p3.hasLabel = true; 362 } 363 364 /** 365 * Start point of the base line. 366 * @memberOf Slider.prototype 367 * @name point1 368 * @type JXG.Point 369 */ 370 p3.point1 = p1; 371 372 /** 373 * End point of the base line. 374 * @memberOf Slider.prototype 375 * @name point2 376 * @type JXG.Point 377 */ 378 p3.point2 = p2; 379 380 /** 381 * The baseline the glider is bound to. 382 * @memberOf Slider.prototype 383 * @name baseline 384 * @type JXG.Line 385 */ 386 p3.baseline = l1; 387 388 /** 389 * A line on top of the baseline, indicating the slider's progress. 390 * @memberOf Slider.prototype 391 * @name highline 392 * @type JXG.Line 393 */ 394 p3.highline = l2; 395 396 if (withTicks) { 397 // Function to generate correct label texts 398 399 attr = Type.copyAttributes(attributes, board.options, "slider", "ticks"); 400 if (!Type.exists(attr.generatelabeltext)) { 401 attr.generateLabelText = function (tick, zero, value) { 402 var labelText, 403 dFull = p3.point1.Dist(p3.point2), 404 smin = p3._smin, 405 smax = p3._smax, 406 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 407 408 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 409 // Point is zero 410 labelText = "0"; 411 } else { 412 labelText = this.formatLabelText(val); 413 } 414 return labelText; 415 }; 416 } 417 ticks = 2; 418 ti = board.create( 419 "ticks", 420 [ 421 p3.baseline, 422 p3.point1.Dist(p1) / ticks, 423 424 function (tick) { 425 var dFull = p3.point1.Dist(p3.point2), 426 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 427 428 if (dFull < Mat.eps) { 429 return 0; 430 } 431 432 return (d / dFull) * sdiff + smin; 433 } 434 ], 435 attr 436 ); 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 Point.prototype.remove.call(p3); 459 }; 460 461 p1.dump = false; 462 p2.dump = false; 463 l1.dump = false; 464 l2.dump = false; 465 if (withText) { 466 t.dump = false; 467 } 468 469 p3.elType = "slider"; 470 p3.parents = parents; 471 p3.subs = { 472 point1: p1, 473 point2: p2, 474 baseLine: l1, 475 highLine: l2 476 }; 477 p3.inherits.push(p1, p2, l1, l2); 478 479 if (withTicks) { 480 ti.dump = false; 481 p3.subs.ticks = ti; 482 p3.inherits.push(ti); 483 } 484 485 p3.getParents = function () { 486 return [ 487 this.point1.coords.usrCoords.slice(1), 488 this.point2.coords.usrCoords.slice(1), 489 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 490 ]; 491 }; 492 493 p3.baseline.on("up", function (evt) { 494 var pos, c; 495 496 if (Type.evaluate(p3.visProp.moveonup) && !Type.evaluate(p3.visProp.fixed)) { 497 pos = l1.board.getMousePosition(evt, 0); 498 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 499 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 500 p3.triggerEventHandlers(['drag'], [evt]); 501 } 502 }); 503 504 // Save the visibility attribute of the sub-elements 505 // for (el in p3.subs) { 506 // p3.subs[el].status = { 507 // visible: p3.subs[el].visProp.visible 508 // }; 509 // } 510 511 // p3.hideElement = function () { 512 // var el; 513 // GeometryElement.prototype.hideElement.call(this); 514 // 515 // for (el in this.subs) { 516 // // this.subs[el].status.visible = this.subs[el].visProp.visible; 517 // this.subs[el].hideElement(); 518 // } 519 // }; 520 521 // p3.showElement = function () { 522 // var el; 523 // GeometryElement.prototype.showElement.call(this); 524 // 525 // for (el in this.subs) { 526 // // if (this.subs[el].status.visible) { 527 // this.subs[el].showElement(); 528 // // } 529 // } 530 // }; 531 532 // This is necessary to show baseline, highline and ticks 533 // when opening the board in case the visible attributes are set 534 // to 'inherit'. 535 p3.prepareUpdate().update(); 536 if (!board.isSuspendedUpdate) { 537 p3.updateVisibility().updateRenderer(); 538 p3.baseline.updateVisibility().updateRenderer(); 539 p3.highline.updateVisibility().updateRenderer(); 540 if (withTicks) { 541 p3.ticks.updateVisibility().updateRenderer(); 542 } 543 } 544 545 return p3; 546 }; 547 548 JXG.registerElement("slider", JXG.createSlider); 549 550 // export default { 551 // createSlider: JXG.createSlider 552 // }; 553