1 /* 2 Copyright 2008-2026 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, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/ 33 34 /*jslint nomen: true, plusplus: true*/ 35 36 /** 37 * @fileoverview The JXG.Board class is defined in this file. JXG.Board controls all properties and methods 38 * used to manage a geonext board like managing geometric elements, managing mouse and touch events, etc. 39 */ 40 41 import JXG from '../jxg.js'; 42 import Const from './constants.js'; 43 import Coords from './coords.js'; 44 import Options from '../options.js'; 45 import Numerics from '../math/numerics.js'; 46 import Mat from '../math/math.js'; 47 import Geometry from '../math/geometry.js'; 48 import Complex from '../math/complex.js'; 49 import Statistics from '../math/statistics.js'; 50 import JessieCode from '../parser/jessiecode.js'; 51 import Color from '../utils/color.js'; 52 import Type from '../utils/type.js'; 53 import EventEmitter from '../utils/event.js'; 54 import Env from '../utils/env.js'; 55 import Composition from './composition.js'; 56 57 /** 58 * Constructs a new Board object. 59 * @class JXG.Board controls all properties and methods used to manage a geonext board like managing geometric 60 * elements, managing mouse and touch events, etc. You probably don't want to use this constructor directly. 61 * Please use {@link JXG.JSXGraph.initBoard} to initialize a board. 62 * @constructor 63 * @param {String|Object} container The id of or reference to the HTML DOM element 64 * the board is drawn in. This is usually a HTML div. If it is the reference to an HTML element and this element does not have an attribute "id", 65 * this attribute "id" is set to a random value. 66 * @param {JXG.AbstractRenderer} renderer The reference of a renderer. 67 * @param {String} id Unique identifier for the board, may be an empty string or null or even undefined. 68 * @param {JXG.Coords} origin The coordinates where the origin is placed, in user coordinates. 69 * @param {Number} zoomX Zoom factor in x-axis direction 70 * @param {Number} zoomY Zoom factor in y-axis direction 71 * @param {Number} unitX Units in x-axis direction 72 * @param {Number} unitY Units in y-axis direction 73 * @param {Number} canvasWidth The width of canvas 74 * @param {Number} canvasHeight The height of canvas 75 * @param {Object} attributes The attributes object given to {@link JXG.JSXGraph.initBoard} 76 * @borrows JXG.EventEmitter#on as this.on 77 * @borrows JXG.EventEmitter#off as this.off 78 * @borrows JXG.EventEmitter#triggerEventHandlers as this.triggerEventHandlers 79 * @borrows JXG.EventEmitter#eventHandlers as this.eventHandlers 80 */ 81 JXG.Board = function (container, renderer, id, 82 origin, zoomX, zoomY, unitX, unitY, 83 canvasWidth, canvasHeight, attributes) { 84 /** 85 * Board is in no special mode, objects are highlighted on mouse over and objects may be 86 * clicked to start drag&drop. 87 * @type Number 88 * @constant 89 */ 90 this.BOARD_MODE_NONE = 0x0000; 91 92 /** 93 * Board is in drag mode, objects aren't highlighted on mouse over and the object referenced in 94 * {@link JXG.Board#mouse} is updated on mouse movement. 95 * @type Number 96 * @constant 97 */ 98 this.BOARD_MODE_DRAG = 0x0001; 99 100 /** 101 * In this mode a mouse move changes the origin's screen coordinates. 102 * @type Number 103 * @constant 104 */ 105 this.BOARD_MODE_MOVE_ORIGIN = 0x0002; 106 107 /** 108 * This mode is active when the user zooms 109 * @type Number 110 * @constant 111 */ 112 this.BOARD_MODE_ZOOM = 0x0011; 113 114 /** 115 * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points. 116 * @type Number 117 * @constant 118 * @see JXG.Board#updateQuality 119 */ 120 this.BOARD_QUALITY_LOW = 0x1; 121 122 /** 123 * Update is made with high quality, e.g. graphs are evaluated at much more points. 124 * @type Number 125 * @constant 126 * @see JXG.Board#updateQuality 127 */ 128 this.BOARD_QUALITY_HIGH = 0x2; 129 130 /** 131 * Pointer to the document element containing the board. 132 * @type Object 133 */ 134 if (Type.exists(attributes.document) && attributes.document !== false) { 135 this.document = attributes.document; 136 } else if (Env.isBrowser) { 137 this.document = document; 138 } 139 140 /** 141 * The html-id of the html element containing the board. 142 * @type String 143 */ 144 this.container = ''; // container 145 146 /** 147 * ID of the board 148 * @type String 149 */ 150 this.id = ''; 151 152 /** 153 * Pointer to the html element containing the board. 154 * @type Object 155 */ 156 this.containerObj = null; // (Env.isBrowser ? this.document.getElementById(this.container) : null); 157 158 // Set this.container and this.containerObj 159 if (Type.isString(container)) { 160 // Hosting div is given as string 161 this.container = container; // container 162 this.containerObj = (Env.isBrowser ? this.document.getElementById(this.container) : null); 163 164 } else if (Env.isBrowser) { 165 166 // Hosting div is given as object pointer 167 this.containerObj = container; 168 this.container = this.containerObj.getAttribute('id'); 169 if (this.container === null) { 170 // Set random ID to this.container, but not to the DOM element 171 172 this.container = 'null' + parseInt(Math.random() * 16777216).toString(); 173 } 174 } 175 176 if (Env.isBrowser && renderer.type !== 'no' && this.containerObj === null) { 177 throw new Error('\nJSXGraph: HTML container element "' + container + '" not found.'); 178 } 179 180 // TODO 181 // Why do we need this.id AND this.container? 182 // There was never a board attribute "id". 183 // The origin seems to be that in the geonext renderer we use a separate id, extracted from the GEONExT file. 184 if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) { 185 // If the given id is not valid, generate an unique id 186 this.id = id; 187 } else { 188 this.id = this.generateId(); 189 } 190 191 /** 192 * A reference to this boards renderer. 193 * @type JXG.AbstractRenderer 194 * @name JXG.Board#renderer 195 * @private 196 * @ignore 197 */ 198 this.renderer = renderer; 199 200 /** 201 * Grids keeps track of all grids attached to this board. 202 * @type Array 203 * @private 204 */ 205 this.grids = []; 206 207 /** 208 * Copy of the default options 209 * @type JXG.Options 210 */ 211 this.options = Type.deepCopy(Options); // A possible theme is not yet merged in 212 213 /** 214 * Board attributes 215 * @type Object 216 */ 217 this.attr = attributes; 218 219 if (this.attr.theme !== 'default' && Type.exists(JXG.themes[this.attr.theme])) { 220 Type.mergeAttr(this.options, JXG.themes[this.attr.theme], true); 221 } 222 223 /** 224 * Dimension of the board. 225 * @default 2 226 * @type Number 227 */ 228 this.dimension = 2; 229 this.jc = new JessieCode(); 230 this.jc.use(this); 231 232 /** 233 * Coordinates of the boards origin. This a object with the two properties 234 * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords 235 * stores the boards origin in homogeneous screen coordinates. 236 * @type Object 237 * @private 238 */ 239 this.origin = {}; 240 this.origin.usrCoords = [1, 0, 0]; 241 this.origin.scrCoords = [1, origin[0], origin[1]]; 242 243 /** 244 * Zoom factor in X direction. It only stores the zoom factor to be able 245 * to get back to 100% in zoom100(). 246 * @name JXG.Board.zoomX 247 * @type Number 248 * @private 249 * @ignore 250 */ 251 this.zoomX = zoomX; 252 253 /** 254 * Zoom factor in Y direction. It only stores the zoom factor to be able 255 * to get back to 100% in zoom100(). 256 * @name JXG.Board.zoomY 257 * @type Number 258 * @private 259 * @ignore 260 */ 261 this.zoomY = zoomY; 262 263 /** 264 * The number of pixels which represent one unit in user-coordinates in x direction. 265 * @type Number 266 * @private 267 */ 268 this.unitX = unitX * this.zoomX; 269 270 /** 271 * The number of pixels which represent one unit in user-coordinates in y direction. 272 * @type Number 273 * @private 274 */ 275 this.unitY = unitY * this.zoomY; 276 277 /** 278 * Keep aspect ratio if bounding box is set and the width/height ratio differs from the 279 * width/height ratio of the canvas. 280 * @type Boolean 281 * @private 282 */ 283 this.keepaspectratio = false; 284 285 /** 286 * Canvas width. 287 * @type Number 288 * @private 289 */ 290 this.canvasWidth = canvasWidth; 291 292 /** 293 * Canvas Height 294 * @type Number 295 * @private 296 */ 297 this.canvasHeight = canvasHeight; 298 299 EventEmitter.eventify(this); 300 301 this.hooks = []; 302 303 /** 304 * An array containing all other boards that are updated after this board has been updated. 305 * @type Array 306 * @see JXG.Board#addChild 307 * @see JXG.Board#removeChild 308 */ 309 this.dependentBoards = []; 310 311 /** 312 * During the update process this is set to false to prevent an endless loop. 313 * @default false 314 * @type Boolean 315 */ 316 this.inUpdate = false; 317 318 /** 319 * An associative array containing all geometric objects belonging to the board. Key is the id of the object and value is a reference to the object. 320 * @type Object 321 */ 322 this.objects = {}; 323 324 /** 325 * An array containing all geometric objects on the board in the order of construction. 326 * @type Array 327 */ 328 this.objectsList = []; 329 330 /** 331 * An associative array containing all groups belonging to the board. Key is the id of the group and value is a reference to the object. 332 * @type Object 333 */ 334 this.groups = {}; 335 336 /** 337 * Stores all the objects that are currently running an animation. 338 * @type Object 339 */ 340 this.animationObjects = {}; 341 342 /** 343 * An associative array containing all highlighted elements belonging to the board. 344 * @type Object 345 */ 346 this.highlightedObjects = {}; 347 348 /** 349 * Number of objects ever created on this board. This includes every object, even invisible and deleted ones. 350 * @type Number 351 */ 352 this.numObjects = 0; 353 354 /** 355 * An associative array / dictionary to store the objects of the board by name. The name of the object is the key and value is a reference to the object. 356 * @type Object 357 */ 358 this.elementsByName = {}; 359 360 /** 361 * The board mode the board is currently in. Possible values are 362 * <ul> 363 * <li>JXG.Board.BOARD_MODE_NONE</li> 364 * <li>JXG.Board.BOARD_MODE_DRAG</li> 365 * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li> 366 * </ul> 367 * @type Number 368 */ 369 this.mode = this.BOARD_MODE_NONE; 370 371 /** 372 * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}. 373 * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to 374 * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of 375 * evaluation points when plotting functions. Possible values are 376 * <ul> 377 * <li>BOARD_QUALITY_LOW</li> 378 * <li>BOARD_QUALITY_HIGH</li> 379 * </ul> 380 * @type Number 381 * @see JXG.Board#mode 382 */ 383 this.updateQuality = this.BOARD_QUALITY_HIGH; 384 385 /** 386 * If true updates are skipped. 387 * @type Boolean 388 */ 389 this.isSuspendedRedraw = false; 390 391 this.calculateSnapSizes(); 392 393 /** 394 * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button. 395 * @type Number 396 * @see JXG.Board#drag_dy 397 */ 398 this.drag_dx = 0; 399 400 /** 401 * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button. 402 * @type Number 403 * @see JXG.Board#drag_dx 404 */ 405 this.drag_dy = 0; 406 407 /** 408 * The last position where a drag event has been fired. 409 * @type Array 410 * @see JXG.Board#moveObject 411 */ 412 this.drag_position = [0, 0]; 413 414 /** 415 * References to the object that is dragged with the mouse on the board. 416 * @type JXG.GeometryElement 417 * @see JXG.Board#touches 418 */ 419 this.mouse = {}; 420 421 /** 422 * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events. 423 * @type Array 424 * @see JXG.Board#mouse 425 */ 426 this.touches = []; 427 428 /** 429 * A string containing the XML text of the construction. 430 * This is set in {@link JXG.FileReader.parseString}. 431 * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File. 432 * @type String 433 */ 434 this.xmlString = ''; 435 436 /** 437 * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations. 438 * @type Array 439 */ 440 this.cPos = []; 441 442 /** 443 * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since 444 * touchStart because Android's Webkit browser fires too much of them. 445 * @type Number 446 */ 447 this.touchMoveLast = 0; 448 449 /** 450 * Contains the pointerId of the last touchMove event which was not thrown away or since 451 * touchStart because Android's Webkit browser fires too much of them. 452 * @type Number 453 */ 454 this.touchMoveLastId = Infinity; 455 456 /** 457 * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away. 458 * @type Number 459 */ 460 this.positionAccessLast = 0; 461 462 /** 463 * Collects all elements that triggered a mouse down event. 464 * @type Array 465 */ 466 this.downObjects = []; 467 this.clickObjects = {}; 468 469 /** 470 * Collects all elements that have keyboard focus. Should be either one or no element. 471 * Elements are stored with their id. 472 * @type Array 473 */ 474 this.focusObjects = []; 475 476 if (this.attr.showcopyright || this.attr.showlogo) { 477 this.renderer.displayLogo(Const.licenseLogo, parseInt(this.options.text.fontSize, 10), this); 478 } 479 480 if (this.attr.showcopyright) { 481 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 482 } 483 484 /** 485 * Full updates are needed after zoom and axis translates. This saves some time during an update. 486 * @default false 487 * @type Boolean 488 */ 489 this.needsFullUpdate = false; 490 491 /** 492 * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following 493 * elements are updated during mouse move. On mouse up the whole construction is 494 * updated. This enables us to be fast even on very slow devices. 495 * @type Boolean 496 * @default false 497 */ 498 this.reducedUpdate = false; 499 500 /** 501 * The current color blindness deficiency is stored in this property. If color blindness is not emulated 502 * at the moment, it's value is 'none'. 503 */ 504 this.currentCBDef = 'none'; 505 506 /** 507 * If GEONExT constructions are displayed, then this property should be set to true. 508 * At the moment there should be no difference. But this may change. 509 * This is set in {@link JXG.GeonextReader.readGeonext}. 510 * @type Boolean 511 * @default false 512 * @see JXG.GeonextReader.readGeonext 513 */ 514 this.geonextCompatibilityMode = false; 515 516 if (this.options.text.useASCIIMathML && translateASCIIMath) { 517 init(); 518 } else { 519 this.options.text.useASCIIMathML = false; 520 } 521 522 /** 523 * A flag which tells if the board registers mouse events. 524 * @type Boolean 525 * @default false 526 */ 527 this.hasMouseHandlers = false; 528 529 /** 530 * A flag which tells if the board registers touch events. 531 * @type Boolean 532 * @default false 533 */ 534 this.hasTouchHandlers = false; 535 536 /** 537 * A flag which stores if the board registered pointer events. 538 * @type Boolean 539 * @default false 540 */ 541 this.hasPointerHandlers = false; 542 543 /** 544 * A flag which stores if the board registered zoom events, i.e. mouse wheel scroll events. 545 * @type Boolean 546 * @default false 547 */ 548 this.hasWheelHandlers = false; 549 550 /** 551 * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered. 552 * @type Boolean 553 * @default false 554 */ 555 this.hasMouseUp = false; 556 557 /** 558 * A flag which tells if the board the JXG.Board#touchEndListener is currently registered. 559 * @type Boolean 560 * @default false 561 */ 562 this.hasTouchEnd = false; 563 564 /** 565 * A flag which tells us if the board has a pointerUp event registered at the moment. 566 * @type Boolean 567 * @default false 568 */ 569 this.hasPointerUp = false; 570 571 /** 572 * Array containing the events related to resizing that have event listeners. 573 * @type Array 574 * @default [] 575 */ 576 this.resizeHandlers = []; 577 578 /** 579 * Offset for large coords elements like images 580 * @type Array 581 * @private 582 * @default [0, 0] 583 */ 584 this._drag_offset = [0, 0]; 585 586 /** 587 * Stores the input device used in the last down or move event. 588 * @type String 589 * @private 590 * @default 'mouse' 591 */ 592 this._inputDevice = 'mouse'; 593 594 /** 595 * Keeps a list of pointer devices which are currently touching the screen. 596 * @type Array 597 * @private 598 */ 599 this._board_touches = []; 600 601 /** 602 * A flag which tells us if the board is in the selecting mode 603 * @type Boolean 604 * @default false 605 */ 606 this.selectingMode = false; 607 608 /** 609 * A flag which tells us if the user is selecting 610 * @type Boolean 611 * @default false 612 */ 613 this.isSelecting = false; 614 615 /** 616 * A flag which tells us if the user is scrolling the viewport 617 * @type Boolean 618 * @private 619 * @default false 620 * @see JXG.Board#scrollListener 621 */ 622 this._isScrolling = false; 623 624 /** 625 * A flag which tells us if a resize is in process 626 * @type Boolean 627 * @private 628 * @default false 629 * @see JXG.Board#resizeListener 630 */ 631 this._isResizing = false; 632 633 /** 634 * A flag which tells us if the update is triggered by a change of the 635 * 3D view. In that case we only have to update the projection of 636 * the 3D elements and can avoid a full board update. 637 * 638 * @type Boolean 639 * @private 640 * @default false 641 */ 642 this._change3DView = false; 643 644 /** 645 * A bounding box for the selection 646 * @type Array 647 * @default [ [0,0], [0,0] ] 648 */ 649 this.selectingBox = [[0, 0], [0, 0]]; 650 651 /** 652 * Array to log user activity. 653 * Entries are objects of the form '{type, id, start, end}' notifying 654 * the start time as well as the last time of a single event of type 'type' 655 * on a JSXGraph element of id 'id'. 656 * <p> 'start' and 'end' contain the amount of milliseconds elapsed between 1 January 1970 00:00:00 UTC 657 * and the time the event happened. 658 * <p> 659 * For the time being (i.e. v1.5.0) the only supported type is 'drag'. 660 * @type Array 661 */ 662 this.userLog = []; 663 664 /** 665 * Array of length two containing sketchcurves of the board. In case of mouse or pen 666 * only the first entry is used. In case of finger input, sketchcurves 667 * for the first and second finger are possible. 668 * 669 * @example 670 * const board = JXG.JSXGraph.initBoard('jxgbox', { 671 * boundingbox: [-10, 10, 10, -10], 672 * axis: true, 673 * sketches: { 674 * enabled: true, 675 * 0: {strokeWidth: 2, visible: true, maxLength: 20}, 676 * 1: {strokeWidth: 3, visible: true} 677 * } 678 * }); 679 * 680 * // Use event handler to access the actual curve 681 * board.on('move', function(evt) { 682 * console.log('JSXGraph example: move', this.sketches[0].dataX.length); 683 * }); 684 * 685 * // Use event handler to access the actual curve 686 * board.on('up', function(evt) { 687 * console.log('JSXGraph example: up', this.sketches[0].dataX.length); 688 * }); 689 * 690 * </pre><div id="JXGf62e7217-a3ee-45b8-92e4-ce0d0d789df5" class="jxgbox" style="width: 300px; height: 300px;"></div> 691 * <script type="text/javascript"> 692 * (function() { 693 * var board = JXG.JSXGraph.initBoard('JXGf62e7217-a3ee-45b8-92e4-ce0d0d789df5', 694 * { boundingbox: [-10, 10, 10, -10], 695 * axis: true, 696 * sketches: { 697 * enabled: true, 698 * 0: {strokeWidth: 2, visible: true, maxLength: 20}, 699 * 1: {strokeWidth: 3, visible: true} 700 * } 701 * }); 702 * // Use event handler to access the actual curve 703 * board.on('move', function(evt) { 704 * console.log('JSXGraph example: move', this.sketches[0].dataX.length); 705 * }); 706 * 707 * // Use event handler to access the actual curve 708 * board.on('up', function(evt) { 709 * console.log('JSXGraph example: up', this.sketches[0].dataX.length); 710 * }); 711 * 712 * })(); 713 * 714 * </script><pre> 715 * 716 * @type Array 717 * @see SketchCurve 718 * @see JXG.Board#sketch 719 */ 720 this.sketches = [null, null]; 721 722 /** 723 * Alias for the first sketchcurve, i.e. for board.sketches[0]. 724 * @type {JXG.Curve} 725 * @see JXG.Board#sketches 726 */ 727 this.sketch = null; //this.sketches[0]; 728 729 /** 730 * Array of length two of Boolean flags indicating if a pointer device (finger, mouse, pen) is 731 * adding points to board.sketches[i] (i=0,1). i=1 is only used for multi-touch with fingers. 732 * <p> 733 * User-supplied events might use this flag to test if sketching is active. 734 * Usually, this flag is true starting with a down event and ends with the up event. 735 * @type {Array} 736 * @see JXG.Board#sketches 737 * 738 */ 739 this.isSketching = [false, false]; 740 741 /** 742 * 743 */ 744 this.mathLib = Math; // Math or JXG.Math.IntervalArithmetic 745 746 /** 747 * 748 */ 749 this.mathLibJXG = JXG.Math; // JXG.Math or JXG.Math.IntervalArithmetic 750 751 if (this.attr.registerevents === true) { 752 this.attr.registerevents = { 753 fullscreen: true, 754 keyboard: true, 755 pointer: true, 756 resize: true, 757 wheel: true 758 }; 759 } else if (typeof this.attr.registerevents === 'object') { 760 if (!Type.exists(this.attr.registerevents.fullscreen)) { 761 this.attr.registerevents.fullscreen = true; 762 } 763 if (!Type.exists(this.attr.registerevents.keyboard)) { 764 this.attr.registerevents.keyboard = true; 765 } 766 if (!Type.exists(this.attr.registerevents.pointer)) { 767 this.attr.registerevents.pointer = true; 768 } 769 if (!Type.exists(this.attr.registerevents.resize)) { 770 this.attr.registerevents.resize = true; 771 } 772 if (!Type.exists(this.attr.registerevents.wheel)) { 773 this.attr.registerevents.wheel = true; 774 } 775 } 776 if (this.attr.registerevents !== false) { 777 if (this.attr.registerevents.fullscreen) { 778 this.addFullscreenEventHandlers(); 779 } 780 if (this.attr.registerevents.keyboard) { 781 this.addKeyboardEventHandlers(); 782 } 783 if (this.attr.registerevents.pointer) { 784 this.addEventHandlers(); 785 } 786 if (this.attr.registerevents.resize) { 787 this.addResizeEventHandlers(); 788 } 789 if (this.attr.registerevents.wheel) { 790 this.addWheelEventHandlers(); 791 } 792 } 793 }; 794 795 Type.copyMethodMap(JXG.Board, { 796 update: 'update', 797 fullUpdate: 'fullUpdate', 798 on: 'on', 799 off: 'off', 800 trigger: 'trigger', 801 setAttribute: 'setAttribute', 802 setBoundingBox: 'setBoundingBox', 803 setView: 'setBoundingBox', 804 getBoundingBox: 'getBoundingBox', 805 BoundingBox: 'getBoundingBox', 806 getView: 'getBoundingBox', 807 View: 'getBoundingBox', 808 migratePoint: 'migratePoint', 809 colorblind: 'emulateColorblindness', 810 suspendUpdate: 'suspendUpdate', 811 unsuspendUpdate: 'unsuspendUpdate', 812 clearTraces: 'clearTraces', 813 left: 'clickLeftArrow', 814 right: 'clickRightArrow', 815 up: 'clickUpArrow', 816 down: 'clickDownArrow', 817 zoomIn: 'zoomIn', 818 zoomOut: 'zoomOut', 819 zoom100: 'zoom100', 820 zoomElements: 'zoomElements', 821 remove: 'removeObject', 822 removeObject: 'removeObject' 823 }); 824 825 JXG.extend( 826 JXG.Board.prototype, 827 /** @lends JXG.Board.prototype */ { 828 /** 829 * Generates an unique name for the given object. The result depends on the objects type, if the 830 * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line} 831 * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower 832 * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is 833 * generated using lower case characters. prefixed with k_ is used. In any other case, lower case 834 * chars prefixed with s_ is used. 835 * @param {Object} object Reference of an JXG.GeometryElement that is to be named. 836 * @returns {String} Unique name for the object. 837 */ 838 generateName: function (object) { 839 var possibleNames, i, 840 maxNameLength = this.attr.maxnamelength, 841 pre = '', 842 post = '', 843 indices = [], 844 name = ''; 845 846 if (object.type === Const.OBJECT_TYPE_TICKS) { 847 return ''; 848 } 849 850 if (Type.isPoint(object) || Type.isPoint3D(object)) { 851 // points have capital letters 852 possibleNames = [ 853 '', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' 854 ]; 855 } else if (object.type === Const.OBJECT_TYPE_ANGLE) { 856 possibleNames = [ 857 '', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 858 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω' 859 ]; 860 } else { 861 // all other elements get lowercase labels 862 possibleNames = [ 863 '', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' 864 ]; 865 } 866 867 if ( 868 !Type.isPoint(object) && 869 !Type.isPoint3D(object) && 870 object.elementClass !== Const.OBJECT_CLASS_LINE && 871 object.type !== Const.OBJECT_TYPE_ANGLE 872 ) { 873 if (object.type === Const.OBJECT_TYPE_POLYGON) { 874 pre = 'P_{'; 875 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) { 876 pre = 'k_{'; 877 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) { 878 pre = 't_{'; 879 } else { 880 pre = 's_{'; 881 } 882 post = '}'; 883 } 884 885 for (i = 0; i < maxNameLength; i++) { 886 indices[i] = 0; 887 } 888 889 while (indices[maxNameLength - 1] < possibleNames.length) { 890 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) { 891 name = pre; 892 893 for (i = maxNameLength; i > 0; i--) { 894 name += possibleNames[indices[i - 1]]; 895 } 896 897 if (!Type.exists(this.elementsByName[name + post])) { 898 return name + post; 899 } 900 } 901 indices[0] = possibleNames.length; 902 903 for (i = 1; i < maxNameLength; i++) { 904 if (indices[i - 1] === possibleNames.length) { 905 indices[i - 1] = 1; 906 indices[i] += 1; 907 } 908 } 909 } 910 911 return ''; 912 }, 913 914 /** 915 * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'. 916 * @returns {String} Unique id for a board. 917 */ 918 generateId: function () { 919 var r = 1; 920 921 // as long as we don't have a unique id generate a new one 922 while (Type.exists(JXG.boards['jxgBoard' + r])) { 923 r = Math.round(Math.random() * 16777216); 924 } 925 926 return 'jxgBoard' + r; 927 }, 928 929 /** 930 * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the 931 * object type. As a side effect {@link JXG.Board#numObjects} 932 * is updated. 933 * @param {Object} obj Reference of an geometry object that needs an id. 934 * @param {Number} type Type of the object. 935 * @returns {String} Unique id for an element. 936 */ 937 setId: function (obj, type) { 938 var randomNumber, 939 num = this.numObjects, 940 elId = obj.id; 941 942 this.numObjects += 1; 943 944 // If no id is provided or id is empty string, a new one is chosen 945 if (elId === '' || !Type.exists(elId)) { 946 elId = this.id + type + num; 947 while (Type.exists(this.objects[elId])) { 948 randomNumber = Math.round(Math.random() * 65535); 949 elId = this.id + type + num + '-' + randomNumber; 950 } 951 } 952 953 obj.id = elId; 954 this.objects[elId] = obj; 955 obj._pos = this.objectsList.length; 956 this.objectsList[this.objectsList.length] = obj; 957 958 return elId; 959 }, 960 961 /** 962 * After construction of the object the visibility is set 963 * and the label is constructed if necessary. 964 * @param {Object} obj The object to add. 965 */ 966 finalizeAdding: function (obj) { 967 if (obj.evalVisProp('visible') === false) { 968 this.renderer.display(obj, false); 969 } 970 }, 971 972 finalizeLabel: function (obj) { 973 if ( 974 obj.hasLabel && 975 !obj.label.evalVisProp('islabel') && 976 obj.label.evalVisProp('visible') === false 977 ) { 978 this.renderer.display(obj.label, false); 979 } 980 }, 981 982 /********************************************************** 983 * 984 * Event Handler helpers 985 * 986 **********************************************************/ 987 988 /** 989 * Returns false if the event has been triggered faster than the maximum frame rate. 990 * 991 * @param {Event} evt Event object given by the browser (unused) 992 * @returns {Boolean} If the event has been triggered faster than the maximum frame rate, false is returned. 993 * @private 994 * @see JXG.Board#pointerMoveListener 995 * @see JXG.Board#touchMoveListener 996 * @see JXG.Board#mouseMoveListener 997 */ 998 checkFrameRate: function (evt) { 999 var handleEvt = false, 1000 time = new Date().getTime(); 1001 1002 if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) { 1003 handleEvt = true; 1004 this.touchMoveLastId = evt.pointerId; 1005 } 1006 if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) { 1007 handleEvt = true; 1008 } 1009 if (handleEvt) { 1010 this.touchMoveLast = time; 1011 } 1012 return handleEvt; 1013 }, 1014 1015 /** 1016 * Calculates mouse coordinates relative to the boards container. 1017 * @returns {Array} Array of coordinates relative the boards container top left corner. 1018 */ 1019 getCoordsTopLeftCorner: function () { 1020 var cPos, 1021 doc, 1022 crect, 1023 // In ownerDoc we need the 'real' document object. 1024 // The first version is used in the case of shadowDOM, 1025 // the second case in the 'normal' case. 1026 ownerDoc = this.document.ownerDocument || this.document, 1027 docElement = ownerDoc.documentElement || this.document.body.parentNode, 1028 docBody = ownerDoc.body, 1029 container = this.containerObj, 1030 zoom, 1031 o; 1032 1033 /** 1034 * During drags and origin moves the container element is usually not changed. 1035 * Check the position of the upper left corner at most every 1000 msecs 1036 */ 1037 if ( 1038 this.cPos.length > 0 && 1039 (this.mode === this.BOARD_MODE_DRAG || 1040 this.mode === this.BOARD_MODE_MOVE_ORIGIN || 1041 new Date().getTime() - this.positionAccessLast < 1000) 1042 ) { 1043 return this.cPos; 1044 } 1045 this.positionAccessLast = new Date().getTime(); 1046 1047 // Check if getBoundingClientRect exists. If so, use this as this covers *everything* 1048 // even CSS3D transformations etc. 1049 // Supported by all browsers but IE 6, 7. 1050 if (container.getBoundingClientRect) { 1051 crect = container.getBoundingClientRect(); 1052 1053 zoom = 1.0; 1054 // Recursively search for zoom style entries. 1055 // This is necessary for reveal.js on webkit. 1056 // It fails if the user does zooming 1057 o = container; 1058 while (o && Type.exists(o.parentNode)) { 1059 if ( 1060 Type.exists(o.style) && 1061 Type.exists(o.style.zoom) && 1062 o.style.zoom !== '' 1063 ) { 1064 zoom *= parseFloat(o.style.zoom); 1065 } 1066 o = o.parentNode; 1067 } 1068 cPos = [crect.left * zoom, crect.top * zoom]; 1069 1070 // add border width 1071 cPos[0] += Env.getProp(container, 'border-left-width'); 1072 cPos[1] += Env.getProp(container, 'border-top-width'); 1073 1074 // vml seems to ignore paddings 1075 if (this.renderer.type !== 'vml') { 1076 // add padding 1077 cPos[0] += Env.getProp(container, 'padding-left'); 1078 cPos[1] += Env.getProp(container, 'padding-top'); 1079 } 1080 1081 this.cPos = cPos.slice(); 1082 return this.cPos; 1083 } 1084 1085 // 1086 // OLD CODE 1087 // IE 6-7 only: 1088 // 1089 cPos = Env.getOffset(container); 1090 doc = this.document.documentElement.ownerDocument; 1091 1092 if (!this.containerObj.currentStyle && doc.defaultView) { 1093 // Non IE 1094 // this is for hacks like this one used in wordpress for the admin bar: 1095 // html { margin-top: 28px } 1096 // seems like it doesn't work in IE 1097 1098 cPos[0] += Env.getProp(docElement, 'margin-left'); 1099 cPos[1] += Env.getProp(docElement, 'margin-top'); 1100 1101 cPos[0] += Env.getProp(docElement, 'border-left-width'); 1102 cPos[1] += Env.getProp(docElement, 'border-top-width'); 1103 1104 cPos[0] += Env.getProp(docElement, 'padding-left'); 1105 cPos[1] += Env.getProp(docElement, 'padding-top'); 1106 } 1107 1108 if (docBody) { 1109 cPos[0] += Env.getProp(docBody, 'left'); 1110 cPos[1] += Env.getProp(docBody, 'top'); 1111 } 1112 1113 // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX 1114 // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly 1115 // available version so we're doing it the hacky way: Add a fixed offset. 1116 // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J 1117 if (typeof google === 'object' && google.translate) { 1118 cPos[0] += 10; 1119 cPos[1] += 25; 1120 } 1121 1122 // add border width 1123 cPos[0] += Env.getProp(container, 'border-left-width'); 1124 cPos[1] += Env.getProp(container, 'border-top-width'); 1125 1126 // vml seems to ignore paddings 1127 if (this.renderer.type !== 'vml') { 1128 // add padding 1129 cPos[0] += Env.getProp(container, 'padding-left'); 1130 cPos[1] += Env.getProp(container, 'padding-top'); 1131 } 1132 1133 cPos[0] += this.attr.offsetx; 1134 cPos[1] += this.attr.offsety; 1135 1136 this.cPos = cPos.slice(); 1137 return this.cPos; 1138 }, 1139 1140 /** 1141 * This function divides the board into 9 sections and returns an array <tt>[u,v]</tt> which symbolizes the location of <tt>position</tt>. 1142 * Optional a <tt>margin</tt> to the inner of the board is respected.<br> 1143 * 1144 * @name Board#getPointLoc 1145 * @param {Array} position Array of requested position <tt>[x, y]</tt> or <tt>[w, x, y]</tt>. 1146 * @param {Array|Number} [margin] Optional margin for the inner of the board: <tt>[top, right, bottom, left]</tt>. A single number <tt>m</tt> is interpreted as <tt>[m, m, m, m]</tt>. 1147 * @returns {Array} [u,v] with the following meanings: 1148 * <pre> 1149 * v u > | -1 | 0 | 1 | 1150 * ------------------------------------------ 1151 * 1 | [-1,1] | [0,1] | [1,1] | 1152 * ------------------------------------------ 1153 * 0 | [-1,0] | Board | [1,0] | 1154 * ------------------------------------------ 1155 * -1 | [-1,-1] | [0,-1] | [1,-1] | 1156 * </pre> 1157 * Positions inside the board (minus margin) return the value <tt>[0,0]</tt>. 1158 * 1159 * @example 1160 * var point1, point2, point3, point4, margin, 1161 * p1Location, p2Location, p3Location, p4Location, 1162 * helppoint1, helppoint2, helppoint3, helppoint4; 1163 * 1164 * // margin to make the boundingBox virtually smaller 1165 * margin = [2,2,2,2]; 1166 * 1167 * // Points which are seen on screen 1168 * point1 = board.create('point', [0,0]); 1169 * point2 = board.create('point', [0,7]); 1170 * point3 = board.create('point', [7,7]); 1171 * point4 = board.create('point', [-7,-5]); 1172 * 1173 * p1Location = board.getPointLoc(point1.coords.usrCoords, margin); 1174 * p2Location = board.getPointLoc(point2.coords.usrCoords, margin); 1175 * p3Location = board.getPointLoc(point3.coords.usrCoords, margin); 1176 * p4Location = board.getPointLoc(point4.coords.usrCoords, margin); 1177 * 1178 * // Text seen on screen 1179 * board.create('text', [1,-1, "getPointLoc(A): " + "[" + p1Location + "]"]) 1180 * board.create('text', [1,-2, "getPointLoc(B): " + "[" + p2Location + "]"]) 1181 * board.create('text', [1,-3, "getPointLoc(C): " + "[" + p3Location + "]"]) 1182 * board.create('text', [1,-4, "getPointLoc(D): " + "[" + p4Location + "]"]) 1183 * 1184 * 1185 * // Helping points that are used to create the helping lines 1186 * helppoint1 = board.create('point', [(function (){ 1187 * var bbx = board.getBoundingBox(); 1188 * return [bbx[2] - 2, bbx[1] -2]; 1189 * })], { 1190 * visible: false, 1191 * }) 1192 * 1193 * helppoint2 = board.create('point', [(function (){ 1194 * var bbx = board.getBoundingBox(); 1195 * return [bbx[0] + 2, bbx[1] -2]; 1196 * })], { 1197 * visible: false, 1198 * }) 1199 * 1200 * helppoint3 = board.create('point', [(function (){ 1201 * var bbx = board.getBoundingBox(); 1202 * return [bbx[0]+ 2, bbx[3] + 2]; 1203 * })],{ 1204 * visible: false, 1205 * }) 1206 * 1207 * helppoint4 = board.create('point', [(function (){ 1208 * var bbx = board.getBoundingBox(); 1209 * return [bbx[2] -2, bbx[3] + 2]; 1210 * })], { 1211 * visible: false, 1212 * }) 1213 * 1214 * // Helping lines to visualize the 9 sectors and the margin 1215 * board.create('line', [helppoint1, helppoint2]); 1216 * board.create('line', [helppoint2, helppoint3]); 1217 * board.create('line', [helppoint3, helppoint4]); 1218 * board.create('line', [helppoint4, helppoint1]); 1219 * 1220 * </pre><div id="JXG4b3efef5-839d-4fac-bad1-7a14c0a89c70" class="jxgbox" style="width: 500px; height: 500px;"></div> 1221 * <script type="text/javascript"> 1222 * (function() { 1223 * var board = JXG.JSXGraph.initBoard('JXG4b3efef5-839d-4fac-bad1-7a14c0a89c70', 1224 * {boundingbox: [-8, 8, 8,-8], maxboundingbox: [-7.5,7.5,7.5,-7.5], axis: true, showcopyright: false, shownavigation: false, showZoom: false}); 1225 * var point1, point2, point3, point4, margin, 1226 * p1Location, p2Location, p3Location, p4Location, 1227 * helppoint1, helppoint2, helppoint3, helppoint4; 1228 * 1229 * // margin to make the boundingBox virtually smaller 1230 * margin = [2,2,2,2]; 1231 * 1232 * // Points which are seen on screen 1233 * point1 = board.create('point', [0,0]); 1234 * point2 = board.create('point', [0,7]); 1235 * point3 = board.create('point', [7,7]); 1236 * point4 = board.create('point', [-7,-5]); 1237 * 1238 * p1Location = board.getPointLoc(point1.coords.usrCoords, margin); 1239 * p2Location = board.getPointLoc(point2.coords.usrCoords, margin); 1240 * p3Location = board.getPointLoc(point3.coords.usrCoords, margin); 1241 * p4Location = board.getPointLoc(point4.coords.usrCoords, margin); 1242 * 1243 * // Text seen on screen 1244 * board.create('text', [1,-1, "getPointLoc(A): " + "[" + p1Location + "]"]) 1245 * board.create('text', [1,-2, "getPointLoc(B): " + "[" + p2Location + "]"]) 1246 * board.create('text', [1,-3, "getPointLoc(C): " + "[" + p3Location + "]"]) 1247 * board.create('text', [1,-4, "getPointLoc(D): " + "[" + p4Location + "]"]) 1248 * 1249 * 1250 * // Helping points that are used to create the helping lines 1251 * helppoint1 = board.create('point', [(function (){ 1252 * var bbx = board.getBoundingBox(); 1253 * return [bbx[2] - 2, bbx[1] -2]; 1254 * })], { 1255 * visible: false, 1256 * }) 1257 * 1258 * helppoint2 = board.create('point', [(function (){ 1259 * var bbx = board.getBoundingBox(); 1260 * return [bbx[0] + 2, bbx[1] -2]; 1261 * })], { 1262 * visible: false, 1263 * }) 1264 * 1265 * helppoint3 = board.create('point', [(function (){ 1266 * var bbx = board.getBoundingBox(); 1267 * return [bbx[0]+ 2, bbx[3] + 2]; 1268 * })],{ 1269 * visible: false, 1270 * }) 1271 * 1272 * helppoint4 = board.create('point', [(function (){ 1273 * var bbx = board.getBoundingBox(); 1274 * return [bbx[2] -2, bbx[3] + 2]; 1275 * })], { 1276 * visible: false, 1277 * }) 1278 * 1279 * // Helping lines to visualize the 9 sectors and the margin 1280 * board.create('line', [helppoint1, helppoint2]); 1281 * board.create('line', [helppoint2, helppoint3]); 1282 * board.create('line', [helppoint3, helppoint4]); 1283 * board.create('line', [helppoint4, helppoint1]); 1284 * })(); 1285 * 1286 * </script><pre> 1287 * 1288 */ 1289 getPointLoc: function (position, margin) { 1290 var bbox, pos, res, marg; 1291 1292 bbox = this.getBoundingBox(); 1293 pos = position; 1294 if (pos.length === 2) { 1295 pos.unshift(undefined); 1296 } 1297 res = [0, 0]; 1298 marg = margin || 0; 1299 if (Type.isNumber(marg)) { 1300 marg = [marg, marg, marg, marg]; 1301 } 1302 1303 if (pos[1] > (bbox[2] - marg[1])) { 1304 res[0] = 1; 1305 } 1306 if (pos[1] < (bbox[0] + marg[3])) { 1307 res[0] = -1; 1308 } 1309 1310 if (pos[2] > (bbox[1] - marg[0])) { 1311 res[1] = 1; 1312 } 1313 if (pos[2] < (bbox[3] + marg[2])) { 1314 res[1] = -1; 1315 } 1316 1317 return res; 1318 }, 1319 1320 /** 1321 * This function calculates where the origin is located (@link Board#getPointLoc). 1322 * Optional a <tt>margin</tt> to the inner of the board is respected.<br> 1323 * 1324 * @name Board#getLocationOrigin 1325 * @param {Array|Number} [margin] Optional margin for the inner of the board: <tt>[top, right, bottom, left]</tt>. A single number <tt>m</tt> is interpreted as <tt>[m, m, m, m]</tt>. 1326 * @returns {Array} [u,v] which shows where the origin is located (@link Board#getPointLoc). 1327 */ 1328 getLocationOrigin: function (margin) { 1329 return this.getPointLoc([0, 0], margin); 1330 }, 1331 1332 /** 1333 * Get the position of the pointing device in screen coordinates, relative to the upper left corner 1334 * of the host tag. 1335 * @param {Event} e Event object given by the browser. 1336 * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set 1337 * for mouseevents. 1338 * @returns {Array} Contains the mouse coordinates in screen coordinates, ready for {@link JXG.Coords} 1339 */ 1340 getMousePosition: function (e, i) { 1341 var cPos = this.getCoordsTopLeftCorner(), 1342 absPos, 1343 v; 1344 1345 // Position of cursor using clientX/Y 1346 absPos = Env.getPosition(e, i, this.document); 1347 1348 // Old: 1349 // This seems to be obsolete anyhow: 1350 // "In case there has been no down event before." 1351 // if (!Type.exists(this.cssTransMat)) { 1352 // this.updateCSSTransforms(); 1353 // } 1354 // New: 1355 // We have to update the CSS transform matrix all the time, 1356 // since libraries like ZIMJS do not notify JSXGraph about a change. 1357 // In particular, sending a resize event event to JSXGraph 1358 // would be necessary. 1359 this.updateCSSTransforms(); 1360 1361 // Position relative to the top left corner 1362 v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]]; 1363 v = Mat.matVecMult(this.cssTransMat, v); 1364 v[1] /= v[0]; 1365 v[2] /= v[0]; 1366 return [v[1], v[2]]; 1367 1368 // Method without CSS transformation 1369 /* 1370 return [absPos[0] - cPos[0], absPos[1] - cPos[1]]; 1371 */ 1372 }, 1373 1374 /** 1375 * Initiate moving the origin. This is used in mouseDown and touchStart listeners. 1376 * @param {Number} x Current mouse/touch coordinates 1377 * @param {Number} y Current mouse/touch coordinates 1378 */ 1379 initMoveOrigin: function (x, y) { 1380 this.drag_dx = x - this.origin.scrCoords[1]; 1381 this.drag_dy = y - this.origin.scrCoords[2]; 1382 1383 this.mode = this.BOARD_MODE_MOVE_ORIGIN; 1384 this._change3DView = false; 1385 this.updateQuality = this.BOARD_QUALITY_LOW; 1386 }, 1387 1388 /** 1389 * Collects all elements below the current mouse pointer and fulfilling the following constraints: 1390 * <ul> 1391 * <li>isDraggable</li> 1392 * <li>visible</li> 1393 * <li>not fixed</li> 1394 * <li>not frozen</li> 1395 * </ul> 1396 * @param {Number} x Current mouse/touch coordinates 1397 * @param {Number} y current mouse/touch coordinates 1398 * @param {Object} evt An event object 1399 * @param {String} type What type of event? 'touch', 'mouse' or 'pen'. 1400 * @returns {Array} A list of geometric elements. 1401 */ 1402 initMoveObject: function (x, y, evt, type) { 1403 var pEl, 1404 el, 1405 collect = [], 1406 offset = [], 1407 haspoint, 1408 len = this.objectsList.length, 1409 dragEl = { visProp: { layer: -10000 } }; 1410 1411 // Store status of key presses for 3D movement 1412 this._shiftKey = evt.shiftKey; 1413 this._ctrlKey = evt.ctrlKey; 1414 1415 //for (el in this.objects) { 1416 for (el = 0; el < len; el++) { 1417 pEl = this.objectsList[el]; 1418 haspoint = pEl.hasPoint && pEl.hasPoint(x, y); 1419 1420 if (pEl.visPropCalc.visible && haspoint) { 1421 pEl.triggerEventHandlers([type + 'down', 'down'], [evt]); 1422 this.downObjects.push(pEl); 1423 } 1424 1425 if (haspoint && 1426 pEl.isDraggable && 1427 pEl.visPropCalc.visible && 1428 ((this.geonextCompatibilityMode && 1429 (Type.isPoint(pEl) || pEl.elementClass === Const.OBJECT_CLASS_TEXT)) || 1430 !this.geonextCompatibilityMode) && 1431 !pEl.evalVisProp('fixed') 1432 /*(!pEl.visProp.frozen) &&*/ 1433 ) { 1434 // Elements in the highest layer get priority. 1435 if ( 1436 pEl.visProp.layer > dragEl.visProp.layer || 1437 (pEl.visProp.layer === dragEl.visProp.layer && 1438 pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime()) 1439 ) { 1440 // If an element and its label have the focus 1441 // simultaneously, the element is taken. 1442 // This only works if we assume that every browser runs 1443 // through this.objects in the right order, i.e. an element A 1444 // added before element B turns up here before B does. 1445 if ( 1446 !this.attr.ignorelabels || 1447 !Type.exists(dragEl.label) || 1448 pEl !== dragEl.label 1449 ) { 1450 dragEl = pEl; 1451 collect.push(dragEl); 1452 1453 // Store offset for large coords elements. 1454 if (Type.exists(dragEl.coords)) { 1455 if (dragEl.elementClass === Const.OBJECT_CLASS_POINT || 1456 dragEl.relativeCoords // Relative texts like labels 1457 ) { 1458 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y])); 1459 } else { 1460 // Images and texts 1461 offset.push(Statistics.subtract(dragEl.actualCoords.scrCoords.slice(1), [x, y])); 1462 } 1463 } else { 1464 offset.push([0, 0]); 1465 } 1466 1467 // We can't drop out of this loop because of the event handling system 1468 //if (this.attr.takefirst) { 1469 // return collect; 1470 //} 1471 } 1472 } 1473 } 1474 } 1475 1476 if (this.attr.drag.enabled && collect.length > 0) { 1477 this.mode = this.BOARD_MODE_DRAG; 1478 } 1479 1480 // A one-element array is returned. 1481 if (this.attr.takefirst) { 1482 collect.length = 1; 1483 this._drag_offset = offset[0]; 1484 } else { 1485 collect = collect.slice(-1); 1486 this._drag_offset = offset[offset.length - 1]; 1487 } 1488 1489 if (!this._drag_offset) { 1490 this._drag_offset = [0, 0]; 1491 } 1492 1493 // Move drag element to the top of the layer 1494 if (this.renderer.type === 'svg' && Type.exists(collect[0]) && 1495 collect.length === 1 && Type.exists(collect[0].rendNode) 1496 ) { 1497 // Move object to top 1498 if (collect[0].evalVisProp('dragtotopoflayer')) { 1499 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode); 1500 } 1501 // Move object's label to top 1502 if (collect[0].hasLabel && 1503 collect[0].label.evalVisProp('display') === 'html' && 1504 collect[0].label.evalVisProp('dragtotopoflayer') 1505 ) { 1506 collect[0].label.rendNode.parentNode.appendChild(collect[0].label.rendNode); 1507 } 1508 } 1509 1510 // // Init rotation angle and scale factor for two finger movements 1511 // this.previousRotation = 0.0; 1512 // this.previousScale = 1.0; 1513 1514 if (collect.length >= 1) { 1515 collect[0].highlight(true); 1516 this.triggerEventHandlers(['mousehit', 'hit'], [evt, collect[0]]); 1517 } 1518 1519 return collect; 1520 }, 1521 1522 /** 1523 * Moves an object. 1524 * @param {Number} x Coordinate 1525 * @param {Number} y Coordinate 1526 * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}. 1527 * @param {Object} evt The event object. 1528 * @param {String} type Mouse or touch event? 1529 */ 1530 moveObject: function (x, y, o, evt, type) { 1531 var newPos = new Coords( 1532 Const.COORDS_BY_SCREEN, 1533 this.getScrCoordsOfMouse(x, y), 1534 this 1535 ), 1536 drag, 1537 dragScrCoords, 1538 newDragScrCoords; 1539 1540 if (!(o && o.obj)) { 1541 return; 1542 } 1543 drag = o.obj; 1544 1545 // Avoid updates for very small movements of coordsElements, see below 1546 if (drag.coords) { 1547 dragScrCoords = drag.coords.scrCoords.slice(); 1548 } 1549 1550 this.addLogEntry('drag', drag, newPos.usrCoords.slice(1)); 1551 1552 // Store the position and add the correctionvector from the mouse 1553 // position to the object's coords. 1554 this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]]; 1555 this.drag_position = Statistics.add(this.drag_position, this._drag_offset); 1556 1557 // Store status of key presses for 3D movement 1558 this._shiftKey = evt.shiftKey; 1559 this._ctrlKey = evt.ctrlKey; 1560 1561 // 1562 // We have to distinguish between CoordsElements and other elements like lines. 1563 // The latter need the difference between two move events. 1564 if (Type.exists(drag.coords)) { 1565 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position, [x, y]); 1566 } else { 1567 this.displayInfobox(false); 1568 // Hide infobox in case the user has touched an intersection point 1569 // and drags the underlying line now. 1570 1571 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) { 1572 drag.setPositionDirectly( 1573 Const.COORDS_BY_SCREEN, 1574 [newPos.scrCoords[1], newPos.scrCoords[2]], 1575 [o.targets[0].Xprev, o.targets[0].Yprev] 1576 ); 1577 } 1578 // Remember the actual position for the next move event. Then we are able to 1579 // compute the difference vector. 1580 o.targets[0].Xprev = newPos.scrCoords[1]; 1581 o.targets[0].Yprev = newPos.scrCoords[2]; 1582 } 1583 // This may be necessary for some gliders and labels 1584 if (Type.exists(drag.coords)) { 1585 drag.prepareUpdate().update(false).updateRenderer(); 1586 this.updateInfobox(drag); 1587 drag.prepareUpdate().update(true).updateRenderer(); 1588 } 1589 1590 if (drag.coords) { 1591 newDragScrCoords = drag.coords.scrCoords; 1592 } 1593 // No updates for very small movements of coordsElements 1594 if ( 1595 !drag.coords || 1596 dragScrCoords[1] !== newDragScrCoords[1] || 1597 dragScrCoords[2] !== newDragScrCoords[2] 1598 ) { 1599 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]); 1600 // Update all elements of the board 1601 this.update(drag); 1602 } 1603 drag.highlight(true); 1604 this.triggerEventHandlers(['mousehit', 'hit'], [evt, drag]); 1605 1606 drag.lastDragTime = new Date(); 1607 }, 1608 1609 /** 1610 * Moves elements in multitouch mode. 1611 * @param {Array} p1 x,y coordinates of first touch 1612 * @param {Array} p2 x,y coordinates of second touch 1613 * @param {Object} o The touch object that is dragged: {JXG.Board#touches}. 1614 * @param {Object} evt The event object that lead to this movement. 1615 */ 1616 twoFingerMove: function (o, id, evt) { 1617 var drag; 1618 1619 if (Type.exists(o) && Type.exists(o.obj)) { 1620 drag = o.obj; 1621 } else { 1622 return; 1623 } 1624 1625 if ( 1626 drag.elementClass === Const.OBJECT_CLASS_LINE || 1627 drag.type === Const.OBJECT_TYPE_POLYGON 1628 ) { 1629 this.twoFingerTouchObject(o.targets, drag, id); 1630 } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1631 this.twoFingerTouchCircle(o.targets, drag, id); 1632 } 1633 1634 if (evt) { 1635 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]); 1636 } 1637 }, 1638 1639 /** 1640 * Compute the transformation matrix to move an element according to the 1641 * previous and actual positions of finger 1 and finger 2. 1642 * See also https://math.stackexchange.com/questions/4010538/solve-for-2d-translation-rotation-and-scale-given-two-touch-point-movements 1643 * 1644 * @param {Object} finger1 Actual and previous position of finger 1 1645 * @param {Object} finger1 Actual and previous position of finger 1 1646 * @param {Boolean} scalable Flag if element may be scaled 1647 * @param {Boolean} rotatable Flag if element may be rotated 1648 * @returns {Array} 1649 */ 1650 getTwoFingerTransform(finger1, finger2, scalable, rotatable) { 1651 var crd, 1652 x1, y1, x2, y2, 1653 dx, dy, 1654 xx1, yy1, xx2, yy2, 1655 dxx, dyy, 1656 C, S, LL, tx, ty, lbda; 1657 1658 crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.Xprev, finger1.Yprev], this).usrCoords; 1659 x1 = crd[1]; 1660 y1 = crd[2]; 1661 crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.Xprev, finger2.Yprev], this).usrCoords; 1662 x2 = crd[1]; 1663 y2 = crd[2]; 1664 1665 crd = new Coords(Const.COORDS_BY_SCREEN, [finger1.X, finger1.Y], this).usrCoords; 1666 xx1 = crd[1]; 1667 yy1 = crd[2]; 1668 crd = new Coords(Const.COORDS_BY_SCREEN, [finger2.X, finger2.Y], this).usrCoords; 1669 xx2 = crd[1]; 1670 yy2 = crd[2]; 1671 1672 dx = x2 - x1; 1673 dy = y2 - y1; 1674 dxx = xx2 - xx1; 1675 dyy = yy2 - yy1; 1676 1677 LL = dx * dx + dy * dy; 1678 C = (dxx * dx + dyy * dy) / LL; 1679 S = (dyy * dx - dxx * dy) / LL; 1680 if (!scalable) { 1681 lbda = Mat.hypot(C, S); 1682 C /= lbda; 1683 S /= lbda; 1684 } 1685 if (!rotatable) { 1686 S = 0; 1687 } 1688 tx = 0.5 * (xx1 + xx2 - C * (x1 + x2) + S * (y1 + y2)); 1689 ty = 0.5 * (yy1 + yy2 - S * (x1 + x2) - C * (y1 + y2)); 1690 1691 return [1, 0, 0, 1692 tx, C, -S, 1693 ty, S, C]; 1694 }, 1695 1696 /** 1697 * Moves, rotates and scales a line or polygon with two fingers. 1698 * <p> 1699 * If one vertex of the polygon snaps to the grid or to points or is not draggable, 1700 * two-finger-movement is cancelled. 1701 * 1702 * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}. 1703 * @param {object} drag The object that is dragged: 1704 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1705 */ 1706 twoFingerTouchObject: function (tar, drag, id) { 1707 var t, T, 1708 ar, i, len, 1709 snap = false; 1710 1711 if ( 1712 Type.exists(tar[0]) && 1713 Type.exists(tar[1]) && 1714 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev) 1715 ) { 1716 1717 T = this.getTwoFingerTransform( 1718 tar[0], tar[1], 1719 drag.evalVisProp('scalable'), 1720 drag.evalVisProp('rotatable')); 1721 t = this.create('transform', T, { type: 'generic' }); 1722 t.update(); 1723 1724 if (drag.elementClass === Const.OBJECT_CLASS_LINE) { 1725 ar = []; 1726 if (drag.point1.draggable()) { 1727 ar.push(drag.point1); 1728 } 1729 if (drag.point2.draggable()) { 1730 ar.push(drag.point2); 1731 } 1732 t.applyOnce(ar); 1733 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) { 1734 len = drag.vertices.length - 1; 1735 snap = drag.evalVisProp('snaptogrid') || drag.evalVisProp('snaptopoints'); 1736 for (i = 0; i < len && !snap; ++i) { 1737 snap = snap || drag.vertices[i].evalVisProp('snaptogrid') || drag.vertices[i].evalVisProp('snaptopoints'); 1738 snap = snap || (!drag.vertices[i].draggable()); 1739 } 1740 if (!snap) { 1741 ar = []; 1742 for (i = 0; i < len; ++i) { 1743 if (drag.vertices[i].draggable()) { 1744 ar.push(drag.vertices[i]); 1745 } 1746 } 1747 t.applyOnce(ar); 1748 } 1749 } 1750 1751 this.update(); 1752 drag.highlight(true); 1753 } 1754 }, 1755 1756 /* 1757 * Moves, rotates and scales a circle with two fingers. 1758 * @param {Array} tar Array containing touch event objects: {JXG.Board#touches.targets}. 1759 * @param {object} drag The object that is dragged: 1760 * @param {Number} id pointerId of the event. In case of old touch event this is emulated. 1761 */ 1762 twoFingerTouchCircle: function (tar, drag, id) { 1763 var fixEl, moveEl, np, op, fix, d, alpha, t1, t2, t3, t4; 1764 1765 if (drag.method === 'pointCircle' || drag.method === 'pointLine') { 1766 return; 1767 } 1768 1769 if ( 1770 Type.exists(tar[0]) && 1771 Type.exists(tar[1]) && 1772 !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev) 1773 ) { 1774 if (id === tar[0].num) { 1775 fixEl = tar[1]; 1776 moveEl = tar[0]; 1777 } else { 1778 fixEl = tar[0]; 1779 moveEl = tar[1]; 1780 } 1781 1782 fix = new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this) 1783 .usrCoords; 1784 // Previous finger position 1785 op = new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this) 1786 .usrCoords; 1787 // New finger position 1788 np = new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this).usrCoords; 1789 1790 alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1)); 1791 1792 // Rotate and scale by the movement of the second finger 1793 t1 = this.create('transform', [-fix[1], -fix[2]], { 1794 type: 'translate' 1795 }); 1796 t2 = this.create('transform', [alpha], { type: 'rotate' }); 1797 t1.melt(t2); 1798 if (drag.evalVisProp('scalable')) { 1799 d = Geometry.distance(fix, np) / Geometry.distance(fix, op); 1800 t3 = this.create('transform', [d, d], { type: 'scale' }); 1801 t1.melt(t3); 1802 } 1803 t4 = this.create('transform', [fix[1], fix[2]], { 1804 type: 'translate' 1805 }); 1806 t1.melt(t4); 1807 1808 if (drag.center.draggable()) { 1809 t1.applyOnce([drag.center]); 1810 } 1811 1812 if (drag.method === 'twoPoints') { 1813 if (drag.point2.draggable()) { 1814 t1.applyOnce([drag.point2]); 1815 } 1816 } else if (drag.method === 'pointRadius') { 1817 if (Type.isNumber(drag.updateRadius.origin)) { 1818 drag.setRadius(drag.radius * d); 1819 } 1820 } 1821 1822 this.update(drag.center); 1823 drag.highlight(true); 1824 } 1825 }, 1826 1827 highlightElements: function (x, y, evt, target) { 1828 var el, 1829 pEl, 1830 pId, 1831 overObjects = {}, 1832 len = this.objectsList.length; 1833 1834 // Elements below the mouse pointer which are not highlighted yet will be highlighted. 1835 for (el = 0; el < len; el++) { 1836 pEl = this.objectsList[el]; 1837 pId = pEl.id; 1838 if ( 1839 Type.exists(pEl.hasPoint) && 1840 pEl.visPropCalc.visible && 1841 pEl.hasPoint(x, y) 1842 ) { 1843 // this is required in any case because otherwise the box won't be shown until the point is dragged 1844 this.updateInfobox(pEl); 1845 1846 if (!Type.exists(this.highlightedObjects[pId])) { 1847 // highlight only if not highlighted 1848 overObjects[pId] = pEl; 1849 pEl.highlight(); 1850 // triggers board event. 1851 this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]); 1852 } 1853 1854 if (pEl.mouseover) { 1855 pEl.triggerEventHandlers(['mousemove', 'move'], [evt]); 1856 } else { 1857 pEl.triggerEventHandlers(['mouseover', 'over'], [evt]); 1858 pEl.mouseover = true; 1859 } 1860 } 1861 } 1862 1863 for (el = 0; el < len; el++) { 1864 pEl = this.objectsList[el]; 1865 pId = pEl.id; 1866 if (pEl.mouseover) { 1867 if (!overObjects[pId]) { 1868 pEl.triggerEventHandlers(['mouseout', 'out'], [evt]); 1869 pEl.mouseover = false; 1870 } 1871 } 1872 } 1873 }, 1874 1875 /** 1876 * Helper function which returns a reasonable starting point for the object being dragged. 1877 * Formerly known as initXYstart(). 1878 * @private 1879 * @param {JXG.GeometryElement} obj The object to be dragged 1880 * @param {Array} targets Array of targets. It is changed by this function. 1881 */ 1882 saveStartPos: function (obj, targets) { 1883 var xy = [], 1884 i, 1885 len; 1886 1887 if (obj.type === Const.OBJECT_TYPE_TICKS) { 1888 xy.push([1, NaN, NaN]); 1889 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) { 1890 xy.push(obj.point1.coords.usrCoords); 1891 xy.push(obj.point2.coords.usrCoords); 1892 } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1893 xy.push(obj.center.coords.usrCoords); 1894 if (obj.method === 'twoPoints') { 1895 xy.push(obj.point2.coords.usrCoords); 1896 } 1897 } else if (obj.type === Const.OBJECT_TYPE_POLYGON) { 1898 len = obj.vertices.length - 1; 1899 for (i = 0; i < len; i++) { 1900 xy.push(obj.vertices[i].coords.usrCoords); 1901 } 1902 } else if (obj.type === Const.OBJECT_TYPE_SECTOR) { 1903 xy.push(obj.point1.coords.usrCoords); 1904 xy.push(obj.point2.coords.usrCoords); 1905 xy.push(obj.point3.coords.usrCoords); 1906 } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) { 1907 xy.push(obj.coords.usrCoords); 1908 } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) { 1909 // if (Type.exists(obj.parents)) { 1910 // len = obj.parents.length; 1911 // if (len > 0) { 1912 // for (i = 0; i < len; i++) { 1913 // xy.push(this.select(obj.parents[i]).coords.usrCoords); 1914 // } 1915 // } else 1916 // } 1917 if (obj.points.length > 0) { 1918 xy.push(obj.points[0].usrCoords); 1919 } 1920 } else { 1921 try { 1922 xy.push(obj.coords.usrCoords); 1923 } catch (e) { 1924 JXG.debug( 1925 'JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e 1926 ); 1927 } 1928 } 1929 1930 len = xy.length; 1931 for (i = 0; i < len; i++) { 1932 targets.Zstart.push(xy[i][0]); 1933 targets.Xstart.push(xy[i][1]); 1934 targets.Ystart.push(xy[i][2]); 1935 } 1936 }, 1937 1938 mouseOriginMoveStart: function (evt) { 1939 var r, pos; 1940 1941 r = this._isRequiredKeyPressed(evt, 'pan'); 1942 if (r) { 1943 pos = this.getMousePosition(evt); 1944 this.initMoveOrigin(pos[0], pos[1]); 1945 } 1946 1947 return r; 1948 }, 1949 1950 mouseOriginMove: function (evt) { 1951 var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN, 1952 pos; 1953 1954 if (r) { 1955 pos = this.getMousePosition(evt); 1956 this.moveOrigin(pos[0], pos[1], true); 1957 } 1958 1959 return r; 1960 }, 1961 1962 /** 1963 * Start moving the origin with one finger. 1964 * @private 1965 * @param {Object} evt Event from touchStartListener 1966 * @return {Boolean} returns if the origin is moved. 1967 */ 1968 touchStartMoveOriginOneFinger: function (evt) { 1969 var touches = evt['touches'], 1970 conditions, 1971 pos; 1972 1973 conditions = 1974 this.attr.pan.enabled && !this.attr.pan.needtwofingers && touches.length === 1; 1975 1976 if (conditions) { 1977 pos = this.getMousePosition(evt, 0); 1978 this.initMoveOrigin(pos[0], pos[1]); 1979 } 1980 1981 return conditions; 1982 }, 1983 1984 /** 1985 * Move the origin with one finger 1986 * @private 1987 * @param {Object} evt Event from touchMoveListener 1988 * @return {Boolean} returns if the origin is moved. 1989 */ 1990 touchOriginMove: function (evt) { 1991 var r = this.mode === this.BOARD_MODE_MOVE_ORIGIN, 1992 pos; 1993 1994 if (r) { 1995 pos = this.getMousePosition(evt, 0); 1996 this.moveOrigin(pos[0], pos[1], true); 1997 } 1998 1999 return r; 2000 }, 2001 2002 /** 2003 * Stop moving the origin with one finger 2004 * @return {null} null 2005 * @private 2006 */ 2007 originMoveEnd: function () { 2008 this.updateQuality = this.BOARD_QUALITY_HIGH; 2009 this.mode = this.BOARD_MODE_NONE; 2010 }, 2011 2012 /********************************************************** 2013 * 2014 * Event Handler 2015 * 2016 **********************************************************/ 2017 2018 /** 2019 * Suppresses the default event handling. 2020 * Used for context menu. 2021 * 2022 * @param {Event} e 2023 * @returns {Boolean} false 2024 */ 2025 suppressDefault: function (e) { 2026 if (Type.exists(e)) { 2027 e.preventDefault(); 2028 } 2029 return false; 2030 }, 2031 2032 /** 2033 * Add all possible event handlers to the board object 2034 * that move objects, i.e. mouse, pointer and touch events. 2035 */ 2036 addEventHandlers: function () { 2037 if (Env.supportsPointerEvents()) { 2038 this.addPointerEventHandlers(); 2039 } else { 2040 this.addMouseEventHandlers(); 2041 this.addTouchEventHandlers(); 2042 } 2043 2044 if (this.containerObj !== null) { 2045 // this.containerObj.oncontextmenu = this.suppressDefault; 2046 Env.addEvent(this.containerObj, 'contextmenu', this.suppressDefault, this); 2047 } 2048 2049 // This one produces errors on IE 2050 // // Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this); 2051 // This one works on IE, Firefox and Chromium with default configurations. On some Safari 2052 // or Opera versions the user must explicitly allow the deactivation of the context menu. 2053 }, 2054 2055 /** 2056 * Remove all event handlers from the board object 2057 */ 2058 removeEventHandlers: function () { 2059 if ((this.hasPointerHandlers || this.hasMouseHandlers || this.hasTouchHandlers) && 2060 this.containerObj !== null 2061 ) { 2062 Env.removeEvent(this.containerObj, 'contextmenu', this.suppressDefault, this); 2063 } 2064 2065 this.removeMouseEventHandlers(); 2066 this.removeTouchEventHandlers(); 2067 this.removePointerEventHandlers(); 2068 2069 this.removeFullscreenEventHandlers(); 2070 this.removeKeyboardEventHandlers(); 2071 this.removeResizeEventHandlers(); 2072 2073 // if (Env.isBrowser) { 2074 // if (Type.exists(this.resizeObserver)) { 2075 // this.stopResizeObserver(); 2076 // } else { 2077 // Env.removeEvent(window, 'resize', this.resizeListener, this); 2078 // this.stopIntersectionObserver(); 2079 // } 2080 // Env.removeEvent(window, 'scroll', this.scrollListener, this); 2081 // } 2082 }, 2083 2084 /** 2085 * Add resize related event handlers 2086 * 2087 */ 2088 addResizeEventHandlers: function () { 2089 // var that = this; 2090 2091 this.resizeHandlers = []; 2092 if (Env.isBrowser) { 2093 try { 2094 // Supported by all new browsers 2095 // resizeObserver: triggered if size of the JSXGraph div changes. 2096 this.startResizeObserver(); 2097 this.resizeHandlers.push('resizeobserver'); 2098 } catch (err) { 2099 // Certain Safari and edge version do not support 2100 // resizeObserver, but intersectionObserver. 2101 // resize event: triggered if size of window changes 2102 Env.addEvent(window, 'resize', this.resizeListener, this); 2103 // intersectionObserver: triggered if JSXGraph becomes visible. 2104 this.startIntersectionObserver(); 2105 this.resizeHandlers.push('resize'); 2106 } 2107 // Scroll event: needs to be captured since on mobile devices 2108 // sometimes a header bar is displayed / hidden, which triggers a 2109 // resize event. 2110 Env.addEvent(window, 'scroll', this.scrollListener, this); 2111 this.resizeHandlers.push('scroll'); 2112 2113 // On browser print: 2114 // we need to call the listener when having @media: print. 2115 try { 2116 // window.matchMedia('print').addEventListener('change', this.printListenerMatch.apply(this, arguments)); 2117 window.matchMedia('print').addEventListener('change', this.printListenerMatch.bind(this)); 2118 window.matchMedia('screen').addEventListener('change', this.printListenerMatch.bind(this)); 2119 this.resizeHandlers.push('print'); 2120 } catch (err) { 2121 JXG.debug("Error adding printListener", err); 2122 } 2123 // if (Type.isFunction(MediaQueryList.prototype.addEventListener)) { 2124 // window.matchMedia('print').addEventListener('change', function (mql) { 2125 // if (mql.matches) { 2126 // that.printListener(); 2127 // } 2128 // }); 2129 // } else if (Type.isFunction(MediaQueryList.prototype.addListener)) { // addListener might be deprecated 2130 // window.matchMedia('print').addListener(function (mql, ev) { 2131 // if (mql.matches) { 2132 // that.printListener(ev); 2133 // } 2134 // }); 2135 // } 2136 2137 // When closing the print dialog we again have to resize. 2138 // Env.addEvent(window, 'afterprint', this.printListener, this); 2139 // this.resizeHandlers.push('afterprint'); 2140 } 2141 }, 2142 2143 /** 2144 * Remove resize related event handlers 2145 * 2146 */ 2147 removeResizeEventHandlers: function () { 2148 var i, e; 2149 if (this.resizeHandlers.length > 0 && Env.isBrowser) { 2150 for (i = 0; i < this.resizeHandlers.length; i++) { 2151 e = this.resizeHandlers[i]; 2152 switch (e) { 2153 case 'resizeobserver': 2154 if (Type.exists(this.resizeObserver)) { 2155 this.stopResizeObserver(); 2156 } 2157 break; 2158 case 'resize': 2159 Env.removeEvent(window, 'resize', this.resizeListener, this); 2160 if (Type.exists(this.intersectionObserver)) { 2161 this.stopIntersectionObserver(); 2162 } 2163 break; 2164 case 'scroll': 2165 Env.removeEvent(window, 'scroll', this.scrollListener, this); 2166 break; 2167 case 'print': 2168 window.matchMedia('print').removeEventListener('change', this.printListenerMatch.bind(this), false); 2169 window.matchMedia('screen').removeEventListener('change', this.printListenerMatch.bind(this), false); 2170 break; 2171 // case 'afterprint': 2172 // Env.removeEvent(window, 'afterprint', this.printListener, this); 2173 // break; 2174 } 2175 } 2176 this.resizeHandlers = []; 2177 } 2178 }, 2179 2180 2181 /** 2182 * Registers pointer event handlers. 2183 */ 2184 addPointerEventHandlers: function () { 2185 if (!this.hasPointerHandlers && Env.isBrowser) { 2186 var moveTarget = this.attr.movetarget || this.containerObj; 2187 2188 if (window.navigator.msPointerEnabled) { 2189 // IE10- 2190 // Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 2191 Env.addEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this); 2192 Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 2193 } else { 2194 // Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 2195 Env.addEvent(moveTarget, 'pointerdown', this.pointerDownListener, this); 2196 Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 2197 Env.addEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 2198 Env.addEvent(moveTarget, 'click', this.pointerClickListener, this); 2199 Env.addEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this); 2200 } 2201 2202 if (this.containerObj !== null) { 2203 // This is needed for capturing touch events. 2204 // It is in jsxgraph.css, for ms-touch-action... 2205 this.containerObj.style.touchAction = 'none'; 2206 // this.containerObj.style.touchAction = 'auto'; 2207 } 2208 2209 this.hasPointerHandlers = true; 2210 } 2211 }, 2212 2213 /** 2214 * Registers mouse move, down and wheel event handlers. 2215 */ 2216 addMouseEventHandlers: function () { 2217 if (!this.hasMouseHandlers && Env.isBrowser) { 2218 var moveTarget = this.attr.movetarget || this.containerObj; 2219 2220 // Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 2221 Env.addEvent(moveTarget, 'mousedown', this.mouseDownListener, this); 2222 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 2223 Env.addEvent(moveTarget, 'click', this.mouseClickListener, this); 2224 Env.addEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this); 2225 2226 this.hasMouseHandlers = true; 2227 } 2228 }, 2229 2230 /** 2231 * Register touch start and move and gesture start and change event handlers. 2232 * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers 2233 * will not be registered. 2234 * 2235 * Since iOS 13, touch events were abandoned in favour of pointer events 2236 */ 2237 addTouchEventHandlers: function (appleGestures) { 2238 if (!this.hasTouchHandlers && Env.isBrowser) { 2239 var moveTarget = this.attr.movetarget || this.containerObj; 2240 2241 // Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 2242 Env.addEvent(moveTarget, 'touchstart', this.touchStartListener, this); 2243 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 2244 2245 /* 2246 if (!Type.exists(appleGestures) || appleGestures) { 2247 // Gesture listener are called in touchStart and touchMove. 2248 //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 2249 //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 2250 } 2251 */ 2252 2253 this.hasTouchHandlers = true; 2254 } 2255 }, 2256 2257 /** 2258 * Registers pointer event handlers. 2259 */ 2260 addWheelEventHandlers: function () { 2261 if (!this.hasWheelHandlers && Env.isBrowser) { 2262 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2263 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 2264 this.hasWheelHandlers = true; 2265 } 2266 }, 2267 2268 /** 2269 * Add fullscreen events which update the CSS transformation matrix to correct 2270 * the mouse/touch/pointer positions in case of CSS transformations. 2271 */ 2272 addFullscreenEventHandlers: function () { 2273 var i, 2274 // standard/Edge, firefox, chrome/safari, IE11 2275 events = [ 2276 'fullscreenchange', 2277 'mozfullscreenchange', 2278 'webkitfullscreenchange', 2279 'msfullscreenchange' 2280 ], 2281 le = events.length; 2282 2283 if (!this.hasFullscreenEventHandlers && Env.isBrowser) { 2284 for (i = 0; i < le; i++) { 2285 Env.addEvent(this.document, events[i], this.fullscreenListener, this); 2286 } 2287 this.hasFullscreenEventHandlers = true; 2288 } 2289 }, 2290 2291 /** 2292 * Register keyboard event handlers. 2293 */ 2294 addKeyboardEventHandlers: function () { 2295 if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) { 2296 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this); 2297 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 2298 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 2299 this.hasKeyboardHandlers = true; 2300 } 2301 }, 2302 2303 /** 2304 * Remove all registered touch event handlers. 2305 */ 2306 removeKeyboardEventHandlers: function () { 2307 if (this.hasKeyboardHandlers && Env.isBrowser) { 2308 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this); 2309 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 2310 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 2311 this.hasKeyboardHandlers = false; 2312 } 2313 }, 2314 2315 /** 2316 * Remove all registered event handlers regarding fullscreen mode. 2317 */ 2318 removeFullscreenEventHandlers: function () { 2319 var i, 2320 // standard/Edge, firefox, chrome/safari, IE11 2321 events = [ 2322 'fullscreenchange', 2323 'mozfullscreenchange', 2324 'webkitfullscreenchange', 2325 'msfullscreenchange' 2326 ], 2327 le = events.length; 2328 2329 if (this.hasFullscreenEventHandlers && Env.isBrowser) { 2330 for (i = 0; i < le; i++) { 2331 Env.removeEvent(this.document, events[i], this.fullscreenListener, this); 2332 } 2333 this.hasFullscreenEventHandlers = false; 2334 } 2335 }, 2336 2337 /** 2338 * Remove MSPointer* Event handlers. 2339 */ 2340 removePointerEventHandlers: function () { 2341 if (this.hasPointerHandlers && Env.isBrowser) { 2342 var moveTarget = this.attr.movetarget || this.containerObj; 2343 2344 if (window.navigator.msPointerEnabled) { 2345 // IE10- 2346 // Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 2347 Env.removeEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this); 2348 Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 2349 } else { 2350 // Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 2351 Env.removeEvent(moveTarget, 'pointerdown', this.pointerDownListener, this); 2352 Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 2353 Env.removeEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 2354 Env.removeEvent(moveTarget, 'click', this.pointerClickListener, this); 2355 Env.removeEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this); 2356 } 2357 2358 if (this.hasWheelHandlers) { 2359 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2360 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 2361 } 2362 2363 if (this.hasPointerUp) { 2364 if (window.navigator.msPointerEnabled) { 2365 // IE10- 2366 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2367 } else { 2368 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 2369 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2370 } 2371 this.hasPointerUp = false; 2372 } 2373 2374 this.hasPointerHandlers = false; 2375 } 2376 }, 2377 2378 /** 2379 * De-register mouse event handlers. 2380 */ 2381 removeMouseEventHandlers: function () { 2382 if (this.hasMouseHandlers && Env.isBrowser) { 2383 var moveTarget = this.attr.movetarget || this.containerObj; 2384 2385 // Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 2386 Env.removeEvent(moveTarget, 'mousedown', this.mouseDownListener, this); 2387 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 2388 Env.removeEvent(moveTarget, 'click', this.mouseClickListener, this); 2389 Env.removeEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this); 2390 2391 if (this.hasMouseUp) { 2392 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 2393 this.hasMouseUp = false; 2394 } 2395 2396 if (this.hasWheelHandlers) { 2397 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2398 Env.removeEvent( 2399 this.containerObj, 2400 'DOMMouseScroll', 2401 this.mouseWheelListener, 2402 this 2403 ); 2404 } 2405 2406 this.hasMouseHandlers = false; 2407 } 2408 }, 2409 2410 /** 2411 * Remove all registered touch event handlers. 2412 */ 2413 removeTouchEventHandlers: function () { 2414 if (this.hasTouchHandlers && Env.isBrowser) { 2415 var moveTarget = this.attr.movetarget || this.containerObj; 2416 2417 // Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 2418 Env.removeEvent(moveTarget, 'touchstart', this.touchStartListener, this); 2419 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 2420 2421 if (this.hasTouchEnd) { 2422 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 2423 this.hasTouchEnd = false; 2424 } 2425 2426 this.hasTouchHandlers = false; 2427 } 2428 }, 2429 2430 /** 2431 * Handler for click on left arrow in the navigation bar 2432 * @returns {JXG.Board} Reference to the board 2433 */ 2434 clickLeftArrow: function () { 2435 this.moveOrigin( 2436 this.origin.scrCoords[1] + this.canvasWidth * 0.1, 2437 this.origin.scrCoords[2] 2438 ); 2439 return this; 2440 }, 2441 2442 /** 2443 * Handler for click on right arrow in the navigation bar 2444 * @returns {JXG.Board} Reference to the board 2445 */ 2446 clickRightArrow: function () { 2447 this.moveOrigin( 2448 this.origin.scrCoords[1] - this.canvasWidth * 0.1, 2449 this.origin.scrCoords[2] 2450 ); 2451 return this; 2452 }, 2453 2454 /** 2455 * Handler for click on up arrow in the navigation bar 2456 * @returns {JXG.Board} Reference to the board 2457 */ 2458 clickUpArrow: function () { 2459 this.moveOrigin( 2460 this.origin.scrCoords[1], 2461 this.origin.scrCoords[2] - this.canvasHeight * 0.1 2462 ); 2463 return this; 2464 }, 2465 2466 /** 2467 * Handler for click on down arrow in the navigation bar 2468 * @returns {JXG.Board} Reference to the board 2469 */ 2470 clickDownArrow: function () { 2471 this.moveOrigin( 2472 this.origin.scrCoords[1], 2473 this.origin.scrCoords[2] + this.canvasHeight * 0.1 2474 ); 2475 return this; 2476 }, 2477 2478 /** 2479 * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. 2480 * Works on iOS/Safari and Android. 2481 * @param {Event} evt Browser event object 2482 * @returns {Boolean} 2483 */ 2484 gestureChangeListener: function (evt) { 2485 var c, 2486 dir1 = [], 2487 dir2 = [], 2488 angle, 2489 mi = 10, 2490 isPinch = false, 2491 // Save zoomFactors 2492 zx = this.attr.zoom.factorx, 2493 zy = this.attr.zoom.factory, 2494 factor, dist, theta, bound, 2495 zoomCenter, 2496 doZoom = false, 2497 dx, dy, cx, cy; 2498 2499 if (this.mode !== this.BOARD_MODE_ZOOM) { 2500 return true; 2501 } 2502 evt.preventDefault(); 2503 2504 dist = Geometry.distance( 2505 [evt.touches[0].clientX, evt.touches[0].clientY], 2506 [evt.touches[1].clientX, evt.touches[1].clientY], 2507 2 2508 ); 2509 2510 // Android pinch to zoom 2511 // evt.scale was available in iOS touch events (pre iOS 13) 2512 // evt.scale is undefined in Android 2513 if (evt.scale === undefined) { 2514 evt.scale = dist / this.prevDist; 2515 } 2516 2517 if (!Type.exists(this.prevCoords)) { 2518 return false; 2519 } 2520 // Compute the angle of the two finger directions 2521 dir1 = [ 2522 evt.touches[0].clientX - this.prevCoords[0][0], 2523 evt.touches[0].clientY - this.prevCoords[0][1] 2524 ]; 2525 dir2 = [ 2526 evt.touches[1].clientX - this.prevCoords[1][0], 2527 evt.touches[1].clientY - this.prevCoords[1][1] 2528 ]; 2529 2530 if ( 2531 dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi && 2532 dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi 2533 ) { 2534 return false; 2535 } 2536 2537 angle = Geometry.rad(dir1, [0, 0], dir2); 2538 if ( 2539 this.isPreviousGesture !== 'pan' && 2540 Math.abs(angle) > Math.PI * 0.2 && 2541 Math.abs(angle) < Math.PI * 1.8 2542 ) { 2543 isPinch = true; 2544 } 2545 2546 if (this.isPreviousGesture !== 'pan' && !isPinch) { 2547 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) { 2548 isPinch = true; 2549 } 2550 } 2551 2552 factor = evt.scale / this.prevScale; 2553 this.prevScale = evt.scale; 2554 this.prevCoords = [ 2555 [evt.touches[0].clientX, evt.touches[0].clientY], 2556 [evt.touches[1].clientX, evt.touches[1].clientY] 2557 ]; 2558 2559 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this); 2560 2561 if (this.attr.pan.enabled && this.attr.pan.needtwofingers && !isPinch) { 2562 // Pan detected 2563 this.isPreviousGesture = 'pan'; 2564 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true); 2565 2566 } else if (this.attr.zoom.enabled && Math.abs(factor - 1.0) < 0.5) { 2567 doZoom = false; 2568 zoomCenter = this.attr.zoom.center; 2569 // Pinch detected 2570 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) { 2571 dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX); 2572 dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY); 2573 theta = Math.abs(Math.atan2(dy, dx)); 2574 bound = (Math.PI * this.attr.zoom.pinchsensitivity) / 90.0; 2575 } 2576 2577 if (!this.keepaspectratio && 2578 this.attr.zoom.pinchhorizontal && 2579 theta < bound) { 2580 this.attr.zoom.factorx = factor; 2581 this.attr.zoom.factory = 1.0; 2582 cx = 0; 2583 cy = 0; 2584 doZoom = true; 2585 } else if (!this.keepaspectratio && 2586 this.attr.zoom.pinchvertical && 2587 Math.abs(theta - Math.PI * 0.5) < bound 2588 ) { 2589 this.attr.zoom.factorx = 1.0; 2590 this.attr.zoom.factory = factor; 2591 cx = 0; 2592 cy = 0; 2593 doZoom = true; 2594 } else if (this.attr.zoom.pinch) { 2595 this.attr.zoom.factorx = factor; 2596 this.attr.zoom.factory = factor; 2597 cx = c.usrCoords[1]; 2598 cy = c.usrCoords[2]; 2599 doZoom = true; 2600 } 2601 2602 if (doZoom) { 2603 if (zoomCenter === 'board') { 2604 this.zoomIn(); 2605 } else { // including zoomCenter === 'auto' 2606 this.zoomIn(cx, cy); 2607 } 2608 2609 // Restore zoomFactors 2610 this.attr.zoom.factorx = zx; 2611 this.attr.zoom.factory = zy; 2612 } 2613 } 2614 2615 return false; 2616 }, 2617 2618 /** 2619 * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari, 2620 * on Android we emulate it. 2621 * @param {Event} evt 2622 * @returns {Boolean} 2623 */ 2624 gestureStartListener: function (evt) { 2625 var pos; 2626 2627 evt.preventDefault(); 2628 this.prevScale = 1.0; 2629 // Android pinch to zoom 2630 this.prevDist = Geometry.distance( 2631 [evt.touches[0].clientX, evt.touches[0].clientY], 2632 [evt.touches[1].clientX, evt.touches[1].clientY], 2633 2 2634 ); 2635 this.prevCoords = [ 2636 [evt.touches[0].clientX, evt.touches[0].clientY], 2637 [evt.touches[1].clientX, evt.touches[1].clientY] 2638 ]; 2639 this.isPreviousGesture = 'none'; 2640 2641 // If pinch-to-zoom is interpreted as panning 2642 // we have to prepare move origin 2643 pos = this.getMousePosition(evt, 0); 2644 this.initMoveOrigin(pos[0], pos[1]); 2645 2646 this.mode = this.BOARD_MODE_ZOOM; 2647 this._change3DView = false; 2648 return false; 2649 }, 2650 2651 /** 2652 * Test if the required key combination is pressed for wheel zoom, move origin and 2653 * selection 2654 * @private 2655 * @param {Object} evt Mouse or pen event 2656 * @param {String} action String containing the action: 'zoom', 'pan', 'selection'. 2657 * Corresponds to the attribute subobject. 2658 * @return {Boolean} true or false. 2659 */ 2660 _isRequiredKeyPressed: function (evt, action) { 2661 var obj = this.attr[action]; 2662 if (!obj.enabled) { 2663 return false; 2664 } 2665 2666 if ( 2667 ((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) && 2668 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey)) 2669 ) { 2670 return true; 2671 } 2672 2673 return false; 2674 }, 2675 2676 /* 2677 * Pointer events 2678 */ 2679 2680 /** 2681 * 2682 * Check if pointer event is already registered in {@link JXG.Board#_board_touches}. 2683 * 2684 * @param {Object} evt Event object 2685 * @return {Boolean} true if down event has already been sent. 2686 * @private 2687 */ 2688 _isPointerRegistered: function (evt) { 2689 var i, 2690 len = this._board_touches.length; 2691 2692 for (i = 0; i < len; i++) { 2693 if (this._board_touches[i].pointerId === evt.pointerId) { 2694 return true; 2695 } 2696 } 2697 return false; 2698 }, 2699 2700 /** 2701 * 2702 * Store the position of a pointer event. 2703 * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}. 2704 * Allows to follow the path of that finger on the screen. 2705 * Only two simultaneous touches are supported. 2706 * 2707 * @param {Object} evt Event object 2708 * @returns {JXG.Board} Reference to the board 2709 * @private 2710 */ 2711 _pointerStorePosition: function (evt) { 2712 var i, found; 2713 2714 for (i = 0, found = false; i < this._board_touches.length; i++) { 2715 if (this._board_touches[i].pointerId === evt.pointerId) { 2716 this._board_touches[i].clientX = evt.clientX; 2717 this._board_touches[i].clientY = evt.clientY; 2718 found = true; 2719 break; 2720 } 2721 } 2722 2723 // Restrict the number of simultaneous touches to 2 2724 if (!found && this._board_touches.length < 2) { 2725 this._board_touches.push({ 2726 pointerId: evt.pointerId, 2727 clientX: evt.clientX, 2728 clientY: evt.clientY 2729 }); 2730 } 2731 2732 return this; 2733 }, 2734 2735 /** 2736 * Deregisters a pointer event in {@link JXG.Board#_board_touches}. 2737 * It happens if a finger has been lifted from the screen. 2738 * 2739 * @param {Object} evt Event object 2740 * @returns {JXG.Board} Reference to the board 2741 * @private 2742 */ 2743 _pointerRemoveTouches: function (evt) { 2744 var i; 2745 for (i = 0; i < this._board_touches.length; i++) { 2746 if (this._board_touches[i].pointerId === evt.pointerId) { 2747 this._board_touches.splice(i, 1); 2748 break; 2749 } 2750 } 2751 2752 return this; 2753 }, 2754 2755 /** 2756 * Remove all registered fingers from {@link JXG.Board#_board_touches}. 2757 * This might be necessary if too many fingers have been registered. 2758 * @returns {JXG.Board} Reference to the board 2759 * @private 2760 */ 2761 _pointerClearTouches: function (pId) { 2762 // var i; 2763 // if (pId) { 2764 // for (i = 0; i < this._board_touches.length; i++) { 2765 // if (pId === this._board_touches[i].pointerId) { 2766 // this._board_touches.splice(i, i); 2767 // break; 2768 // } 2769 // } 2770 // } else { 2771 // } 2772 if (this._board_touches.length > 0) { 2773 this.dehighlightAll(); 2774 } 2775 this.updateQuality = this.BOARD_QUALITY_HIGH; 2776 this.mode = this.BOARD_MODE_NONE; 2777 this._board_touches = []; 2778 this.touches = []; 2779 }, 2780 2781 /** 2782 * Determine which input device is used for this action. 2783 * Possible devices are 'touch', 'pen' and 'mouse'. 2784 * This affects the precision and certain events. 2785 * In case of no browser, 'mouse' is used. 2786 * 2787 * @see JXG.Board#pointerDownListener 2788 * @see JXG.Board#pointerMoveListener 2789 * @see JXG.Board#initMoveObject 2790 * @see JXG.Board#moveObject 2791 * 2792 * @param {Event} evt The browsers event object. 2793 * @returns {String} 'mouse', 'pen', or 'touch' 2794 * @private 2795 */ 2796 _getPointerInputDevice: function (evt) { 2797 if (Env.isBrowser) { 2798 if ( 2799 evt.pointerType === 'touch' || // New 2800 (window.navigator.msMaxTouchPoints && // Old 2801 window.navigator.msMaxTouchPoints > 1) 2802 ) { 2803 return 'touch'; 2804 } 2805 if (evt.pointerType === 'mouse') { 2806 return 'mouse'; 2807 } 2808 if (evt.pointerType === 'pen') { 2809 return 'pen'; 2810 } 2811 } 2812 return 'mouse'; 2813 }, 2814 2815 /** 2816 * This method is called by the browser when a pointing device is pressed on the screen. 2817 * @param {Event} evt The browsers event object. 2818 * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter 2819 * @param {Boolean} [allowDefaultEventHandling=false] If true event is not canceled, i.e. prevent call of evt.preventDefault() 2820 * @returns {Boolean} false if the first finger event is sent twice, or not a browser, or in selection mode. Otherwise returns true. 2821 */ 2822 pointerDownListener: function (evt, object, allowDefaultEventHandling) { 2823 var i, j, k, pos, 2824 elements, sel, target_obj, 2825 type = 'mouse', // Used in case of no browser 2826 found, target, ta; 2827 2828 // Fix for Firefox browser: When using a second finger, the 2829 // touch event for the first finger is sent again. 2830 if (!object && this._isPointerRegistered(evt)) { 2831 return false; 2832 } 2833 2834 if (Type.evaluate(this.attr.movetarget) === null && 2835 Type.exists(evt.target) && Type.exists(evt.target.releasePointerCapture)) { 2836 evt.target.releasePointerCapture(evt.pointerId); 2837 } 2838 2839 if (!object && evt.isPrimary) { 2840 // First finger down. To be on the safe side this._board_touches is cleared. 2841 // this._pointerClearTouches(); 2842 } 2843 2844 if (!this.hasPointerUp) { 2845 if (window.navigator.msPointerEnabled) { 2846 // IE10- 2847 Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2848 } else { 2849 // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android 2850 Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this); 2851 Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2852 } 2853 this.hasPointerUp = true; 2854 } 2855 2856 if (this.hasMouseHandlers) { 2857 this.removeMouseEventHandlers(); 2858 } 2859 2860 if (this.hasTouchHandlers) { 2861 this.removeTouchEventHandlers(); 2862 } 2863 2864 // Prevent accidental selection of text 2865 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2866 this.document.selection.empty(); 2867 } else if (window.getSelection) { 2868 sel = window.getSelection(); 2869 if (sel.removeAllRanges) { 2870 try { 2871 sel.removeAllRanges(); 2872 } catch (e) { } 2873 } 2874 } 2875 2876 // Mouse, touch or pen device 2877 this._inputDevice = this._getPointerInputDevice(evt); 2878 type = this._inputDevice; 2879 this.options.precision.hasPoint = this.options.precision[type]; 2880 2881 // Handling of multi touch with pointer events should be easier than with touch events. 2882 // Every pointer device has its own pointerId, e.g. the mouse 2883 // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will 2884 // keep this id until a pointerUp event is fired. What we have to do here is: 2885 // 1. collect all elements under the current pointer 2886 // 2. run through the touches control structure 2887 // a. look for the object collected in step 1. 2888 // b. if an object is found, check the number of pointers. If appropriate, add the pointer. 2889 pos = this.getMousePosition(evt); 2890 2891 // Handle selection rectangle 2892 this._testForSelection(evt); 2893 if (this.selectingMode) { 2894 this._startSelecting(pos); 2895 this.triggerEventHandlers( 2896 ['touchstartselecting', 'pointerstartselecting', 'startselecting'], 2897 [evt] 2898 ); 2899 return; // don't continue as a normal click 2900 } 2901 2902 if (this.attr.drag.enabled && object) { 2903 elements = [object]; 2904 this.mode = this.BOARD_MODE_DRAG; 2905 } else { 2906 elements = this.initMoveObject(pos[0], pos[1], evt, type); 2907 } 2908 2909 target_obj = { 2910 num: evt.pointerId, 2911 X: pos[0], 2912 Y: pos[1], 2913 Xprev: NaN, 2914 Yprev: NaN, 2915 Xstart: [], 2916 Ystart: [], 2917 Zstart: [] 2918 }; 2919 2920 // If no draggable object can be found, get out here immediately 2921 if (elements.length > 0) { 2922 // check touches structure 2923 target = elements[elements.length - 1]; 2924 found = false; 2925 2926 // Reminder: this.touches is the list of elements which 2927 // currently 'possess' a pointer (mouse, pen, finger) 2928 for (i = 0; i < this.touches.length; i++) { 2929 // An element receives a further touch, i.e. 2930 // the target is already in our touches array, add the pointer to the existing touch 2931 if (this.touches[i].obj === target) { 2932 j = i; 2933 k = this.touches[i].targets.push(target_obj) - 1; 2934 found = true; 2935 break; 2936 } 2937 } 2938 if (!found) { 2939 // A new element has been touched. 2940 k = 0; 2941 j = 2942 this.touches.push({ 2943 obj: target, 2944 targets: [target_obj] 2945 }) - 1; 2946 } 2947 2948 this.dehighlightAll(); 2949 target.highlight(true); 2950 2951 this.saveStartPos(target, this.touches[j].targets[k]); 2952 2953 // Prevent accidental text selection 2954 // this could get us new trouble: input fields, links and drop down boxes placed as text 2955 // on the board don't work anymore. 2956 if (evt && evt.preventDefault && !allowDefaultEventHandling) { 2957 // All browser supporting pointer events know preventDefault() 2958 evt.preventDefault(); 2959 } 2960 } 2961 2962 if (this.touches.length > 0 && !allowDefaultEventHandling) { 2963 evt.preventDefault(); 2964 evt.stopPropagation(); 2965 } 2966 2967 if (!Env.isBrowser) { 2968 return false; 2969 } 2970 if (this._getPointerInputDevice(evt) !== 'touch') { 2971 if (this.mode === this.BOARD_MODE_NONE) { 2972 this.mouseOriginMoveStart(evt); 2973 } 2974 } else { 2975 this._pointerStorePosition(evt); 2976 evt.touches = this._board_touches; 2977 2978 // Touch events on empty areas of the board are handled here, see also touchStartListener 2979 // 1. case: one finger. If allowed, this triggers pan with one finger 2980 if ( 2981 evt.touches.length === 1 && 2982 this.mode === this.BOARD_MODE_NONE && 2983 this.touchStartMoveOriginOneFinger(evt) 2984 ) { 2985 // Empty by purpose 2986 } else if ( 2987 evt.touches.length === 2 && 2988 (this.mode === this.BOARD_MODE_NONE || 2989 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 2990 ) { 2991 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2992 // This happens when the second finger hits the device. First, the 2993 // 'one finger pan mode' has to be cancelled. 2994 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 2995 this.originMoveEnd(); 2996 } 2997 2998 this.gestureStartListener(evt); 2999 } 3000 } 3001 3002 this.initSketchCurve(evt); 3003 3004 // Allow browser scrolling 3005 // For this: pan by one finger has to be disabled 3006 3007 ta = 'none'; // JSXGraph catches all user touch events 3008 if (this.mode === this.BOARD_MODE_NONE && 3009 (Type.evaluate(this.attr.browserpan) === true || Type.evaluate(this.attr.browserpan.enabled) === true) && 3010 // One-finger pan has priority over browserPan 3011 (Type.evaluate(this.attr.pan.enabled) === false || Type.evaluate(this.attr.pan.needtwofingers) === true) 3012 ) { 3013 // ta = 'pan-x pan-y'; // JSXGraph allows browser scrolling 3014 ta = 'auto'; // JSXGraph allows browser scrolling 3015 } 3016 this.containerObj.style.touchAction = ta; 3017 3018 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]); 3019 3020 return true; 3021 }, 3022 3023 /** 3024 * Internal handling of click events for pointers and mouse. 3025 * 3026 * @param {Event} evt The browsers event object. 3027 * @param {Array} evtArray list of event names 3028 * @private 3029 */ 3030 _handleClicks: function(evt, evtArray) { 3031 var that = this, 3032 el, delay, suppress; 3033 3034 if (this.selectingMode) { 3035 evt.stopPropagation(); 3036 return; 3037 } 3038 3039 delay = Type.evaluate(this.attr.clickdelay); 3040 suppress = Type.evaluate(this.attr.dblclicksuppressclick); 3041 3042 if (suppress) { 3043 // dblclick suppresses previous click events 3044 this._preventSingleClick = false; 3045 3046 // Wait if there is a dblclick event. 3047 // If not fire a click event 3048 this._singleClickTimer = setTimeout(function() { 3049 if (!that._preventSingleClick) { 3050 // Fire click event and remove element from click list 3051 that.triggerEventHandlers(evtArray, [evt]); 3052 for (el in that.clickObjects) { 3053 if (that.clickObjects.hasOwnProperty(el)) { 3054 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 3055 delete that.clickObjects[el]; 3056 } 3057 } 3058 } 3059 }, delay); 3060 } else { 3061 // dblclick is preceded by two click events 3062 3063 // Fire click events 3064 that.triggerEventHandlers(evtArray, [evt]); 3065 for (el in that.clickObjects) { 3066 if (that.clickObjects.hasOwnProperty(el)) { 3067 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 3068 } 3069 } 3070 3071 // Clear list of clicked elements with a delay 3072 setTimeout(function() { 3073 for (el in that.clickObjects) { 3074 if (that.clickObjects.hasOwnProperty(el)) { 3075 delete that.clickObjects[el]; 3076 } 3077 } 3078 }, delay); 3079 } 3080 evt.stopPropagation(); 3081 }, 3082 3083 /** 3084 * Internal handling of dblclick events for pointers and mouse. 3085 * 3086 * @param {Event} evt The browsers event object. 3087 * @param {Array} evtArray list of event names 3088 * @private 3089 */ 3090 _handleDblClicks: function(evt, evtArray) { 3091 var el; 3092 3093 if (this.selectingMode) { 3094 evt.stopPropagation(); 3095 return; 3096 } 3097 3098 // Notify that a dblclick has happened 3099 this._preventSingleClick = true; 3100 clearTimeout(this._singleClickTimer); 3101 3102 // Fire dblclick event 3103 this.triggerEventHandlers(evtArray, [evt]); 3104 for (el in this.clickObjects) { 3105 if (this.clickObjects.hasOwnProperty(el)) { 3106 this.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 3107 delete this.clickObjects[el]; 3108 } 3109 } 3110 3111 evt.stopPropagation(); 3112 }, 3113 3114 /** 3115 * This method is called by the browser when a pointer device clicks on the screen. 3116 * @param {Event} evt The browsers event object. 3117 */ 3118 pointerClickListener: function (evt) { 3119 this._handleClicks(evt, ['click', 'pointerclick']); 3120 }, 3121 3122 /** 3123 * This method is called by the browser when a pointer device double clicks on the screen. 3124 * @param {Event} evt The browsers event object. 3125 */ 3126 pointerDblClickListener: function (evt) { 3127 this._handleDblClicks(evt, ['dblclick', 'pointerdblclick']); 3128 }, 3129 3130 /** 3131 * This method is called by the browser when the mouse device clicks on the screen. 3132 * @param {Event} evt The browsers event object. 3133 */ 3134 mouseClickListener: function (evt) { 3135 this._handleClicks(evt, ['click', 'mouseclick']); 3136 }, 3137 3138 /** 3139 * This method is called by the browser when the mouse device double clicks on the screen. 3140 * @param {Event} evt The browsers event object. 3141 */ 3142 mouseDblClickListener: function (evt) { 3143 this._handleDblClicks(evt, ['dblclick', 'mousedblclick']); 3144 }, 3145 3146 // /** 3147 // * Called if pointer leaves an HTML tag. It is called by the inner-most tag. 3148 // * That means, if a JSXGraph text, i.e. an HTML div, is placed close 3149 // * to the border of the board, this pointerout event will be ignored. 3150 // * @param {Event} evt 3151 // * @return {Boolean} 3152 // */ 3153 // pointerOutListener: function (evt) { 3154 // if (evt.target === this.containerObj || 3155 // (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) { 3156 // this.pointerUpListener(evt); 3157 // } 3158 // return this.mode === this.BOARD_MODE_NONE; 3159 // }, 3160 3161 /** 3162 * Called periodically by the browser while the user moves a pointing device across the screen. 3163 * @param {Event} evt 3164 * @returns {Boolean} 3165 */ 3166 pointerMoveListener: function (evt) { 3167 var i, j, pos, 3168 eps, 3169 touchTargets, 3170 type = 'mouse'; // in case of no browser 3171 3172 if ( 3173 this._getPointerInputDevice(evt) === 'touch' && 3174 !this._isPointerRegistered(evt) 3175 ) { 3176 // Test, if there was a previous down event of this _getPointerId 3177 // (in case it is a touch event). 3178 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry. 3179 return this.BOARD_MODE_NONE; 3180 } 3181 3182 if (!this.checkFrameRate(evt)) { 3183 return false; 3184 } 3185 3186 if (this.mode !== this.BOARD_MODE_DRAG) { 3187 this.dehighlightAll(); 3188 this.displayInfobox(false); 3189 } 3190 3191 if (this.mode !== this.BOARD_MODE_NONE) { 3192 evt.preventDefault(); 3193 evt.stopPropagation(); 3194 } 3195 3196 this.updateQuality = this.BOARD_QUALITY_LOW; 3197 // Mouse, touch or pen device 3198 this._inputDevice = this._getPointerInputDevice(evt); 3199 type = this._inputDevice; 3200 this.options.precision.hasPoint = this.options.precision[type]; 3201 eps = this.options.precision.hasPoint * 0.3333; 3202 3203 pos = this.getMousePosition(evt); 3204 // Ignore pointer move event if too close at the border 3205 // and setPointerCapture is off 3206 if (Type.evaluate(this.attr.movetarget) === null && 3207 (pos[0] <= eps || pos[1] <= eps || 3208 pos[0] >= this.canvasWidth - eps || 3209 pos[1] >= this.canvasHeight - eps) 3210 ) { 3211 return this.mode === this.BOARD_MODE_NONE; 3212 } 3213 3214 // selection 3215 if (this.selectingMode) { 3216 this._moveSelecting(pos); 3217 this.triggerEventHandlers( 3218 ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], 3219 [evt, this.mode] 3220 ); 3221 } else if (!this.mouseOriginMove(evt)) { 3222 3223 this.addToSketchCurve(evt); 3224 3225 if (this.mode === this.BOARD_MODE_DRAG) { 3226 // Run through all jsxgraph elements which are touched by at least one finger. 3227 for (i = 0; i < this.touches.length; i++) { 3228 touchTargets = this.touches[i].targets; 3229 // Run through all touch events which have been started on this jsxgraph element. 3230 for (j = 0; j < touchTargets.length; j++) { 3231 if (touchTargets[j].num === evt.pointerId) { 3232 touchTargets[j].X = pos[0]; 3233 touchTargets[j].Y = pos[1]; 3234 3235 if (touchTargets.length === 1) { 3236 // Touch by one finger: this is possible for all elements that can be dragged 3237 this.moveObject(pos[0], pos[1], this.touches[i], evt, type); 3238 } else if (touchTargets.length === 2) { 3239 // Touch by two fingers: e.g. moving lines 3240 this.twoFingerMove(this.touches[i], evt.pointerId, evt); 3241 3242 touchTargets[j].Xprev = pos[0]; 3243 touchTargets[j].Yprev = pos[1]; 3244 } 3245 3246 // There is only one pointer in the evt object, so there's no point in looking further 3247 break; 3248 } 3249 } 3250 } 3251 } else { 3252 if (this._getPointerInputDevice(evt) === 'touch') { 3253 this._pointerStorePosition(evt); 3254 3255 if (this._board_touches.length === 2) { 3256 evt.touches = this._board_touches; 3257 this.gestureChangeListener(evt); 3258 } 3259 } 3260 3261 // Move event without dragging an element 3262 this.highlightElements(pos[0], pos[1], evt, -1); 3263 } 3264 } 3265 3266 // Hiding the infobox is commented out, since it prevents showing the infobox 3267 // on IE 11+ on 'over' 3268 //if (this.mode !== this.BOARD_MODE_DRAG) { 3269 //this.displayInfobox(false); 3270 //} 3271 this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]); 3272 this.updateQuality = this.BOARD_QUALITY_HIGH; 3273 3274 return this.mode === this.BOARD_MODE_NONE; 3275 }, 3276 3277 /** 3278 * Triggered as soon as the user stops touching the device with at least one finger. 3279 * 3280 * @param {Event} evt 3281 * @returns {Boolean} 3282 */ 3283 pointerUpListener: function (evt) { 3284 var i, j, found, eh, 3285 touchTargets, 3286 updateNeeded = false; 3287 3288 this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 3289 this.displayInfobox(false); 3290 3291 if (evt) { 3292 for (i = 0; i < this.touches.length; i++) { 3293 touchTargets = this.touches[i].targets; 3294 for (j = 0; j < touchTargets.length; j++) { 3295 if (touchTargets[j].num === evt.pointerId) { 3296 touchTargets.splice(j, 1); 3297 if (touchTargets.length === 0) { 3298 this.touches.splice(i, 1); 3299 } 3300 break; 3301 } 3302 } 3303 } 3304 } 3305 3306 this.finalizeSketchCurve(evt); 3307 this.originMoveEnd(); 3308 this.update(); 3309 3310 // selection 3311 if (this.selectingMode) { 3312 this._stopSelecting(evt); 3313 this.triggerEventHandlers( 3314 ['touchstopselecting', 'pointerstopselecting', 'stopselecting'], 3315 [evt] 3316 ); 3317 this.stopSelectionMode(); 3318 } else { 3319 for (i = this.downObjects.length - 1; i > -1; i--) { 3320 found = false; 3321 for (j = 0; j < this.touches.length; j++) { 3322 if (this.touches[j].obj.id === this.downObjects[i].id) { 3323 found = true; 3324 } 3325 } 3326 if (!found) { 3327 this.downObjects[i].triggerEventHandlers( 3328 ['touchend', 'up', 'pointerup', 'MSPointerUp'], 3329 [evt] 3330 ); 3331 if (!Type.exists(this.downObjects[i].coords)) { 3332 // snapTo methods have to be called e.g. for line elements here. 3333 // For coordsElements there might be a conflict with 3334 // attractors, see commit from 2022.04.08, 11:12:18. 3335 this.downObjects[i].snapToGrid(); 3336 this.downObjects[i].snapToPoints(); 3337 updateNeeded = true; 3338 } 3339 3340 // Check if we have to keep the element for a click or dblclick event 3341 // Otherwise remove it from downObjects 3342 eh = this.downObjects[i].eventHandlers; 3343 if ((Type.exists(eh.click) && eh.click.length > 0) || 3344 (Type.exists(eh.pointerclick) && eh.pointerclick.length > 0) || 3345 (Type.exists(eh.dblclick) && eh.dblclick.length > 0) || 3346 (Type.exists(eh.pointerdblclick) && eh.pointerdblclick.length > 0) 3347 ) { 3348 this.clickObjects[this.downObjects[i].id] = this.downObjects[i]; 3349 } 3350 this.downObjects.splice(i, 1); 3351 } 3352 } 3353 } 3354 3355 if (this.hasPointerUp) { 3356 if (window.navigator.msPointerEnabled) { 3357 // IE10- 3358 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 3359 } else { 3360 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 3361 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 3362 } 3363 this.hasPointerUp = false; 3364 } 3365 3366 // After one finger leaves the screen the gesture is stopped. 3367 this._pointerClearTouches(evt.pointerId); 3368 if (this._getPointerInputDevice(evt) !== 'touch') { 3369 this.dehighlightAll(); 3370 } 3371 3372 if (updateNeeded) { 3373 this.update(); 3374 } 3375 3376 return true; 3377 }, 3378 3379 /** 3380 * Triggered by the pointerleave event. This is needed in addition to 3381 * {@link JXG.Board#pointerUpListener} in the situation that a pen is used 3382 * and after an up event the pen leaves the hover range vertically. Here, it happens that 3383 * after the pointerup event further pointermove events are fired and elements get highlighted. 3384 * This highlighting has to be cancelled. 3385 * 3386 * @param {Event} evt 3387 * @returns {Boolean} 3388 */ 3389 pointerLeaveListener: function (evt) { 3390 this.displayInfobox(false); 3391 this.dehighlightAll(); 3392 3393 return true; 3394 }, 3395 3396 /** 3397 * Touch-Events 3398 */ 3399 3400 /** 3401 * This method is called by the browser when a finger touches the surface of the touch-device. 3402 * @param {Event} evt The browsers event object. 3403 * @returns {Boolean} ... 3404 */ 3405 touchStartListener: function (evt) { 3406 var i, j, k, 3407 pos, elements, obj, 3408 eps = this.options.precision.touch, 3409 evtTouches = evt['touches'], 3410 found, 3411 targets, target, 3412 touchTargets; 3413 3414 if (!this.hasTouchEnd) { 3415 Env.addEvent(this.document, 'touchend', this.touchEndListener, this); 3416 this.hasTouchEnd = true; 3417 } 3418 3419 // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen. 3420 //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); } 3421 3422 // prevent accidental selection of text 3423 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3424 this.document.selection.empty(); 3425 } else if (window.getSelection) { 3426 window.getSelection().removeAllRanges(); 3427 } 3428 3429 // multitouch 3430 this._inputDevice = 'touch'; 3431 this.options.precision.hasPoint = this.options.precision.touch; 3432 3433 // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our 3434 // previous touches. once this is done we run through the existing touches again and watch out for free touches that can be attached to our existing 3435 // touches, e.g. we translate (parallel translation) a line with one finger, now a second finger is over this line. this should change the operation to 3436 // a rotational translation. or one finger moves a circle, a second finger can be attached to the circle: this now changes the operation from translation to 3437 // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations: 3438 // * points have higher priority over other elements. 3439 // * if we find a targettouch over an element that could be transformed with more than one finger, we search the rest of the targettouches, if they are over 3440 // this element and add them. 3441 // ADDENDUM 11/10/11: 3442 // (1) run through the touches control object, 3443 // (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch 3444 // for every target in our touches objects 3445 // (3) if one of the targettouches was bound to a touches targets array, mark it 3446 // (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch: 3447 // (a) if no element could be found: mark the target touches and continue 3448 // --- in the following cases, 'init' means: 3449 // (i) check if the element is already used in another touches element, if so, mark the targettouch and continue 3450 // (ii) if not, init a new touches element, add the targettouch to the touches property and mark it 3451 // (b) if the element is a point, init 3452 // (c) if the element is a line, init and try to find a second targettouch on that line. if a second one is found, add and mark it 3453 // (d) if the element is a circle, init and try to find TWO other targettouches on that circle. if only one is found, mark it and continue. otherwise 3454 // add both to the touches array and mark them. 3455 for (i = 0; i < evtTouches.length; i++) { 3456 evtTouches[i].jxg_isused = false; 3457 } 3458 3459 for (i = 0; i < this.touches.length; i++) { 3460 touchTargets = this.touches[i].targets; 3461 for (j = 0; j < touchTargets.length; j++) { 3462 touchTargets[j].num = -1; 3463 eps = this.options.precision.touch; 3464 3465 do { 3466 for (k = 0; k < evtTouches.length; k++) { 3467 // find the new targettouches 3468 if ( 3469 Math.abs( 3470 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3471 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3472 ) < 3473 eps * eps 3474 ) { 3475 touchTargets[j].num = k; 3476 touchTargets[j].X = evtTouches[k].screenX; 3477 touchTargets[j].Y = evtTouches[k].screenY; 3478 evtTouches[k].jxg_isused = true; 3479 break; 3480 } 3481 } 3482 3483 eps *= 2; 3484 } while ( 3485 touchTargets[j].num === -1 && 3486 eps < this.options.precision.touchMax 3487 ); 3488 3489 if (touchTargets[j].num === -1) { 3490 JXG.debug( 3491 "i couldn't find a targettouches for target no " + 3492 j + 3493 ' on ' + 3494 this.touches[i].obj.name + 3495 ' (' + 3496 this.touches[i].obj.id + 3497 '). Removed the target.' 3498 ); 3499 JXG.debug( 3500 'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax 3501 ); 3502 touchTargets.splice(i, 1); 3503 } 3504 } 3505 } 3506 3507 // we just re-mapped the targettouches to our existing touches list. 3508 // now we have to initialize some touches from additional targettouches 3509 for (i = 0; i < evtTouches.length; i++) { 3510 if (!evtTouches[i].jxg_isused) { 3511 pos = this.getMousePosition(evt, i); 3512 // selection 3513 // this._testForSelection(evt); // we do not have shift or ctrl keys yet. 3514 if (this.selectingMode) { 3515 this._startSelecting(pos); 3516 this.triggerEventHandlers( 3517 ['touchstartselecting', 'startselecting'], 3518 [evt] 3519 ); 3520 evt.preventDefault(); 3521 evt.stopPropagation(); 3522 this.options.precision.hasPoint = this.options.precision.mouse; 3523 return this.touches.length > 0; // don't continue as a normal click 3524 } 3525 3526 elements = this.initMoveObject(pos[0], pos[1], evt, 'touch'); 3527 if (elements.length !== 0) { 3528 obj = elements[elements.length - 1]; 3529 target = { 3530 num: i, 3531 X: evtTouches[i].screenX, 3532 Y: evtTouches[i].screenY, 3533 Xprev: NaN, 3534 Yprev: NaN, 3535 Xstart: [], 3536 Ystart: [], 3537 Zstart: [] 3538 }; 3539 3540 if ( 3541 Type.isPoint(obj) || 3542 obj.elementClass === Const.OBJECT_CLASS_TEXT || 3543 obj.type === Const.OBJECT_TYPE_TICKS || 3544 obj.type === Const.OBJECT_TYPE_IMAGE 3545 ) { 3546 // It's a point, so it's single touch, so we just push it to our touches 3547 targets = [target]; 3548 3549 // For the UNDO/REDO of object moves 3550 this.saveStartPos(obj, targets[0]); 3551 3552 this.touches.push({ obj: obj, targets: targets }); 3553 obj.highlight(true); 3554 } else if ( 3555 obj.elementClass === Const.OBJECT_CLASS_LINE || 3556 obj.elementClass === Const.OBJECT_CLASS_CIRCLE || 3557 obj.elementClass === Const.OBJECT_CLASS_CURVE || 3558 obj.type === Const.OBJECT_TYPE_POLYGON 3559 ) { 3560 found = false; 3561 3562 // first check if this geometric object is already captured in this.touches 3563 for (j = 0; j < this.touches.length; j++) { 3564 if (obj.id === this.touches[j].obj.id) { 3565 found = true; 3566 // only add it, if we don't have two targets in there already 3567 if (this.touches[j].targets.length === 1) { 3568 // For the UNDO/REDO of object moves 3569 this.saveStartPos(obj, target); 3570 this.touches[j].targets.push(target); 3571 } 3572 3573 evtTouches[i].jxg_isused = true; 3574 } 3575 } 3576 3577 // we couldn't find it in touches, so we just init a new touches 3578 // IF there is a second touch targetting this line, we will find it later on, and then add it to 3579 // the touches control object. 3580 if (!found) { 3581 targets = [target]; 3582 3583 // For the UNDO/REDO of object moves 3584 this.saveStartPos(obj, targets[0]); 3585 this.touches.push({ obj: obj, targets: targets }); 3586 obj.highlight(true); 3587 } 3588 } 3589 } 3590 3591 evtTouches[i].jxg_isused = true; 3592 } 3593 } 3594 3595 if (this.touches.length > 0) { 3596 evt.preventDefault(); 3597 evt.stopPropagation(); 3598 } 3599 3600 // Touch events on empty areas of the board are handled here: 3601 // 1. case: one finger. If allowed, this triggers pan with one finger 3602 if ( 3603 evtTouches.length === 1 && 3604 this.mode === this.BOARD_MODE_NONE && 3605 this.touchStartMoveOriginOneFinger(evt) 3606 ) { 3607 } else if ( 3608 evtTouches.length === 2 && 3609 (this.mode === this.BOARD_MODE_NONE || 3610 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 3611 ) { 3612 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 3613 // This happens when the second finger hits the device. First, the 3614 // 'one finger pan mode' has to be cancelled. 3615 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 3616 this.originMoveEnd(); 3617 } 3618 this.gestureStartListener(evt); 3619 } 3620 3621 this.initSketchCurve(evt); 3622 3623 this.options.precision.hasPoint = this.options.precision.mouse; 3624 this.triggerEventHandlers(['touchstart', 'down'], [evt]); 3625 3626 return false; 3627 //return this.touches.length > 0; 3628 }, 3629 3630 /** 3631 * Called periodically by the browser while the user moves his fingers across the device. 3632 * @param {Event} evt 3633 * @returns {Boolean} 3634 */ 3635 touchMoveListener: function (evt) { 3636 var i, 3637 pos1, pos2, 3638 touchTargets, 3639 evtTouches = evt['touches']; 3640 3641 if (!this.checkFrameRate(evt)) { 3642 return false; 3643 } 3644 3645 if (this.mode !== this.BOARD_MODE_NONE) { 3646 evt.preventDefault(); 3647 evt.stopPropagation(); 3648 } 3649 3650 if (this.mode !== this.BOARD_MODE_DRAG) { 3651 this.dehighlightAll(); 3652 this.displayInfobox(false); 3653 } 3654 3655 this._inputDevice = 'touch'; 3656 this.options.precision.hasPoint = this.options.precision.touch; 3657 this.updateQuality = this.BOARD_QUALITY_LOW; 3658 3659 // selection 3660 if (this.selectingMode) { 3661 for (i = 0; i < evtTouches.length; i++) { 3662 if (!evtTouches[i].jxg_isused) { 3663 pos1 = this.getMousePosition(evt, i); 3664 this._moveSelecting(pos1); 3665 this.triggerEventHandlers( 3666 ['touchmoves', 'moveselecting'], 3667 [evt, this.mode] 3668 ); 3669 break; 3670 } 3671 } 3672 } else { 3673 if (!this.touchOriginMove(evt)) { 3674 3675 this.addToSketchCurve(evt); 3676 3677 if (this.mode === this.BOARD_MODE_DRAG) { 3678 // Runs over through all elements which are touched 3679 // by at least one finger. 3680 for (i = 0; i < this.touches.length; i++) { 3681 touchTargets = this.touches[i].targets; 3682 if (touchTargets.length === 1) { 3683 // Touch by one finger: this is possible for all elements that can be dragged 3684 if (evtTouches[touchTargets[0].num]) { 3685 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3686 if ( 3687 pos1[0] < 0 || 3688 pos1[0] > this.canvasWidth || 3689 pos1[1] < 0 || 3690 pos1[1] > this.canvasHeight 3691 ) { 3692 return; 3693 } 3694 touchTargets[0].X = pos1[0]; 3695 touchTargets[0].Y = pos1[1]; 3696 this.moveObject( 3697 pos1[0], 3698 pos1[1], 3699 this.touches[i], 3700 evt, 3701 'touch' 3702 ); 3703 } 3704 } else if ( 3705 touchTargets.length === 2 && 3706 touchTargets[0].num > -1 && 3707 touchTargets[1].num > -1 3708 ) { 3709 // Touch by two fingers: moving lines, ... 3710 if ( 3711 evtTouches[touchTargets[0].num] && 3712 evtTouches[touchTargets[1].num] 3713 ) { 3714 // Get coordinates of the two touches 3715 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3716 pos2 = this.getMousePosition(evt, touchTargets[1].num); 3717 if ( 3718 pos1[0] < 0 || 3719 pos1[0] > this.canvasWidth || 3720 pos1[1] < 0 || 3721 pos1[1] > this.canvasHeight || 3722 pos2[0] < 0 || 3723 pos2[0] > this.canvasWidth || 3724 pos2[1] < 0 || 3725 pos2[1] > this.canvasHeight 3726 ) { 3727 return; 3728 } 3729 3730 touchTargets[0].X = pos1[0]; 3731 touchTargets[0].Y = pos1[1]; 3732 touchTargets[1].X = pos2[0]; 3733 touchTargets[1].Y = pos2[1]; 3734 3735 this.twoFingerMove( 3736 this.touches[i], 3737 touchTargets[0].num, 3738 evt 3739 ); 3740 3741 touchTargets[0].Xprev = pos1[0]; 3742 touchTargets[0].Yprev = pos1[1]; 3743 touchTargets[1].Xprev = pos2[0]; 3744 touchTargets[1].Yprev = pos2[1]; 3745 } 3746 } 3747 } 3748 } else { 3749 if (evtTouches.length === 2) { 3750 this.gestureChangeListener(evt); 3751 } 3752 // Move event without dragging an element 3753 pos1 = this.getMousePosition(evt, 0); 3754 this.highlightElements(pos1[0], pos1[1], evt, -1); 3755 } 3756 } 3757 } 3758 3759 if (this.mode !== this.BOARD_MODE_DRAG) { 3760 this.displayInfobox(false); 3761 } 3762 3763 this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 3764 this.options.precision.hasPoint = this.options.precision.mouse; 3765 this.updateQuality = this.BOARD_QUALITY_HIGH; 3766 3767 return this.mode === this.BOARD_MODE_NONE; 3768 }, 3769 3770 /** 3771 * Triggered as soon as the user stops touching the device with at least one finger. 3772 * @param {Event} evt 3773 * @returns {Boolean} 3774 */ 3775 touchEndListener: function (evt) { 3776 var i, 3777 j, 3778 k, 3779 eps = this.options.precision.touch, 3780 tmpTouches = [], 3781 found, 3782 foundNumber, 3783 evtTouches = evt && evt['touches'], 3784 touchTargets, 3785 updateNeeded = false; 3786 3787 this.triggerEventHandlers(['touchend', 'up'], [evt]); 3788 this.displayInfobox(false); 3789 3790 this.finalizeSketchCurve(evt); 3791 3792 // selection 3793 if (this.selectingMode) { 3794 this._stopSelecting(evt); 3795 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]); 3796 this.stopSelectionMode(); 3797 } else if (evtTouches && evtTouches.length > 0) { 3798 for (i = 0; i < this.touches.length; i++) { 3799 tmpTouches[i] = this.touches[i]; 3800 } 3801 this.touches.length = 0; 3802 3803 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted, 3804 // convert the operation to a simple one-finger-translation. 3805 // ADDENDUM 11/10/11: 3806 // see addendum to touchStartListener from 11/10/11 3807 // (1) run through the tmptouches 3808 // (2) check the touches.obj, if it is a 3809 // (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch. 3810 // (b) line with 3811 // (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch. 3812 // (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches 3813 // (c) circle with [proceed like in line] 3814 3815 // init the targettouches marker 3816 for (i = 0; i < evtTouches.length; i++) { 3817 evtTouches[i].jxg_isused = false; 3818 } 3819 3820 for (i = 0; i < tmpTouches.length; i++) { 3821 // could all targets of the current this.touches.obj be assigned to targettouches? 3822 found = false; 3823 foundNumber = 0; 3824 touchTargets = tmpTouches[i].targets; 3825 3826 for (j = 0; j < touchTargets.length; j++) { 3827 touchTargets[j].found = false; 3828 for (k = 0; k < evtTouches.length; k++) { 3829 if ( 3830 Math.abs( 3831 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3832 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3833 ) < 3834 eps * eps 3835 ) { 3836 touchTargets[j].found = true; 3837 touchTargets[j].num = k; 3838 touchTargets[j].X = evtTouches[k].screenX; 3839 touchTargets[j].Y = evtTouches[k].screenY; 3840 foundNumber += 1; 3841 break; 3842 } 3843 } 3844 } 3845 3846 if (Type.isPoint(tmpTouches[i].obj)) { 3847 found = touchTargets[0] && touchTargets[0].found; 3848 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) { 3849 found = 3850 (touchTargets[0] && touchTargets[0].found) || 3851 (touchTargets[1] && touchTargets[1].found); 3852 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 3853 found = foundNumber === 1 || foundNumber === 3; 3854 } 3855 3856 // if we found this object to be still dragged by the user, add it back to this.touches 3857 if (found) { 3858 this.touches.push({ 3859 obj: tmpTouches[i].obj, 3860 targets: [] 3861 }); 3862 3863 for (j = 0; j < touchTargets.length; j++) { 3864 if (touchTargets[j].found) { 3865 this.touches[this.touches.length - 1].targets.push({ 3866 num: touchTargets[j].num, 3867 X: touchTargets[j].screenX, 3868 Y: touchTargets[j].screenY, 3869 Xprev: NaN, 3870 Yprev: NaN, 3871 Xstart: touchTargets[j].Xstart, 3872 Ystart: touchTargets[j].Ystart, 3873 Zstart: touchTargets[j].Zstart 3874 }); 3875 } 3876 } 3877 } else { 3878 tmpTouches[i].obj.noHighlight(); 3879 } 3880 } 3881 } else { 3882 this.touches.length = 0; 3883 } 3884 3885 for (i = this.downObjects.length - 1; i > -1; i--) { 3886 found = false; 3887 for (j = 0; j < this.touches.length; j++) { 3888 if (this.touches[j].obj.id === this.downObjects[i].id) { 3889 found = true; 3890 } 3891 } 3892 if (!found) { 3893 this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]); 3894 if (!Type.exists(this.downObjects[i].coords)) { 3895 // snapTo methods have to be called e.g. for line elements here. 3896 // For coordsElements there might be a conflict with 3897 // attractors, see commit from 2022.04.08, 11:12:18. 3898 this.downObjects[i].snapToGrid(); 3899 this.downObjects[i].snapToPoints(); 3900 updateNeeded = true; 3901 } 3902 this.downObjects.splice(i, 1); 3903 } 3904 } 3905 3906 if (!evtTouches || evtTouches.length === 0) { 3907 if (this.hasTouchEnd) { 3908 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 3909 this.hasTouchEnd = false; 3910 } 3911 3912 this.dehighlightAll(); 3913 this.updateQuality = this.BOARD_QUALITY_HIGH; 3914 3915 this.originMoveEnd(); 3916 if (updateNeeded) { 3917 this.update(); 3918 } 3919 } 3920 3921 return true; 3922 }, 3923 3924 /** 3925 * This method is called by the browser when the mouse button is clicked. 3926 * @param {Event} evt The browsers event object. 3927 * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise. 3928 */ 3929 mouseDownListener: function (evt) { 3930 var pos, elements, result; 3931 3932 // prevent accidental selection of text 3933 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3934 this.document.selection.empty(); 3935 } else if (window.getSelection) { 3936 window.getSelection().removeAllRanges(); 3937 } 3938 3939 if (!this.hasMouseUp) { 3940 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this); 3941 this.hasMouseUp = true; 3942 } else { 3943 // In case this.hasMouseUp==true, it may be that there was a 3944 // mousedown event before which was not followed by an mouseup event. 3945 // This seems to happen with interactive whiteboard pens sometimes. 3946 return; 3947 } 3948 3949 this._inputDevice = 'mouse'; 3950 this.options.precision.hasPoint = this.options.precision.mouse; 3951 pos = this.getMousePosition(evt); 3952 3953 // selection 3954 this._testForSelection(evt); 3955 if (this.selectingMode) { 3956 this._startSelecting(pos); 3957 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]); 3958 return; // don't continue as a normal click 3959 } 3960 3961 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 3962 3963 // if no draggable object can be found, get out here immediately 3964 if (elements.length === 0) { 3965 this.mode = this.BOARD_MODE_NONE; 3966 result = true; 3967 } else { 3968 this.mouse = { 3969 obj: null, 3970 targets: [ 3971 { 3972 X: pos[0], 3973 Y: pos[1], 3974 Xprev: NaN, 3975 Yprev: NaN 3976 } 3977 ] 3978 }; 3979 this.mouse.obj = elements[elements.length - 1]; 3980 3981 this.dehighlightAll(); 3982 this.mouse.obj.highlight(true); 3983 3984 this.mouse.targets[0].Xstart = []; 3985 this.mouse.targets[0].Ystart = []; 3986 this.mouse.targets[0].Zstart = []; 3987 3988 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]); 3989 3990 // prevent accidental text selection 3991 // this could get us new trouble: input fields, links and drop down boxes placed as text 3992 // on the board don't work anymore. 3993 if (evt && evt.preventDefault) { 3994 evt.preventDefault(); 3995 } else if (window.event) { 3996 window.event.returnValue = false; 3997 } 3998 } 3999 4000 if (this.mode === this.BOARD_MODE_NONE) { 4001 result = this.mouseOriginMoveStart(evt); 4002 } 4003 4004 this.initSketchCurve(evt); 4005 this.triggerEventHandlers(['mousedown', 'down'], [evt]); 4006 4007 return result; 4008 }, 4009 4010 /** 4011 * This method is called by the browser when the mouse is moved. 4012 * @param {Event} evt The browsers event object. 4013 */ 4014 mouseMoveListener: function (evt) { 4015 var pos; 4016 4017 if (!this.checkFrameRate(evt)) { 4018 return false; 4019 } 4020 4021 pos = this.getMousePosition(evt); 4022 4023 this.updateQuality = this.BOARD_QUALITY_LOW; 4024 4025 if (this.mode !== this.BOARD_MODE_DRAG) { 4026 this.dehighlightAll(); 4027 this.displayInfobox(false); 4028 } 4029 4030 // we have to check for four cases: 4031 // * user moves origin 4032 // * user drags an object 4033 // * user just moves the mouse, here highlight all elements at 4034 // the current mouse position 4035 // * the user is selecting 4036 4037 // selection 4038 if (this.selectingMode) { 4039 this._moveSelecting(pos); 4040 this.triggerEventHandlers( 4041 ['mousemoveselecting', 'moveselecting'], 4042 [evt, this.mode] 4043 ); 4044 } else if (!this.mouseOriginMove(evt)) { 4045 4046 this.addToSketchCurve(evt); 4047 4048 if (this.mode === this.BOARD_MODE_DRAG) { 4049 this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse'); 4050 } else { 4051 // BOARD_MODE_NONE 4052 // Move event without dragging an element 4053 this.highlightElements(pos[0], pos[1], evt, -1); 4054 } 4055 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]); 4056 } 4057 this.updateQuality = this.BOARD_QUALITY_HIGH; 4058 }, 4059 4060 /** 4061 * This method is called by the browser when the mouse button is released. 4062 * @param {Event} evt 4063 */ 4064 mouseUpListener: function (evt) { 4065 var i; 4066 4067 if (this.selectingMode === false) { 4068 this.triggerEventHandlers(['mouseup', 'up'], [evt]); 4069 } 4070 4071 // redraw with high precision 4072 this.updateQuality = this.BOARD_QUALITY_HIGH; 4073 4074 if (this.mouse && this.mouse.obj) { 4075 if (!Type.exists(this.mouse.obj.coords)) { 4076 // snapTo methods have to be called e.g. for line elements here. 4077 // For coordsElements there might be a conflict with 4078 // attractors, see commit from 2022.04.08, 11:12:18. 4079 // The parameter is needed for lines with snapToGrid enabled 4080 this.mouse.obj.snapToGrid(this.mouse.targets[0]); 4081 this.mouse.obj.snapToPoints(); 4082 } 4083 } 4084 4085 this.finalizeSketchCurve(evt); 4086 this.originMoveEnd(); 4087 this.dehighlightAll(); 4088 this.update(); 4089 4090 // selection 4091 if (this.selectingMode) { 4092 this._stopSelecting(evt); 4093 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]); 4094 this.stopSelectionMode(); 4095 } else { 4096 for (i = 0; i < this.downObjects.length; i++) { 4097 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]); 4098 } 4099 } 4100 4101 this.downObjects.length = 0; 4102 4103 if (this.hasMouseUp) { 4104 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 4105 this.hasMouseUp = false; 4106 } 4107 4108 // release dragged mouse object 4109 this.mouse = null; 4110 }, 4111 4112 /** 4113 * Handler for mouse wheel events. Used to zoom in and out of the board. 4114 * @param {Event} evt 4115 * @returns {Boolean} 4116 */ 4117 mouseWheelListener: function (evt) { 4118 var wd, zoomCenter, pos; 4119 4120 if (!this.attr.zoom.enabled || 4121 !this.attr.zoom.wheel || 4122 !this._isRequiredKeyPressed(evt, 'zoom')) { 4123 4124 return true; 4125 } 4126 4127 evt = evt || window.event; 4128 wd = evt.detail ? -evt.detail : evt.wheelDelta / 40; 4129 zoomCenter = this.attr.zoom.center; 4130 4131 if (zoomCenter === 'board') { 4132 pos = []; 4133 } else { // including zoomCenter === 'auto' 4134 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this).usrCoords; 4135 } 4136 4137 // pos == [] does not throw an error 4138 if (wd > 0) { 4139 this.zoomIn(pos[1], pos[2]); 4140 } else { 4141 this.zoomOut(pos[1], pos[2]); 4142 } 4143 4144 this.triggerEventHandlers(['mousewheel'], [evt]); 4145 4146 evt.preventDefault(); 4147 return false; 4148 }, 4149 4150 /** 4151 * Allow moving of JSXGraph elements with arrow keys. 4152 * The selection of the element is done with the tab key. For this, 4153 * the attribute 'tabindex' of the element has to be set to some number (default=0). 4154 * tabindex corresponds to the HTML and SVG attribute of the same name. 4155 * <p> 4156 * Panning of the construction is done with arrow keys 4157 * if the pan key (shift or ctrl - depending on the board attributes) is pressed. 4158 * <p> 4159 * Zooming is triggered with the keys +, o, -, if 4160 * the pan key (shift or ctrl - depending on the board attributes) is pressed. 4161 * <p> 4162 * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus. 4163 * 4164 * @param {Event} evt The browser's event object 4165 * 4166 * @see JXG.Board#keyboard 4167 * @see JXG.Board#keyFocusInListener 4168 * @see JXG.Board#keyFocusOutListener 4169 * 4170 */ 4171 keyDownListener: function (evt) { 4172 var id_node = evt.target.id, 4173 id, el, res, doc, 4174 sX = 0, 4175 sY = 0, 4176 // dx, dy are provided in screen units and 4177 // are converted to user coordinates 4178 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX, 4179 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY, 4180 // u = 100, 4181 doZoom = false, 4182 done = true, 4183 dir, 4184 actPos; 4185 4186 if (!this.attr.keyboard.enabled || id_node === '') { 4187 return false; 4188 } 4189 4190 // Tab key should be handled by the browser 4191 if (evt.keyCode === 9) { 4192 return false; 4193 } 4194 4195 // dx = Math.round(dx * u) / u; 4196 // dy = Math.round(dy * u) / u; 4197 4198 // An element of type input or textarea has focus, get out of here. 4199 doc = this.containerObj.shadowRoot || document; 4200 if (doc.activeElement) { 4201 el = doc.activeElement; 4202 if (el.tagName === 'INPUT' || el.tagName === 'textarea') { 4203 return false; 4204 } 4205 } 4206 4207 // Get the JSXGraph id from the id of the SVG node. 4208 id = id_node.replace(this.containerObj.id + '_', ''); 4209 el = this.select(id); 4210 4211 if (Type.exists(el.coords)) { 4212 actPos = el.coords.usrCoords.slice(1); 4213 } 4214 4215 if ( 4216 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) || 4217 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey) 4218 ) { 4219 // Pan key has been pressed 4220 4221 if (Type.evaluate(this.attr.zoom.enabled) === true) { 4222 doZoom = true; 4223 } 4224 4225 // Arrow keys 4226 if (evt.keyCode === 38) { 4227 // up 4228 this.clickUpArrow(); 4229 } else if (evt.keyCode === 40) { 4230 // down 4231 this.clickDownArrow(); 4232 } else if (evt.keyCode === 37) { 4233 // left 4234 this.clickLeftArrow(); 4235 } else if (evt.keyCode === 39) { 4236 // right 4237 this.clickRightArrow(); 4238 4239 // Zoom keys 4240 } else if (doZoom && evt.keyCode === 171) { 4241 // + 4242 this.zoomIn(); 4243 } else if (doZoom && evt.keyCode === 173) { 4244 // - 4245 this.zoomOut(); 4246 } else if (doZoom && evt.keyCode === 79) { 4247 // o 4248 this.zoom100(); 4249 } else { 4250 done = false; 4251 } 4252 } else if (!evt.shiftKey && !evt.ctrlKey) { // Move an element if neither shift or ctrl are pressed 4253 // Adapt dx, dy to snapToGrid and attractToGrid. 4254 // snapToGrid has priority. 4255 if (Type.exists(el.visProp)) { 4256 if ( 4257 Type.exists(el.visProp.snaptogrid) && 4258 el.visProp.snaptogrid && 4259 el.evalVisProp('snapsizex') && 4260 el.evalVisProp('snapsizey') 4261 ) { 4262 // Adapt dx, dy such that snapToGrid is possible 4263 res = el.getSnapSizes(); 4264 sX = res[0]; 4265 sY = res[1]; 4266 // If snaptogrid is true, 4267 // we can only jump from grid point to grid point. 4268 dx = sX; 4269 dy = sY; 4270 } else if ( 4271 Type.exists(el.visProp.attracttogrid) && 4272 el.visProp.attracttogrid && 4273 el.evalVisProp('attractordistance') && 4274 el.evalVisProp('attractorunit') 4275 ) { 4276 // Adapt dx, dy such that attractToGrid is possible 4277 sX = 1.1 * el.evalVisProp('attractordistance'); 4278 sY = sX; 4279 4280 if (el.evalVisProp('attractorunit') === 'screen') { 4281 sX /= this.unitX; 4282 sY /= this.unitX; 4283 } 4284 dx = Math.max(sX, dx); 4285 dy = Math.max(sY, dy); 4286 } 4287 } 4288 4289 if (evt.keyCode === 38) { 4290 // up 4291 dir = [0, dy]; 4292 } else if (evt.keyCode === 40) { 4293 // down 4294 dir = [0, -dy]; 4295 } else if (evt.keyCode === 37) { 4296 // left 4297 dir = [-dx, 0]; 4298 } else if (evt.keyCode === 39) { 4299 // right 4300 dir = [dx, 0]; 4301 } else { 4302 done = false; 4303 } 4304 4305 if (dir && el.isDraggable && 4306 el.visPropCalc.visible && 4307 ((this.geonextCompatibilityMode && 4308 (Type.isPoint(el) || 4309 el.elementClass === Const.OBJECT_CLASS_TEXT) 4310 ) || !this.geonextCompatibilityMode) && 4311 !el.evalVisProp('fixed') 4312 ) { 4313 this.mode = this.BOARD_MODE_DRAG; 4314 if (Type.exists(el.coords)) { 4315 dir[0] += actPos[0]; 4316 dir[1] += actPos[1]; 4317 } 4318 // For coordsElement setPosition has to call setPositionDirectly. 4319 // Otherwise the position is set by a translation. 4320 if (Type.exists(el.coords)) { 4321 el.setPosition(JXG.COORDS_BY_USER, dir); 4322 this.updateInfobox(el); 4323 } else { 4324 this.displayInfobox(false); 4325 el.setPositionDirectly( 4326 Const.COORDS_BY_USER, 4327 dir, 4328 [0, 0] 4329 ); 4330 } 4331 4332 this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]); 4333 el.triggerEventHandlers(['keydrag', 'drag'], [evt]); 4334 this.mode = this.BOARD_MODE_NONE; 4335 } 4336 } 4337 4338 this.update(); 4339 4340 if (done && Type.exists(evt.preventDefault)) { 4341 evt.preventDefault(); 4342 } 4343 return done; 4344 }, 4345 4346 /** 4347 * Event listener for SVG elements getting focus. 4348 * This is needed for highlighting when using keyboard control. 4349 * Only elements having the attribute 'tabindex' can receive focus. 4350 * 4351 * @see JXG.Board#keyFocusOutListener 4352 * @see JXG.Board#keyDownListener 4353 * @see JXG.Board#keyboard 4354 * 4355 * @param {Event} evt The browser's event object 4356 */ 4357 keyFocusInListener: function (evt) { 4358 var id_node = evt.target.id, 4359 id, 4360 el; 4361 4362 if (!this.attr.keyboard.enabled || id_node === '') { 4363 return false; 4364 } 4365 4366 // Get JSXGraph id from node id 4367 id = id_node.replace(this.containerObj.id + '_', ''); 4368 el = this.select(id); 4369 if (Type.exists(el.highlight)) { 4370 el.highlight(true); 4371 this.focusObjects = [id]; 4372 el.triggerEventHandlers(['hit'], [evt]); 4373 } 4374 if (Type.exists(el.coords)) { 4375 this.updateInfobox(el); 4376 } 4377 }, 4378 4379 /** 4380 * Event listener for SVG elements losing focus. 4381 * This is needed for dehighlighting when using keyboard control. 4382 * Only elements having the attribute 'tabindex' can receive focus. 4383 * 4384 * @see JXG.Board#keyFocusInListener 4385 * @see JXG.Board#keyDownListener 4386 * @see JXG.Board#keyboard 4387 * 4388 * @param {Event} evt The browser's event object 4389 */ 4390 keyFocusOutListener: function (evt) { 4391 if (!this.attr.keyboard.enabled) { 4392 return false; 4393 } 4394 this.focusObjects = []; // This has to be before displayInfobox(false) 4395 this.dehighlightAll(); 4396 this.displayInfobox(false); 4397 }, 4398 4399 /** 4400 * Update the width and height of the JSXGraph container div element. 4401 * If width and height are not supplied, read actual values with offsetWidth/Height, 4402 * and call board.resizeContainer() with this values. 4403 * <p> 4404 * If necessary, also call setBoundingBox(). 4405 * @param {Number} [width=this.containerObj.offsetWidth] Width of the container element 4406 * @param {Number} [height=this.containerObj.offsetHeight] Height of the container element 4407 * @returns {JXG.Board} Reference to the board 4408 * 4409 * @see JXG.Board#startResizeObserver 4410 * @see JXG.Board#resizeListener 4411 * @see JXG.Board#resizeContainer 4412 * @see JXG.Board#setBoundingBox 4413 * 4414 */ 4415 updateContainerDims: function (width, height) { 4416 var w = width, 4417 h = height, 4418 // bb, 4419 css, 4420 width_adjustment, height_adjustment; 4421 4422 if (width === undefined) { 4423 // Get size of the board's container div 4424 // 4425 // offsetWidth/Height ignores CSS transforms, 4426 // getBoundingClientRect includes CSS transforms 4427 // 4428 // bb = this.containerObj.getBoundingClientRect(); 4429 // w = bb.width; 4430 // h = bb.height; 4431 w = this.containerObj.offsetWidth; 4432 h = this.containerObj.offsetHeight; 4433 } 4434 4435 if (width === undefined && window && window.getComputedStyle) { 4436 // Subtract the border size 4437 css = window.getComputedStyle(this.containerObj, null); 4438 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width')); 4439 if (!isNaN(width_adjustment)) { 4440 w -= width_adjustment; 4441 } 4442 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width')); 4443 if (!isNaN(height_adjustment)) { 4444 h -= height_adjustment; 4445 } 4446 } 4447 4448 // If div is invisible - do nothing 4449 if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { 4450 return this; 4451 } 4452 4453 // If bounding box is not yet initialized, do it now. 4454 if (isNaN(this.getBoundingBox()[0])) { 4455 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep'); 4456 } 4457 4458 // Do nothing if the dimension did not change since being visible 4459 // the last time. Note that if the div had display:none in the mean time, 4460 // we did not store this._prevDim. 4461 if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) { 4462 return this; 4463 } 4464 // Set the size of the SVG or canvas element 4465 this.resizeContainer(w, h, true); 4466 this._prevDim = { 4467 w: w, 4468 h: h 4469 }; 4470 return this; 4471 }, 4472 4473 /** 4474 * Start observer which reacts to size changes of the JSXGraph 4475 * container div element. Calls updateContainerDims(). 4476 * If not available, an event listener for the window-resize event is started. 4477 * On mobile devices also scrolling might trigger resizes. 4478 * However, resize events triggered by scrolling events should be ignored. 4479 * Therefore, also a scrollListener is started. 4480 * Resize can be controlled with the board attribute resize. 4481 * 4482 * @see JXG.Board#updateContainerDims 4483 * @see JXG.Board#resizeListener 4484 * @see JXG.Board#scrollListener 4485 * @see JXG.Board#resize 4486 * 4487 */ 4488 startResizeObserver: function () { 4489 var that = this; 4490 4491 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4492 return; 4493 } 4494 4495 this.resizeObserver = new ResizeObserver(function (entries) { 4496 var bb; 4497 if (!that._isResizing) { 4498 that._isResizing = true; 4499 bb = entries[0].contentRect; 4500 window.setTimeout(function () { 4501 try { 4502 that.updateContainerDims(bb.width, bb.height); 4503 } catch (e) { 4504 JXG.debug(e); // Used to log errors during board.update() 4505 that.stopResizeObserver(); 4506 } finally { 4507 that._isResizing = false; 4508 } 4509 }, that.attr.resize.throttle); 4510 } 4511 }); 4512 this.resizeObserver.observe(this.containerObj); 4513 }, 4514 4515 /** 4516 * Stops the resize observer. 4517 * @see JXG.Board#startResizeObserver 4518 * 4519 */ 4520 stopResizeObserver: function () { 4521 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4522 return; 4523 } 4524 4525 if (Type.exists(this.resizeObserver)) { 4526 this.resizeObserver.unobserve(this.containerObj); 4527 } 4528 }, 4529 4530 /** 4531 * Fallback solutions if there is no resizeObserver available in the browser. 4532 * Reacts to resize events of the window (only). Otherwise similar to 4533 * startResizeObserver(). To handle changes of the visibility 4534 * of the JSXGraph container element, additionally an intersection observer is used. 4535 * which watches changes in the visibility of the JSXGraph container element. 4536 * This is necessary e.g. for register tabs or dia shows. 4537 * 4538 * @see JXG.Board#startResizeObserver 4539 * @see JXG.Board#startIntersectionObserver 4540 */ 4541 resizeListener: function () { 4542 var that = this; 4543 4544 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4545 return; 4546 } 4547 if (!this._isScrolling && !this._isResizing) { 4548 this._isResizing = true; 4549 window.setTimeout(function () { 4550 that.updateContainerDims(); 4551 that._isResizing = false; 4552 }, this.attr.resize.throttle); 4553 } 4554 }, 4555 4556 /** 4557 * Listener to watch for scroll events. Sets board._isScrolling = true 4558 * @param {Event} evt The browser's event object 4559 * 4560 * @see JXG.Board#startResizeObserver 4561 * @see JXG.Board#resizeListener 4562 * 4563 */ 4564 scrollListener: function (evt) { 4565 var that = this; 4566 4567 if (!Env.isBrowser) { 4568 return; 4569 } 4570 if (!this._isScrolling) { 4571 this._isScrolling = true; 4572 window.setTimeout(function () { 4573 that._isScrolling = false; 4574 }, 66); 4575 } 4576 }, 4577 4578 /** 4579 * Watch for changes of the visibility of the JSXGraph container element. 4580 * 4581 * @see JXG.Board#startResizeObserver 4582 * @see JXG.Board#resizeListener 4583 * 4584 */ 4585 startIntersectionObserver: function () { 4586 var that = this, 4587 options = { 4588 root: null, 4589 rootMargin: '0px', 4590 threshold: 0.8 4591 }; 4592 4593 try { 4594 this.intersectionObserver = new IntersectionObserver(function (entries) { 4595 // If bounding box is not yet initialized, do it now. 4596 if (isNaN(that.getBoundingBox()[0])) { 4597 that.updateContainerDims(); 4598 } 4599 }, options); 4600 this.intersectionObserver.observe(that.containerObj); 4601 } catch (err) { 4602 JXG.debug('JSXGraph: IntersectionObserver not available in this browser.'); 4603 } 4604 }, 4605 4606 /** 4607 * Stop the intersection observer 4608 * 4609 * @see JXG.Board#startIntersectionObserver 4610 * 4611 */ 4612 stopIntersectionObserver: function () { 4613 if (Type.exists(this.intersectionObserver)) { 4614 this.intersectionObserver.unobserve(this.containerObj); 4615 } 4616 }, 4617 4618 /** 4619 * Update the container before and after printing. 4620 * @param {Event} [evt] 4621 */ 4622 printListener: function(evt) { 4623 this.updateContainerDims(); 4624 }, 4625 4626 /** 4627 * Wrapper for printListener to be used in mediaQuery matches. 4628 * @param {MediaQueryList} mql 4629 */ 4630 printListenerMatch: function (mql) { 4631 if (mql.matches) { 4632 this.printListener(); 4633 } 4634 }, 4635 4636 /********************************************************** 4637 * 4638 * End of Event Handlers 4639 * 4640 **********************************************************/ 4641 4642 /** 4643 * Initialize the info box object which is used to display 4644 * the coordinates of points near the mouse pointer, 4645 * @returns {JXG.Board} Reference to the board 4646 */ 4647 initInfobox: function (attributes) { 4648 var attr = Type.copyAttributes(attributes, this.options, 'infobox'); 4649 4650 attr.id = this.id + '_infobox'; 4651 4652 /** 4653 * Infobox close to points in which the points' coordinates are displayed. 4654 * This is simply a JXG.Text element. Access through board.infobox. 4655 * Uses CSS class .JXGinfobox. 4656 * 4657 * @namespace 4658 * @name JXG.Board.infobox 4659 * @type JXG.Text 4660 * 4661 * @example 4662 * const board = JXG.JSXGraph.initBoard(BOARDID, { 4663 * boundingbox: [-0.5, 0.5, 0.5, -0.5], 4664 * intl: { 4665 * enabled: false, 4666 * locale: 'de-DE' 4667 * }, 4668 * keepaspectratio: true, 4669 * axis: true, 4670 * infobox: { 4671 * distanceY: 40, 4672 * intl: { 4673 * enabled: true, 4674 * options: { 4675 * minimumFractionDigits: 1, 4676 * maximumFractionDigits: 2 4677 * } 4678 * } 4679 * } 4680 * }); 4681 * var p = board.create('point', [0.1, 0.1], {}); 4682 * 4683 * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div> 4684 * <script type="text/javascript"> 4685 * (function() { 4686 * const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', { 4687 * boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false, 4688 * intl: { 4689 * enabled: false, 4690 * locale: 'de-DE' 4691 * }, 4692 * keepaspectratio: true, 4693 * axis: true, 4694 * infobox: { 4695 * distanceY: 40, 4696 * intl: { 4697 * enabled: true, 4698 * options: { 4699 * minimumFractionDigits: 1, 4700 * maximumFractionDigits: 2 4701 * } 4702 * } 4703 * } 4704 * }); 4705 * var p = board.create('point', [0.1, 0.1], {}); 4706 * })(); 4707 * 4708 * </script><pre> 4709 * 4710 */ 4711 this.infobox = this.create('text', [0, 0, '0,0'], attr); 4712 // this.infobox.needsUpdateSize = false; // That is not true, but it speeds drawing up. 4713 this.infobox.dump = false; 4714 4715 this.displayInfobox(false); 4716 return this; 4717 }, 4718 4719 /** 4720 * Updates and displays a little info box to show coordinates of current selected points. 4721 * @param {JXG.GeometryElement} el A GeometryElement 4722 * @returns {JXG.Board} Reference to the board 4723 * @see JXG.Board#displayInfobox 4724 * @see JXG.Board#showInfobox 4725 * @see Point#showInfobox 4726 * 4727 */ 4728 updateInfobox: function (el) { 4729 var x, y, xc, yc, 4730 vpinfoboxdigits, 4731 distX, distY, 4732 vpsi = el.evalVisProp('showinfobox'); 4733 4734 if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) { 4735 return this; 4736 } 4737 4738 if (Type.isPoint(el)) { 4739 xc = el.coords.usrCoords[1]; 4740 yc = el.coords.usrCoords[2]; 4741 distX = this.infobox.evalVisProp('distancex'); 4742 distY = this.infobox.evalVisProp('distancey'); 4743 4744 this.infobox.setCoords( 4745 xc + distX / this.unitX, 4746 yc + distY / this.unitY 4747 ); 4748 4749 vpinfoboxdigits = el.evalVisProp('infoboxdigits'); 4750 if (typeof el.infoboxText !== 'string') { 4751 if (vpinfoboxdigits === 'auto') { 4752 if (this.infobox.useLocale()) { 4753 x = this.infobox.formatNumberLocale(xc); 4754 y = this.infobox.formatNumberLocale(yc); 4755 } else { 4756 x = Type.autoDigits(xc); 4757 y = Type.autoDigits(yc); 4758 } 4759 } else if (Type.isNumber(vpinfoboxdigits)) { 4760 if (this.infobox.useLocale()) { 4761 x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits); 4762 y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits); 4763 } else { 4764 x = Type.toFixed(xc, vpinfoboxdigits); 4765 y = Type.toFixed(yc, vpinfoboxdigits); 4766 } 4767 4768 } else { 4769 x = xc; 4770 y = yc; 4771 } 4772 4773 this.highlightInfobox(x, y, el); 4774 } else { 4775 this.highlightCustomInfobox(el.infoboxText, el); 4776 } 4777 4778 this.displayInfobox(true); 4779 } 4780 return this; 4781 }, 4782 4783 /** 4784 * Set infobox visible / invisible. 4785 * 4786 * It uses its property hiddenByParent to memorize its status. 4787 * In this way, many DOM access can be avoided. 4788 * 4789 * @param {Boolean} val true for visible, false for invisible 4790 * @returns {JXG.Board} Reference to the board. 4791 * @see JXG.Board#updateInfobox 4792 * 4793 */ 4794 displayInfobox: function (val) { 4795 if (!val && this.focusObjects.length > 0 && 4796 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) { 4797 // If an element has focus we do not hide its infobox 4798 return this; 4799 } 4800 if (this.infobox.hiddenByParent === val) { 4801 this.infobox.hiddenByParent = !val; 4802 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer(); 4803 } 4804 return this; 4805 }, 4806 4807 // Alias for displayInfobox to be backwards compatible. 4808 // The method showInfobox clashes with the board attribute showInfobox 4809 showInfobox: function (val) { 4810 return this.displayInfobox(val); 4811 }, 4812 4813 /** 4814 * Changes the text of the info box to show the given coordinates. 4815 * @param {Number} x 4816 * @param {Number} y 4817 * @param {JXG.GeometryElement} [el] The element the mouse is pointing at 4818 * @returns {JXG.Board} Reference to the board. 4819 */ 4820 highlightInfobox: function (x, y, el) { 4821 this.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 4822 return this; 4823 }, 4824 4825 /** 4826 * Changes the text of the info box to what is provided via text. 4827 * @param {String} text 4828 * @param {JXG.GeometryElement} [el] 4829 * @returns {JXG.Board} Reference to the board. 4830 */ 4831 highlightCustomInfobox: function (text, el) { 4832 this.infobox.setText(text); 4833 return this; 4834 }, 4835 4836 /** 4837 * Remove highlighting of all elements. 4838 * @returns {JXG.Board} Reference to the board. 4839 */ 4840 dehighlightAll: function () { 4841 var el, 4842 pEl, 4843 stillHighlighted = {}, 4844 needsDeHighlight = false; 4845 4846 for (el in this.highlightedObjects) { 4847 if (this.highlightedObjects.hasOwnProperty(el)) { 4848 4849 pEl = this.highlightedObjects[el]; 4850 if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus 4851 if (this.hasMouseHandlers || this.hasPointerHandlers) { 4852 pEl.noHighlight(); 4853 } 4854 needsDeHighlight = true; 4855 } else { 4856 stillHighlighted[el] = pEl; 4857 } 4858 // In highlightedObjects should only be objects which fulfill all these conditions 4859 // And in case of complex elements, like a turtle based fractal, it should be faster to 4860 // just de-highlight the element instead of checking hasPoint... 4861 // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible) 4862 } 4863 } 4864 4865 this.highlightedObjects = stillHighlighted; 4866 4867 // We do not need to redraw during dehighlighting in CanvasRenderer 4868 // because we are redrawing anyhow 4869 // -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until 4870 // another object is highlighted. 4871 if (this.renderer.type === 'canvas' && needsDeHighlight) { 4872 this.prepareUpdate(); 4873 this.renderer.suspendRedraw(this); 4874 this.updateRenderer(); 4875 this.renderer.unsuspendRedraw(); 4876 } 4877 4878 return this; 4879 }, 4880 4881 /** 4882 * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose 4883 * once. 4884 * @private 4885 * @param {Number} x X coordinate in screen coordinates 4886 * @param {Number} y Y coordinate in screen coordinates 4887 * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates. 4888 * @see JXG.Board#getUsrCoordsOfMouse 4889 */ 4890 getScrCoordsOfMouse: function (x, y) { 4891 return [x, y]; 4892 }, 4893 4894 /** 4895 * This method calculates the user coords of the current mouse coordinates. 4896 * @param {Event} evt Event object containing the mouse coordinates. 4897 * @returns {Array} Coordinates [x, y] of the mouse in user coordinates. 4898 * @example 4899 * board.on('up', function (evt) { 4900 * var a = board.getUsrCoordsOfMouse(evt), 4901 * x = a[0], 4902 * y = a[1], 4903 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4904 * // Shorter version: 4905 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4906 * }); 4907 * 4908 * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div> 4909 * <script type='text/javascript'> 4910 * (function() { 4911 * var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746', 4912 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 4913 * board.on('up', function (evt) { 4914 * var a = board.getUsrCoordsOfMouse(evt), 4915 * x = a[0], 4916 * y = a[1], 4917 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4918 * // Shorter version: 4919 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4920 * }); 4921 * 4922 * })(); 4923 * 4924 * </script><pre> 4925 * 4926 * @see JXG.Board#getScrCoordsOfMouse 4927 * @see JXG.Board#getAllUnderMouse 4928 */ 4929 getUsrCoordsOfMouse: function (evt) { 4930 var cPos = this.getCoordsTopLeftCorner(), 4931 absPos = Env.getPosition(evt, null, this.document), 4932 x = absPos[0] - cPos[0], 4933 y = absPos[1] - cPos[1], 4934 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this); 4935 4936 return newCoords.usrCoords.slice(1); 4937 }, 4938 4939 /** 4940 * Collects all elements under current mouse position plus current user coordinates of mouse cursor. 4941 * @param {Event} evt Event object containing the mouse coordinates. 4942 * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse. 4943 * @see JXG.Board#getUsrCoordsOfMouse 4944 * @see JXG.Board#getAllObjectsUnderMouse 4945 */ 4946 getAllUnderMouse: function (evt) { 4947 var elList = this.getAllObjectsUnderMouse(evt); 4948 elList.push(this.getUsrCoordsOfMouse(evt)); 4949 4950 return elList; 4951 }, 4952 4953 /** 4954 * Collects all elements under current mouse position. 4955 * @param {Event} evt Event object containing the mouse coordinates. 4956 * @returns {Array} Array of elements at the current mouse position. 4957 * @see JXG.Board#getAllUnderMouse 4958 */ 4959 getAllObjectsUnderMouse: function (evt) { 4960 var cPos = this.getCoordsTopLeftCorner(), 4961 absPos = Env.getPosition(evt, null, this.document), 4962 dx = absPos[0] - cPos[0], 4963 dy = absPos[1] - cPos[1], 4964 elList = [], 4965 el, 4966 pEl, 4967 len = this.objectsList.length; 4968 4969 for (el = 0; el < len; el++) { 4970 pEl = this.objectsList[el]; 4971 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) { 4972 elList[elList.length] = pEl; 4973 } 4974 } 4975 4976 return elList; 4977 }, 4978 4979 /** 4980 * Update the coords object of all elements which possess this 4981 * property. This is necessary after changing the viewport. 4982 * @returns {JXG.Board} Reference to this board. 4983 **/ 4984 updateCoords: function () { 4985 var el, ob, 4986 len = this.objectsList.length; 4987 4988 for (ob = 0; ob < len; ob++) { 4989 el = this.objectsList[ob]; 4990 4991 if (Type.exists(el.coords)) { 4992 if (el.evalVisProp('frozen') === true) { 4993 if (el.is3D) { 4994 el.element2D.coords.screen2usr(); 4995 } else { 4996 el.coords.screen2usr(); 4997 } 4998 } else { 4999 if (el.is3D) { 5000 el.element2D.coords.usr2screen(); 5001 } else { 5002 el.coords.usr2screen(); 5003 if (Type.exists(el.actualCoords)) { 5004 el.actualCoords.usr2screen(); 5005 } 5006 } 5007 } 5008 } 5009 } 5010 return this; 5011 }, 5012 5013 /** 5014 * Moves the origin and initializes an update of all elements. 5015 * @param {Number} x 5016 * @param {Number} y 5017 * @param {Boolean} [diff=false] 5018 * @returns {JXG.Board} Reference to this board. 5019 */ 5020 moveOrigin: function (x, y, diff) { 5021 var ox, oy, ul, lr; 5022 if (Type.exists(x) && Type.exists(y)) { 5023 ox = this.origin.scrCoords[1]; 5024 oy = this.origin.scrCoords[2]; 5025 5026 this.origin.scrCoords[1] = x; 5027 this.origin.scrCoords[2] = y; 5028 5029 if (diff) { 5030 this.origin.scrCoords[1] -= this.drag_dx; 5031 this.origin.scrCoords[2] -= this.drag_dy; 5032 } 5033 5034 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords; 5035 lr = new Coords( 5036 Const.COORDS_BY_SCREEN, 5037 [this.canvasWidth, this.canvasHeight], 5038 this 5039 ).usrCoords; 5040 if ( 5041 ul[1] < this.maxboundingbox[0] - Mat.eps || 5042 ul[2] > this.maxboundingbox[1] + Mat.eps || 5043 lr[1] > this.maxboundingbox[2] + Mat.eps || 5044 lr[2] < this.maxboundingbox[3] - Mat.eps 5045 ) { 5046 this.origin.scrCoords[1] = ox; 5047 this.origin.scrCoords[2] = oy; 5048 } 5049 } 5050 5051 this.updateCoords().clearTraces().fullUpdate(); 5052 this.triggerEventHandlers(['boundingbox']); 5053 5054 return this; 5055 }, 5056 5057 /** 5058 * Add conditional updates to the elements. 5059 * @param {String} str String containing conditional update in geonext syntax 5060 */ 5061 addConditions: function (str) { 5062 var term, 5063 m, 5064 left, 5065 right, 5066 name, 5067 el, 5068 property, 5069 functions = [], 5070 // plaintext = 'var el, x, y, c, rgbo;\n', 5071 i = str.indexOf('<data>'), 5072 j = str.indexOf('<' + '/data>'), 5073 xyFun = function (board, el, f, what) { 5074 return function () { 5075 var e, t; 5076 5077 e = board.select(el.id); 5078 t = e.coords.usrCoords[what]; 5079 5080 if (what === 2) { 5081 e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]); 5082 } else { 5083 e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]); 5084 } 5085 e.prepareUpdate().update(); 5086 }; 5087 }, 5088 visFun = function (board, el, f) { 5089 return function () { 5090 var e, v; 5091 5092 e = board.select(el.id); 5093 v = f(); 5094 5095 e.setAttribute({ visible: v }); 5096 }; 5097 }, 5098 colFun = function (board, el, f, what) { 5099 return function () { 5100 var e, v; 5101 5102 e = board.select(el.id); 5103 v = f(); 5104 5105 if (what === 'strokewidth') { 5106 e.visProp.strokewidth = v; 5107 } else { 5108 v = Color.rgba2rgbo(v); 5109 e.visProp[what + 'color'] = v[0]; 5110 e.visProp[what + 'opacity'] = v[1]; 5111 } 5112 }; 5113 }, 5114 posFun = function (board, el, f) { 5115 return function () { 5116 var e = board.select(el.id); 5117 5118 e.position = f(); 5119 }; 5120 }, 5121 styleFun = function (board, el, f) { 5122 return function () { 5123 var e = board.select(el.id); 5124 5125 e.setStyle(f()); 5126 }; 5127 }; 5128 5129 if (i < 0) { 5130 return; 5131 } 5132 5133 while (i >= 0) { 5134 term = str.slice(i + 6, j); // throw away <data> 5135 m = term.indexOf('='); 5136 left = term.slice(0, m); 5137 right = term.slice(m + 1); 5138 m = left.indexOf('.'); // Resulting variable names must not contain dots, e.g. ' Steuern akt.' 5139 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace) 5140 el = this.elementsByName[Type.unescapeHTML(name)]; 5141 5142 property = left 5143 .slice(m + 1) 5144 .replace(/\s+/g, '') 5145 .toLowerCase(); // remove whitespace in property 5146 right = Type.createFunction(right, this, '', true); 5147 5148 // Debug 5149 if (!Type.exists(this.elementsByName[name])) { 5150 JXG.debug('debug conditions: |' + name + '| undefined'); 5151 } else { 5152 // plaintext += 'el = this.objects[\'' + el.id + '\'];\n'; 5153 5154 switch (property) { 5155 case 'x': 5156 functions.push(xyFun(this, el, right, 2)); 5157 break; 5158 case 'y': 5159 functions.push(xyFun(this, el, right, 1)); 5160 break; 5161 case 'visible': 5162 functions.push(visFun(this, el, right)); 5163 break; 5164 case 'position': 5165 functions.push(posFun(this, el, right)); 5166 break; 5167 case 'stroke': 5168 functions.push(colFun(this, el, right, 'stroke')); 5169 break; 5170 case 'style': 5171 functions.push(styleFun(this, el, right)); 5172 break; 5173 case 'strokewidth': 5174 functions.push(colFun(this, el, right, 'strokewidth')); 5175 break; 5176 case 'fill': 5177 functions.push(colFun(this, el, right, 'fill')); 5178 break; 5179 case 'label': 5180 break; 5181 default: 5182 JXG.debug( 5183 'property "' + 5184 property + 5185 '" in conditions not yet implemented:' + 5186 right 5187 ); 5188 break; 5189 } 5190 } 5191 str = str.slice(j + 7); // cut off '</data>' 5192 i = str.indexOf('<data>'); 5193 j = str.indexOf('<' + '/data>'); 5194 } 5195 5196 this.updateConditions = function () { 5197 var i; 5198 5199 for (i = 0; i < functions.length; i++) { 5200 functions[i](); 5201 } 5202 5203 this.prepareUpdate().updateElements(); 5204 return true; 5205 }; 5206 this.updateConditions(); 5207 }, 5208 5209 /** 5210 * Computes the commands in the conditions-section of the gxt file. 5211 * It is evaluated after an update, before the unsuspendRedraw. 5212 * The function is generated in 5213 * @see JXG.Board#addConditions 5214 * @private 5215 */ 5216 updateConditions: function () { 5217 return false; 5218 }, 5219 5220 /** 5221 * Calculates adequate snap sizes. 5222 * @returns {JXG.Board} Reference to the board. 5223 */ 5224 calculateSnapSizes: function () { 5225 var p1, p2, 5226 bbox = this.getBoundingBox(), 5227 gridStep = Type.evaluate(this.options.grid.majorStep), 5228 gridX = Type.evaluate(this.options.grid.gridX), 5229 gridY = Type.evaluate(this.options.grid.gridY), 5230 x, y; 5231 5232 if (!Type.isArray(gridStep)) { 5233 gridStep = [gridStep, gridStep]; 5234 } 5235 if (gridStep.length < 2) { 5236 gridStep = [gridStep[0], gridStep[0]]; 5237 } 5238 if (Type.exists(gridX)) { 5239 gridStep[0] = gridX; 5240 } 5241 if (Type.exists(gridY)) { 5242 gridStep[1] = gridY; 5243 } 5244 5245 if (gridStep[0] === 'auto') { 5246 gridStep[0] = 1; 5247 } else { 5248 gridStep[0] = Type.parseNumber(gridStep[0], Math.abs(bbox[1] - bbox[3]), 1 / this.unitX); 5249 } 5250 if (gridStep[1] === 'auto') { 5251 gridStep[1] = 1; 5252 } else { 5253 gridStep[1] = Type.parseNumber(gridStep[1], Math.abs(bbox[0] - bbox[2]), 1 / this.unitY); 5254 } 5255 5256 p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this); 5257 p2 = new Coords( 5258 Const.COORDS_BY_USER, 5259 [gridStep[0], gridStep[1]], 5260 this 5261 ); 5262 x = p1.scrCoords[1] - p2.scrCoords[1]; 5263 y = p1.scrCoords[2] - p2.scrCoords[2]; 5264 5265 this.options.grid.snapSizeX = gridStep[0]; 5266 while (Math.abs(x) > 25) { 5267 this.options.grid.snapSizeX *= 2; 5268 x /= 2; 5269 } 5270 5271 this.options.grid.snapSizeY = gridStep[1]; 5272 while (Math.abs(y) > 25) { 5273 this.options.grid.snapSizeY *= 2; 5274 y /= 2; 5275 } 5276 5277 return this; 5278 }, 5279 5280 /** 5281 * Apply update on all objects with the new zoom-factors. Clears all traces. 5282 * @returns {JXG.Board} Reference to the board. 5283 */ 5284 applyZoom: function () { 5285 this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate(); 5286 5287 return this; 5288 }, 5289 5290 /** 5291 * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5292 * The zoom operation is centered at x, y. 5293 * @param {Number} [x] 5294 * @param {Number} [y] 5295 * @returns {JXG.Board} Reference to the board 5296 */ 5297 zoomIn: function (x, y) { 5298 var bb = this.getBoundingBox(), 5299 zX = Type.evaluate(this.attr.zoom.factorx), 5300 zY = Type.evaluate(this.attr.zoom.factory), 5301 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX), 5302 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY), 5303 lr = 0.5, 5304 tr = 0.5, 5305 ma = Type.evaluate(this.attr.zoom.max), 5306 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated 5307 5308 if ( 5309 (this.zoomX > ma && zX > 1.0) || 5310 (this.zoomY > ma && zY > 1.0) || 5311 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices 5312 (this.zoomY < mi && zY < 1.0) 5313 ) { 5314 return this; 5315 } 5316 5317 if (Type.isNumber(x) && Type.isNumber(y)) { 5318 lr = (x - bb[0]) / (bb[2] - bb[0]); 5319 tr = (bb[1] - y) / (bb[1] - bb[3]); 5320 } 5321 5322 this.setBoundingBox( 5323 [ 5324 bb[0] + dX * lr, 5325 bb[1] - dY * tr, 5326 bb[2] - dX * (1 - lr), 5327 bb[3] + dY * (1 - tr) 5328 ], 5329 this.keepaspectratio, 5330 'update' 5331 ); 5332 return this.applyZoom(); 5333 }, 5334 5335 /** 5336 * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5337 * The zoom operation is centered at x, y. 5338 * 5339 * @param {Number} [x] 5340 * @param {Number} [y] 5341 * @returns {JXG.Board} Reference to the board 5342 */ 5343 zoomOut: function (x, y) { 5344 var bb = this.getBoundingBox(), 5345 zX = Type.evaluate(this.attr.zoom.factorx), 5346 zY = Type.evaluate(this.attr.zoom.factory), 5347 dX = (bb[2] - bb[0]) * (1.0 - zX), 5348 dY = (bb[1] - bb[3]) * (1.0 - zY), 5349 lr = 0.5, 5350 tr = 0.5, 5351 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated 5352 5353 if (this.zoomX < mi || this.zoomY < mi) { 5354 return this; 5355 } 5356 5357 if (Type.isNumber(x) && Type.isNumber(y)) { 5358 lr = (x - bb[0]) / (bb[2] - bb[0]); 5359 tr = (bb[1] - y) / (bb[1] - bb[3]); 5360 } 5361 5362 this.setBoundingBox( 5363 [ 5364 bb[0] + dX * lr, 5365 bb[1] - dY * tr, 5366 bb[2] - dX * (1 - lr), 5367 bb[3] + dY * (1 - tr) 5368 ], 5369 this.keepaspectratio, 5370 'update' 5371 ); 5372 5373 return this.applyZoom(); 5374 }, 5375 5376 /** 5377 * Reset the zoom level to the original zoom level from initBoard(); 5378 * Additionally, if the board as been initialized with a boundingBox (which is the default), 5379 * restore the viewport to the original viewport during initialization. Otherwise, 5380 * (i.e. if the board as been initialized with unitX/Y and originX/Y), 5381 * just set the zoom level to 100%. 5382 * 5383 * @returns {JXG.Board} Reference to the board 5384 */ 5385 zoom100: function () { 5386 var bb, dX, dY; 5387 5388 if (Type.exists(this.attr.boundingbox)) { 5389 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset'); 5390 } else { 5391 // Board has been set up with unitX/Y and originX/Y 5392 bb = this.getBoundingBox(); 5393 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5; 5394 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5; 5395 this.setBoundingBox( 5396 [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], 5397 this.keepaspectratio, 5398 'reset' 5399 ); 5400 } 5401 return this.applyZoom(); 5402 }, 5403 5404 /** 5405 * Zooms the board so every visible point is shown. Keeps aspect ratio. 5406 * @returns {JXG.Board} Reference to the board 5407 */ 5408 zoomAllPoints: function () { 5409 var el, 5410 border, 5411 borderX, 5412 borderY, 5413 pEl, 5414 minX = 0, 5415 maxX = 0, 5416 minY = 0, 5417 maxY = 0, 5418 len = this.objectsList.length; 5419 5420 for (el = 0; el < len; el++) { 5421 pEl = this.objectsList[el]; 5422 5423 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) { 5424 if (pEl.coords.usrCoords[1] < minX) { 5425 minX = pEl.coords.usrCoords[1]; 5426 } else if (pEl.coords.usrCoords[1] > maxX) { 5427 maxX = pEl.coords.usrCoords[1]; 5428 } 5429 if (pEl.coords.usrCoords[2] > maxY) { 5430 maxY = pEl.coords.usrCoords[2]; 5431 } else if (pEl.coords.usrCoords[2] < minY) { 5432 minY = pEl.coords.usrCoords[2]; 5433 } 5434 } 5435 } 5436 5437 border = 50; 5438 borderX = border / this.unitX; 5439 borderY = border / this.unitY; 5440 5441 this.setBoundingBox( 5442 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], 5443 this.keepaspectratio, 5444 'update' 5445 ); 5446 5447 return this.applyZoom(); 5448 }, 5449 5450 /** 5451 * Reset the bounding box and the zoom level to 100% such that a given set of elements is 5452 * within the board's viewport. 5453 * @param {Array} elements A set of elements given by id, reference, or name. 5454 * @returns {JXG.Board} Reference to the board. 5455 */ 5456 zoomElements: function (elements) { 5457 var i, e, 5458 box, 5459 newBBox = [Infinity, -Infinity, -Infinity, Infinity], 5460 cx, cy, 5461 dx, dy, 5462 d; 5463 5464 if (!Type.isArray(elements) || elements.length === 0) { 5465 return this; 5466 } 5467 5468 for (i = 0; i < elements.length; i++) { 5469 e = this.select(elements[i]); 5470 5471 box = e.bounds(); 5472 if (Type.isArray(box)) { 5473 if (box[0] < newBBox[0]) { 5474 newBBox[0] = box[0]; 5475 } 5476 if (box[1] > newBBox[1]) { 5477 newBBox[1] = box[1]; 5478 } 5479 if (box[2] > newBBox[2]) { 5480 newBBox[2] = box[2]; 5481 } 5482 if (box[3] < newBBox[3]) { 5483 newBBox[3] = box[3]; 5484 } 5485 } 5486 } 5487 5488 if (Type.isArray(newBBox)) { 5489 cx = 0.5 * (newBBox[0] + newBBox[2]); 5490 cy = 0.5 * (newBBox[1] + newBBox[3]); 5491 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5; 5492 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5; 5493 d = Math.max(dx, dy); 5494 this.setBoundingBox( 5495 [cx - d, cy + d, cx + d, cy - d], 5496 this.keepaspectratio, 5497 'update' 5498 ); 5499 } 5500 5501 return this; 5502 }, 5503 5504 /** 5505 * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>. 5506 * @param {Number} fX 5507 * @param {Number} fY 5508 * @returns {JXG.Board} Reference to the board. 5509 */ 5510 setZoom: function (fX, fY) { 5511 var oX = this.attr.zoom.factorx, 5512 oY = this.attr.zoom.factory; 5513 5514 this.attr.zoom.factorx = fX / this.zoomX; 5515 this.attr.zoom.factory = fY / this.zoomY; 5516 5517 this.zoomIn(); 5518 5519 this.attr.zoom.factorx = oX; 5520 this.attr.zoom.factory = oY; 5521 5522 return this; 5523 }, 5524 5525 /** 5526 * Inner, recursive method of removeObject. 5527 * 5528 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5529 * The element(s) is/are given by name, id or a reference. 5530 * @param {Boolean} [saveMethod=false] If saveMethod=true, the algorithm runs through all elements 5531 * and tests if the element to be deleted is a child element. If this is the case, it will be 5532 * removed from the list of child elements. If saveMethod=false (default), the element 5533 * is removed from the lists of child elements of all its ancestors. 5534 * The latter should be much faster. 5535 * @returns {JXG.Board} Reference to the board 5536 * @private 5537 */ 5538 _removeObj: function (object, saveMethod) { 5539 var el, o, i; 5540 5541 if (Type.isArray(object)) { 5542 for (i = 0; i < object.length; i++) { 5543 this._removeObj(object[i], saveMethod); 5544 } 5545 5546 return this; 5547 } 5548 5549 object = this.select(object); 5550 5551 // If the object which is about to be removed is unknown or a string, do nothing. 5552 // it is a string if a string was given and could not be resolved to an element. 5553 if (!Type.exists(object) || Type.isString(object)) { 5554 return this; 5555 } 5556 5557 try { 5558 // remove all children. 5559 for (el in object.childElements) { 5560 if (object.childElements.hasOwnProperty(el)) { 5561 object.childElements[el].board._removeObj(object.childElements[el]); 5562 } 5563 } 5564 5565 // Remove all children in elements like turtle 5566 for (el in object.objects) { 5567 if (object.objects.hasOwnProperty(el)) { 5568 object.objects[el].board._removeObj(object.objects[el]); 5569 } 5570 } 5571 5572 // Remove the element from the childElement list and the descendant list of all elements. 5573 if (saveMethod) { 5574 // Running through all objects has quadratic complexity if many objects are deleted. 5575 for (el in this.objects) { 5576 if (this.objects.hasOwnProperty(el)) { 5577 if ( 5578 Type.exists(this.objects[el].childElements) && 5579 Type.exists( 5580 this.objects[el].childElements.hasOwnProperty(object.id) 5581 ) 5582 ) { 5583 delete this.objects[el].childElements[object.id]; 5584 delete this.objects[el].descendants[object.id]; 5585 } 5586 } 5587 } 5588 } else if (Type.exists(object.ancestors)) { 5589 // Running through the ancestors should be much more efficient. 5590 for (el in object.ancestors) { 5591 if (object.ancestors.hasOwnProperty(el)) { 5592 if ( 5593 Type.exists(object.ancestors[el].childElements) && 5594 Type.exists( 5595 object.ancestors[el].childElements.hasOwnProperty(object.id) 5596 ) 5597 ) { 5598 delete object.ancestors[el].childElements[object.id]; 5599 delete object.ancestors[el].descendants[object.id]; 5600 } 5601 } 5602 } 5603 } 5604 5605 // remove the object itself from our control structures 5606 if (object._pos > -1) { 5607 this.objectsList.splice(object._pos, 1); 5608 // Quadratic complexity for reindexing the positions: 5609 for (i = object._pos; i < this.objectsList.length; i++) { 5610 o = this.objectsList[i]; 5611 if (o._pos > -1) { 5612 o._pos--; 5613 } 5614 } 5615 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) { 5616 JXG.debug( 5617 'Board.removeObject: object ' + object.id + ' not found in list.' 5618 ); 5619 } 5620 5621 delete this.objects[object.id]; 5622 delete this.elementsByName[object.name]; 5623 5624 if (object.visProp && object.evalVisProp('trace')) { 5625 object.clearTrace(); 5626 } 5627 5628 // the object deletion itself is handled by the object. 5629 if (Type.exists(object.remove)) { 5630 object.remove(); 5631 } 5632 } catch (e) { 5633 JXG.debug(object.id + ': Could not be removed: ' + e); 5634 } 5635 5636 return this; 5637 }, 5638 5639 /** 5640 * Removes object from board and from the renderer object. 5641 * <p> 5642 * <b>Performance hints:</b> It is recommended to use the JSXGraph object's id. 5643 * If many elements are removed, it is best to either 5644 * <ul> 5645 * <li> remove the whole array if the elements are contained in an array instead 5646 * of looping through the array OR 5647 * <li> call <tt>board.suspendUpdate()</tt> 5648 * before looping through the elements to be removed and call 5649 * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop 5650 * in reverse order, i.e. remove the object in reverse order of their creation time. 5651 * </ul> 5652 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5653 * The element(s) is/are given by name, id or a reference. 5654 * @param {Boolean} saveMethod If true, the algorithm runs through all elements 5655 * and tests if the element to be deleted is a child element. If yes, it will be 5656 * removed from the list of child elements. If false (default), the element 5657 * is removed from the lists of child elements of all its ancestors. 5658 * This should be much faster. 5659 * @returns {JXG.Board} Reference to the board 5660 */ 5661 removeObject: function (object, saveMethod) { 5662 var i; 5663 5664 this.renderer.suspendRedraw(this); 5665 if (Type.isArray(object)) { 5666 for (i = 0; i < object.length; i++) { 5667 this._removeObj(object[i], saveMethod); 5668 } 5669 } else { 5670 this._removeObj(object, saveMethod); 5671 } 5672 this.renderer.unsuspendRedraw(); 5673 5674 this.update(); 5675 return this; 5676 }, 5677 5678 /** 5679 * Removes the ancestors of an object an the object itself from board and renderer. 5680 * @param {JXG.GeometryElement} object The object to remove. 5681 * @returns {JXG.Board} Reference to the board 5682 */ 5683 removeAncestors: function (object) { 5684 var anc; 5685 5686 for (anc in object.ancestors) { 5687 if (object.ancestors.hasOwnProperty(anc)) { 5688 this.removeAncestors(object.ancestors[anc]); 5689 } 5690 } 5691 5692 this.removeObject(object); 5693 5694 return this; 5695 }, 5696 5697 /** 5698 * Initialize some objects which are contained in every GEONExT construction by default, 5699 * but are not contained in the gxt files. 5700 * @returns {JXG.Board} Reference to the board 5701 */ 5702 initGeonextBoard: function () { 5703 var p1, p2, p3; 5704 5705 p1 = this.create('point', [0, 0], { 5706 id: this.id + 'g00e0', 5707 name: 'Ursprung', 5708 withLabel: false, 5709 visible: false, 5710 fixed: true 5711 }); 5712 5713 p2 = this.create('point', [1, 0], { 5714 id: this.id + 'gX0e0', 5715 name: 'Punkt_1_0', 5716 withLabel: false, 5717 visible: false, 5718 fixed: true 5719 }); 5720 5721 p3 = this.create('point', [0, 1], { 5722 id: this.id + 'gY0e0', 5723 name: 'Punkt_0_1', 5724 withLabel: false, 5725 visible: false, 5726 fixed: true 5727 }); 5728 5729 this.create('line', [p1, p2], { 5730 id: this.id + 'gXLe0', 5731 name: 'X-Achse', 5732 withLabel: false, 5733 visible: false 5734 }); 5735 5736 this.create('line', [p1, p3], { 5737 id: this.id + 'gYLe0', 5738 name: 'Y-Achse', 5739 withLabel: false, 5740 visible: false 5741 }); 5742 5743 return this; 5744 }, 5745 5746 /** 5747 * Change the height and width of the board's container. 5748 * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using 5749 * the actual size of the bounding box and the actual value of keepaspectratio. 5750 * If setBoundingbox() should not be called automatically, 5751 * call resizeContainer with dontSetBoundingBox == true. 5752 * @param {Number} canvasWidth New width of the container. 5753 * @param {Number} canvasHeight New height of the container. 5754 * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element. 5755 * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(), but keep view centered around original visible center. 5756 * @returns {JXG.Board} Reference to the board 5757 */ 5758 resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) { 5759 var box, 5760 oldWidth, oldHeight, 5761 oX, oY; 5762 5763 oldWidth = this.canvasWidth; 5764 oldHeight = this.canvasHeight; 5765 5766 if (!dontSetBoundingBox) { 5767 box = this.getBoundingBox(); // This is the actual bounding box. 5768 } 5769 5770 // this.canvasWidth = Math.max(parseFloat(canvasWidth), Mat.eps); 5771 // this.canvasHeight = Math.max(parseFloat(canvasHeight), Mat.eps); 5772 this.canvasWidth = parseFloat(canvasWidth); 5773 this.canvasHeight = parseFloat(canvasHeight); 5774 5775 if (!dontset) { 5776 this.containerObj.style.width = this.canvasWidth + 'px'; 5777 this.containerObj.style.height = this.canvasHeight + 'px'; 5778 } 5779 this.renderer.resize(this.canvasWidth, this.canvasHeight); 5780 5781 if (!dontSetBoundingBox) { 5782 this.setBoundingBox(box, this.keepaspectratio, 'keep'); 5783 } else { 5784 oX = (this.canvasWidth - oldWidth) * 0.5; 5785 oY = (this.canvasHeight - oldHeight) * 0.5; 5786 5787 this.moveOrigin( 5788 this.origin.scrCoords[1] + oX, 5789 this.origin.scrCoords[2] + oY 5790 ); 5791 } 5792 5793 return this; 5794 }, 5795 5796 /** 5797 * Lists the dependencies graph in a new HTML-window. 5798 * @returns {JXG.Board} Reference to the board 5799 */ 5800 showDependencies: function () { 5801 var el, t, c, f, i; 5802 5803 t = '<p>\n'; 5804 for (el in this.objects) { 5805 if (this.objects.hasOwnProperty(el)) { 5806 i = 0; 5807 for (c in this.objects[el].childElements) { 5808 if (this.objects[el].childElements.hasOwnProperty(c)) { 5809 i += 1; 5810 } 5811 } 5812 if (i >= 0) { 5813 t += '<strong>' + this.objects[el].id + ':<' + '/strong> '; 5814 } 5815 5816 for (c in this.objects[el].childElements) { 5817 if (this.objects[el].childElements.hasOwnProperty(c)) { 5818 t += 5819 this.objects[el].childElements[c].id + 5820 '(' + 5821 this.objects[el].childElements[c].name + 5822 ')' + 5823 ', '; 5824 } 5825 } 5826 t += '<p>\n'; 5827 } 5828 } 5829 t += '<' + '/p>\n'; 5830 f = window.open(); 5831 f.document.open(); 5832 f.document.write(t); 5833 f.document.close(); 5834 return this; 5835 }, 5836 5837 /** 5838 * Lists the XML code of the construction in a new HTML-window. 5839 * @returns {JXG.Board} Reference to the board 5840 */ 5841 showXML: function () { 5842 var f = window.open(''); 5843 f.document.open(); 5844 f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>'); 5845 f.document.close(); 5846 return this; 5847 }, 5848 5849 /** 5850 * Sets for all objects the needsUpdate flag to 'true'. 5851 * @param{JXG.GeometryElement} [drag=undefined] Optional element that is dragged. 5852 * @returns {JXG.Board} Reference to the board 5853 */ 5854 prepareUpdate: function (drag) { 5855 var el, i, 5856 pEl, 5857 len = this.objectsList.length; 5858 5859 /* 5860 if (this.attr.updatetype === 'hierarchical') { 5861 return this; 5862 } 5863 */ 5864 5865 for (el = 0; el < len; el++) { 5866 pEl = this.objectsList[el]; 5867 if (this._change3DView || 5868 (Type.exists(drag) && drag.elType === 'view3d_slider') 5869 ) { 5870 // The 3D view has changed. No elements are recomputed, 5871 // only 3D elements are projected to the new view. 5872 pEl.needsUpdate = 5873 pEl.visProp.element3d || 5874 pEl.elType === 'view3d' || 5875 pEl.elType === 'view3d_slider' || 5876 this.needsFullUpdate; 5877 5878 // Special case sphere3d in central projection: 5879 // We have to update the defining points of the ellipse 5880 if (pEl.visProp.element3d && 5881 pEl.visProp.element3d.type === Const.OBJECT_TYPE_SPHERE3D 5882 ) { 5883 for (i = 0; i < pEl.parents.length; i++) { 5884 this.objects[pEl.parents[i]].needsUpdate = true; 5885 } 5886 } 5887 } else { 5888 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5889 } 5890 } 5891 5892 for (el in this.groups) { 5893 if (this.groups.hasOwnProperty(el)) { 5894 pEl = this.groups[el]; 5895 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5896 } 5897 } 5898 5899 return this; 5900 }, 5901 5902 /** 5903 * Runs through all elements and calls their update() method. 5904 * @param {JXG.GeometryElement} drag Element that caused the update. 5905 * @returns {JXG.Board} Reference to the board 5906 */ 5907 updateElements: function (drag) { 5908 var el, pEl; 5909 //var childId, i = 0; 5910 5911 drag = this.select(drag); 5912 5913 /* 5914 if (Type.exists(drag)) { 5915 for (el = 0; el < this.objectsList.length; el++) { 5916 pEl = this.objectsList[el]; 5917 if (pEl.id === drag.id) { 5918 i = el; 5919 break; 5920 } 5921 } 5922 } 5923 */ 5924 for (el = 0; el < this.objectsList.length; el++) { 5925 pEl = this.objectsList[el]; 5926 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) { 5927 pEl.updateSize(); 5928 } 5929 5930 // For updates of an element we distinguish if the dragged element is updated or 5931 // other elements are updated. 5932 // The difference lies in the treatment of gliders and points based on transformations. 5933 pEl.update(!Type.exists(drag) || pEl.id !== drag.id).updateVisibility(); 5934 } 5935 5936 // update groups last 5937 for (el in this.groups) { 5938 if (this.groups.hasOwnProperty(el)) { 5939 this.groups[el].update(drag); 5940 } 5941 } 5942 5943 return this; 5944 }, 5945 5946 /** 5947 * Runs through all elements and calls their update() method. 5948 * @returns {JXG.Board} Reference to the board 5949 */ 5950 updateRenderer: function () { 5951 var el, 5952 len = this.objectsList.length, 5953 autoPositionLabelList = [], 5954 currentIndex, randomIndex; 5955 5956 if (!this.renderer) { 5957 return; 5958 } 5959 5960 /* 5961 objs = this.objectsList.slice(0); 5962 objs.sort(function (a, b) { 5963 if (a.visProp.layer < b.visProp.layer) { 5964 return -1; 5965 } else if (a.visProp.layer === b.visProp.layer) { 5966 return b.lastDragTime.getTime() - a.lastDragTime.getTime(); 5967 } else { 5968 return 1; 5969 } 5970 }); 5971 */ 5972 5973 if (this.renderer.type === 'canvas') { 5974 this.updateRendererCanvas(); 5975 } else { 5976 for (el = 0; el < len; el++) { 5977 if (this.objectsList[el].visProp.islabel && this.objectsList[el].visProp.autoposition) { 5978 autoPositionLabelList.push(el); 5979 } else { 5980 this.objectsList[el].updateRenderer(); 5981 } 5982 } 5983 5984 currentIndex = autoPositionLabelList.length; 5985 5986 // Randomize the order of the labels 5987 while (currentIndex !== 0) { 5988 randomIndex = Math.floor(Math.random() * currentIndex); 5989 currentIndex--; 5990 [autoPositionLabelList[currentIndex], autoPositionLabelList[randomIndex]] = [autoPositionLabelList[randomIndex], autoPositionLabelList[currentIndex]]; 5991 } 5992 5993 for (el = 0; el < autoPositionLabelList.length; el++) { 5994 this.objectsList[autoPositionLabelList[el]].updateRenderer(); 5995 } 5996 /* 5997 for (el = autoPositionLabelList.length - 1; el >= 0; el--) { 5998 this.objectsList[autoPositionLabelList[el]].updateRenderer(); 5999 } 6000 */ 6001 } 6002 return this; 6003 }, 6004 6005 /** 6006 * Runs through all elements and calls their update() method. 6007 * This is a special version for the CanvasRenderer. 6008 * Here, we have to do our own layer handling. 6009 * @returns {JXG.Board} Reference to the board 6010 */ 6011 updateRendererCanvas: function () { 6012 var el, pEl, 6013 olen = this.objectsList.length, 6014 // i, minim, lay, 6015 // layers = this.options.layer, 6016 // len = this.options.layer.numlayers, 6017 // last = Number.NEGATIVE_INFINITY.toExponential, 6018 depth_order_layers = [], 6019 objects_sorted, 6020 6021 /** 6022 * Function to sort elements for depth ordering in canvas renderer. 6023 * Only relevant for elements having a zIndex. 6024 * Sort the elements for the canvas rendering according to 6025 * their layer, _pos, depthOrder (with this priority). 6026 * @param {JXG.GeometryObject} a 6027 * @param {JXG.GeometryObject} b 6028 * @returns Number 6029 * @private 6030 */ 6031 _compareDepth = function(a, b) { 6032 if (a.visProp.layer !== b.visProp.layer) { 6033 // For elements in different layers, the element in the 6034 // higher layer is in front. 6035 return a.visProp.layer - b.visProp.layer; 6036 } 6037 6038 // From here on, both objects are in the same layer. 6039 6040 if (depth_order_layers.indexOf(a.visProp.layer) === -1) { 6041 // The layer is not depth ordered. 6042 return a._pos - b._pos; 6043 } 6044 6045 // From here on, both objects are in the same layer 6046 // and the layer is depth ordered. 6047 6048 // The objects are in the same layer and the layer is depth ordered 6049 // But neither element is the 2D element of a 3D element. 6050 if (!a.visProp.element3d && !b.visProp.element3d) { 6051 return a._pos - b._pos; 6052 } 6053 6054 if (a.visProp.element3d && !b.visProp.element3d) { 6055 return -1; 6056 } 6057 6058 if (!a.visProp.element3d && b.visProp.element3d) { 6059 return 1; 6060 } 6061 6062 // Finqally, both elements are 2D elements of a 3D element. 6063 return a.visProp.element3d.zIndex - b.visProp.element3d.zIndex; 6064 }; 6065 6066 // Only one view3d element is supported. Get the depth order layers and 6067 // update the zIndices of the 3D elements. 6068 for (el = 0; el < olen; el++) { 6069 pEl = this.objectsList[el]; 6070 if (pEl.elType === 'view3d' && 6071 pEl.evalVisProp('depthorder.enabled') 6072 ) { 6073 depth_order_layers = pEl.evalVisProp('depthorder.layers'); 6074 pEl.updateRenderer(); 6075 break; 6076 } 6077 } 6078 6079 // objects_sorted = this.objectsList.toSorted(_compareDepth); 6080 6081 // 3D elements are not rendered, but their subelements element2D 6082 objects_sorted = this.objectsList.filter(function(e) { return !e.is3D; }).toSorted(_compareDepth); 6083 olen = objects_sorted.length; 6084 for (el = 0; el < olen; el++) { 6085 if ( 6086 objects_sorted[el].visPropCalc.visible && 6087 objects_sorted[el].type !== Const.OBJECT_TYPE_FACE3D // For these, updateRenderer is triggered in polyhedron3d.updateRenderer 6088 ) { 6089 objects_sorted[el].prepareUpdate().updateRenderer(); 6090 } 6091 } 6092 6093 return this; 6094 }, 6095 6096 /** 6097 * Please use {@link JXG.Board.on} instead. 6098 * @param {Function} hook A function to be called by the board after an update occurred. 6099 * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>. 6100 * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the 6101 * board object the hook is attached to. 6102 * @returns {Number} Id of the hook, required to remove the hook from the board. 6103 * @deprecated 6104 */ 6105 addHook: function (hook, m, context) { 6106 JXG.deprecated('Board.addHook()', 'Board.on()'); 6107 m = Type.def(m, 'update'); 6108 6109 context = Type.def(context, this); 6110 6111 this.hooks.push([m, hook]); 6112 this.on(m, hook, context); 6113 6114 return this.hooks.length - 1; 6115 }, 6116 6117 /** 6118 * Alias of {@link JXG.Board.on}. 6119 */ 6120 addEvent: JXG.shortcut(JXG.Board.prototype, 'on'), 6121 6122 /** 6123 * Please use {@link JXG.Board.off} instead. 6124 * @param {Number|function} id The number you got when you added the hook or a reference to the event handler. 6125 * @returns {JXG.Board} Reference to the board 6126 * @deprecated 6127 */ 6128 removeHook: function (id) { 6129 JXG.deprecated('Board.removeHook()', 'Board.off()'); 6130 if (this.hooks[id]) { 6131 this.off(this.hooks[id][0], this.hooks[id][1]); 6132 this.hooks[id] = null; 6133 } 6134 6135 return this; 6136 }, 6137 6138 /** 6139 * Alias of {@link JXG.Board.off}. 6140 */ 6141 removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'), 6142 6143 /** 6144 * Runs through all hooked functions and calls them. 6145 * @returns {JXG.Board} Reference to the board 6146 * @deprecated 6147 */ 6148 updateHooks: function (m) { 6149 var arg = Array.prototype.slice.call(arguments, 0); 6150 6151 JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()'); 6152 6153 arg[0] = Type.def(arg[0], 'update'); 6154 this.triggerEventHandlers([arg[0]], arguments); 6155 6156 return this; 6157 }, 6158 6159 /** 6160 * Adds a dependent board to this board. 6161 * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred. 6162 * @returns {JXG.Board} Reference to the board 6163 */ 6164 addChild: function (board) { 6165 if (Type.exists(board) && Type.exists(board.containerObj)) { 6166 this.dependentBoards.push(board); 6167 this.update(); 6168 } 6169 return this; 6170 }, 6171 6172 /** 6173 * Deletes a board from the list of dependent boards. 6174 * @param {JXG.Board} board Reference to the board which will be removed. 6175 * @returns {JXG.Board} Reference to the board 6176 */ 6177 removeChild: function (board) { 6178 var i; 6179 6180 for (i = this.dependentBoards.length - 1; i >= 0; i--) { 6181 if (this.dependentBoards[i] === board) { 6182 this.dependentBoards.splice(i, 1); 6183 } 6184 } 6185 return this; 6186 }, 6187 6188 /** 6189 * Runs through most elements and calls their update() method and update the conditions. 6190 * @param {JXG.GeometryElement} [drag] Element that caused the update. 6191 * @returns {JXG.Board} Reference to the board 6192 */ 6193 update: function (drag) { 6194 var i, len, b, insert, storeActiveEl; 6195 6196 if (this.inUpdate || this.isSuspendedUpdate) { 6197 return this; 6198 } 6199 this.inUpdate = true; 6200 6201 if ( 6202 this.attr.minimizereflow === 'all' && 6203 this.containerObj && 6204 this.renderer.type !== 'vml' 6205 ) { 6206 storeActiveEl = this.document.activeElement; // Store focus element 6207 insert = this.renderer.removeToInsertLater(this.containerObj); 6208 } 6209 6210 if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') { 6211 storeActiveEl = this.document.activeElement; 6212 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot); 6213 } 6214 6215 this.prepareUpdate(drag).updateElements(drag).updateConditions(); 6216 6217 this.renderer.suspendRedraw(this); 6218 this.updateRenderer(); 6219 this.renderer.unsuspendRedraw(); 6220 this.triggerEventHandlers(['update'], []); 6221 6222 if (insert) { 6223 insert(); 6224 storeActiveEl.focus(); // Restore focus element 6225 } 6226 6227 // To resolve dependencies between boards 6228 // for (var board in JXG.boards) { 6229 len = this.dependentBoards.length; 6230 for (i = 0; i < len; i++) { 6231 b = this.dependentBoards[i]; 6232 if (Type.exists(b) && b !== this) { 6233 b.updateQuality = this.updateQuality; 6234 b.prepareUpdate().updateElements().updateConditions(); 6235 b.renderer.suspendRedraw(this); 6236 b.updateRenderer(); 6237 b.renderer.unsuspendRedraw(); 6238 b.triggerEventHandlers(['update'], []); 6239 } 6240 } 6241 6242 this.inUpdate = false; 6243 return this; 6244 }, 6245 6246 /** 6247 * Runs through all elements and calls their update() method and update the conditions. 6248 * This is necessary after zooming and changing the bounding box. 6249 * @returns {JXG.Board} Reference to the board 6250 */ 6251 fullUpdate: function () { 6252 this.needsFullUpdate = true; 6253 this.update(); 6254 this.needsFullUpdate = false; 6255 return this; 6256 }, 6257 6258 /** 6259 * Adds a grid to the board according to the settings given in board.options. 6260 * @returns {JXG.Board} Reference to the board. 6261 */ 6262 addGrid: function () { 6263 this.create('grid', []); 6264 6265 return this; 6266 }, 6267 6268 /** 6269 * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or 6270 * more of the grids. 6271 * @returns {JXG.Board} Reference to the board object. 6272 */ 6273 removeGrids: function () { 6274 var i; 6275 6276 for (i = 0; i < this.grids.length; i++) { 6277 this.removeObject(this.grids[i]); 6278 } 6279 6280 this.grids.length = 0; 6281 this.update(); // required for canvas renderer 6282 6283 return this; 6284 }, 6285 6286 /** 6287 * Creates a new geometric element of type elementType. 6288 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'. 6289 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two 6290 * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 6291 * methods for a list of possible parameters. 6292 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 6293 * Common attributes are name, visible, strokeColor. 6294 * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing 6295 * two or more elements. 6296 */ 6297 create: function (elementType, parents, attributes) { 6298 var el, i; 6299 6300 elementType = elementType.toLowerCase(); 6301 6302 if (!Type.exists(parents)) { 6303 parents = []; 6304 } 6305 6306 if (!Type.exists(attributes)) { 6307 attributes = {}; 6308 } 6309 6310 for (i = 0; i < parents.length; i++) { 6311 if ( 6312 Type.isString(parents[i]) && 6313 !(elementType === 'text' && i === 2) && 6314 !(elementType === 'solidofrevolution3d' && i === 2) && 6315 !(elementType === 'text3d' && (i === 2 || i === 4)) && 6316 !( 6317 (elementType === 'input' || 6318 elementType === 'checkbox' || 6319 elementType === 'button') && 6320 (i === 2 || i === 3) 6321 ) && 6322 !(elementType === 'curve' /*&& i > 0*/) && // Allow curve plots with jessiecode, parents[0] is the 6323 // variable name 6324 !(elementType === 'functiongraph') && // Prevent problems with function terms like 'x', 'y' 6325 !(elementType === 'implicitcurve') 6326 ) { 6327 if (i > 0 && parents[0].elType === 'view3d') { 6328 // 3D elements are based on 3D elements, only 6329 parents[i] = parents[0].select(parents[i]); 6330 } else { 6331 parents[i] = this.select(parents[i]); 6332 } 6333 } 6334 } 6335 6336 if (Type.isFunction(JXG.elements[elementType])) { 6337 el = JXG.elements[elementType](this, parents, attributes); 6338 } else { 6339 throw new Error('JSXGraph: create: Unknown element type given: ' + elementType); 6340 } 6341 6342 if (!Type.exists(el)) { 6343 JXG.debug('JSXGraph: create: failure creating ' + elementType); 6344 return el; 6345 } 6346 6347 if (el.prepareUpdate && el.update && el.updateRenderer) { 6348 el.fullUpdate(); 6349 } 6350 return el; 6351 }, 6352 6353 /** 6354 * Deprecated name for {@link JXG.Board.create}. 6355 * @deprecated 6356 */ 6357 createElement: function () { 6358 JXG.deprecated('Board.createElement()', 'Board.create()'); 6359 return this.create.apply(this, arguments); 6360 }, 6361 6362 /** 6363 * Delete the elements drawn as part of a trace of an element. 6364 * @returns {JXG.Board} Reference to the board 6365 */ 6366 clearTraces: function () { 6367 var el; 6368 6369 for (el = 0; el < this.objectsList.length; el++) { 6370 this.objectsList[el].clearTrace(); 6371 } 6372 6373 this.numTraces = 0; 6374 return this; 6375 }, 6376 6377 /** 6378 * Stop updates of the board. 6379 * @returns {JXG.Board} Reference to the board 6380 */ 6381 suspendUpdate: function () { 6382 if (!this.inUpdate) { 6383 this.isSuspendedUpdate = true; 6384 } 6385 return this; 6386 }, 6387 6388 /** 6389 * Enable updates of the board. 6390 * @returns {JXG.Board} Reference to the board 6391 */ 6392 unsuspendUpdate: function () { 6393 if (this.isSuspendedUpdate) { 6394 this.isSuspendedUpdate = false; 6395 this.fullUpdate(); 6396 } 6397 return this; 6398 }, 6399 6400 /** 6401 * Set the bounding box of the board. 6402 * @param {Array} bbox New bounding box [x1,y1,x2,y2] 6403 * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but 6404 * the resulting viewport may be larger. 6405 * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset' 6406 * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0). 6407 * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing. 6408 * @returns {JXG.Board} Reference to the board 6409 */ 6410 setBoundingBox: function (bbox, keepaspectratio, setZoom) { 6411 var h, w, ux, uy, 6412 offX = 0, 6413 offY = 0, 6414 zoom_ratio = 1, 6415 ratio, dx, dy, prev_w, prev_h, 6416 dim = Env.getDimensions(this.containerObj, this.document); 6417 6418 if (!Type.isArray(bbox)) { 6419 return this; 6420 } 6421 6422 if ( 6423 bbox[0] < this.maxboundingbox[0] - Mat.eps || 6424 bbox[1] > this.maxboundingbox[1] + Mat.eps || 6425 bbox[2] > this.maxboundingbox[2] + Mat.eps || 6426 bbox[3] < this.maxboundingbox[3] - Mat.eps 6427 ) { 6428 return this; 6429 } 6430 6431 if (!Type.exists(setZoom)) { 6432 setZoom = 'reset'; 6433 } 6434 6435 ux = this.unitX; 6436 uy = this.unitY; 6437 this.canvasWidth = parseFloat(dim.width); // parseInt(dim.width, 10); 6438 this.canvasHeight = parseFloat(dim.height); // parseInt(dim.height, 10); 6439 w = this.canvasWidth; 6440 h = this.canvasHeight; 6441 if (keepaspectratio) { 6442 if (this.keepaspectratio) { 6443 ratio = ux / uy; // Keep this ratio if keepaspectratio was true 6444 if (isNaN(ratio)) { 6445 ratio = 1.0; 6446 } 6447 } else { 6448 ratio = 1.0; 6449 } 6450 if (setZoom === 'keep') { 6451 zoom_ratio = this.zoomX / this.zoomY; 6452 } 6453 dx = bbox[2] - bbox[0]; 6454 dy = bbox[1] - bbox[3]; 6455 prev_w = ux * dx; 6456 prev_h = uy * dy; 6457 if (w >= h) { 6458 if (prev_w >= prev_h) { 6459 this.unitY = h / dy; 6460 this.unitX = this.unitY * ratio; 6461 } else { 6462 // Switch dominating interval 6463 this.unitY = h / Math.abs(dx) * Mat.sign(dy) / zoom_ratio; 6464 this.unitX = this.unitY * ratio; 6465 } 6466 } else { 6467 if (prev_h > prev_w) { 6468 this.unitX = w / dx; 6469 this.unitY = this.unitX / ratio; 6470 } else { 6471 // Switch dominating interval 6472 this.unitX = w / Math.abs(dy) * Mat.sign(dx) * zoom_ratio; 6473 this.unitY = this.unitX / ratio; 6474 } 6475 } 6476 // Add the additional units in equal portions left and right 6477 offX = (w / this.unitX - dx) * 0.5; 6478 // Add the additional units in equal portions above and below 6479 offY = (h / this.unitY - dy) * 0.5; 6480 this.keepaspectratio = true; 6481 } else { 6482 this.unitX = w / (bbox[2] - bbox[0]); 6483 this.unitY = h / (bbox[1] - bbox[3]); 6484 this.keepaspectratio = false; 6485 } 6486 6487 this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY)); 6488 6489 if (setZoom === 'update') { 6490 this.zoomX *= this.unitX / ux; 6491 this.zoomY *= this.unitY / uy; 6492 } else if (setZoom === 'reset') { 6493 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0; 6494 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0; 6495 } 6496 6497 return this; 6498 }, 6499 6500 /** 6501 * Get the bounding box of the board. 6502 * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner 6503 */ 6504 getBoundingBox: function () { 6505 var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords, 6506 lr = new Coords( 6507 Const.COORDS_BY_SCREEN, 6508 [this.canvasWidth, this.canvasHeight], 6509 this 6510 ).usrCoords; 6511 return [ul[1], ul[2], lr[1], lr[2]]; 6512 }, 6513 6514 /** 6515 * Sets the value of attribute <tt>key</tt> to <tt>value</tt>. 6516 * @param {String} key The attribute's name. 6517 * @param value The new value 6518 * @private 6519 */ 6520 _set: function (key, value) { 6521 key = key.toLocaleLowerCase(); 6522 6523 if ( 6524 value !== null && 6525 Type.isObject(value) && 6526 !Type.exists(value.id) && 6527 !Type.exists(value.name) 6528 ) { 6529 // value is of type {prop: val, prop: val,...} 6530 // Convert these attributes to lowercase, too 6531 // this.attr[key] = {}; 6532 // for (el in value) { 6533 // if (value.hasOwnProperty(el)) { 6534 // this.attr[key][el.toLocaleLowerCase()] = value[el]; 6535 // } 6536 // } 6537 Type.mergeAttr(this.attr[key], value); 6538 } else { 6539 this.attr[key] = value; 6540 } 6541 }, 6542 6543 /** 6544 * Sets an arbitrary number of attributes. This method has one or more 6545 * parameters of the following types: 6546 * <ul> 6547 * <li> object: {key1:value1,key2:value2,...} 6548 * <li> string: 'key:value' 6549 * <li> array: ['key', value] 6550 * </ul> 6551 * Some board attributes are immutable, like e.g. the renderer type. 6552 * 6553 * @param {Object} attributes An object with attributes 6554 * @param {Boolean} [force=false] if true the attributes are set regardless of the previous setting was identical. 6555 * @returns {JXG.Board} Reference to the board 6556 * 6557 * @example 6558 * const board = JXG.JSXGraph.initBoard('jxgbox', { 6559 * boundingbox: [-5, 5, 5, -5], 6560 * keepAspectRatio: false, 6561 * axis:true, 6562 * showFullscreen: true, 6563 * showScreenshot: true, 6564 * showCopyright: false 6565 * }); 6566 * 6567 * board.setAttribute({ 6568 * animationDelay: 10, 6569 * boundingbox: [-10, 5, 10, -5], 6570 * defaultAxes: { 6571 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6572 * }, 6573 * description: 'test', 6574 * fullscreen: { 6575 * scale: 0.5 6576 * }, 6577 * intl: { 6578 * enabled: true, 6579 * locale: 'de-DE' 6580 * } 6581 * }); 6582 * 6583 * board.setAttribute({ 6584 * selection: { 6585 * enabled: true, 6586 * fillColor: 'blue' 6587 * }, 6588 * showInfobox: false, 6589 * zoomX: 0.5, 6590 * zoomY: 2, 6591 * fullscreen: { symbol: 'x' }, 6592 * screenshot: { symbol: 'y' }, 6593 * showCopyright: true, 6594 * showFullscreen: false, 6595 * showScreenshot: false, 6596 * showZoom: false, 6597 * showNavigation: false 6598 * }); 6599 * board.setAttribute('showCopyright:false'); 6600 * 6601 * var p = board.create('point', [1, 1], {size: 10, 6602 * label: { 6603 * fontSize: 24, 6604 * highlightStrokeOpacity: 0.1, 6605 * offset: [5, 0] 6606 * } 6607 * }); 6608 * 6609 * 6610 * </pre><div id="JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc" class="jxgbox" style="width: 300px; height: 300px;"></div> 6611 * <script type="text/javascript"> 6612 * (function() { 6613 * const board = JXG.JSXGraph.initBoard('JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc', { 6614 * boundingbox: [-5, 5, 5, -5], 6615 * keepAspectRatio: false, 6616 * axis:true, 6617 * showFullscreen: true, 6618 * showScreenshot: true, 6619 * showCopyright: false 6620 * }); 6621 * 6622 * board.setAttribute({ 6623 * animationDelay: 10, 6624 * boundingbox: [-10, 5, 10, -5], 6625 * defaultAxes: { 6626 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6627 * }, 6628 * description: 'test', 6629 * fullscreen: { 6630 * scale: 0.5 6631 * }, 6632 * intl: { 6633 * enabled: true, 6634 * locale: 'de-DE' 6635 * } 6636 * }); 6637 * 6638 * board.setAttribute({ 6639 * selection: { 6640 * enabled: true, 6641 * fillColor: 'blue' 6642 * }, 6643 * showInfobox: false, 6644 * zoomX: 0.5, 6645 * zoomY: 2, 6646 * fullscreen: { symbol: 'x' }, 6647 * screenshot: { symbol: 'y' }, 6648 * showCopyright: true, 6649 * showFullscreen: false, 6650 * showScreenshot: false, 6651 * showZoom: false, 6652 * showNavigation: false 6653 * }); 6654 * 6655 * board.setAttribute('showCopyright:false'); 6656 * 6657 * var p = board.create('point', [1, 1], {size: 10, 6658 * label: { 6659 * fontSize: 24, 6660 * highlightStrokeOpacity: 0.1, 6661 * offset: [5, 0] 6662 * } 6663 * }); 6664 * 6665 * 6666 * })(); 6667 * 6668 * </script><pre> 6669 * 6670 * 6671 */ 6672 setAttribute: function (attr, force) { 6673 var i, arg, pair, 6674 key, value, oldvalue,// j, le, 6675 node, lst, e, 6676 attributes = {}; 6677 6678 // Normalize the user input 6679 for (i = 0; i < arguments.length; i++) { 6680 arg = arguments[i]; 6681 if (Type.isString(arg)) { 6682 // pairRaw is string of the form 'key:value' 6683 pair = arg.split(":"); 6684 attributes[Type.trim(pair[0])] = Type.trim(pair[1]); 6685 } else if (!Type.isArray(arg)) { 6686 // pairRaw consists of objects of the form {key1:value1,key2:value2,...} 6687 JXG.extend(attributes, arg); 6688 } else { 6689 // pairRaw consists of array [key,value] 6690 attributes[arg[0]] = arg[1]; 6691 } 6692 } 6693 6694 for (i in attributes) { 6695 if (attributes.hasOwnProperty(i)) { 6696 key = i.replace(/\s+/g, "").toLowerCase(); 6697 value = attributes[i]; 6698 } 6699 value = (value.toLowerCase && value.toLowerCase() === 'false') 6700 ? false 6701 : value; 6702 oldvalue = this.attr[key]; 6703 if (!force && oldvalue === value) { 6704 continue; 6705 } 6706 switch (key) { 6707 case 'axis': 6708 if (value === false) { 6709 if (Type.exists(this.defaultAxes)) { 6710 this.defaultAxes.x.setAttribute({ visible: false }); 6711 this.defaultAxes.y.setAttribute({ visible: false }); 6712 } 6713 } else { 6714 // TODO 6715 } 6716 break; 6717 case 'cssstyle': 6718 lst = Type.css2js(value); 6719 node = this.containerObj; 6720 // node = this.renderer.svgRoot; 6721 for (e in lst) if (lst.hasOwnProperty(e)) { 6722 pair = lst[e]; 6723 node.style[pair.key] = pair.val; 6724 } 6725 6726 this._set(key, value); 6727 break; 6728 case 'boundingbox': 6729 this.setBoundingBox(value, this.keepaspectratio); 6730 this._set(key, value); 6731 break; 6732 case 'defaultaxes': 6733 if (Type.exists(this.defaultAxes.x) && Type.exists(value.x)) { 6734 this.defaultAxes.x.setAttribute(value.x); 6735 } 6736 if (Type.exists(this.defaultAxes.y) && Type.exists(value.y)) { 6737 this.defaultAxes.y.setAttribute(value.y); 6738 } 6739 break; 6740 case 'title': 6741 this.document.getElementById(this.container + '_ARIAlabel') 6742 .innerText = value; 6743 this._set(key, value); 6744 break; 6745 case 'keepaspectratio': 6746 this._set(key, value); 6747 this.setBoundingBox(this.getBoundingBox(), value, 'keep'); 6748 break; 6749 6750 // /* eslint-disable no-fallthrough */ 6751 case 'document': 6752 case 'maxboundingbox': 6753 this[key] = value; 6754 this._set(key, value); 6755 break; 6756 6757 case 'zoomx': 6758 case 'zoomy': 6759 this[key] = value; 6760 this._set(key, value); 6761 this.setZoom(this.attr.zoomx, this.attr.zoomy); 6762 break; 6763 6764 case 'registerevents': 6765 case 'renderer': 6766 // immutable, i.e. ignored 6767 break; 6768 6769 case 'fullscreen': 6770 case 'screenshot': 6771 node = this.containerObj.ownerDocument.getElementById( 6772 this.container + '_navigation_' + key); 6773 if (node && Type.exists(value.symbol)) { 6774 node.innerText = Type.evaluate(value.symbol); 6775 } 6776 this._set(key, value); 6777 break; 6778 6779 case 'selection': 6780 value.visible = false; 6781 value.withLines = false; 6782 value.vertices = { visible: false }; 6783 this._set(key, value); 6784 break; 6785 6786 case 'showcopyright': 6787 if (this.renderer.type === 'svg') { 6788 node = this.containerObj.ownerDocument.getElementById( 6789 this.renderer.uniqName('licenseText') 6790 ); 6791 if (node) { 6792 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none'); 6793 } else if (Type.evaluate(value)) { 6794 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 6795 } 6796 } 6797 this._set(key, value); 6798 break; 6799 6800 case 'showlogo': 6801 if (this.renderer.type === 'svg') { 6802 node = this.containerObj.ownerDocument.getElementById( 6803 this.renderer.uniqName('licenseLogo') 6804 ); 6805 if (node) { 6806 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none'); 6807 } else if (Type.evaluate(value)) { 6808 this.renderer.displayLogo(Const.licenseLogo, parseInt(this.options.text.fontSize, 10)); 6809 } 6810 } 6811 this._set(key, value); 6812 break; 6813 6814 default: 6815 if (Type.exists(this.attr[key])) { 6816 this._set(key, value); 6817 } 6818 break; 6819 // /* eslint-enable no-fallthrough */ 6820 } 6821 } 6822 6823 // Redraw navbar to handle the remaining show* attributes 6824 node = this.containerObj.ownerDocument.getElementById(this.container + "_navigationbar"); 6825 if (Type.exists(node)) { 6826 node.remove(); 6827 this.renderer.drawNavigationBar(this, this.attr.navbar); 6828 } 6829 6830 this.triggerEventHandlers(["attribute"], [attributes, this]); 6831 this.fullUpdate(); 6832 6833 return this; 6834 }, 6835 6836 /** 6837 * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the 6838 * animated elements. This function tells the board about new elements to animate. 6839 * @param {JXG.GeometryElement} element The element which is to be animated. 6840 * @returns {JXG.Board} Reference to the board 6841 */ 6842 addAnimation: function (element) { 6843 var that = this; 6844 6845 this.animationObjects[element.id] = element; 6846 6847 if (!this.animationIntervalCode) { 6848 this.animationIntervalCode = window.setInterval(function () { 6849 that.animate(); 6850 }, element.board.attr.animationdelay); 6851 } 6852 6853 return this; 6854 }, 6855 6856 /** 6857 * Cancels all running animations. 6858 * @returns {JXG.Board} Reference to the board 6859 */ 6860 stopAllAnimation: function () { 6861 var el; 6862 6863 for (el in this.animationObjects) { 6864 if ( 6865 this.animationObjects.hasOwnProperty(el) && 6866 Type.exists(this.animationObjects[el]) 6867 ) { 6868 this.animationObjects[el] = null; 6869 delete this.animationObjects[el]; 6870 } 6871 } 6872 6873 window.clearInterval(this.animationIntervalCode); 6874 delete this.animationIntervalCode; 6875 6876 return this; 6877 }, 6878 6879 /** 6880 * General purpose animation function. This currently only supports moving points from one place to another. This 6881 * is faster than managing the animation per point, especially if there is more than one animated point at the same time. 6882 * @returns {JXG.Board} Reference to the board 6883 */ 6884 animate: function () { 6885 var props, 6886 el, 6887 o, 6888 newCoords, 6889 r, 6890 p, 6891 c, 6892 cbtmp, 6893 count = 0, 6894 obj = null; 6895 6896 for (el in this.animationObjects) { 6897 if ( 6898 this.animationObjects.hasOwnProperty(el) && 6899 Type.exists(this.animationObjects[el]) 6900 ) { 6901 count += 1; 6902 o = this.animationObjects[el]; 6903 6904 if (o.animationPath) { 6905 if (Type.isFunction(o.animationPath)) { 6906 newCoords = o.animationPath( 6907 new Date().getTime() - o.animationStart 6908 ); 6909 } else { 6910 newCoords = o.animationPath.pop(); 6911 } 6912 6913 if ( 6914 !Type.exists(newCoords) || 6915 (!Type.isArray(newCoords) && isNaN(newCoords)) 6916 ) { 6917 delete o.animationPath; 6918 } else { 6919 o.setPositionDirectly(Const.COORDS_BY_USER, newCoords); 6920 o.fullUpdate(); 6921 obj = o; 6922 } 6923 } 6924 if (o.animationData) { 6925 c = 0; 6926 6927 for (r in o.animationData) { 6928 if (o.animationData.hasOwnProperty(r)) { 6929 p = o.animationData[r].pop(); 6930 6931 if (!Type.exists(p)) { 6932 delete o.animationData[p]; 6933 } else { 6934 c += 1; 6935 props = {}; 6936 props[r] = p; 6937 o.setAttribute(props); 6938 } 6939 } 6940 } 6941 6942 if (c === 0) { 6943 delete o.animationData; 6944 } 6945 } 6946 6947 if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) { 6948 this.animationObjects[el] = null; 6949 delete this.animationObjects[el]; 6950 6951 if (Type.exists(o.animationCallback)) { 6952 cbtmp = o.animationCallback; 6953 o.animationCallback = null; 6954 cbtmp(); 6955 } 6956 } 6957 } 6958 } 6959 6960 if (count === 0) { 6961 window.clearInterval(this.animationIntervalCode); 6962 delete this.animationIntervalCode; 6963 } else { 6964 this.update(obj); 6965 } 6966 6967 return this; 6968 }, 6969 6970 /** 6971 * Migrate the dependency properties of the point src 6972 * to the point dest and delete the point src. 6973 * For example, a circle around the point src 6974 * receives the new center dest. The old center src 6975 * will be deleted. 6976 * @param {JXG.Point} src Original point which will be deleted 6977 * @param {JXG.Point} dest New point with the dependencies of src. 6978 * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the 6979 * dest element. 6980 * @returns {JXG.Board} Reference to the board 6981 */ 6982 migratePoint: function (src, dest, copyName) { 6983 var child, 6984 childId, 6985 prop, 6986 found, 6987 i, 6988 srcLabelId, 6989 srcHasLabel = false; 6990 6991 src = this.select(src); 6992 dest = this.select(dest); 6993 6994 if (Type.exists(src.label)) { 6995 srcLabelId = src.label.id; 6996 srcHasLabel = true; 6997 this.removeObject(src.label); 6998 } 6999 7000 for (childId in src.childElements) { 7001 if (src.childElements.hasOwnProperty(childId)) { 7002 child = src.childElements[childId]; 7003 found = false; 7004 7005 for (prop in child) { 7006 if (child.hasOwnProperty(prop)) { 7007 if (child[prop] === src) { 7008 child[prop] = dest; 7009 found = true; 7010 } 7011 } 7012 } 7013 7014 if (found) { 7015 delete src.childElements[childId]; 7016 } 7017 7018 for (i = 0; i < child.parents.length; i++) { 7019 if (child.parents[i] === src.id) { 7020 child.parents[i] = dest.id; 7021 } 7022 } 7023 7024 dest.addChild(child); 7025 } 7026 } 7027 7028 // The destination object should receive the name 7029 // and the label of the originating (src) object 7030 if (copyName) { 7031 if (srcHasLabel) { 7032 delete dest.childElements[srcLabelId]; 7033 delete dest.descendants[srcLabelId]; 7034 } 7035 7036 if (dest.label) { 7037 this.removeObject(dest.label); 7038 } 7039 7040 delete this.elementsByName[dest.name]; 7041 dest.name = src.name; 7042 if (srcHasLabel) { 7043 dest.createLabel(); 7044 } 7045 } 7046 7047 this.removeObject(src); 7048 7049 if (Type.exists(dest.name) && dest.name !== '') { 7050 this.elementsByName[dest.name] = dest; 7051 } 7052 7053 this.fullUpdate(); 7054 7055 return this; 7056 }, 7057 7058 /** 7059 * Initializes color blindness simulation. 7060 * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'. 7061 * @returns {JXG.Board} Reference to the board 7062 */ 7063 emulateColorblindness: function (deficiency) { 7064 var e, o; 7065 7066 if (!Type.exists(deficiency)) { 7067 deficiency = 'none'; 7068 } 7069 7070 if (this.currentCBDef === deficiency) { 7071 return this; 7072 } 7073 7074 for (e in this.objects) { 7075 if (this.objects.hasOwnProperty(e)) { 7076 o = this.objects[e]; 7077 7078 if (deficiency !== 'none') { 7079 if (this.currentCBDef === 'none') { 7080 // this could be accomplished by JXG.extend, too. But do not use 7081 // JXG.deepCopy as this could result in an infinite loop because in 7082 // visProp there could be geometry elements which contain the board which 7083 // contains all objects which contain board etc. 7084 o.visPropOriginal = { 7085 strokecolor: o.visProp.strokecolor, 7086 fillcolor: o.visProp.fillcolor, 7087 highlightstrokecolor: o.visProp.highlightstrokecolor, 7088 highlightfillcolor: o.visProp.highlightfillcolor 7089 }; 7090 } 7091 o.setAttribute({ 7092 strokecolor: Color.rgb2cb( 7093 o.eval(o.visPropOriginal.strokecolor), 7094 deficiency 7095 ), 7096 fillcolor: Color.rgb2cb( 7097 o.eval(o.visPropOriginal.fillcolor), 7098 deficiency 7099 ), 7100 highlightstrokecolor: Color.rgb2cb( 7101 o.eval(o.visPropOriginal.highlightstrokecolor), 7102 deficiency 7103 ), 7104 highlightfillcolor: Color.rgb2cb( 7105 o.eval(o.visPropOriginal.highlightfillcolor), 7106 deficiency 7107 ) 7108 }); 7109 } else if (Type.exists(o.visPropOriginal)) { 7110 JXG.extend(o.visProp, o.visPropOriginal); 7111 } 7112 } 7113 } 7114 this.currentCBDef = deficiency; 7115 this.update(); 7116 7117 return this; 7118 }, 7119 7120 /** 7121 * Select a single or multiple elements at once. 7122 * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will 7123 * be used as a filter to return multiple elements at once filtered by the properties of the object. 7124 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 7125 * The advanced filters consisting of objects or functions are ignored. 7126 * @returns {JXG.GeometryElement|JXG.Composition} 7127 * @example 7128 * // select the element with name A 7129 * board.select('A'); 7130 * 7131 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 7132 * board.select({ 7133 * strokeColor: 'red' 7134 * }); 7135 * 7136 * // select all points on or below the x axis and make them black. 7137 * board.select({ 7138 * elementClass: JXG.OBJECT_CLASS_POINT, 7139 * Y: function (v) { 7140 * return v <= 0; 7141 * } 7142 * }).setAttribute({color: 'black'}); 7143 * 7144 * // select all elements 7145 * board.select(function (el) { 7146 * return true; 7147 * }); 7148 */ 7149 select: function (str, onlyByIdOrName) { 7150 var flist, 7151 olist, 7152 i, 7153 l, 7154 s = str; 7155 7156 if (s === null) { 7157 return s; 7158 } 7159 7160 // It's a string, most likely an id or a name. 7161 if (Type.isString(s) && s !== '') { 7162 // Search by ID 7163 if (Type.exists(this.objects[s])) { 7164 s = this.objects[s]; 7165 // Search by name 7166 } else if (Type.exists(this.elementsByName[s])) { 7167 s = this.elementsByName[s]; 7168 // Search by group ID 7169 } else if (Type.exists(this.groups[s])) { 7170 s = this.groups[s]; 7171 } 7172 7173 // It's a function or an object, but not an element 7174 } else if ( 7175 !onlyByIdOrName && 7176 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) 7177 ) { 7178 flist = Type.filterElements(this.objectsList, s); 7179 7180 olist = {}; 7181 l = flist.length; 7182 for (i = 0; i < l; i++) { 7183 olist[flist[i].id] = flist[i]; 7184 } 7185 s = new Composition(olist); 7186 7187 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 7188 } else if ( 7189 Type.isObject(s) && 7190 Type.exists(s.id) && 7191 !Type.exists(this.objects[s.id]) 7192 ) { 7193 s = null; 7194 } 7195 7196 return s; 7197 }, 7198 7199 /** 7200 * Checks if the given point is inside the boundingbox. 7201 * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object. 7202 * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object. 7203 * @returns {Boolean} 7204 */ 7205 hasPoint: function (x, y) { 7206 var px = x, 7207 py = y, 7208 bbox = this.getBoundingBox(); 7209 7210 if (Type.exists(x) && Type.isArray(x.usrCoords)) { 7211 px = x.usrCoords[1]; 7212 py = x.usrCoords[2]; 7213 } 7214 7215 return !!( 7216 Type.isNumber(px) && 7217 Type.isNumber(py) && 7218 bbox[0] < px && 7219 px < bbox[2] && 7220 bbox[1] > py && 7221 py > bbox[3] 7222 ); 7223 }, 7224 7225 /** 7226 * Update CSS transformations of type scaling. It is used to correct the mouse position 7227 * in {@link JXG.Board.getMousePosition}. 7228 * The inverse transformation matrix is updated on each mouseDown and touchStart event. 7229 * 7230 * It is up to the user to call this method after an update of the CSS transformation 7231 * in the DOM. 7232 */ 7233 updateCSSTransforms: function () { 7234 var obj = this.containerObj, 7235 o = obj, 7236 o2 = obj; 7237 7238 this.cssTransMat = Env.getCSSTransformMatrix(o); 7239 7240 // Newer variant of walking up the tree. 7241 // We walk up all parent nodes and collect possible CSS transforms. 7242 // Works also for ShadowDOM 7243 if (Type.exists(o.getRootNode)) { 7244 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 7245 while (o) { 7246 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7247 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 7248 } 7249 this.cssTransMat = Mat.inverse(this.cssTransMat); 7250 } else { 7251 /* 7252 * This is necessary for IE11 7253 */ 7254 o = o.offsetParent; 7255 while (o) { 7256 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7257 7258 o2 = o2.parentNode; 7259 while (o2 !== o) { 7260 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7261 o2 = o2.parentNode; 7262 } 7263 o = o.offsetParent; 7264 } 7265 this.cssTransMat = Mat.inverse(this.cssTransMat); 7266 } 7267 return this; 7268 }, 7269 7270 /** 7271 * Start selection mode. This function can either be triggered from outside or by 7272 * a down event together with correct key pressing. The default keys are 7273 * shift+ctrl. But this can be changed in the options. 7274 * 7275 * Starting from out side can be realized for example with a button like this: 7276 * <pre> 7277 * <button onclick='board.startSelectionMode()'>Start</button> 7278 * </pre> 7279 * @example 7280 * // 7281 * // Set a new bounding box from the selection rectangle 7282 * // 7283 * var board = JXG.JSXGraph.initBoard('jxgbox', { 7284 * boundingBox:[-3,2,3,-2], 7285 * keepAspectRatio: false, 7286 * axis:true, 7287 * selection: { 7288 * enabled: true, 7289 * needShift: false, 7290 * needCtrl: true, 7291 * withLines: false, 7292 * vertices: { 7293 * visible: false 7294 * }, 7295 * fillColor: '#ffff00', 7296 * } 7297 * }); 7298 * 7299 * var f = function f(x) { return Math.cos(x); }, 7300 * curve = board.create('functiongraph', [f]); 7301 * 7302 * board.on('stopselecting', function(){ 7303 * var box = board.stopSelectionMode(), 7304 * 7305 * // bbox has the coordinates of the selection rectangle. 7306 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7307 * // are homogeneous coordinates. 7308 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7309 * 7310 * // Set a new bounding box 7311 * board.setBoundingBox(bbox, false); 7312 * }); 7313 * 7314 * 7315 * </pre><div class='jxgbox' id='JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723' style='width: 300px; height: 300px;'></div> 7316 * <script type='text/javascript'> 7317 * (function() { 7318 * // 7319 * // Set a new bounding box from the selection rectangle 7320 * // 7321 * var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', { 7322 * boundingBox:[-3,2,3,-2], 7323 * keepAspectRatio: false, 7324 * axis:true, 7325 * selection: { 7326 * enabled: true, 7327 * needShift: false, 7328 * needCtrl: true, 7329 * withLines: false, 7330 * vertices: { 7331 * visible: false 7332 * }, 7333 * fillColor: '#ffff00', 7334 * } 7335 * }); 7336 * 7337 * var f = function f(x) { return Math.cos(x); }, 7338 * curve = board.create('functiongraph', [f]); 7339 * 7340 * board.on('stopselecting', function(){ 7341 * var box = board.stopSelectionMode(), 7342 * 7343 * // bbox has the coordinates of the selection rectangle. 7344 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7345 * // are homogeneous coordinates. 7346 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7347 * 7348 * // Set a new bounding box 7349 * board.setBoundingBox(bbox, false); 7350 * }); 7351 * })(); 7352 * 7353 * </script><pre> 7354 * 7355 */ 7356 startSelectionMode: function () { 7357 this.selectingMode = true; 7358 this.selectionPolygon.setAttribute({ visible: true }); 7359 this.selectingBox = [ 7360 [0, 0], 7361 [0, 0] 7362 ]; 7363 this._setSelectionPolygonFromBox(); 7364 this.selectionPolygon.fullUpdate(); 7365 }, 7366 7367 /** 7368 * Finalize the selection: disable selection mode and return the coordinates 7369 * of the selection rectangle. 7370 * @returns {Array} Coordinates of the selection rectangle. The array 7371 * contains two {@link JXG.Coords} objects. One the upper left corner and 7372 * the second for the lower right corner. 7373 */ 7374 stopSelectionMode: function () { 7375 this.selectingMode = false; 7376 this.selectionPolygon.setAttribute({ visible: false }); 7377 return [ 7378 this.selectionPolygon.vertices[0].coords, 7379 this.selectionPolygon.vertices[2].coords 7380 ]; 7381 }, 7382 7383 /** 7384 * Start the selection of a region. 7385 * @private 7386 * @param {Array} pos Screen coordiates of the upper left corner of the 7387 * selection rectangle. 7388 */ 7389 _startSelecting: function (pos) { 7390 this.isSelecting = true; 7391 this.selectingBox = [ 7392 [pos[0], pos[1]], 7393 [pos[0], pos[1]] 7394 ]; 7395 this._setSelectionPolygonFromBox(); 7396 }, 7397 7398 /** 7399 * Update the selection rectangle during a move event. 7400 * @private 7401 * @param {Array} pos Screen coordiates of the move event 7402 */ 7403 _moveSelecting: function (pos) { 7404 if (this.isSelecting) { 7405 this.selectingBox[1] = [pos[0], pos[1]]; 7406 this._setSelectionPolygonFromBox(); 7407 this.selectionPolygon.fullUpdate(); 7408 } 7409 }, 7410 7411 /** 7412 * Update the selection rectangle during an up event. Stop selection. 7413 * @private 7414 * @param {Object} evt Event object 7415 */ 7416 _stopSelecting: function (evt) { 7417 var pos = this.getMousePosition(evt); 7418 7419 this.isSelecting = false; 7420 this.selectingBox[1] = [pos[0], pos[1]]; 7421 this._setSelectionPolygonFromBox(); 7422 }, 7423 7424 /** 7425 * Update the Selection rectangle. 7426 * @private 7427 */ 7428 _setSelectionPolygonFromBox: function () { 7429 var A = this.selectingBox[0], 7430 B = this.selectingBox[1]; 7431 7432 this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7433 A[0], 7434 A[1] 7435 ]); 7436 this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7437 A[0], 7438 B[1] 7439 ]); 7440 this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7441 B[0], 7442 B[1] 7443 ]); 7444 this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7445 B[0], 7446 A[1] 7447 ]); 7448 }, 7449 7450 /** 7451 * Test if a down event should start a selection. Test if the 7452 * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called. 7453 * @param {Object} evt Event object 7454 */ 7455 _testForSelection: function (evt) { 7456 if (this._isRequiredKeyPressed(evt, 'selection')) { 7457 if (!Type.exists(this.selectionPolygon)) { 7458 this._createSelectionPolygon(this.attr); 7459 } 7460 this.startSelectionMode(); 7461 } 7462 }, 7463 7464 /** 7465 * Create the internal selection polygon, which will be available as board.selectionPolygon. 7466 * @private 7467 * @param {Object} attr board attributes, e.g. the subobject board.attr. 7468 * @returns {Object} pointer to the board to enable chaining. 7469 */ 7470 _createSelectionPolygon: function (attr) { 7471 var selectionattr; 7472 7473 if (!Type.exists(this.selectionPolygon)) { 7474 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection'); 7475 if (selectionattr.enabled === true) { 7476 this.selectionPolygon = this.create( 7477 'polygon', 7478 [ 7479 [0, 0], 7480 [0, 0], 7481 [0, 0], 7482 [0, 0] 7483 ], 7484 selectionattr 7485 ); 7486 } 7487 } 7488 7489 return this; 7490 }, 7491 7492 /** 7493 * Reset the sketchcurves in board.sketches[] to length 0 and add the position 7494 * of the event as first point of the sketch curve. Called at down events. 7495 * <p> 7496 * Sets board.isSketching[i] = true where i depends on the finger (1st or 2nd). 7497 * 7498 * @private 7499 * @param {Object} evt Event object 7500 * @see JXG.Board#addToSketchCurve 7501 * @see JXG.Board#finalizeSketchCurve 7502 */ 7503 initSketchCurve: function(evt) { 7504 var i, c; 7505 // Init sketchcurves 7506 if (this.mode !== this.BOARD_MODE_MOVE_ORIGIN) { 7507 // Add coords to sketch curves 7508 // Only first and second finger are stored 7509 c = this.getUsrCoordsOfMouse(evt); 7510 i = (evt.isPrimary) ? 0 : 1; 7511 if (Type.exists(this.sketches[i])) { 7512 this.sketches[i].dataX = [c[0]]; 7513 this.sketches[i].dataY = [c[1]]; 7514 this.isSketching[i] = true; 7515 } 7516 } 7517 }, 7518 7519 /** 7520 * Add the position of the event to the sketchcurve i in board.sketches[]. 7521 * Called at move events. 7522 * Point is only added if board.isSketching[i] = true. 7523 * 7524 * @private 7525 * @param {Object} evt Event object 7526 * @see JXG.Board#initSketchCurve 7527 * @see JXG.Board#finalizeSketchCurve 7528 */ 7529 addToSketchCurve: function(evt) { 7530 var i, c, len; 7531 7532 // Add coords to sketchcurves 7533 // Only first and second finger are stored 7534 i = (evt.isPrimary) ? 0 : 1; 7535 if (this.attr.sketches.enabled && this.isSketching[i] === true) { 7536 if (Type.exists(this.sketches[i])) { 7537 c = this.getUsrCoordsOfMouse(evt); 7538 this.sketches[i].dataX.push(c[0]); 7539 this.sketches[i].dataY.push(c[1]); 7540 7541 len = this.sketches[i].evalVisProp('maxlength'); 7542 if (len !== null && this.sketches[i].dataX.length > len) { 7543 this.sketches[i].dataX = this.sketches[i].dataX.slice(-len); 7544 this.sketches[i].dataY = this.sketches[i].dataY.slice(-len); 7545 } 7546 if (this.sketches[i].evalVisProp('visible')) { 7547 this.update(); 7548 } 7549 } 7550 } 7551 }, 7552 7553 /** 7554 * Ends adding points to the sketchcurve i in board.sketches[]. 7555 * Called at up events. 7556 * Sets board.isSketching[i] = false. 7557 * Empties the curve if deleteOnUp==true; 7558 * 7559 * @private 7560 * @param {Object} evt Event object 7561 * @see JXG.Board#initSketchCurve 7562 * @see JXG.Board#addToSketchCurve 7563 */ 7564 finalizeSketchCurve: function(evt) { 7565 var i; 7566 7567 // Stop sketching into this.sketches 7568 i = (evt.isPrimary) ? 0 : 1; 7569 if (this.attr.sketches.enabled) { 7570 if (Type.exists(this.sketches[i])) { 7571 this.isSketching[i] = false; 7572 if (this.sketches[i].evalVisProp('deleteOnUp')) { 7573 this.sketches[i].dataX = []; 7574 this.sketches[i].dataY = []; 7575 } 7576 } 7577 } 7578 }, 7579 7580 /* ************************** 7581 * EVENT DEFINITION 7582 * for documentation purposes 7583 * ************************** */ 7584 7585 //region Event handler documentation 7586 7587 /** 7588 * @event 7589 * @description Whenever the {@link JXG.Board#setAttribute} is called. 7590 * @name JXG.Board#attribute 7591 * @param {Event} e The browser's event object. 7592 */ 7593 __evt__attribute: function (e) { }, 7594 7595 /** 7596 * @event 7597 * @description Whenever the user starts to touch or click the board. 7598 * @name JXG.Board#down 7599 * @param {Event} e The browser's event object. 7600 */ 7601 __evt__down: function (e) { }, 7602 7603 /** 7604 * @event 7605 * @description Whenever the user starts to click on the board. 7606 * @name JXG.Board#mousedown 7607 * @param {Event} e The browser's event object. 7608 */ 7609 __evt__mousedown: function (e) { }, 7610 7611 /** 7612 * @event 7613 * @description Whenever the user taps the pen on the board. 7614 * @name JXG.Board#pendown 7615 * @param {Event} e The browser's event object. 7616 */ 7617 __evt__pendown: function (e) { }, 7618 7619 /** 7620 * @event 7621 * @description Whenever the user starts to click on the board with a 7622 * device sending pointer events. 7623 * @name JXG.Board#pointerdown 7624 * @param {Event} e The browser's event object. 7625 */ 7626 __evt__pointerdown: function (e) { }, 7627 7628 /** 7629 * @event 7630 * @description Whenever the user starts to touch the board. 7631 * @name JXG.Board#touchstart 7632 * @param {Event} e The browser's event object. 7633 */ 7634 __evt__touchstart: function (e) { }, 7635 7636 /** 7637 * @event 7638 * @description Whenever the user stops to touch or click the board. 7639 * @name JXG.Board#up 7640 * @param {Event} e The browser's event object. 7641 */ 7642 __evt__up: function (e) { }, 7643 7644 /** 7645 * @event 7646 * @description Whenever the user releases the mousebutton over the board. 7647 * @name JXG.Board#mouseup 7648 * @param {Event} e The browser's event object. 7649 */ 7650 __evt__mouseup: function (e) { }, 7651 7652 /** 7653 * @event 7654 * @description Whenever the user releases the mousebutton over the board with a 7655 * device sending pointer events. 7656 * @name JXG.Board#pointerup 7657 * @param {Event} e The browser's event object. 7658 */ 7659 __evt__pointerup: function (e) { }, 7660 7661 /** 7662 * @event 7663 * @description Whenever the user stops touching the board. 7664 * @name JXG.Board#touchend 7665 * @param {Event} e The browser's event object. 7666 */ 7667 __evt__touchend: function (e) { }, 7668 7669 /** 7670 * @event 7671 * @description Whenever the user clicks on the board. 7672 * @name JXG.Board#click 7673 * @see JXG.Board#clickDelay 7674 * @param {Event} e The browser's event object. 7675 */ 7676 __evt__click: function (e) { }, 7677 7678 /** 7679 * @event 7680 * @description Whenever the user double clicks on the board. 7681 * This event works on desktop browser, but is undefined 7682 * on mobile browsers. 7683 * @name JXG.Board#dblclick 7684 * @see JXG.Board#clickDelay 7685 * @see JXG.Board#dblClickSuppressClick 7686 * @param {Event} e The browser's event object. 7687 */ 7688 __evt__dblclick: function (e) { }, 7689 7690 /** 7691 * @event 7692 * @description Whenever the user clicks on the board with a mouse device. 7693 * @name JXG.Board#mouseclick 7694 * @param {Event} e The browser's event object. 7695 */ 7696 __evt__mouseclick: function (e) { }, 7697 7698 /** 7699 * @event 7700 * @description Whenever the user double clicks on the board with a mouse device. 7701 * @name JXG.Board#mousedblclick 7702 * @see JXG.Board#clickDelay 7703 * @param {Event} e The browser's event object. 7704 */ 7705 __evt__mousedblclick: function (e) { }, 7706 7707 /** 7708 * @event 7709 * @description Whenever the user clicks on the board with a pointer device. 7710 * @name JXG.Board#pointerclick 7711 * @param {Event} e The browser's event object. 7712 */ 7713 __evt__pointerclick: function (e) { }, 7714 7715 /** 7716 * @event 7717 * @description Whenever the user double clicks on the board with a pointer device. 7718 * This event works on desktop browser, but is undefined 7719 * on mobile browsers. 7720 * @name JXG.Board#pointerdblclick 7721 * @see JXG.Board#clickDelay 7722 * @param {Event} e The browser's event object. 7723 */ 7724 __evt__pointerdblclick: function (e) { }, 7725 7726 /** 7727 * @event 7728 * @description This event is fired whenever the user is moving the finger or mouse pointer over the board. 7729 * @name JXG.Board#move 7730 * @param {Event} e The browser's event object. 7731 * @param {Number} mode The mode the board currently is in 7732 * @see JXG.Board#mode 7733 */ 7734 __evt__move: function (e, mode) { }, 7735 7736 /** 7737 * @event 7738 * @description This event is fired whenever the user is moving the mouse over the board. 7739 * @name JXG.Board#mousemove 7740 * @param {Event} e The browser's event object. 7741 * @param {Number} mode The mode the board currently is in 7742 * @see JXG.Board#mode 7743 */ 7744 __evt__mousemove: function (e, mode) { }, 7745 7746 /** 7747 * @event 7748 * @description This event is fired whenever the user is moving the pen over the board. 7749 * @name JXG.Board#penmove 7750 * @param {Event} e The browser's event object. 7751 * @param {Number} mode The mode the board currently is in 7752 * @see JXG.Board#mode 7753 */ 7754 __evt__penmove: function (e, mode) { }, 7755 7756 /** 7757 * @event 7758 * @description This event is fired whenever the user is moving the mouse over the board with a 7759 * device sending pointer events. 7760 * @name JXG.Board#pointermove 7761 * @param {Event} e The browser's event object. 7762 * @param {Number} mode The mode the board currently is in 7763 * @see JXG.Board#mode 7764 */ 7765 __evt__pointermove: function (e, mode) { }, 7766 7767 /** 7768 * @event 7769 * @description This event is fired whenever the user is moving the finger over the board. 7770 * @name JXG.Board#touchmove 7771 * @param {Event} e The browser's event object. 7772 * @param {Number} mode The mode the board currently is in 7773 * @see JXG.Board#mode 7774 */ 7775 __evt__touchmove: function (e, mode) { }, 7776 7777 /** 7778 * @event 7779 * @description This event is fired whenever the user is moving an element over the board by 7780 * pressing arrow keys on a keyboard. 7781 * @name JXG.Board#keymove 7782 * @param {Event} e The browser's event object. 7783 * @param {Number} mode The mode the board currently is in 7784 * @see JXG.Board#mode 7785 */ 7786 __evt__keymove: function (e, mode) { }, 7787 7788 /** 7789 * @event 7790 * @description Whenever an element is highlighted this event is fired. 7791 * @name JXG.Board#hit 7792 * @param {Event} e The browser's event object. 7793 * @param {JXG.GeometryElement} el The hit element. 7794 * @param target 7795 * 7796 * @example 7797 * var c = board.create('circle', [[1, 1], 2]); 7798 * board.on('hit', function(evt, el) { 7799 * console.log('JSXGraph example: Hit element', el); 7800 * }); 7801 * 7802 * </pre><div id='JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7803 * <script type='text/javascript'> 7804 * (function() { 7805 * var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723', 7806 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 7807 * var c = board.create('circle', [[1, 1], 2]); 7808 * board.on('hit', function(evt, el) { 7809 * console.log('JSXGraph example: Hit element', el); 7810 * }); 7811 * 7812 * })(); 7813 * 7814 * </script><pre> 7815 */ 7816 __evt__hit: function (e, el, target) { }, 7817 7818 /** 7819 * @event 7820 * @description Whenever an element is highlighted this event is fired. 7821 * @name JXG.Board#mousehit 7822 * @see JXG.Board#hit 7823 * @param {Event} e The browser's event object. 7824 * @param {JXG.GeometryElement} el The hit element. 7825 * @param target 7826 */ 7827 __evt__mousehit: function (e, el, target) { }, 7828 7829 /** 7830 * @event 7831 * @description This board is updated. 7832 * @name JXG.Board#update 7833 */ 7834 __evt__update: function () { }, 7835 7836 /** 7837 * @event 7838 * @description The bounding box of the board has changed. 7839 * @name JXG.Board#boundingbox 7840 */ 7841 __evt__boundingbox: function () { }, 7842 7843 /** 7844 * @event 7845 * @description Select a region is started during a down event or by calling 7846 * {@link JXG.Board.startSelectionMode} 7847 * @name JXG.Board#startselecting 7848 */ 7849 __evt__startselecting: function () { }, 7850 7851 /** 7852 * @event 7853 * @description Select a region is started during a down event 7854 * from a device sending mouse events or by calling 7855 * {@link JXG.Board.startSelectionMode}. 7856 * @name JXG.Board#mousestartselecting 7857 */ 7858 __evt__mousestartselecting: function () { }, 7859 7860 /** 7861 * @event 7862 * @description Select a region is started during a down event 7863 * from a device sending pointer events or by calling 7864 * {@link JXG.Board.startSelectionMode}. 7865 * @name JXG.Board#pointerstartselecting 7866 */ 7867 __evt__pointerstartselecting: function () { }, 7868 7869 /** 7870 * @event 7871 * @description Select a region is started during a down event 7872 * from a device sending touch events or by calling 7873 * {@link JXG.Board.startSelectionMode}. 7874 * @name JXG.Board#touchstartselecting 7875 */ 7876 __evt__touchstartselecting: function () { }, 7877 7878 /** 7879 * @event 7880 * @description Selection of a region is stopped during an up event. 7881 * @name JXG.Board#stopselecting 7882 */ 7883 __evt__stopselecting: function () { }, 7884 7885 /** 7886 * @event 7887 * @description Selection of a region is stopped during an up event 7888 * from a device sending mouse events. 7889 * @name JXG.Board#mousestopselecting 7890 */ 7891 __evt__mousestopselecting: function () { }, 7892 7893 /** 7894 * @event 7895 * @description Selection of a region is stopped during an up event 7896 * from a device sending pointer events. 7897 * @name JXG.Board#pointerstopselecting 7898 */ 7899 __evt__pointerstopselecting: function () { }, 7900 7901 /** 7902 * @event 7903 * @description Selection of a region is stopped during an up event 7904 * from a device sending touch events. 7905 * @name JXG.Board#touchstopselecting 7906 */ 7907 __evt__touchstopselecting: function () { }, 7908 7909 /** 7910 * @event 7911 * @description A move event while selecting of a region is active. 7912 * @name JXG.Board#moveselecting 7913 */ 7914 __evt__moveselecting: function () { }, 7915 7916 /** 7917 * @event 7918 * @description A move event while selecting of a region is active 7919 * from a device sending mouse events. 7920 * @name JXG.Board#mousemoveselecting 7921 */ 7922 __evt__mousemoveselecting: function () { }, 7923 7924 /** 7925 * @event 7926 * @description Select a region is started during a down event 7927 * from a device sending mouse events. 7928 * @name JXG.Board#pointermoveselecting 7929 */ 7930 __evt__pointermoveselecting: function () { }, 7931 7932 /** 7933 * @event 7934 * @description Select a region is started during a down event 7935 * from a device sending touch events. 7936 * @name JXG.Board#touchmoveselecting 7937 */ 7938 __evt__touchmoveselecting: function () { }, 7939 7940 /** 7941 * @ignore 7942 */ 7943 __evt: function () { }, 7944 7945 //endregion 7946 7947 /** 7948 * Expand the JSXGraph construction to fullscreen. 7949 * In order to preserve the proportions of the JSXGraph element, 7950 * a wrapper div is created which is set to fullscreen. 7951 * This function is called when fullscreen mode is triggered 7952 * <b>and</b> when it is closed. 7953 * <p> 7954 * The wrapping div has the CSS class 'jxgbox_wrap_private' which is 7955 * defined in the file 'jsxgraph.css' 7956 * <p> 7957 * This feature is not available on iPhones (as of December 2021). 7958 * 7959 * @param {String} id (Optional) id of the div element which is brought to fullscreen. 7960 * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick 7961 * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied. 7962 * 7963 * @return {JXG.Board} Reference to the board 7964 * 7965 * @example 7966 * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div> 7967 * <button onClick='board.toFullscreen()'>Fullscreen</button> 7968 * 7969 * <script language='Javascript' type='text/javascript'> 7970 * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]}); 7971 * var p = board.create('point', [0, 1]); 7972 * </script> 7973 * 7974 * </pre><div id='JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7975 * <script type='text/javascript'> 7976 * var board_d5bab8b6; 7977 * (function() { 7978 * var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723', 7979 * {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false}); 7980 * var p = board.create('point', [0, 1]); 7981 * board_d5bab8b6 = board; 7982 * })(); 7983 * </script> 7984 * <button onClick='board_d5bab8b6.toFullscreen()'>Fullscreen</button> 7985 * <pre> 7986 * 7987 * @example 7988 * <div id='outer' style='max-width: 500px; margin: 0 auto;'> 7989 * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div> 7990 * </div> 7991 * <button onClick='board.toFullscreen('outer')'>Fullscreen</button> 7992 * 7993 * <script language='Javascript' type='text/javascript'> 7994 * var board = JXG.JSXGraph.initBoard('jxgbox', { 7995 * axis:true, 7996 * boundingbox:[-5,5,5,-5], 7997 * fullscreen: { id: 'outer' }, 7998 * showFullscreen: true 7999 * }); 8000 * var p = board.create('point', [-2, 3], {}); 8001 * </script> 8002 * 8003 * </pre><div id='JXG7103f6b_outer' style='max-width: 500px; margin: 0 auto;'> 8004 * <div id='JXG7103f6be-6993-4ff8-8133-c78e50a8afac' class='jxgbox' style='height: 0; padding-bottom: 100%;'></div> 8005 * </div> 8006 * <button onClick='board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')'>Fullscreen</button> 8007 * <script type='text/javascript'> 8008 * var board_JXG7103f6be; 8009 * (function() { 8010 * var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac', 8011 * {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true, 8012 * showcopyright: false, shownavigation: false}); 8013 * var p = board.create('point', [-2, 3], {}); 8014 * board_JXG7103f6be = board; 8015 * })(); 8016 * 8017 * </script><pre> 8018 * 8019 * 8020 */ 8021 toFullscreen: function (id) { 8022 var wrap_id, 8023 wrap_node, 8024 inner_node, 8025 dim, 8026 doc = this.document, 8027 fullscreenElement; 8028 8029 id = id || this.container; 8030 this._fullscreen_inner_id = id; 8031 inner_node = doc.getElementById(id); 8032 wrap_id = 'fullscreenwrap_' + id; 8033 8034 if (!Type.exists(inner_node._cssFullscreenStore)) { 8035 // Store the actual, absolute size of the div 8036 // This is used in scaleJSXGraphDiv 8037 dim = this.containerObj.getBoundingClientRect(); 8038 inner_node._cssFullscreenStore = { 8039 w: dim.width, 8040 h: dim.height 8041 }; 8042 } 8043 8044 // Wrap a div around the JSXGraph div. 8045 // It is removed when fullscreen mode is closed. 8046 if (doc.getElementById(wrap_id)) { 8047 wrap_node = doc.getElementById(wrap_id); 8048 } else { 8049 wrap_node = document.createElement('div'); 8050 wrap_node.classList.add('JXG_wrap_private'); 8051 wrap_node.setAttribute('id', wrap_id); 8052 inner_node.parentNode.insertBefore(wrap_node, inner_node); 8053 wrap_node.appendChild(inner_node); 8054 } 8055 8056 // Trigger fullscreen mode 8057 wrap_node.requestFullscreen = 8058 wrap_node.requestFullscreen || 8059 wrap_node.webkitRequestFullscreen || 8060 wrap_node.mozRequestFullScreen || 8061 wrap_node.msRequestFullscreen; 8062 8063 if (doc.fullscreenElement !== undefined) { 8064 fullscreenElement = doc.fullscreenElement; 8065 } else if (doc.webkitFullscreenElement !== undefined) { 8066 fullscreenElement = doc.webkitFullscreenElement; 8067 } else { 8068 fullscreenElement = doc.msFullscreenElement; 8069 } 8070 8071 if (fullscreenElement === null) { 8072 // Start fullscreen mode 8073 if (wrap_node.requestFullscreen) { 8074 wrap_node.requestFullscreen(); 8075 this.startFullscreenResizeObserver(wrap_node); 8076 } 8077 } else { 8078 this.stopFullscreenResizeObserver(wrap_node); 8079 if (Type.exists(document.exitFullscreen)) { 8080 document.exitFullscreen(); 8081 } else if (Type.exists(document.webkitExitFullscreen)) { 8082 document.webkitExitFullscreen(); 8083 } 8084 } 8085 8086 return this; 8087 }, 8088 8089 /** 8090 * If fullscreen mode is toggled, the possible CSS transformations 8091 * which are applied to the JSXGraph canvas have to be reread. 8092 * Otherwise the position of upper left corner is wrongly interpreted. 8093 * 8094 * @param {Object} evt fullscreen event object (unused) 8095 */ 8096 fullscreenListener: function (evt) { 8097 var inner_id, 8098 inner_node, 8099 fullscreenElement, 8100 doc = this.document; 8101 8102 inner_id = this._fullscreen_inner_id; 8103 if (!Type.exists(inner_id)) { 8104 return; 8105 } 8106 8107 if (doc.fullscreenElement !== undefined) { 8108 fullscreenElement = doc.fullscreenElement; 8109 } else if (doc.webkitFullscreenElement !== undefined) { 8110 fullscreenElement = doc.webkitFullscreenElement; 8111 } else { 8112 fullscreenElement = doc.msFullscreenElement; 8113 } 8114 8115 inner_node = doc.getElementById(inner_id); 8116 // If full screen mode is started we have to remove CSS margin around the JSXGraph div. 8117 // Otherwise, the positioning of the fullscreen div will be false. 8118 // When leaving the fullscreen mode, the margin is put back in. 8119 if (fullscreenElement) { 8120 // Just entered fullscreen mode 8121 8122 // Store the original data. 8123 // Further, the CSS margin has to be removed when in fullscreen mode, 8124 // and must be restored later. 8125 // 8126 // Obsolete: 8127 // It is used in AbstractRenderer.updateText to restore the scaling matrix 8128 // which is removed by MathJax. 8129 inner_node._cssFullscreenStore.id = fullscreenElement.id; 8130 inner_node._cssFullscreenStore.isFullscreen = true; 8131 inner_node._cssFullscreenStore.margin = inner_node.style.margin; 8132 inner_node._cssFullscreenStore.width = inner_node.style.width; 8133 inner_node._cssFullscreenStore.height = inner_node.style.height; 8134 inner_node._cssFullscreenStore.transform = inner_node.style.transform; 8135 // Be sure to replace relative width / height units by absolute units 8136 inner_node.style.width = inner_node._cssFullscreenStore.w + 'px'; 8137 inner_node.style.height = inner_node._cssFullscreenStore.h + 'px'; 8138 inner_node.style.margin = ''; 8139 8140 // Do the shifting and scaling via CSS properties 8141 // We do this after fullscreen mode has been established to get the correct size 8142 // of the JSXGraph div. 8143 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 8144 Type.evaluate(this.attr.fullscreen.scale)); 8145 8146 // Clear this.doc.fullscreenElement, because Safari doesn't to it and 8147 // when leaving full screen mode it is still set. 8148 fullscreenElement = null; 8149 } else if (Type.exists(inner_node._cssFullscreenStore)) { 8150 // Just left the fullscreen mode 8151 8152 inner_node._cssFullscreenStore.isFullscreen = false; 8153 inner_node.style.margin = inner_node._cssFullscreenStore.margin; 8154 inner_node.style.width = inner_node._cssFullscreenStore.width; 8155 inner_node.style.height = inner_node._cssFullscreenStore.height; 8156 inner_node.style.transform = inner_node._cssFullscreenStore.transform; 8157 inner_node._cssFullscreenStore = null; 8158 8159 // Remove the wrapper div 8160 inner_node.parentElement.replaceWith(inner_node); 8161 } 8162 8163 this.updateCSSTransforms(); 8164 }, 8165 8166 /** 8167 * Start resize observer to handle 8168 * orientation changes in fullscreen mode. 8169 * 8170 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 8171 * around the JSXGraph div. 8172 * @returns {JXG.Board} Reference to the board 8173 * @private 8174 * @see JXG.Board#toFullscreen 8175 * 8176 */ 8177 startFullscreenResizeObserver: function(node) { 8178 var that = this; 8179 8180 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 8181 return this; 8182 } 8183 8184 this.resizeObserver = new ResizeObserver(function (entries) { 8185 var inner_id, 8186 fullscreenElement, 8187 doc = that.document; 8188 8189 if (!that._isResizing) { 8190 that._isResizing = true; 8191 window.setTimeout(function () { 8192 try { 8193 inner_id = that._fullscreen_inner_id; 8194 if (doc.fullscreenElement !== undefined) { 8195 fullscreenElement = doc.fullscreenElement; 8196 } else if (doc.webkitFullscreenElement !== undefined) { 8197 fullscreenElement = doc.webkitFullscreenElement; 8198 } else { 8199 fullscreenElement = doc.msFullscreenElement; 8200 } 8201 if (fullscreenElement !== null) { 8202 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 8203 Type.evaluate(that.attr.fullscreen.scale)); 8204 } 8205 } catch (err) { 8206 that.stopFullscreenResizeObserver(node); 8207 } finally { 8208 that._isResizing = false; 8209 } 8210 }, that.attr.resize.throttle); 8211 } 8212 }); 8213 this.resizeObserver.observe(node); 8214 return this; 8215 }, 8216 8217 /** 8218 * Remove resize observer to handle orientation changes in fullscreen mode. 8219 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 8220 * around the JSXGraph div. 8221 * @returns {JXG.Board} Reference to the board 8222 * @private 8223 * @see JXG.Board#toFullscreen 8224 */ 8225 stopFullscreenResizeObserver: function(node) { 8226 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 8227 return this; 8228 } 8229 8230 if (Type.exists(this.resizeObserver)) { 8231 this.resizeObserver.unobserve(node); 8232 } 8233 return this; 8234 }, 8235 8236 /** 8237 * Add user activity to the array 'board.userLog'. 8238 * 8239 * @param {String} type Event type, e.g. 'drag' 8240 * @param {Object} obj JSXGraph element object 8241 * 8242 * @see JXG.Board#userLog 8243 * @return {JXG.Board} Reference to the board 8244 */ 8245 addLogEntry: function (type, obj, pos) { 8246 var t, id, 8247 last = this.userLog.length - 1; 8248 8249 if (Type.exists(obj.elementClass)) { 8250 id = obj.id; 8251 } 8252 if (Type.evaluate(this.attr.logging.enabled)) { 8253 t = (new Date()).getTime(); 8254 if (last >= 0 && 8255 this.userLog[last].type === type && 8256 this.userLog[last].id === id && 8257 // Distinguish consecutive drag events of 8258 // the same element 8259 t - this.userLog[last].end < 500) { 8260 8261 this.userLog[last].end = t; 8262 this.userLog[last].endpos = pos; 8263 } else { 8264 this.userLog.push({ 8265 type: type, 8266 id: id, 8267 start: t, 8268 startpos: pos, 8269 end: t, 8270 endpos: pos, 8271 bbox: this.getBoundingBox(), 8272 canvas: [this.canvasWidth, this.canvasHeight], 8273 zoom: [this.zoomX, this.zoomY] 8274 }); 8275 } 8276 } 8277 return this; 8278 }, 8279 8280 /** 8281 * Function to animate a curve rolling on another curve. 8282 * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls 8283 * @param {Curve} c2 JSXGraph curve which rolls on c1. 8284 * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the 8285 * rolling process 8286 * @param {Number} stepsize Increase in t in each step for the curve c1 8287 * @param {Number} direction 8288 * @param {Number} time Delay time for setInterval() 8289 * @param {Array} pointlist Array of points which are rolled in each step. This list should contain 8290 * all points which define c2 and gliders on c2. 8291 * 8292 * @example 8293 * 8294 * // Line which will be the floor to roll upon. 8295 * var line = board.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 8296 * // Center of the rolling circle 8297 * var C = board.create('point',[0,2],{name:'C'}); 8298 * // Starting point of the rolling circle 8299 * var P = board.create('point',[0,1],{name:'P', trace:true}); 8300 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 8301 * var circle = board.create('curve',[ 8302 * function (t){var d = P.Dist(C), 8303 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8304 * t += beta; 8305 * return C.X()+d*Math.cos(t); 8306 * }, 8307 * function (t){var d = P.Dist(C), 8308 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8309 * t += beta; 8310 * return C.Y()+d*Math.sin(t); 8311 * }, 8312 * 0,2*Math.PI], 8313 * {strokeWidth:6, strokeColor:'green'}); 8314 * 8315 * // Point on circle 8316 * var B = board.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 8317 * var roll = board.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 8318 * roll.start() // Start the rolling, to be stopped by roll.stop() 8319 * 8320 * </pre><div class='jxgbox' id='JXGe5e1b53c-a036-4a46-9e35-190d196beca5' style='width: 300px; height: 300px;'></div> 8321 * <script type='text/javascript'> 8322 * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false}); 8323 * // Line which will be the floor to roll upon. 8324 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 8325 * // Center of the rolling circle 8326 * var C = brd.create('point',[0,2],{name:'C'}); 8327 * // Starting point of the rolling circle 8328 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 8329 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 8330 * var circle = brd.create('curve',[ 8331 * function (t){var d = P.Dist(C), 8332 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8333 * t += beta; 8334 * return C.X()+d*Math.cos(t); 8335 * }, 8336 * function (t){var d = P.Dist(C), 8337 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8338 * t += beta; 8339 * return C.Y()+d*Math.sin(t); 8340 * }, 8341 * 0,2*Math.PI], 8342 * {strokeWidth:6, strokeColor:'green'}); 8343 * 8344 * // Point on circle 8345 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 8346 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 8347 * roll.start() // Start the rolling, to be stopped by roll.stop() 8348 * </script><pre> 8349 */ 8350 createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) { 8351 var brd = this, 8352 Roulette = function () { 8353 var alpha = 0, 8354 Tx = 0, 8355 Ty = 0, 8356 t1 = start_c1, 8357 t2 = Numerics.root( 8358 function (t) { 8359 var c1x = c1.X(t1), 8360 c1y = c1.Y(t1), 8361 c2x = c2.X(t), 8362 c2y = c2.Y(t); 8363 8364 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y); 8365 }, 8366 [0, Math.PI * 2] 8367 ), 8368 t1_new = 0.0, 8369 t2_new = 0.0, 8370 c1dist, 8371 rotation = brd.create( 8372 'transform', 8373 [ 8374 function () { 8375 return alpha; 8376 } 8377 ], 8378 { type: 'rotate' } 8379 ), 8380 rotationLocal = brd.create( 8381 'transform', 8382 [ 8383 function () { 8384 return alpha; 8385 }, 8386 function () { 8387 return c1.X(t1); 8388 }, 8389 function () { 8390 return c1.Y(t1); 8391 } 8392 ], 8393 { type: 'rotate' } 8394 ), 8395 translate = brd.create( 8396 'transform', 8397 [ 8398 function () { 8399 return Tx; 8400 }, 8401 function () { 8402 return Ty; 8403 } 8404 ], 8405 { type: 'translate' } 8406 ), 8407 // arc length via Simpson's rule. 8408 arclen = function (c, a, b) { 8409 var cpxa = Numerics.D(c.X)(a), 8410 cpya = Numerics.D(c.Y)(a), 8411 cpxb = Numerics.D(c.X)(b), 8412 cpyb = Numerics.D(c.Y)(b), 8413 cpxab = Numerics.D(c.X)((a + b) * 0.5), 8414 cpyab = Numerics.D(c.Y)((a + b) * 0.5), 8415 fa = Mat.hypot(cpxa, cpya), 8416 fb = Mat.hypot(cpxb, cpyb), 8417 fab = Mat.hypot(cpxab, cpyab); 8418 8419 return ((fa + 4 * fab + fb) * (b - a)) / 6; 8420 }, 8421 exactDist = function (t) { 8422 return c1dist - arclen(c2, t2, t); 8423 }, 8424 beta = Math.PI / 18, 8425 beta9 = beta * 9, 8426 interval = null; 8427 8428 this.rolling = function () { 8429 var h, g, hp, gp, z; 8430 8431 t1_new = t1 + direction * stepsize; 8432 8433 // arc length between c1(t1) and c1(t1_new) 8434 c1dist = arclen(c1, t1, t1_new); 8435 8436 // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist. 8437 t2_new = Numerics.root(exactDist, t2); 8438 8439 // c1(t) as complex number 8440 h = new Complex(c1.X(t1_new), c1.Y(t1_new)); 8441 8442 // c2(t) as complex number 8443 g = new Complex(c2.X(t2_new), c2.Y(t2_new)); 8444 8445 hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new)); 8446 gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new)); 8447 8448 // z is angle between the tangents of c1 at t1_new, and c2 at t2_new 8449 z = Complex.C.div(hp, gp); 8450 8451 alpha = Math.atan2(z.imaginary, z.real); 8452 // Normalizing the quotient 8453 z.div(Complex.C.abs(z)); 8454 z.mult(g); 8455 Tx = h.real - z.real; 8456 8457 // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new); 8458 Ty = h.imaginary - z.imaginary; 8459 8460 // -(10-90) degrees: make corners roll smoothly 8461 if (alpha < -beta && alpha > -beta9) { 8462 alpha = -beta; 8463 rotationLocal.applyOnce(pointlist); 8464 } else if (alpha > beta && alpha < beta9) { 8465 alpha = beta; 8466 rotationLocal.applyOnce(pointlist); 8467 } else { 8468 rotation.applyOnce(pointlist); 8469 translate.applyOnce(pointlist); 8470 t1 = t1_new; 8471 t2 = t2_new; 8472 } 8473 brd.update(); 8474 }; 8475 8476 this.start = function () { 8477 if (time > 0) { 8478 interval = window.setInterval(this.rolling, time); 8479 } 8480 return this; 8481 }; 8482 8483 this.stop = function () { 8484 window.clearInterval(interval); 8485 return this; 8486 }; 8487 return this; 8488 }; 8489 return new Roulette(); 8490 } 8491 } 8492 ); 8493 8494 export default JXG.Board; 8495