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 this.printListenerMatchBound = this.printListenerMatch.bind(this); 2118 this.printMediaQuery = window.matchMedia('print'); 2119 this.screenMediaQuery = window.matchMedia('screen'); 2120 this.printMediaQuery.addEventListener('change', this.printListenerMatchBound); 2121 this.screenMediaQuery.addEventListener('change', this.printListenerMatchBound); 2122 this.resizeHandlers.push('print'); 2123 } catch (err) { 2124 JXG.debug("Error adding printListener", err); 2125 } 2126 // if (Type.isFunction(MediaQueryList.prototype.addEventListener)) { 2127 // window.matchMedia('print').addEventListener('change', function (mql) { 2128 // if (mql.matches) { 2129 // that.printListener(); 2130 // } 2131 // }); 2132 // } else if (Type.isFunction(MediaQueryList.prototype.addListener)) { // addListener might be deprecated 2133 // window.matchMedia('print').addListener(function (mql, ev) { 2134 // if (mql.matches) { 2135 // that.printListener(ev); 2136 // } 2137 // }); 2138 // } 2139 2140 // When closing the print dialog we again have to resize. 2141 // Env.addEvent(window, 'afterprint', this.printListener, this); 2142 // this.resizeHandlers.push('afterprint'); 2143 } 2144 }, 2145 2146 /** 2147 * Remove resize related event handlers 2148 * 2149 */ 2150 removeResizeEventHandlers: function () { 2151 var i, e; 2152 if (this.resizeHandlers.length > 0 && Env.isBrowser) { 2153 for (i = 0; i < this.resizeHandlers.length; i++) { 2154 e = this.resizeHandlers[i]; 2155 switch (e) { 2156 case 'resizeobserver': 2157 if (Type.exists(this.resizeObserver)) { 2158 this.stopResizeObserver(); 2159 } 2160 break; 2161 case 'resize': 2162 Env.removeEvent(window, 'resize', this.resizeListener, this); 2163 if (Type.exists(this.intersectionObserver)) { 2164 this.stopIntersectionObserver(); 2165 } 2166 break; 2167 case 'scroll': 2168 Env.removeEvent(window, 'scroll', this.scrollListener, this); 2169 break; 2170 case 'print': 2171 this.printMediaQuery.removeEventListener('change', this.printListenerMatchBound, false); 2172 this.screenMediaQuery.removeEventListener('change', this.printListenerMatchBound, false); 2173 this.printMediaQuery = null; 2174 this.screenMediaQuery = null; 2175 this.printListenerMatchBound = null; 2176 break; 2177 // case 'afterprint': 2178 // Env.removeEvent(window, 'afterprint', this.printListener, this); 2179 // break; 2180 } 2181 } 2182 this.resizeHandlers = []; 2183 } 2184 }, 2185 2186 2187 /** 2188 * Registers pointer event handlers. 2189 */ 2190 addPointerEventHandlers: function () { 2191 if (!this.hasPointerHandlers && Env.isBrowser) { 2192 var moveTarget = this.attr.movetarget || this.containerObj; 2193 2194 if (window.navigator.msPointerEnabled) { 2195 // IE10- 2196 // Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 2197 Env.addEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this); 2198 Env.addEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 2199 } else { 2200 // Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 2201 Env.addEvent(moveTarget, 'pointerdown', this.pointerDownListener, this); 2202 Env.addEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 2203 Env.addEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 2204 Env.addEvent(moveTarget, 'click', this.pointerClickListener, this); 2205 Env.addEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this); 2206 } 2207 2208 if (this.containerObj !== null) { 2209 // This is needed for capturing touch events. 2210 // It is in jsxgraph.css, for ms-touch-action... 2211 this.containerObj.style.touchAction = 'none'; 2212 // this.containerObj.style.touchAction = 'auto'; 2213 } 2214 2215 this.hasPointerHandlers = true; 2216 } 2217 }, 2218 2219 /** 2220 * Registers mouse move, down and wheel event handlers. 2221 */ 2222 addMouseEventHandlers: function () { 2223 if (!this.hasMouseHandlers && Env.isBrowser) { 2224 var moveTarget = this.attr.movetarget || this.containerObj; 2225 2226 // Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 2227 Env.addEvent(moveTarget, 'mousedown', this.mouseDownListener, this); 2228 Env.addEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 2229 Env.addEvent(moveTarget, 'click', this.mouseClickListener, this); 2230 Env.addEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this); 2231 2232 this.hasMouseHandlers = true; 2233 } 2234 }, 2235 2236 /** 2237 * Register touch start and move and gesture start and change event handlers. 2238 * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers 2239 * will not be registered. 2240 * 2241 * Since iOS 13, touch events were abandoned in favour of pointer events 2242 */ 2243 addTouchEventHandlers: function (appleGestures) { 2244 if (!this.hasTouchHandlers && Env.isBrowser) { 2245 var moveTarget = this.attr.movetarget || this.containerObj; 2246 2247 // Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 2248 Env.addEvent(moveTarget, 'touchstart', this.touchStartListener, this); 2249 Env.addEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 2250 2251 /* 2252 if (!Type.exists(appleGestures) || appleGestures) { 2253 // Gesture listener are called in touchStart and touchMove. 2254 //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 2255 //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 2256 } 2257 */ 2258 2259 this.hasTouchHandlers = true; 2260 } 2261 }, 2262 2263 /** 2264 * Registers pointer event handlers. 2265 */ 2266 addWheelEventHandlers: function () { 2267 if (!this.hasWheelHandlers && Env.isBrowser) { 2268 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2269 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 2270 this.hasWheelHandlers = true; 2271 } 2272 }, 2273 2274 /** 2275 * Add fullscreen events which update the CSS transformation matrix to correct 2276 * the mouse/touch/pointer positions in case of CSS transformations. 2277 */ 2278 addFullscreenEventHandlers: function () { 2279 var i, 2280 // standard/Edge, firefox, chrome/safari, IE11 2281 events = [ 2282 'fullscreenchange', 2283 'mozfullscreenchange', 2284 'webkitfullscreenchange', 2285 'msfullscreenchange' 2286 ], 2287 le = events.length; 2288 2289 if (!this.hasFullscreenEventHandlers && Env.isBrowser) { 2290 for (i = 0; i < le; i++) { 2291 Env.addEvent(this.document, events[i], this.fullscreenListener, this); 2292 } 2293 this.hasFullscreenEventHandlers = true; 2294 } 2295 }, 2296 2297 /** 2298 * Register keyboard event handlers. 2299 */ 2300 addKeyboardEventHandlers: function () { 2301 if (this.attr.keyboard.enabled && !this.hasKeyboardHandlers && Env.isBrowser) { 2302 Env.addEvent(this.containerObj, 'keydown', this.keyDownListener, this); 2303 Env.addEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 2304 Env.addEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 2305 this.hasKeyboardHandlers = true; 2306 } 2307 }, 2308 2309 /** 2310 * Remove all registered touch event handlers. 2311 */ 2312 removeKeyboardEventHandlers: function () { 2313 if (this.hasKeyboardHandlers && Env.isBrowser) { 2314 Env.removeEvent(this.containerObj, 'keydown', this.keyDownListener, this); 2315 Env.removeEvent(this.containerObj, 'focusin', this.keyFocusInListener, this); 2316 Env.removeEvent(this.containerObj, 'focusout', this.keyFocusOutListener, this); 2317 this.hasKeyboardHandlers = false; 2318 } 2319 }, 2320 2321 /** 2322 * Remove all registered event handlers regarding fullscreen mode. 2323 */ 2324 removeFullscreenEventHandlers: function () { 2325 var i, 2326 // standard/Edge, firefox, chrome/safari, IE11 2327 events = [ 2328 'fullscreenchange', 2329 'mozfullscreenchange', 2330 'webkitfullscreenchange', 2331 'msfullscreenchange' 2332 ], 2333 le = events.length; 2334 2335 if (this.hasFullscreenEventHandlers && Env.isBrowser) { 2336 for (i = 0; i < le; i++) { 2337 Env.removeEvent(this.document, events[i], this.fullscreenListener, this); 2338 } 2339 this.hasFullscreenEventHandlers = false; 2340 } 2341 }, 2342 2343 /** 2344 * Remove MSPointer* Event handlers. 2345 */ 2346 removePointerEventHandlers: function () { 2347 if (this.hasPointerHandlers && Env.isBrowser) { 2348 var moveTarget = this.attr.movetarget || this.containerObj; 2349 2350 if (window.navigator.msPointerEnabled) { 2351 // IE10- 2352 // Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 2353 Env.removeEvent(moveTarget, 'MSPointerDown', this.pointerDownListener, this); 2354 Env.removeEvent(moveTarget, 'MSPointerMove', this.pointerMoveListener, this); 2355 } else { 2356 // Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 2357 Env.removeEvent(moveTarget, 'pointerdown', this.pointerDownListener, this); 2358 Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this); 2359 Env.removeEvent(moveTarget, 'pointerleave', this.pointerLeaveListener, this); 2360 Env.removeEvent(moveTarget, 'click', this.pointerClickListener, this); 2361 Env.removeEvent(moveTarget, 'dblclick', this.pointerDblClickListener, this); 2362 } 2363 2364 if (this.hasWheelHandlers) { 2365 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2366 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 2367 } 2368 2369 if (this.hasPointerUp) { 2370 if (window.navigator.msPointerEnabled) { 2371 // IE10- 2372 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2373 } else { 2374 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 2375 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2376 } 2377 this.hasPointerUp = false; 2378 } 2379 2380 this.hasPointerHandlers = false; 2381 } 2382 }, 2383 2384 /** 2385 * De-register mouse event handlers. 2386 */ 2387 removeMouseEventHandlers: function () { 2388 if (this.hasMouseHandlers && Env.isBrowser) { 2389 var moveTarget = this.attr.movetarget || this.containerObj; 2390 2391 // Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 2392 Env.removeEvent(moveTarget, 'mousedown', this.mouseDownListener, this); 2393 Env.removeEvent(moveTarget, 'mousemove', this.mouseMoveListener, this); 2394 Env.removeEvent(moveTarget, 'click', this.mouseClickListener, this); 2395 Env.removeEvent(moveTarget, 'dblclick', this.mouseDblClickListener, this); 2396 2397 if (this.hasMouseUp) { 2398 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 2399 this.hasMouseUp = false; 2400 } 2401 2402 if (this.hasWheelHandlers) { 2403 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 2404 Env.removeEvent( 2405 this.containerObj, 2406 'DOMMouseScroll', 2407 this.mouseWheelListener, 2408 this 2409 ); 2410 } 2411 2412 this.hasMouseHandlers = false; 2413 } 2414 }, 2415 2416 /** 2417 * Remove all registered touch event handlers. 2418 */ 2419 removeTouchEventHandlers: function () { 2420 if (this.hasTouchHandlers && Env.isBrowser) { 2421 var moveTarget = this.attr.movetarget || this.containerObj; 2422 2423 // Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 2424 Env.removeEvent(moveTarget, 'touchstart', this.touchStartListener, this); 2425 Env.removeEvent(moveTarget, 'touchmove', this.touchMoveListener, this); 2426 2427 if (this.hasTouchEnd) { 2428 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 2429 this.hasTouchEnd = false; 2430 } 2431 2432 this.hasTouchHandlers = false; 2433 } 2434 }, 2435 2436 /** 2437 * Handler for click on left arrow in the navigation bar 2438 * @returns {JXG.Board} Reference to the board 2439 */ 2440 clickLeftArrow: function () { 2441 this.moveOrigin( 2442 this.origin.scrCoords[1] + this.canvasWidth * 0.1, 2443 this.origin.scrCoords[2] 2444 ); 2445 return this; 2446 }, 2447 2448 /** 2449 * Handler for click on right arrow in the navigation bar 2450 * @returns {JXG.Board} Reference to the board 2451 */ 2452 clickRightArrow: function () { 2453 this.moveOrigin( 2454 this.origin.scrCoords[1] - this.canvasWidth * 0.1, 2455 this.origin.scrCoords[2] 2456 ); 2457 return this; 2458 }, 2459 2460 /** 2461 * Handler for click on up arrow in the navigation bar 2462 * @returns {JXG.Board} Reference to the board 2463 */ 2464 clickUpArrow: function () { 2465 this.moveOrigin( 2466 this.origin.scrCoords[1], 2467 this.origin.scrCoords[2] - this.canvasHeight * 0.1 2468 ); 2469 return this; 2470 }, 2471 2472 /** 2473 * Handler for click on down arrow in the navigation bar 2474 * @returns {JXG.Board} Reference to the board 2475 */ 2476 clickDownArrow: function () { 2477 this.moveOrigin( 2478 this.origin.scrCoords[1], 2479 this.origin.scrCoords[2] + this.canvasHeight * 0.1 2480 ); 2481 return this; 2482 }, 2483 2484 /** 2485 * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. 2486 * Works on iOS/Safari and Android. 2487 * @param {Event} evt Browser event object 2488 * @returns {Boolean} 2489 */ 2490 gestureChangeListener: function (evt) { 2491 var c, 2492 dir1 = [], 2493 dir2 = [], 2494 angle, 2495 mi = 10, 2496 isPinch = false, 2497 // Save zoomFactors 2498 zx = this.attr.zoom.factorx, 2499 zy = this.attr.zoom.factory, 2500 factor, dist, theta, bound, 2501 zoomCenter, 2502 doZoom = false, 2503 dx, dy, cx, cy; 2504 2505 if (this.mode !== this.BOARD_MODE_ZOOM) { 2506 return true; 2507 } 2508 evt.preventDefault(); 2509 2510 dist = Geometry.distance( 2511 [evt.touches[0].clientX, evt.touches[0].clientY], 2512 [evt.touches[1].clientX, evt.touches[1].clientY], 2513 2 2514 ); 2515 2516 // Android pinch to zoom 2517 // evt.scale was available in iOS touch events (pre iOS 13) 2518 // evt.scale is undefined in Android 2519 if (evt.scale === undefined) { 2520 evt.scale = dist / this.prevDist; 2521 } 2522 2523 if (!Type.exists(this.prevCoords)) { 2524 return false; 2525 } 2526 // Compute the angle of the two finger directions 2527 dir1 = [ 2528 evt.touches[0].clientX - this.prevCoords[0][0], 2529 evt.touches[0].clientY - this.prevCoords[0][1] 2530 ]; 2531 dir2 = [ 2532 evt.touches[1].clientX - this.prevCoords[1][0], 2533 evt.touches[1].clientY - this.prevCoords[1][1] 2534 ]; 2535 2536 if ( 2537 dir1[0] * dir1[0] + dir1[1] * dir1[1] < mi * mi && 2538 dir2[0] * dir2[0] + dir2[1] * dir2[1] < mi * mi 2539 ) { 2540 return false; 2541 } 2542 2543 angle = Geometry.rad(dir1, [0, 0], dir2); 2544 if ( 2545 this.isPreviousGesture !== 'pan' && 2546 Math.abs(angle) > Math.PI * 0.2 && 2547 Math.abs(angle) < Math.PI * 1.8 2548 ) { 2549 isPinch = true; 2550 } 2551 2552 if (this.isPreviousGesture !== 'pan' && !isPinch) { 2553 if (Math.abs(evt.scale) < 0.77 || Math.abs(evt.scale) > 1.3) { 2554 isPinch = true; 2555 } 2556 } 2557 2558 factor = evt.scale / this.prevScale; 2559 this.prevScale = evt.scale; 2560 this.prevCoords = [ 2561 [evt.touches[0].clientX, evt.touches[0].clientY], 2562 [evt.touches[1].clientX, evt.touches[1].clientY] 2563 ]; 2564 2565 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this); 2566 2567 if (this.attr.pan.enabled && this.attr.pan.needtwofingers && !isPinch) { 2568 // Pan detected 2569 this.isPreviousGesture = 'pan'; 2570 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true); 2571 2572 } else if (this.attr.zoom.enabled && Math.abs(factor - 1.0) < 0.5) { 2573 doZoom = false; 2574 zoomCenter = this.attr.zoom.center; 2575 // Pinch detected 2576 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) { 2577 dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX); 2578 dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY); 2579 theta = Math.abs(Math.atan2(dy, dx)); 2580 bound = (Math.PI * this.attr.zoom.pinchsensitivity) / 90.0; 2581 } 2582 2583 if (!this.keepaspectratio && 2584 this.attr.zoom.pinchhorizontal && 2585 theta < bound) { 2586 this.attr.zoom.factorx = factor; 2587 this.attr.zoom.factory = 1.0; 2588 cx = 0; 2589 cy = 0; 2590 doZoom = true; 2591 } else if (!this.keepaspectratio && 2592 this.attr.zoom.pinchvertical && 2593 Math.abs(theta - Math.PI * 0.5) < bound 2594 ) { 2595 this.attr.zoom.factorx = 1.0; 2596 this.attr.zoom.factory = factor; 2597 cx = 0; 2598 cy = 0; 2599 doZoom = true; 2600 } else if (this.attr.zoom.pinch) { 2601 this.attr.zoom.factorx = factor; 2602 this.attr.zoom.factory = factor; 2603 cx = c.usrCoords[1]; 2604 cy = c.usrCoords[2]; 2605 doZoom = true; 2606 } 2607 2608 if (doZoom) { 2609 if (zoomCenter === 'board') { 2610 this.zoomIn(); 2611 } else { // including zoomCenter === 'auto' 2612 this.zoomIn(cx, cy); 2613 } 2614 2615 // Restore zoomFactors 2616 this.attr.zoom.factorx = zx; 2617 this.attr.zoom.factory = zy; 2618 } 2619 } 2620 2621 return false; 2622 }, 2623 2624 /** 2625 * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari, 2626 * on Android we emulate it. 2627 * @param {Event} evt 2628 * @returns {Boolean} 2629 */ 2630 gestureStartListener: function (evt) { 2631 var pos; 2632 2633 evt.preventDefault(); 2634 this.prevScale = 1.0; 2635 // Android pinch to zoom 2636 this.prevDist = Geometry.distance( 2637 [evt.touches[0].clientX, evt.touches[0].clientY], 2638 [evt.touches[1].clientX, evt.touches[1].clientY], 2639 2 2640 ); 2641 this.prevCoords = [ 2642 [evt.touches[0].clientX, evt.touches[0].clientY], 2643 [evt.touches[1].clientX, evt.touches[1].clientY] 2644 ]; 2645 this.isPreviousGesture = 'none'; 2646 2647 // If pinch-to-zoom is interpreted as panning 2648 // we have to prepare move origin 2649 pos = this.getMousePosition(evt, 0); 2650 this.initMoveOrigin(pos[0], pos[1]); 2651 2652 this.mode = this.BOARD_MODE_ZOOM; 2653 this._change3DView = false; 2654 return false; 2655 }, 2656 2657 /** 2658 * Test if the required key combination is pressed for wheel zoom, move origin and 2659 * selection 2660 * @private 2661 * @param {Object} evt Mouse or pen event 2662 * @param {String} action String containing the action: 'zoom', 'pan', 'selection'. 2663 * Corresponds to the attribute subobject. 2664 * @return {Boolean} true or false. 2665 */ 2666 _isRequiredKeyPressed: function (evt, action) { 2667 var obj = this.attr[action]; 2668 if (!obj.enabled) { 2669 return false; 2670 } 2671 2672 if ( 2673 ((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) && 2674 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey)) 2675 ) { 2676 return true; 2677 } 2678 2679 return false; 2680 }, 2681 2682 /* 2683 * Pointer events 2684 */ 2685 2686 /** 2687 * 2688 * Check if pointer event is already registered in {@link JXG.Board#_board_touches}. 2689 * 2690 * @param {Object} evt Event object 2691 * @return {Boolean} true if down event has already been sent. 2692 * @private 2693 */ 2694 _isPointerRegistered: function (evt) { 2695 var i, 2696 len = this._board_touches.length; 2697 2698 for (i = 0; i < len; i++) { 2699 if (this._board_touches[i].pointerId === evt.pointerId) { 2700 return true; 2701 } 2702 } 2703 return false; 2704 }, 2705 2706 /** 2707 * 2708 * Store the position of a pointer event. 2709 * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}. 2710 * Allows to follow the path of that finger on the screen. 2711 * Only two simultaneous touches are supported. 2712 * 2713 * @param {Object} evt Event object 2714 * @returns {JXG.Board} Reference to the board 2715 * @private 2716 */ 2717 _pointerStorePosition: function (evt) { 2718 var i, found; 2719 2720 for (i = 0, found = false; i < this._board_touches.length; i++) { 2721 if (this._board_touches[i].pointerId === evt.pointerId) { 2722 this._board_touches[i].clientX = evt.clientX; 2723 this._board_touches[i].clientY = evt.clientY; 2724 found = true; 2725 break; 2726 } 2727 } 2728 2729 // Restrict the number of simultaneous touches to 2 2730 if (!found && this._board_touches.length < 2) { 2731 this._board_touches.push({ 2732 pointerId: evt.pointerId, 2733 clientX: evt.clientX, 2734 clientY: evt.clientY 2735 }); 2736 } 2737 2738 return this; 2739 }, 2740 2741 /** 2742 * Deregisters a pointer event in {@link JXG.Board#_board_touches}. 2743 * It happens if a finger has been lifted from the screen. 2744 * 2745 * @param {Object} evt Event object 2746 * @returns {JXG.Board} Reference to the board 2747 * @private 2748 */ 2749 _pointerRemoveTouches: function (evt) { 2750 var i; 2751 for (i = 0; i < this._board_touches.length; i++) { 2752 if (this._board_touches[i].pointerId === evt.pointerId) { 2753 this._board_touches.splice(i, 1); 2754 break; 2755 } 2756 } 2757 2758 return this; 2759 }, 2760 2761 /** 2762 * Remove all registered fingers from {@link JXG.Board#_board_touches}. 2763 * This might be necessary if too many fingers have been registered. 2764 * @returns {JXG.Board} Reference to the board 2765 * @private 2766 */ 2767 _pointerClearTouches: function (pId) { 2768 // var i; 2769 // if (pId) { 2770 // for (i = 0; i < this._board_touches.length; i++) { 2771 // if (pId === this._board_touches[i].pointerId) { 2772 // this._board_touches.splice(i, i); 2773 // break; 2774 // } 2775 // } 2776 // } else { 2777 // } 2778 if (this._board_touches.length > 0) { 2779 this.dehighlightAll(); 2780 } 2781 this.updateQuality = this.BOARD_QUALITY_HIGH; 2782 this.mode = this.BOARD_MODE_NONE; 2783 this._board_touches = []; 2784 this.touches = []; 2785 }, 2786 2787 /** 2788 * Determine which input device is used for this action. 2789 * Possible devices are 'touch', 'pen' and 'mouse'. 2790 * This affects the precision and certain events. 2791 * In case of no browser, 'mouse' is used. 2792 * 2793 * @see JXG.Board#pointerDownListener 2794 * @see JXG.Board#pointerMoveListener 2795 * @see JXG.Board#initMoveObject 2796 * @see JXG.Board#moveObject 2797 * 2798 * @param {Event} evt The browsers event object. 2799 * @returns {String} 'mouse', 'pen', or 'touch' 2800 * @private 2801 */ 2802 _getPointerInputDevice: function (evt) { 2803 if (Env.isBrowser) { 2804 if ( 2805 evt.pointerType === 'touch' || // New 2806 (window.navigator.msMaxTouchPoints && // Old 2807 window.navigator.msMaxTouchPoints > 1) 2808 ) { 2809 return 'touch'; 2810 } 2811 if (evt.pointerType === 'mouse') { 2812 return 'mouse'; 2813 } 2814 if (evt.pointerType === 'pen') { 2815 return 'pen'; 2816 } 2817 } 2818 return 'mouse'; 2819 }, 2820 2821 /** 2822 * This method is called by the browser when a pointing device is pressed on the screen. 2823 * @param {Event} evt The browsers event object. 2824 * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter 2825 * @param {Boolean} [allowDefaultEventHandling=false] If true event is not canceled, i.e. prevent call of evt.preventDefault() 2826 * @returns {Boolean} false if the first finger event is sent twice, or not a browser, or in selection mode. Otherwise returns true. 2827 */ 2828 pointerDownListener: function (evt, object, allowDefaultEventHandling) { 2829 var i, j, k, pos, 2830 elements, sel, target_obj, 2831 type = 'mouse', // Used in case of no browser 2832 found, target, ta; 2833 2834 // Fix for Firefox browser: When using a second finger, the 2835 // touch event for the first finger is sent again. 2836 if (!object && this._isPointerRegistered(evt)) { 2837 return false; 2838 } 2839 2840 if (Type.evaluate(this.attr.movetarget) === null && 2841 Type.exists(evt.target) && Type.exists(evt.target.releasePointerCapture)) { 2842 evt.target.releasePointerCapture(evt.pointerId); 2843 } 2844 2845 if (!object && evt.isPrimary) { 2846 // First finger down. To be on the safe side this._board_touches is cleared. 2847 // this._pointerClearTouches(); 2848 } 2849 2850 if (!this.hasPointerUp) { 2851 if (window.navigator.msPointerEnabled) { 2852 // IE10- 2853 Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2854 } else { 2855 // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android 2856 Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this); 2857 Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this); 2858 } 2859 this.hasPointerUp = true; 2860 } 2861 2862 if (this.hasMouseHandlers) { 2863 this.removeMouseEventHandlers(); 2864 } 2865 2866 if (this.hasTouchHandlers) { 2867 this.removeTouchEventHandlers(); 2868 } 2869 2870 // Prevent accidental selection of text 2871 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2872 this.document.selection.empty(); 2873 } else if (window.getSelection) { 2874 sel = window.getSelection(); 2875 if (sel.removeAllRanges) { 2876 try { 2877 sel.removeAllRanges(); 2878 } catch (e) { } 2879 } 2880 } 2881 2882 // Mouse, touch or pen device 2883 this._inputDevice = this._getPointerInputDevice(evt); 2884 type = this._inputDevice; 2885 this.options.precision.hasPoint = this.options.precision[type]; 2886 2887 // Handling of multi touch with pointer events should be easier than with touch events. 2888 // Every pointer device has its own pointerId, e.g. the mouse 2889 // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will 2890 // keep this id until a pointerUp event is fired. What we have to do here is: 2891 // 1. collect all elements under the current pointer 2892 // 2. run through the touches control structure 2893 // a. look for the object collected in step 1. 2894 // b. if an object is found, check the number of pointers. If appropriate, add the pointer. 2895 pos = this.getMousePosition(evt); 2896 2897 // Handle selection rectangle 2898 this._testForSelection(evt); 2899 if (this.selectingMode) { 2900 this._startSelecting(pos); 2901 this.triggerEventHandlers( 2902 ['touchstartselecting', 'pointerstartselecting', 'startselecting'], 2903 [evt] 2904 ); 2905 return; // don't continue as a normal click 2906 } 2907 2908 if (this.attr.drag.enabled && object) { 2909 elements = [object]; 2910 this.mode = this.BOARD_MODE_DRAG; 2911 } else { 2912 elements = this.initMoveObject(pos[0], pos[1], evt, type); 2913 } 2914 2915 target_obj = { 2916 num: evt.pointerId, 2917 X: pos[0], 2918 Y: pos[1], 2919 Xprev: NaN, 2920 Yprev: NaN, 2921 Xstart: [], 2922 Ystart: [], 2923 Zstart: [] 2924 }; 2925 2926 // If no draggable object can be found, get out here immediately 2927 if (elements.length > 0) { 2928 // check touches structure 2929 target = elements[elements.length - 1]; 2930 found = false; 2931 2932 // Reminder: this.touches is the list of elements which 2933 // currently 'possess' a pointer (mouse, pen, finger) 2934 for (i = 0; i < this.touches.length; i++) { 2935 // An element receives a further touch, i.e. 2936 // the target is already in our touches array, add the pointer to the existing touch 2937 if (this.touches[i].obj === target) { 2938 j = i; 2939 k = this.touches[i].targets.push(target_obj) - 1; 2940 found = true; 2941 break; 2942 } 2943 } 2944 if (!found) { 2945 // A new element has been touched. 2946 k = 0; 2947 j = 2948 this.touches.push({ 2949 obj: target, 2950 targets: [target_obj] 2951 }) - 1; 2952 } 2953 2954 this.dehighlightAll(); 2955 target.highlight(true); 2956 2957 this.saveStartPos(target, this.touches[j].targets[k]); 2958 2959 // Prevent accidental text selection 2960 // this could get us new trouble: input fields, links and drop down boxes placed as text 2961 // on the board don't work anymore. 2962 if (evt && evt.preventDefault && !allowDefaultEventHandling) { 2963 // All browser supporting pointer events know preventDefault() 2964 evt.preventDefault(); 2965 } 2966 } 2967 2968 if (this.touches.length > 0 && !allowDefaultEventHandling) { 2969 evt.preventDefault(); 2970 evt.stopPropagation(); 2971 } 2972 2973 if (!Env.isBrowser) { 2974 return false; 2975 } 2976 if (this._getPointerInputDevice(evt) !== 'touch') { 2977 if (this.mode === this.BOARD_MODE_NONE) { 2978 this.mouseOriginMoveStart(evt); 2979 } 2980 } else { 2981 this._pointerStorePosition(evt); 2982 evt.touches = this._board_touches; 2983 2984 // Touch events on empty areas of the board are handled here, see also touchStartListener 2985 // 1. case: one finger. If allowed, this triggers pan with one finger 2986 if ( 2987 evt.touches.length === 1 && 2988 this.mode === this.BOARD_MODE_NONE && 2989 this.touchStartMoveOriginOneFinger(evt) 2990 ) { 2991 // Empty by purpose 2992 } else if ( 2993 evt.touches.length === 2 && 2994 (this.mode === this.BOARD_MODE_NONE || 2995 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 2996 ) { 2997 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2998 // This happens when the second finger hits the device. First, the 2999 // 'one finger pan mode' has to be cancelled. 3000 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 3001 this.originMoveEnd(); 3002 } 3003 3004 this.gestureStartListener(evt); 3005 } 3006 } 3007 3008 this.initSketchCurve(evt); 3009 3010 // Allow browser scrolling 3011 // For this: pan by one finger has to be disabled 3012 3013 ta = 'none'; // JSXGraph catches all user touch events 3014 if (this.mode === this.BOARD_MODE_NONE && 3015 (Type.evaluate(this.attr.browserpan) === true || Type.evaluate(this.attr.browserpan.enabled) === true) && 3016 // One-finger pan has priority over browserPan 3017 (Type.evaluate(this.attr.pan.enabled) === false || Type.evaluate(this.attr.pan.needtwofingers) === true) 3018 ) { 3019 // ta = 'pan-x pan-y'; // JSXGraph allows browser scrolling 3020 ta = 'auto'; // JSXGraph allows browser scrolling 3021 } 3022 this.containerObj.style.touchAction = ta; 3023 3024 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]); 3025 3026 return true; 3027 }, 3028 3029 /** 3030 * Internal handling of click events for pointers and mouse. 3031 * 3032 * @param {Event} evt The browsers event object. 3033 * @param {Array} evtArray list of event names 3034 * @private 3035 */ 3036 _handleClicks: function(evt, evtArray) { 3037 var that = this, 3038 el, delay, suppress; 3039 3040 if (this.selectingMode) { 3041 evt.stopPropagation(); 3042 return; 3043 } 3044 3045 delay = Type.evaluate(this.attr.clickdelay); 3046 suppress = Type.evaluate(this.attr.dblclicksuppressclick); 3047 3048 if (suppress) { 3049 // dblclick suppresses previous click events 3050 this._preventSingleClick = false; 3051 3052 // Wait if there is a dblclick event. 3053 // If not fire a click event 3054 this._singleClickTimer = setTimeout(function() { 3055 if (!that._preventSingleClick) { 3056 // Fire click event and remove element from click list 3057 that.triggerEventHandlers(evtArray, [evt]); 3058 for (el in that.clickObjects) { 3059 if (that.clickObjects.hasOwnProperty(el)) { 3060 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 3061 delete that.clickObjects[el]; 3062 } 3063 } 3064 } 3065 }, delay); 3066 } else { 3067 // dblclick is preceded by two click events 3068 3069 // Fire click events 3070 that.triggerEventHandlers(evtArray, [evt]); 3071 for (el in that.clickObjects) { 3072 if (that.clickObjects.hasOwnProperty(el)) { 3073 that.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 3074 } 3075 } 3076 3077 // Clear list of clicked elements with a delay 3078 setTimeout(function() { 3079 for (el in that.clickObjects) { 3080 if (that.clickObjects.hasOwnProperty(el)) { 3081 delete that.clickObjects[el]; 3082 } 3083 } 3084 }, delay); 3085 } 3086 evt.stopPropagation(); 3087 }, 3088 3089 /** 3090 * Internal handling of dblclick events for pointers and mouse. 3091 * 3092 * @param {Event} evt The browsers event object. 3093 * @param {Array} evtArray list of event names 3094 * @private 3095 */ 3096 _handleDblClicks: function(evt, evtArray) { 3097 var el; 3098 3099 if (this.selectingMode) { 3100 evt.stopPropagation(); 3101 return; 3102 } 3103 3104 // Notify that a dblclick has happened 3105 this._preventSingleClick = true; 3106 clearTimeout(this._singleClickTimer); 3107 3108 // Fire dblclick event 3109 this.triggerEventHandlers(evtArray, [evt]); 3110 for (el in this.clickObjects) { 3111 if (this.clickObjects.hasOwnProperty(el)) { 3112 this.clickObjects[el].triggerEventHandlers(evtArray, [evt]); 3113 delete this.clickObjects[el]; 3114 } 3115 } 3116 3117 evt.stopPropagation(); 3118 }, 3119 3120 /** 3121 * This method is called by the browser when a pointer device clicks on the screen. 3122 * @param {Event} evt The browsers event object. 3123 */ 3124 pointerClickListener: function (evt) { 3125 this._handleClicks(evt, ['click', 'pointerclick']); 3126 }, 3127 3128 /** 3129 * This method is called by the browser when a pointer device double clicks on the screen. 3130 * @param {Event} evt The browsers event object. 3131 */ 3132 pointerDblClickListener: function (evt) { 3133 this._handleDblClicks(evt, ['dblclick', 'pointerdblclick']); 3134 }, 3135 3136 /** 3137 * This method is called by the browser when the mouse device clicks on the screen. 3138 * @param {Event} evt The browsers event object. 3139 */ 3140 mouseClickListener: function (evt) { 3141 this._handleClicks(evt, ['click', 'mouseclick']); 3142 }, 3143 3144 /** 3145 * This method is called by the browser when the mouse device double clicks on the screen. 3146 * @param {Event} evt The browsers event object. 3147 */ 3148 mouseDblClickListener: function (evt) { 3149 this._handleDblClicks(evt, ['dblclick', 'mousedblclick']); 3150 }, 3151 3152 // /** 3153 // * Called if pointer leaves an HTML tag. It is called by the inner-most tag. 3154 // * That means, if a JSXGraph text, i.e. an HTML div, is placed close 3155 // * to the border of the board, this pointerout event will be ignored. 3156 // * @param {Event} evt 3157 // * @return {Boolean} 3158 // */ 3159 // pointerOutListener: function (evt) { 3160 // if (evt.target === this.containerObj || 3161 // (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) { 3162 // this.pointerUpListener(evt); 3163 // } 3164 // return this.mode === this.BOARD_MODE_NONE; 3165 // }, 3166 3167 /** 3168 * Called periodically by the browser while the user moves a pointing device across the screen. 3169 * @param {Event} evt 3170 * @returns {Boolean} 3171 */ 3172 pointerMoveListener: function (evt) { 3173 var i, j, pos, 3174 eps, 3175 touchTargets, 3176 type = 'mouse'; // in case of no browser 3177 3178 if ( 3179 this._getPointerInputDevice(evt) === 'touch' && 3180 !this._isPointerRegistered(evt) 3181 ) { 3182 // Test, if there was a previous down event of this _getPointerId 3183 // (in case it is a touch event). 3184 // Otherwise this move event is ignored. This is necessary e.g. for sketchometry. 3185 return this.BOARD_MODE_NONE; 3186 } 3187 3188 if (!this.checkFrameRate(evt)) { 3189 return false; 3190 } 3191 3192 if (this.mode !== this.BOARD_MODE_DRAG) { 3193 this.dehighlightAll(); 3194 this.displayInfobox(false); 3195 } 3196 3197 if (this.mode !== this.BOARD_MODE_NONE) { 3198 evt.preventDefault(); 3199 evt.stopPropagation(); 3200 } 3201 3202 this.updateQuality = this.BOARD_QUALITY_LOW; 3203 // Mouse, touch or pen device 3204 this._inputDevice = this._getPointerInputDevice(evt); 3205 type = this._inputDevice; 3206 this.options.precision.hasPoint = this.options.precision[type]; 3207 eps = this.options.precision.hasPoint * 0.3333; 3208 3209 pos = this.getMousePosition(evt); 3210 // Ignore pointer move event if too close at the border 3211 // and setPointerCapture is off 3212 if (Type.evaluate(this.attr.movetarget) === null && 3213 (pos[0] <= eps || pos[1] <= eps || 3214 pos[0] >= this.canvasWidth - eps || 3215 pos[1] >= this.canvasHeight - eps) 3216 ) { 3217 return this.mode === this.BOARD_MODE_NONE; 3218 } 3219 3220 // selection 3221 if (this.selectingMode) { 3222 this._moveSelecting(pos); 3223 this.triggerEventHandlers( 3224 ['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], 3225 [evt, this.mode] 3226 ); 3227 } else if (!this.mouseOriginMove(evt)) { 3228 3229 this.addToSketchCurve(evt); 3230 3231 if (this.mode === this.BOARD_MODE_DRAG) { 3232 // Run through all jsxgraph elements which are touched by at least one finger. 3233 for (i = 0; i < this.touches.length; i++) { 3234 touchTargets = this.touches[i].targets; 3235 // Run through all touch events which have been started on this jsxgraph element. 3236 for (j = 0; j < touchTargets.length; j++) { 3237 if (touchTargets[j].num === evt.pointerId) { 3238 touchTargets[j].X = pos[0]; 3239 touchTargets[j].Y = pos[1]; 3240 3241 if (touchTargets.length === 1) { 3242 // Touch by one finger: this is possible for all elements that can be dragged 3243 this.moveObject(pos[0], pos[1], this.touches[i], evt, type); 3244 } else if (touchTargets.length === 2) { 3245 // Touch by two fingers: e.g. moving lines 3246 this.twoFingerMove(this.touches[i], evt.pointerId, evt); 3247 3248 touchTargets[j].Xprev = pos[0]; 3249 touchTargets[j].Yprev = pos[1]; 3250 } 3251 3252 // There is only one pointer in the evt object, so there's no point in looking further 3253 break; 3254 } 3255 } 3256 } 3257 } else { 3258 if (this._getPointerInputDevice(evt) === 'touch') { 3259 this._pointerStorePosition(evt); 3260 3261 if (this._board_touches.length === 2) { 3262 evt.touches = this._board_touches; 3263 this.gestureChangeListener(evt); 3264 } 3265 } 3266 3267 // Move event without dragging an element 3268 this.highlightElements(pos[0], pos[1], evt, -1); 3269 } 3270 } 3271 3272 // Hiding the infobox is commented out, since it prevents showing the infobox 3273 // on IE 11+ on 'over' 3274 //if (this.mode !== this.BOARD_MODE_DRAG) { 3275 //this.displayInfobox(false); 3276 //} 3277 this.triggerEventHandlers(['pointermove', 'MSPointerMove', 'move'], [evt, this.mode]); 3278 this.updateQuality = this.BOARD_QUALITY_HIGH; 3279 3280 return this.mode === this.BOARD_MODE_NONE; 3281 }, 3282 3283 /** 3284 * Triggered as soon as the user stops touching the device with at least one finger. 3285 * 3286 * @param {Event} evt 3287 * @returns {Boolean} 3288 */ 3289 pointerUpListener: function (evt) { 3290 var i, j, found, eh, 3291 touchTargets, 3292 updateNeeded = false; 3293 3294 this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 3295 this.displayInfobox(false); 3296 3297 if (evt) { 3298 for (i = 0; i < this.touches.length; i++) { 3299 touchTargets = this.touches[i].targets; 3300 for (j = 0; j < touchTargets.length; j++) { 3301 if (touchTargets[j].num === evt.pointerId) { 3302 touchTargets.splice(j, 1); 3303 if (touchTargets.length === 0) { 3304 this.touches.splice(i, 1); 3305 } 3306 break; 3307 } 3308 } 3309 } 3310 } 3311 3312 this.finalizeSketchCurve(evt); 3313 this.originMoveEnd(); 3314 this.update(); 3315 3316 // selection 3317 if (this.selectingMode) { 3318 this._stopSelecting(evt); 3319 this.triggerEventHandlers( 3320 ['touchstopselecting', 'pointerstopselecting', 'stopselecting'], 3321 [evt] 3322 ); 3323 this.stopSelectionMode(); 3324 } else { 3325 for (i = this.downObjects.length - 1; i > -1; i--) { 3326 found = false; 3327 for (j = 0; j < this.touches.length; j++) { 3328 if (this.touches[j].obj.id === this.downObjects[i].id) { 3329 found = true; 3330 } 3331 } 3332 if (!found) { 3333 this.downObjects[i].triggerEventHandlers( 3334 ['touchend', 'up', 'pointerup', 'MSPointerUp'], 3335 [evt] 3336 ); 3337 if (!Type.exists(this.downObjects[i].coords)) { 3338 // snapTo methods have to be called e.g. for line elements here. 3339 // For coordsElements there might be a conflict with 3340 // attractors, see commit from 2022.04.08, 11:12:18. 3341 this.downObjects[i].snapToGrid(); 3342 this.downObjects[i].snapToPoints(); 3343 updateNeeded = true; 3344 } 3345 3346 // Check if we have to keep the element for a click or dblclick event 3347 // Otherwise remove it from downObjects 3348 eh = this.downObjects[i].eventHandlers; 3349 if ((Type.exists(eh.click) && eh.click.length > 0) || 3350 (Type.exists(eh.pointerclick) && eh.pointerclick.length > 0) || 3351 (Type.exists(eh.dblclick) && eh.dblclick.length > 0) || 3352 (Type.exists(eh.pointerdblclick) && eh.pointerdblclick.length > 0) 3353 ) { 3354 this.clickObjects[this.downObjects[i].id] = this.downObjects[i]; 3355 } 3356 this.downObjects.splice(i, 1); 3357 } 3358 } 3359 } 3360 3361 if (this.hasPointerUp) { 3362 if (window.navigator.msPointerEnabled) { 3363 // IE10- 3364 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 3365 } else { 3366 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 3367 Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this); 3368 } 3369 this.hasPointerUp = false; 3370 } 3371 3372 // After one finger leaves the screen the gesture is stopped. 3373 this._pointerClearTouches(evt.pointerId); 3374 if (this._getPointerInputDevice(evt) !== 'touch') { 3375 this.dehighlightAll(); 3376 } 3377 3378 if (updateNeeded) { 3379 this.update(); 3380 } 3381 3382 return true; 3383 }, 3384 3385 /** 3386 * Triggered by the pointerleave event. This is needed in addition to 3387 * {@link JXG.Board#pointerUpListener} in the situation that a pen is used 3388 * and after an up event the pen leaves the hover range vertically. Here, it happens that 3389 * after the pointerup event further pointermove events are fired and elements get highlighted. 3390 * This highlighting has to be cancelled. 3391 * 3392 * @param {Event} evt 3393 * @returns {Boolean} 3394 */ 3395 pointerLeaveListener: function (evt) { 3396 this.displayInfobox(false); 3397 this.dehighlightAll(); 3398 3399 return true; 3400 }, 3401 3402 /** 3403 * Touch-Events 3404 */ 3405 3406 /** 3407 * This method is called by the browser when a finger touches the surface of the touch-device. 3408 * @param {Event} evt The browsers event object. 3409 * @returns {Boolean} ... 3410 */ 3411 touchStartListener: function (evt) { 3412 var i, j, k, 3413 pos, elements, obj, 3414 eps = this.options.precision.touch, 3415 evtTouches = evt['touches'], 3416 found, 3417 targets, target, 3418 touchTargets; 3419 3420 if (!this.hasTouchEnd) { 3421 Env.addEvent(this.document, 'touchend', this.touchEndListener, this); 3422 this.hasTouchEnd = true; 3423 } 3424 3425 // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen. 3426 //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); } 3427 3428 // prevent accidental selection of text 3429 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3430 this.document.selection.empty(); 3431 } else if (window.getSelection) { 3432 window.getSelection().removeAllRanges(); 3433 } 3434 3435 // multitouch 3436 this._inputDevice = 'touch'; 3437 this.options.precision.hasPoint = this.options.precision.touch; 3438 3439 // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our 3440 // 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 3441 // 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 3442 // 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 3443 // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations: 3444 // * points have higher priority over other elements. 3445 // * 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 3446 // this element and add them. 3447 // ADDENDUM 11/10/11: 3448 // (1) run through the touches control object, 3449 // (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch 3450 // for every target in our touches objects 3451 // (3) if one of the targettouches was bound to a touches targets array, mark it 3452 // (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch: 3453 // (a) if no element could be found: mark the target touches and continue 3454 // --- in the following cases, 'init' means: 3455 // (i) check if the element is already used in another touches element, if so, mark the targettouch and continue 3456 // (ii) if not, init a new touches element, add the targettouch to the touches property and mark it 3457 // (b) if the element is a point, init 3458 // (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 3459 // (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 3460 // add both to the touches array and mark them. 3461 for (i = 0; i < evtTouches.length; i++) { 3462 evtTouches[i].jxg_isused = false; 3463 } 3464 3465 for (i = 0; i < this.touches.length; i++) { 3466 touchTargets = this.touches[i].targets; 3467 for (j = 0; j < touchTargets.length; j++) { 3468 touchTargets[j].num = -1; 3469 eps = this.options.precision.touch; 3470 3471 do { 3472 for (k = 0; k < evtTouches.length; k++) { 3473 // find the new targettouches 3474 if ( 3475 Math.abs( 3476 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3477 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3478 ) < 3479 eps * eps 3480 ) { 3481 touchTargets[j].num = k; 3482 touchTargets[j].X = evtTouches[k].screenX; 3483 touchTargets[j].Y = evtTouches[k].screenY; 3484 evtTouches[k].jxg_isused = true; 3485 break; 3486 } 3487 } 3488 3489 eps *= 2; 3490 } while ( 3491 touchTargets[j].num === -1 && 3492 eps < this.options.precision.touchMax 3493 ); 3494 3495 if (touchTargets[j].num === -1) { 3496 JXG.debug( 3497 "i couldn't find a targettouches for target no " + 3498 j + 3499 ' on ' + 3500 this.touches[i].obj.name + 3501 ' (' + 3502 this.touches[i].obj.id + 3503 '). Removed the target.' 3504 ); 3505 JXG.debug( 3506 'eps = ' + eps + ', touchMax = ' + Options.precision.touchMax 3507 ); 3508 touchTargets.splice(i, 1); 3509 } 3510 } 3511 } 3512 3513 // we just re-mapped the targettouches to our existing touches list. 3514 // now we have to initialize some touches from additional targettouches 3515 for (i = 0; i < evtTouches.length; i++) { 3516 if (!evtTouches[i].jxg_isused) { 3517 pos = this.getMousePosition(evt, i); 3518 // selection 3519 // this._testForSelection(evt); // we do not have shift or ctrl keys yet. 3520 if (this.selectingMode) { 3521 this._startSelecting(pos); 3522 this.triggerEventHandlers( 3523 ['touchstartselecting', 'startselecting'], 3524 [evt] 3525 ); 3526 evt.preventDefault(); 3527 evt.stopPropagation(); 3528 this.options.precision.hasPoint = this.options.precision.mouse; 3529 return this.touches.length > 0; // don't continue as a normal click 3530 } 3531 3532 elements = this.initMoveObject(pos[0], pos[1], evt, 'touch'); 3533 if (elements.length !== 0) { 3534 obj = elements[elements.length - 1]; 3535 target = { 3536 num: i, 3537 X: evtTouches[i].screenX, 3538 Y: evtTouches[i].screenY, 3539 Xprev: NaN, 3540 Yprev: NaN, 3541 Xstart: [], 3542 Ystart: [], 3543 Zstart: [] 3544 }; 3545 3546 if ( 3547 Type.isPoint(obj) || 3548 obj.elementClass === Const.OBJECT_CLASS_TEXT || 3549 obj.type === Const.OBJECT_TYPE_TICKS || 3550 obj.type === Const.OBJECT_TYPE_IMAGE 3551 ) { 3552 // It's a point, so it's single touch, so we just push it to our touches 3553 targets = [target]; 3554 3555 // For the UNDO/REDO of object moves 3556 this.saveStartPos(obj, targets[0]); 3557 3558 this.touches.push({ obj: obj, targets: targets }); 3559 obj.highlight(true); 3560 } else if ( 3561 obj.elementClass === Const.OBJECT_CLASS_LINE || 3562 obj.elementClass === Const.OBJECT_CLASS_CIRCLE || 3563 obj.elementClass === Const.OBJECT_CLASS_CURVE || 3564 obj.type === Const.OBJECT_TYPE_POLYGON 3565 ) { 3566 found = false; 3567 3568 // first check if this geometric object is already captured in this.touches 3569 for (j = 0; j < this.touches.length; j++) { 3570 if (obj.id === this.touches[j].obj.id) { 3571 found = true; 3572 // only add it, if we don't have two targets in there already 3573 if (this.touches[j].targets.length === 1) { 3574 // For the UNDO/REDO of object moves 3575 this.saveStartPos(obj, target); 3576 this.touches[j].targets.push(target); 3577 } 3578 3579 evtTouches[i].jxg_isused = true; 3580 } 3581 } 3582 3583 // we couldn't find it in touches, so we just init a new touches 3584 // IF there is a second touch targetting this line, we will find it later on, and then add it to 3585 // the touches control object. 3586 if (!found) { 3587 targets = [target]; 3588 3589 // For the UNDO/REDO of object moves 3590 this.saveStartPos(obj, targets[0]); 3591 this.touches.push({ obj: obj, targets: targets }); 3592 obj.highlight(true); 3593 } 3594 } 3595 } 3596 3597 evtTouches[i].jxg_isused = true; 3598 } 3599 } 3600 3601 if (this.touches.length > 0) { 3602 evt.preventDefault(); 3603 evt.stopPropagation(); 3604 } 3605 3606 // Touch events on empty areas of the board are handled here: 3607 // 1. case: one finger. If allowed, this triggers pan with one finger 3608 if ( 3609 evtTouches.length === 1 && 3610 this.mode === this.BOARD_MODE_NONE && 3611 this.touchStartMoveOriginOneFinger(evt) 3612 ) { 3613 } else if ( 3614 evtTouches.length === 2 && 3615 (this.mode === this.BOARD_MODE_NONE || 3616 this.mode === this.BOARD_MODE_MOVE_ORIGIN) 3617 ) { 3618 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 3619 // This happens when the second finger hits the device. First, the 3620 // 'one finger pan mode' has to be cancelled. 3621 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 3622 this.originMoveEnd(); 3623 } 3624 this.gestureStartListener(evt); 3625 } 3626 3627 this.initSketchCurve(evt); 3628 3629 this.options.precision.hasPoint = this.options.precision.mouse; 3630 this.triggerEventHandlers(['touchstart', 'down'], [evt]); 3631 3632 return false; 3633 //return this.touches.length > 0; 3634 }, 3635 3636 /** 3637 * Called periodically by the browser while the user moves his fingers across the device. 3638 * @param {Event} evt 3639 * @returns {Boolean} 3640 */ 3641 touchMoveListener: function (evt) { 3642 var i, 3643 pos1, pos2, 3644 touchTargets, 3645 evtTouches = evt['touches']; 3646 3647 if (!this.checkFrameRate(evt)) { 3648 return false; 3649 } 3650 3651 if (this.mode !== this.BOARD_MODE_NONE) { 3652 evt.preventDefault(); 3653 evt.stopPropagation(); 3654 } 3655 3656 if (this.mode !== this.BOARD_MODE_DRAG) { 3657 this.dehighlightAll(); 3658 this.displayInfobox(false); 3659 } 3660 3661 this._inputDevice = 'touch'; 3662 this.options.precision.hasPoint = this.options.precision.touch; 3663 this.updateQuality = this.BOARD_QUALITY_LOW; 3664 3665 // selection 3666 if (this.selectingMode) { 3667 for (i = 0; i < evtTouches.length; i++) { 3668 if (!evtTouches[i].jxg_isused) { 3669 pos1 = this.getMousePosition(evt, i); 3670 this._moveSelecting(pos1); 3671 this.triggerEventHandlers( 3672 ['touchmoves', 'moveselecting'], 3673 [evt, this.mode] 3674 ); 3675 break; 3676 } 3677 } 3678 } else { 3679 if (!this.touchOriginMove(evt)) { 3680 3681 this.addToSketchCurve(evt); 3682 3683 if (this.mode === this.BOARD_MODE_DRAG) { 3684 // Runs over through all elements which are touched 3685 // by at least one finger. 3686 for (i = 0; i < this.touches.length; i++) { 3687 touchTargets = this.touches[i].targets; 3688 if (touchTargets.length === 1) { 3689 // Touch by one finger: this is possible for all elements that can be dragged 3690 if (evtTouches[touchTargets[0].num]) { 3691 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3692 if ( 3693 pos1[0] < 0 || 3694 pos1[0] > this.canvasWidth || 3695 pos1[1] < 0 || 3696 pos1[1] > this.canvasHeight 3697 ) { 3698 return; 3699 } 3700 touchTargets[0].X = pos1[0]; 3701 touchTargets[0].Y = pos1[1]; 3702 this.moveObject( 3703 pos1[0], 3704 pos1[1], 3705 this.touches[i], 3706 evt, 3707 'touch' 3708 ); 3709 } 3710 } else if ( 3711 touchTargets.length === 2 && 3712 touchTargets[0].num > -1 && 3713 touchTargets[1].num > -1 3714 ) { 3715 // Touch by two fingers: moving lines, ... 3716 if ( 3717 evtTouches[touchTargets[0].num] && 3718 evtTouches[touchTargets[1].num] 3719 ) { 3720 // Get coordinates of the two touches 3721 pos1 = this.getMousePosition(evt, touchTargets[0].num); 3722 pos2 = this.getMousePosition(evt, touchTargets[1].num); 3723 if ( 3724 pos1[0] < 0 || 3725 pos1[0] > this.canvasWidth || 3726 pos1[1] < 0 || 3727 pos1[1] > this.canvasHeight || 3728 pos2[0] < 0 || 3729 pos2[0] > this.canvasWidth || 3730 pos2[1] < 0 || 3731 pos2[1] > this.canvasHeight 3732 ) { 3733 return; 3734 } 3735 3736 touchTargets[0].X = pos1[0]; 3737 touchTargets[0].Y = pos1[1]; 3738 touchTargets[1].X = pos2[0]; 3739 touchTargets[1].Y = pos2[1]; 3740 3741 this.twoFingerMove( 3742 this.touches[i], 3743 touchTargets[0].num, 3744 evt 3745 ); 3746 3747 touchTargets[0].Xprev = pos1[0]; 3748 touchTargets[0].Yprev = pos1[1]; 3749 touchTargets[1].Xprev = pos2[0]; 3750 touchTargets[1].Yprev = pos2[1]; 3751 } 3752 } 3753 } 3754 } else { 3755 if (evtTouches.length === 2) { 3756 this.gestureChangeListener(evt); 3757 } 3758 // Move event without dragging an element 3759 pos1 = this.getMousePosition(evt, 0); 3760 this.highlightElements(pos1[0], pos1[1], evt, -1); 3761 } 3762 } 3763 } 3764 3765 if (this.mode !== this.BOARD_MODE_DRAG) { 3766 this.displayInfobox(false); 3767 } 3768 3769 this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 3770 this.options.precision.hasPoint = this.options.precision.mouse; 3771 this.updateQuality = this.BOARD_QUALITY_HIGH; 3772 3773 return this.mode === this.BOARD_MODE_NONE; 3774 }, 3775 3776 /** 3777 * Triggered as soon as the user stops touching the device with at least one finger. 3778 * @param {Event} evt 3779 * @returns {Boolean} 3780 */ 3781 touchEndListener: function (evt) { 3782 var i, 3783 j, 3784 k, 3785 eps = this.options.precision.touch, 3786 tmpTouches = [], 3787 found, 3788 foundNumber, 3789 evtTouches = evt && evt['touches'], 3790 touchTargets, 3791 updateNeeded = false; 3792 3793 this.triggerEventHandlers(['touchend', 'up'], [evt]); 3794 this.displayInfobox(false); 3795 3796 this.finalizeSketchCurve(evt); 3797 3798 // selection 3799 if (this.selectingMode) { 3800 this._stopSelecting(evt); 3801 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]); 3802 this.stopSelectionMode(); 3803 } else if (evtTouches && evtTouches.length > 0) { 3804 for (i = 0; i < this.touches.length; i++) { 3805 tmpTouches[i] = this.touches[i]; 3806 } 3807 this.touches.length = 0; 3808 3809 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted, 3810 // convert the operation to a simple one-finger-translation. 3811 // ADDENDUM 11/10/11: 3812 // see addendum to touchStartListener from 11/10/11 3813 // (1) run through the tmptouches 3814 // (2) check the touches.obj, if it is a 3815 // (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch. 3816 // (b) line with 3817 // (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch. 3818 // (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches 3819 // (c) circle with [proceed like in line] 3820 3821 // init the targettouches marker 3822 for (i = 0; i < evtTouches.length; i++) { 3823 evtTouches[i].jxg_isused = false; 3824 } 3825 3826 for (i = 0; i < tmpTouches.length; i++) { 3827 // could all targets of the current this.touches.obj be assigned to targettouches? 3828 found = false; 3829 foundNumber = 0; 3830 touchTargets = tmpTouches[i].targets; 3831 3832 for (j = 0; j < touchTargets.length; j++) { 3833 touchTargets[j].found = false; 3834 for (k = 0; k < evtTouches.length; k++) { 3835 if ( 3836 Math.abs( 3837 Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + 3838 Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2) 3839 ) < 3840 eps * eps 3841 ) { 3842 touchTargets[j].found = true; 3843 touchTargets[j].num = k; 3844 touchTargets[j].X = evtTouches[k].screenX; 3845 touchTargets[j].Y = evtTouches[k].screenY; 3846 foundNumber += 1; 3847 break; 3848 } 3849 } 3850 } 3851 3852 if (Type.isPoint(tmpTouches[i].obj)) { 3853 found = touchTargets[0] && touchTargets[0].found; 3854 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) { 3855 found = 3856 (touchTargets[0] && touchTargets[0].found) || 3857 (touchTargets[1] && touchTargets[1].found); 3858 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 3859 found = foundNumber === 1 || foundNumber === 3; 3860 } 3861 3862 // if we found this object to be still dragged by the user, add it back to this.touches 3863 if (found) { 3864 this.touches.push({ 3865 obj: tmpTouches[i].obj, 3866 targets: [] 3867 }); 3868 3869 for (j = 0; j < touchTargets.length; j++) { 3870 if (touchTargets[j].found) { 3871 this.touches[this.touches.length - 1].targets.push({ 3872 num: touchTargets[j].num, 3873 X: touchTargets[j].screenX, 3874 Y: touchTargets[j].screenY, 3875 Xprev: NaN, 3876 Yprev: NaN, 3877 Xstart: touchTargets[j].Xstart, 3878 Ystart: touchTargets[j].Ystart, 3879 Zstart: touchTargets[j].Zstart 3880 }); 3881 } 3882 } 3883 } else { 3884 tmpTouches[i].obj.noHighlight(); 3885 } 3886 } 3887 } else { 3888 this.touches.length = 0; 3889 } 3890 3891 for (i = this.downObjects.length - 1; i > -1; i--) { 3892 found = false; 3893 for (j = 0; j < this.touches.length; j++) { 3894 if (this.touches[j].obj.id === this.downObjects[i].id) { 3895 found = true; 3896 } 3897 } 3898 if (!found) { 3899 this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]); 3900 if (!Type.exists(this.downObjects[i].coords)) { 3901 // snapTo methods have to be called e.g. for line elements here. 3902 // For coordsElements there might be a conflict with 3903 // attractors, see commit from 2022.04.08, 11:12:18. 3904 this.downObjects[i].snapToGrid(); 3905 this.downObjects[i].snapToPoints(); 3906 updateNeeded = true; 3907 } 3908 this.downObjects.splice(i, 1); 3909 } 3910 } 3911 3912 if (!evtTouches || evtTouches.length === 0) { 3913 if (this.hasTouchEnd) { 3914 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 3915 this.hasTouchEnd = false; 3916 } 3917 3918 this.dehighlightAll(); 3919 this.updateQuality = this.BOARD_QUALITY_HIGH; 3920 3921 this.originMoveEnd(); 3922 if (updateNeeded) { 3923 this.update(); 3924 } 3925 } 3926 3927 return true; 3928 }, 3929 3930 /** 3931 * This method is called by the browser when the mouse button is clicked. 3932 * @param {Event} evt The browsers event object. 3933 * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise. 3934 */ 3935 mouseDownListener: function (evt) { 3936 var pos, elements, result; 3937 3938 // prevent accidental selection of text 3939 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 3940 this.document.selection.empty(); 3941 } else if (window.getSelection) { 3942 window.getSelection().removeAllRanges(); 3943 } 3944 3945 if (!this.hasMouseUp) { 3946 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this); 3947 this.hasMouseUp = true; 3948 } else { 3949 // In case this.hasMouseUp==true, it may be that there was a 3950 // mousedown event before which was not followed by an mouseup event. 3951 // This seems to happen with interactive whiteboard pens sometimes. 3952 return; 3953 } 3954 3955 this._inputDevice = 'mouse'; 3956 this.options.precision.hasPoint = this.options.precision.mouse; 3957 pos = this.getMousePosition(evt); 3958 3959 // selection 3960 this._testForSelection(evt); 3961 if (this.selectingMode) { 3962 this._startSelecting(pos); 3963 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]); 3964 return; // don't continue as a normal click 3965 } 3966 3967 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 3968 3969 // if no draggable object can be found, get out here immediately 3970 if (elements.length === 0) { 3971 this.mode = this.BOARD_MODE_NONE; 3972 result = true; 3973 } else { 3974 this.mouse = { 3975 obj: null, 3976 targets: [ 3977 { 3978 X: pos[0], 3979 Y: pos[1], 3980 Xprev: NaN, 3981 Yprev: NaN 3982 } 3983 ] 3984 }; 3985 this.mouse.obj = elements[elements.length - 1]; 3986 3987 this.dehighlightAll(); 3988 this.mouse.obj.highlight(true); 3989 3990 this.mouse.targets[0].Xstart = []; 3991 this.mouse.targets[0].Ystart = []; 3992 this.mouse.targets[0].Zstart = []; 3993 3994 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]); 3995 3996 // prevent accidental text selection 3997 // this could get us new trouble: input fields, links and drop down boxes placed as text 3998 // on the board don't work anymore. 3999 if (evt && evt.preventDefault) { 4000 evt.preventDefault(); 4001 } else if (window.event) { 4002 window.event.returnValue = false; 4003 } 4004 } 4005 4006 if (this.mode === this.BOARD_MODE_NONE) { 4007 result = this.mouseOriginMoveStart(evt); 4008 } 4009 4010 this.initSketchCurve(evt); 4011 this.triggerEventHandlers(['mousedown', 'down'], [evt]); 4012 4013 return result; 4014 }, 4015 4016 /** 4017 * This method is called by the browser when the mouse is moved. 4018 * @param {Event} evt The browsers event object. 4019 */ 4020 mouseMoveListener: function (evt) { 4021 var pos; 4022 4023 if (!this.checkFrameRate(evt)) { 4024 return false; 4025 } 4026 4027 pos = this.getMousePosition(evt); 4028 4029 this.updateQuality = this.BOARD_QUALITY_LOW; 4030 4031 if (this.mode !== this.BOARD_MODE_DRAG) { 4032 this.dehighlightAll(); 4033 this.displayInfobox(false); 4034 } 4035 4036 // we have to check for four cases: 4037 // * user moves origin 4038 // * user drags an object 4039 // * user just moves the mouse, here highlight all elements at 4040 // the current mouse position 4041 // * the user is selecting 4042 4043 // selection 4044 if (this.selectingMode) { 4045 this._moveSelecting(pos); 4046 this.triggerEventHandlers( 4047 ['mousemoveselecting', 'moveselecting'], 4048 [evt, this.mode] 4049 ); 4050 } else if (!this.mouseOriginMove(evt)) { 4051 4052 this.addToSketchCurve(evt); 4053 4054 if (this.mode === this.BOARD_MODE_DRAG) { 4055 this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse'); 4056 } else { 4057 // BOARD_MODE_NONE 4058 // Move event without dragging an element 4059 this.highlightElements(pos[0], pos[1], evt, -1); 4060 } 4061 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]); 4062 } 4063 this.updateQuality = this.BOARD_QUALITY_HIGH; 4064 }, 4065 4066 /** 4067 * This method is called by the browser when the mouse button is released. 4068 * @param {Event} evt 4069 */ 4070 mouseUpListener: function (evt) { 4071 var i; 4072 4073 if (this.selectingMode === false) { 4074 this.triggerEventHandlers(['mouseup', 'up'], [evt]); 4075 } 4076 4077 // redraw with high precision 4078 this.updateQuality = this.BOARD_QUALITY_HIGH; 4079 4080 if (this.mouse && this.mouse.obj) { 4081 if (!Type.exists(this.mouse.obj.coords)) { 4082 // snapTo methods have to be called e.g. for line elements here. 4083 // For coordsElements there might be a conflict with 4084 // attractors, see commit from 2022.04.08, 11:12:18. 4085 // The parameter is needed for lines with snapToGrid enabled 4086 this.mouse.obj.snapToGrid(this.mouse.targets[0]); 4087 this.mouse.obj.snapToPoints(); 4088 } 4089 } 4090 4091 this.finalizeSketchCurve(evt); 4092 this.originMoveEnd(); 4093 this.dehighlightAll(); 4094 this.update(); 4095 4096 // selection 4097 if (this.selectingMode) { 4098 this._stopSelecting(evt); 4099 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]); 4100 this.stopSelectionMode(); 4101 } else { 4102 for (i = 0; i < this.downObjects.length; i++) { 4103 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]); 4104 } 4105 } 4106 4107 this.downObjects.length = 0; 4108 4109 if (this.hasMouseUp) { 4110 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 4111 this.hasMouseUp = false; 4112 } 4113 4114 // release dragged mouse object 4115 this.mouse = null; 4116 }, 4117 4118 /** 4119 * Handler for mouse wheel events. Used to zoom in and out of the board. 4120 * @param {Event} evt 4121 * @returns {Boolean} 4122 */ 4123 mouseWheelListener: function (evt) { 4124 var wd, zoomCenter, pos; 4125 4126 if (!this.attr.zoom.enabled || 4127 !this.attr.zoom.wheel || 4128 !this._isRequiredKeyPressed(evt, 'zoom')) { 4129 4130 return true; 4131 } 4132 4133 evt = evt || window.event; 4134 wd = evt.detail ? -evt.detail : evt.wheelDelta / 40; 4135 zoomCenter = this.attr.zoom.center; 4136 4137 if (zoomCenter === 'board') { 4138 pos = []; 4139 } else { // including zoomCenter === 'auto' 4140 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this).usrCoords; 4141 } 4142 4143 // pos == [] does not throw an error 4144 if (wd > 0) { 4145 this.zoomIn(pos[1], pos[2]); 4146 } else { 4147 this.zoomOut(pos[1], pos[2]); 4148 } 4149 4150 this.triggerEventHandlers(['mousewheel'], [evt]); 4151 4152 evt.preventDefault(); 4153 return false; 4154 }, 4155 4156 /** 4157 * Allow moving of JSXGraph elements with arrow keys. 4158 * The selection of the element is done with the tab key. For this, 4159 * the attribute 'tabindex' of the element has to be set to some number (default=0). 4160 * tabindex corresponds to the HTML and SVG attribute of the same name. 4161 * <p> 4162 * Panning of the construction is done with arrow keys 4163 * if the pan key (shift or ctrl - depending on the board attributes) is pressed. 4164 * <p> 4165 * Zooming is triggered with the keys +, o, -, if 4166 * the pan key (shift or ctrl - depending on the board attributes) is pressed. 4167 * <p> 4168 * Keyboard control (move, pan, and zoom) is disabled if an HTML element of type input or textarea has received focus. 4169 * 4170 * @param {Event} evt The browser's event object 4171 * 4172 * @see JXG.Board#keyboard 4173 * @see JXG.Board#keyFocusInListener 4174 * @see JXG.Board#keyFocusOutListener 4175 * 4176 */ 4177 keyDownListener: function (evt) { 4178 var id_node = evt.target.id, 4179 id, el, res, doc, 4180 sX = 0, 4181 sY = 0, 4182 // dx, dy are provided in screen units and 4183 // are converted to user coordinates 4184 dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX, 4185 dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY, 4186 // u = 100, 4187 doZoom = false, 4188 done = true, 4189 dir, 4190 actPos; 4191 4192 if (!this.attr.keyboard.enabled || id_node === '') { 4193 return false; 4194 } 4195 4196 // Tab key should be handled by the browser 4197 if (evt.keyCode === 9) { 4198 return false; 4199 } 4200 4201 // dx = Math.round(dx * u) / u; 4202 // dy = Math.round(dy * u) / u; 4203 4204 // An element of type input or textarea has focus, get out of here. 4205 doc = this.containerObj.shadowRoot || document; 4206 if (doc.activeElement) { 4207 el = doc.activeElement; 4208 if (el.tagName === 'INPUT' || el.tagName === 'textarea') { 4209 return false; 4210 } 4211 } 4212 4213 // Get the JSXGraph id from the id of the SVG node. 4214 id = id_node.replace(this.containerObj.id + '_', ''); 4215 el = this.select(id); 4216 4217 if (Type.exists(el.coords)) { 4218 actPos = el.coords.usrCoords.slice(1); 4219 } 4220 4221 if ( 4222 (Type.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) || 4223 (Type.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey) 4224 ) { 4225 // Pan key has been pressed 4226 4227 if (Type.evaluate(this.attr.zoom.enabled) === true) { 4228 doZoom = true; 4229 } 4230 4231 // Arrow keys 4232 if (evt.keyCode === 38) { 4233 // up 4234 this.clickUpArrow(); 4235 } else if (evt.keyCode === 40) { 4236 // down 4237 this.clickDownArrow(); 4238 } else if (evt.keyCode === 37) { 4239 // left 4240 this.clickLeftArrow(); 4241 } else if (evt.keyCode === 39) { 4242 // right 4243 this.clickRightArrow(); 4244 4245 // Zoom keys 4246 } else if (doZoom && evt.keyCode === 171) { 4247 // + 4248 this.zoomIn(); 4249 } else if (doZoom && evt.keyCode === 173) { 4250 // - 4251 this.zoomOut(); 4252 } else if (doZoom && evt.keyCode === 79) { 4253 // o 4254 this.zoom100(); 4255 } else { 4256 done = false; 4257 } 4258 } else if (!evt.shiftKey && !evt.ctrlKey) { // Move an element if neither shift or ctrl are pressed 4259 // Adapt dx, dy to snapToGrid and attractToGrid. 4260 // snapToGrid has priority. 4261 if (Type.exists(el.visProp)) { 4262 if ( 4263 Type.exists(el.visProp.snaptogrid) && 4264 el.visProp.snaptogrid && 4265 el.evalVisProp('snapsizex') && 4266 el.evalVisProp('snapsizey') 4267 ) { 4268 // Adapt dx, dy such that snapToGrid is possible 4269 res = el.getSnapSizes(); 4270 sX = res[0]; 4271 sY = res[1]; 4272 // If snaptogrid is true, 4273 // we can only jump from grid point to grid point. 4274 dx = sX; 4275 dy = sY; 4276 } else if ( 4277 Type.exists(el.visProp.attracttogrid) && 4278 el.visProp.attracttogrid && 4279 el.evalVisProp('attractordistance') && 4280 el.evalVisProp('attractorunit') 4281 ) { 4282 // Adapt dx, dy such that attractToGrid is possible 4283 sX = 1.1 * el.evalVisProp('attractordistance'); 4284 sY = sX; 4285 4286 if (el.evalVisProp('attractorunit') === 'screen') { 4287 sX /= this.unitX; 4288 sY /= this.unitX; 4289 } 4290 dx = Math.max(sX, dx); 4291 dy = Math.max(sY, dy); 4292 } 4293 } 4294 4295 if (evt.keyCode === 38) { 4296 // up 4297 dir = [0, dy]; 4298 } else if (evt.keyCode === 40) { 4299 // down 4300 dir = [0, -dy]; 4301 } else if (evt.keyCode === 37) { 4302 // left 4303 dir = [-dx, 0]; 4304 } else if (evt.keyCode === 39) { 4305 // right 4306 dir = [dx, 0]; 4307 } else { 4308 done = false; 4309 } 4310 4311 if (dir && el.isDraggable && 4312 el.visPropCalc.visible && 4313 ((this.geonextCompatibilityMode && 4314 (Type.isPoint(el) || 4315 el.elementClass === Const.OBJECT_CLASS_TEXT) 4316 ) || !this.geonextCompatibilityMode) && 4317 !el.evalVisProp('fixed') 4318 ) { 4319 this.mode = this.BOARD_MODE_DRAG; 4320 if (Type.exists(el.coords)) { 4321 dir[0] += actPos[0]; 4322 dir[1] += actPos[1]; 4323 } 4324 // For coordsElement setPosition has to call setPositionDirectly. 4325 // Otherwise the position is set by a translation. 4326 if (Type.exists(el.coords)) { 4327 el.setPosition(JXG.COORDS_BY_USER, dir); 4328 this.updateInfobox(el); 4329 } else { 4330 this.displayInfobox(false); 4331 el.setPositionDirectly( 4332 Const.COORDS_BY_USER, 4333 dir, 4334 [0, 0] 4335 ); 4336 } 4337 4338 this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]); 4339 el.triggerEventHandlers(['keydrag', 'drag'], [evt]); 4340 this.mode = this.BOARD_MODE_NONE; 4341 } 4342 } 4343 4344 this.update(); 4345 4346 if (done && Type.exists(evt.preventDefault)) { 4347 evt.preventDefault(); 4348 } 4349 return done; 4350 }, 4351 4352 /** 4353 * Event listener for SVG elements getting focus. 4354 * This is needed for highlighting when using keyboard control. 4355 * Only elements having the attribute 'tabindex' can receive focus. 4356 * 4357 * @see JXG.Board#keyFocusOutListener 4358 * @see JXG.Board#keyDownListener 4359 * @see JXG.Board#keyboard 4360 * 4361 * @param {Event} evt The browser's event object 4362 */ 4363 keyFocusInListener: function (evt) { 4364 var id_node = evt.target.id, 4365 id, 4366 el; 4367 4368 if (!this.attr.keyboard.enabled || id_node === '') { 4369 return false; 4370 } 4371 4372 // Get JSXGraph id from node id 4373 id = id_node.replace(this.containerObj.id + '_', ''); 4374 el = this.select(id); 4375 if (Type.exists(el.highlight)) { 4376 el.highlight(true); 4377 this.focusObjects = [id]; 4378 el.triggerEventHandlers(['hit'], [evt]); 4379 } 4380 if (Type.exists(el.coords)) { 4381 this.updateInfobox(el); 4382 } 4383 }, 4384 4385 /** 4386 * Event listener for SVG elements losing focus. 4387 * This is needed for dehighlighting when using keyboard control. 4388 * Only elements having the attribute 'tabindex' can receive focus. 4389 * 4390 * @see JXG.Board#keyFocusInListener 4391 * @see JXG.Board#keyDownListener 4392 * @see JXG.Board#keyboard 4393 * 4394 * @param {Event} evt The browser's event object 4395 */ 4396 keyFocusOutListener: function (evt) { 4397 if (!this.attr.keyboard.enabled) { 4398 return false; 4399 } 4400 this.focusObjects = []; // This has to be before displayInfobox(false) 4401 this.dehighlightAll(); 4402 this.displayInfobox(false); 4403 }, 4404 4405 /** 4406 * Update the width and height of the JSXGraph container div element. 4407 * If width and height are not supplied, read actual values with offsetWidth/Height, 4408 * and call board.resizeContainer() with this values. 4409 * <p> 4410 * If necessary, also call setBoundingBox(). 4411 * @param {Number} [width=this.containerObj.offsetWidth] Width of the container element 4412 * @param {Number} [height=this.containerObj.offsetHeight] Height of the container element 4413 * @returns {JXG.Board} Reference to the board 4414 * 4415 * @see JXG.Board#startResizeObserver 4416 * @see JXG.Board#resizeListener 4417 * @see JXG.Board#resizeContainer 4418 * @see JXG.Board#setBoundingBox 4419 * 4420 */ 4421 updateContainerDims: function (width, height) { 4422 var w = width, 4423 h = height, 4424 // bb, 4425 css, 4426 width_adjustment, height_adjustment; 4427 4428 if (width === undefined) { 4429 // Get size of the board's container div 4430 // 4431 // offsetWidth/Height ignores CSS transforms, 4432 // getBoundingClientRect includes CSS transforms 4433 // 4434 // bb = this.containerObj.getBoundingClientRect(); 4435 // w = bb.width; 4436 // h = bb.height; 4437 w = this.containerObj.offsetWidth; 4438 h = this.containerObj.offsetHeight; 4439 } 4440 4441 if (width === undefined && window && window.getComputedStyle) { 4442 // Subtract the border size 4443 css = window.getComputedStyle(this.containerObj, null); 4444 width_adjustment = parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width')); 4445 if (!isNaN(width_adjustment)) { 4446 w -= width_adjustment; 4447 } 4448 height_adjustment = parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width')); 4449 if (!isNaN(height_adjustment)) { 4450 h -= height_adjustment; 4451 } 4452 } 4453 4454 // If div is invisible - do nothing 4455 if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { 4456 return this; 4457 } 4458 4459 // If bounding box is not yet initialized, do it now. 4460 if (isNaN(this.getBoundingBox()[0])) { 4461 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep'); 4462 } 4463 4464 // Do nothing if the dimension did not change since being visible 4465 // the last time. Note that if the div had display:none in the mean time, 4466 // we did not store this._prevDim. 4467 if (Type.exists(this._prevDim) && this._prevDim.w === w && this._prevDim.h === h) { 4468 return this; 4469 } 4470 // Set the size of the SVG or canvas element 4471 this.resizeContainer(w, h, true); 4472 this._prevDim = { 4473 w: w, 4474 h: h 4475 }; 4476 return this; 4477 }, 4478 4479 /** 4480 * Start observer which reacts to size changes of the JSXGraph 4481 * container div element. Calls updateContainerDims(). 4482 * If not available, an event listener for the window-resize event is started. 4483 * On mobile devices also scrolling might trigger resizes. 4484 * However, resize events triggered by scrolling events should be ignored. 4485 * Therefore, also a scrollListener is started. 4486 * Resize can be controlled with the board attribute resize. 4487 * 4488 * @see JXG.Board#updateContainerDims 4489 * @see JXG.Board#resizeListener 4490 * @see JXG.Board#scrollListener 4491 * @see JXG.Board#resize 4492 * 4493 */ 4494 startResizeObserver: function () { 4495 var that = this; 4496 4497 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4498 return; 4499 } 4500 4501 this.resizeObserver = new ResizeObserver(function (entries) { 4502 var bb; 4503 if (!that._isResizing) { 4504 that._isResizing = true; 4505 bb = entries[0].contentRect; 4506 window.setTimeout(function () { 4507 try { 4508 that.updateContainerDims(bb.width, bb.height); 4509 } catch (e) { 4510 JXG.debug(e); // Used to log errors during board.update() 4511 that.stopResizeObserver(); 4512 } finally { 4513 that._isResizing = false; 4514 } 4515 }, that.attr.resize.throttle); 4516 } 4517 }); 4518 this.resizeObserver.observe(this.containerObj); 4519 }, 4520 4521 /** 4522 * Stops the resize observer. 4523 * @see JXG.Board#startResizeObserver 4524 * 4525 */ 4526 stopResizeObserver: function () { 4527 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4528 return; 4529 } 4530 4531 if (Type.exists(this.resizeObserver)) { 4532 this.resizeObserver.unobserve(this.containerObj); 4533 } 4534 }, 4535 4536 /** 4537 * Fallback solutions if there is no resizeObserver available in the browser. 4538 * Reacts to resize events of the window (only). Otherwise similar to 4539 * startResizeObserver(). To handle changes of the visibility 4540 * of the JSXGraph container element, additionally an intersection observer is used. 4541 * which watches changes in the visibility of the JSXGraph container element. 4542 * This is necessary e.g. for register tabs or dia shows. 4543 * 4544 * @see JXG.Board#startResizeObserver 4545 * @see JXG.Board#startIntersectionObserver 4546 */ 4547 resizeListener: function () { 4548 var that = this; 4549 4550 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 4551 return; 4552 } 4553 if (!this._isScrolling && !this._isResizing) { 4554 this._isResizing = true; 4555 window.setTimeout(function () { 4556 that.updateContainerDims(); 4557 that._isResizing = false; 4558 }, this.attr.resize.throttle); 4559 } 4560 }, 4561 4562 /** 4563 * Listener to watch for scroll events. Sets board._isScrolling = true 4564 * @param {Event} evt The browser's event object 4565 * 4566 * @see JXG.Board#startResizeObserver 4567 * @see JXG.Board#resizeListener 4568 * 4569 */ 4570 scrollListener: function (evt) { 4571 var that = this; 4572 4573 if (!Env.isBrowser) { 4574 return; 4575 } 4576 if (!this._isScrolling) { 4577 this._isScrolling = true; 4578 window.setTimeout(function () { 4579 that._isScrolling = false; 4580 }, 66); 4581 } 4582 }, 4583 4584 /** 4585 * Watch for changes of the visibility of the JSXGraph container element. 4586 * 4587 * @see JXG.Board#startResizeObserver 4588 * @see JXG.Board#resizeListener 4589 * 4590 */ 4591 startIntersectionObserver: function () { 4592 var that = this, 4593 options = { 4594 root: null, 4595 rootMargin: '0px', 4596 threshold: 0.8 4597 }; 4598 4599 try { 4600 this.intersectionObserver = new IntersectionObserver(function (entries) { 4601 // If bounding box is not yet initialized, do it now. 4602 if (isNaN(that.getBoundingBox()[0])) { 4603 that.updateContainerDims(); 4604 } 4605 }, options); 4606 this.intersectionObserver.observe(that.containerObj); 4607 } catch (err) { 4608 JXG.debug('JSXGraph: IntersectionObserver not available in this browser.'); 4609 } 4610 }, 4611 4612 /** 4613 * Stop the intersection observer 4614 * 4615 * @see JXG.Board#startIntersectionObserver 4616 * 4617 */ 4618 stopIntersectionObserver: function () { 4619 if (Type.exists(this.intersectionObserver)) { 4620 this.intersectionObserver.unobserve(this.containerObj); 4621 } 4622 }, 4623 4624 /** 4625 * Update the container before and after printing. 4626 * @param {Event} [evt] 4627 */ 4628 printListener: function(evt) { 4629 this.updateContainerDims(); 4630 }, 4631 4632 /** 4633 * Wrapper for printListener to be used in mediaQuery matches. 4634 * @param {MediaQueryList} mql 4635 */ 4636 printListenerMatch: function (mql) { 4637 if (mql.matches) { 4638 this.printListener(); 4639 } 4640 }, 4641 4642 /********************************************************** 4643 * 4644 * End of Event Handlers 4645 * 4646 **********************************************************/ 4647 4648 /** 4649 * Initialize the info box object which is used to display 4650 * the coordinates of points near the mouse pointer, 4651 * @returns {JXG.Board} Reference to the board 4652 */ 4653 initInfobox: function (attributes) { 4654 var attr = Type.copyAttributes(attributes, this.options, 'infobox'); 4655 4656 attr.id = this.id + '_infobox'; 4657 4658 /** 4659 * Infobox close to points in which the points' coordinates are displayed. 4660 * This is simply a JXG.Text element. Access through board.infobox. 4661 * Uses CSS class .JXGinfobox. 4662 * 4663 * @namespace 4664 * @name JXG.Board.infobox 4665 * @type JXG.Text 4666 * 4667 * @example 4668 * const board = JXG.JSXGraph.initBoard(BOARDID, { 4669 * boundingbox: [-0.5, 0.5, 0.5, -0.5], 4670 * intl: { 4671 * enabled: false, 4672 * locale: 'de-DE' 4673 * }, 4674 * keepaspectratio: true, 4675 * axis: true, 4676 * infobox: { 4677 * distanceY: 40, 4678 * intl: { 4679 * enabled: true, 4680 * options: { 4681 * minimumFractionDigits: 1, 4682 * maximumFractionDigits: 2 4683 * } 4684 * } 4685 * } 4686 * }); 4687 * var p = board.create('point', [0.1, 0.1], {}); 4688 * 4689 * </pre><div id="JXG822161af-fe77-4769-850f-cdf69935eab0" class="jxgbox" style="width: 300px; height: 300px;"></div> 4690 * <script type="text/javascript"> 4691 * (function() { 4692 * const board = JXG.JSXGraph.initBoard('JXG822161af-fe77-4769-850f-cdf69935eab0', { 4693 * boundingbox: [-0.5, 0.5, 0.5, -0.5], showcopyright: false, shownavigation: false, 4694 * intl: { 4695 * enabled: false, 4696 * locale: 'de-DE' 4697 * }, 4698 * keepaspectratio: true, 4699 * axis: true, 4700 * infobox: { 4701 * distanceY: 40, 4702 * intl: { 4703 * enabled: true, 4704 * options: { 4705 * minimumFractionDigits: 1, 4706 * maximumFractionDigits: 2 4707 * } 4708 * } 4709 * } 4710 * }); 4711 * var p = board.create('point', [0.1, 0.1], {}); 4712 * })(); 4713 * 4714 * </script><pre> 4715 * 4716 */ 4717 this.infobox = this.create('text', [0, 0, '0,0'], attr); 4718 // this.infobox.needsUpdateSize = false; // That is not true, but it speeds drawing up. 4719 this.infobox.dump = false; 4720 4721 this.displayInfobox(false); 4722 return this; 4723 }, 4724 4725 /** 4726 * Updates and displays a little info box to show coordinates of current selected points. 4727 * @param {JXG.GeometryElement} el A GeometryElement 4728 * @returns {JXG.Board} Reference to the board 4729 * @see JXG.Board#displayInfobox 4730 * @see JXG.Board#showInfobox 4731 * @see Point#showInfobox 4732 * 4733 */ 4734 updateInfobox: function (el) { 4735 var x, y, xc, yc, 4736 vpinfoboxdigits, 4737 distX, distY, 4738 vpsi = el.evalVisProp('showinfobox'); 4739 4740 if ((!Type.evaluate(this.attr.showinfobox) && vpsi === 'inherit') || !vpsi) { 4741 return this; 4742 } 4743 4744 if (Type.isPoint(el)) { 4745 xc = el.coords.usrCoords[1]; 4746 yc = el.coords.usrCoords[2]; 4747 distX = this.infobox.evalVisProp('distancex'); 4748 distY = this.infobox.evalVisProp('distancey'); 4749 4750 this.infobox.setCoords( 4751 xc + distX / this.unitX, 4752 yc + distY / this.unitY 4753 ); 4754 4755 vpinfoboxdigits = el.evalVisProp('infoboxdigits'); 4756 if (typeof el.infoboxText !== 'string') { 4757 if (vpinfoboxdigits === 'auto') { 4758 if (this.infobox.useLocale()) { 4759 x = this.infobox.formatNumberLocale(xc); 4760 y = this.infobox.formatNumberLocale(yc); 4761 } else { 4762 x = Type.autoDigits(xc); 4763 y = Type.autoDigits(yc); 4764 } 4765 } else if (Type.isNumber(vpinfoboxdigits)) { 4766 if (this.infobox.useLocale()) { 4767 x = this.infobox.formatNumberLocale(xc, vpinfoboxdigits); 4768 y = this.infobox.formatNumberLocale(yc, vpinfoboxdigits); 4769 } else { 4770 x = Type.toFixed(xc, vpinfoboxdigits); 4771 y = Type.toFixed(yc, vpinfoboxdigits); 4772 } 4773 4774 } else { 4775 x = xc; 4776 y = yc; 4777 } 4778 4779 this.highlightInfobox(x, y, el); 4780 } else { 4781 this.highlightCustomInfobox(el.infoboxText, el); 4782 } 4783 4784 this.displayInfobox(true); 4785 } 4786 return this; 4787 }, 4788 4789 /** 4790 * Set infobox visible / invisible. 4791 * 4792 * It uses its property hiddenByParent to memorize its status. 4793 * In this way, many DOM access can be avoided. 4794 * 4795 * @param {Boolean} val true for visible, false for invisible 4796 * @returns {JXG.Board} Reference to the board. 4797 * @see JXG.Board#updateInfobox 4798 * 4799 */ 4800 displayInfobox: function (val) { 4801 if (!val && this.focusObjects.length > 0 && 4802 this.select(this.focusObjects[0]).elementClass === Const.OBJECT_CLASS_POINT) { 4803 // If an element has focus we do not hide its infobox 4804 return this; 4805 } 4806 if (this.infobox.hiddenByParent === val) { 4807 this.infobox.hiddenByParent = !val; 4808 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer(); 4809 } 4810 return this; 4811 }, 4812 4813 // Alias for displayInfobox to be backwards compatible. 4814 // The method showInfobox clashes with the board attribute showInfobox 4815 showInfobox: function (val) { 4816 return this.displayInfobox(val); 4817 }, 4818 4819 /** 4820 * Changes the text of the info box to show the given coordinates. 4821 * @param {Number} x 4822 * @param {Number} y 4823 * @param {JXG.GeometryElement} [el] The element the mouse is pointing at 4824 * @returns {JXG.Board} Reference to the board. 4825 */ 4826 highlightInfobox: function (x, y, el) { 4827 this.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 4828 return this; 4829 }, 4830 4831 /** 4832 * Changes the text of the info box to what is provided via text. 4833 * @param {String} text 4834 * @param {JXG.GeometryElement} [el] 4835 * @returns {JXG.Board} Reference to the board. 4836 */ 4837 highlightCustomInfobox: function (text, el) { 4838 this.infobox.setText(text); 4839 return this; 4840 }, 4841 4842 /** 4843 * Remove highlighting of all elements. 4844 * @returns {JXG.Board} Reference to the board. 4845 */ 4846 dehighlightAll: function () { 4847 var el, 4848 pEl, 4849 stillHighlighted = {}, 4850 needsDeHighlight = false; 4851 4852 for (el in this.highlightedObjects) { 4853 if (this.highlightedObjects.hasOwnProperty(el)) { 4854 4855 pEl = this.highlightedObjects[el]; 4856 if (this.focusObjects.indexOf(el) < 0) { // Element does not have focus 4857 if (this.hasMouseHandlers || this.hasPointerHandlers) { 4858 pEl.noHighlight(); 4859 } 4860 needsDeHighlight = true; 4861 } else { 4862 stillHighlighted[el] = pEl; 4863 } 4864 // In highlightedObjects should only be objects which fulfill all these conditions 4865 // And in case of complex elements, like a turtle based fractal, it should be faster to 4866 // just de-highlight the element instead of checking hasPoint... 4867 // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible) 4868 } 4869 } 4870 4871 this.highlightedObjects = stillHighlighted; 4872 4873 // We do not need to redraw during dehighlighting in CanvasRenderer 4874 // because we are redrawing anyhow 4875 // -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until 4876 // another object is highlighted. 4877 if (this.renderer.type === 'canvas' && needsDeHighlight) { 4878 this.prepareUpdate(); 4879 this.renderer.suspendRedraw(this); 4880 this.updateRenderer(); 4881 this.renderer.unsuspendRedraw(); 4882 } 4883 4884 return this; 4885 }, 4886 4887 /** 4888 * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose 4889 * once. 4890 * @private 4891 * @param {Number} x X coordinate in screen coordinates 4892 * @param {Number} y Y coordinate in screen coordinates 4893 * @returns {Array} Coordinates [x, y] of the mouse in screen coordinates. 4894 * @see JXG.Board#getUsrCoordsOfMouse 4895 */ 4896 getScrCoordsOfMouse: function (x, y) { 4897 return [x, y]; 4898 }, 4899 4900 /** 4901 * This method calculates the user coords of the current mouse coordinates. 4902 * @param {Event} evt Event object containing the mouse coordinates. 4903 * @returns {Array} Coordinates [x, y] of the mouse in user coordinates. 4904 * @example 4905 * board.on('up', function (evt) { 4906 * var a = board.getUsrCoordsOfMouse(evt), 4907 * x = a[0], 4908 * y = a[1], 4909 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4910 * // Shorter version: 4911 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4912 * }); 4913 * 4914 * </pre><div id='JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746' class='jxgbox' style='width: 300px; height: 300px;'></div> 4915 * <script type='text/javascript'> 4916 * (function() { 4917 * var board = JXG.JSXGraph.initBoard('JXG48d5066b-16ba-4920-b8ea-a4f8eff6b746', 4918 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 4919 * board.on('up', function (evt) { 4920 * var a = board.getUsrCoordsOfMouse(evt), 4921 * x = a[0], 4922 * y = a[1], 4923 * somePoint = board.create('point', [x,y], {name:'SomePoint',size:4}); 4924 * // Shorter version: 4925 * //somePoint = board.create('point', a, {name:'SomePoint',size:4}); 4926 * }); 4927 * 4928 * })(); 4929 * 4930 * </script><pre> 4931 * 4932 * @see JXG.Board#getScrCoordsOfMouse 4933 * @see JXG.Board#getAllUnderMouse 4934 */ 4935 getUsrCoordsOfMouse: function (evt) { 4936 var cPos = this.getCoordsTopLeftCorner(), 4937 absPos = Env.getPosition(evt, null, this.document), 4938 x = absPos[0] - cPos[0], 4939 y = absPos[1] - cPos[1], 4940 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this); 4941 4942 return newCoords.usrCoords.slice(1); 4943 }, 4944 4945 /** 4946 * Collects all elements under current mouse position plus current user coordinates of mouse cursor. 4947 * @param {Event} evt Event object containing the mouse coordinates. 4948 * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse. 4949 * @see JXG.Board#getUsrCoordsOfMouse 4950 * @see JXG.Board#getAllObjectsUnderMouse 4951 */ 4952 getAllUnderMouse: function (evt) { 4953 var elList = this.getAllObjectsUnderMouse(evt); 4954 elList.push(this.getUsrCoordsOfMouse(evt)); 4955 4956 return elList; 4957 }, 4958 4959 /** 4960 * Collects all elements under current mouse position. 4961 * @param {Event} evt Event object containing the mouse coordinates. 4962 * @returns {Array} Array of elements at the current mouse position. 4963 * @see JXG.Board#getAllUnderMouse 4964 */ 4965 getAllObjectsUnderMouse: function (evt) { 4966 var cPos = this.getCoordsTopLeftCorner(), 4967 absPos = Env.getPosition(evt, null, this.document), 4968 dx = absPos[0] - cPos[0], 4969 dy = absPos[1] - cPos[1], 4970 elList = [], 4971 el, 4972 pEl, 4973 len = this.objectsList.length; 4974 4975 for (el = 0; el < len; el++) { 4976 pEl = this.objectsList[el]; 4977 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) { 4978 elList[elList.length] = pEl; 4979 } 4980 } 4981 4982 return elList; 4983 }, 4984 4985 /** 4986 * Update the coords object of all elements which possess this 4987 * property. This is necessary after changing the viewport. 4988 * @returns {JXG.Board} Reference to this board. 4989 **/ 4990 updateCoords: function () { 4991 var el, ob, 4992 len = this.objectsList.length; 4993 4994 for (ob = 0; ob < len; ob++) { 4995 el = this.objectsList[ob]; 4996 4997 if (Type.exists(el.coords)) { 4998 if (el.evalVisProp('frozen') === true) { 4999 if (el.is3D) { 5000 el.element2D.coords.screen2usr(); 5001 } else { 5002 el.coords.screen2usr(); 5003 } 5004 } else { 5005 if (el.is3D) { 5006 el.element2D.coords.usr2screen(); 5007 } else { 5008 el.coords.usr2screen(); 5009 if (Type.exists(el.actualCoords)) { 5010 el.actualCoords.usr2screen(); 5011 } 5012 } 5013 } 5014 } 5015 } 5016 return this; 5017 }, 5018 5019 /** 5020 * Moves the origin and initializes an update of all elements. 5021 * @param {Number} x 5022 * @param {Number} y 5023 * @param {Boolean} [diff=false] 5024 * @returns {JXG.Board} Reference to this board. 5025 */ 5026 moveOrigin: function (x, y, diff) { 5027 var ox, oy, ul, lr; 5028 if (Type.exists(x) && Type.exists(y)) { 5029 ox = this.origin.scrCoords[1]; 5030 oy = this.origin.scrCoords[2]; 5031 5032 this.origin.scrCoords[1] = x; 5033 this.origin.scrCoords[2] = y; 5034 5035 if (diff) { 5036 this.origin.scrCoords[1] -= this.drag_dx; 5037 this.origin.scrCoords[2] -= this.drag_dy; 5038 } 5039 5040 ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords; 5041 lr = new Coords( 5042 Const.COORDS_BY_SCREEN, 5043 [this.canvasWidth, this.canvasHeight], 5044 this 5045 ).usrCoords; 5046 if ( 5047 ul[1] < this.maxboundingbox[0] - Mat.eps || 5048 ul[2] > this.maxboundingbox[1] + Mat.eps || 5049 lr[1] > this.maxboundingbox[2] + Mat.eps || 5050 lr[2] < this.maxboundingbox[3] - Mat.eps 5051 ) { 5052 this.origin.scrCoords[1] = ox; 5053 this.origin.scrCoords[2] = oy; 5054 } 5055 } 5056 5057 this.updateCoords().clearTraces().fullUpdate(); 5058 this.triggerEventHandlers(['boundingbox']); 5059 5060 return this; 5061 }, 5062 5063 /** 5064 * Add conditional updates to the elements. 5065 * @param {String} str String containing conditional update in geonext syntax 5066 */ 5067 addConditions: function (str) { 5068 var term, 5069 m, 5070 left, 5071 right, 5072 name, 5073 el, 5074 property, 5075 functions = [], 5076 // plaintext = 'var el, x, y, c, rgbo;\n', 5077 i = str.indexOf('<data>'), 5078 j = str.indexOf('<' + '/data>'), 5079 xyFun = function (board, el, f, what) { 5080 return function () { 5081 var e, t; 5082 5083 e = board.select(el.id); 5084 t = e.coords.usrCoords[what]; 5085 5086 if (what === 2) { 5087 e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]); 5088 } else { 5089 e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]); 5090 } 5091 e.prepareUpdate().update(); 5092 }; 5093 }, 5094 visFun = function (board, el, f) { 5095 return function () { 5096 var e, v; 5097 5098 e = board.select(el.id); 5099 v = f(); 5100 5101 e.setAttribute({ visible: v }); 5102 }; 5103 }, 5104 colFun = function (board, el, f, what) { 5105 return function () { 5106 var e, v; 5107 5108 e = board.select(el.id); 5109 v = f(); 5110 5111 if (what === 'strokewidth') { 5112 e.visProp.strokewidth = v; 5113 } else { 5114 v = Color.rgba2rgbo(v); 5115 e.visProp[what + 'color'] = v[0]; 5116 e.visProp[what + 'opacity'] = v[1]; 5117 } 5118 }; 5119 }, 5120 posFun = function (board, el, f) { 5121 return function () { 5122 var e = board.select(el.id); 5123 5124 e.position = f(); 5125 }; 5126 }, 5127 styleFun = function (board, el, f) { 5128 return function () { 5129 var e = board.select(el.id); 5130 5131 e.setStyle(f()); 5132 }; 5133 }; 5134 5135 if (i < 0) { 5136 return; 5137 } 5138 5139 while (i >= 0) { 5140 term = str.slice(i + 6, j); // throw away <data> 5141 m = term.indexOf('='); 5142 left = term.slice(0, m); 5143 right = term.slice(m + 1); 5144 m = left.indexOf('.'); // Resulting variable names must not contain dots, e.g. ' Steuern akt.' 5145 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace) 5146 el = this.elementsByName[Type.unescapeHTML(name)]; 5147 5148 property = left 5149 .slice(m + 1) 5150 .replace(/\s+/g, '') 5151 .toLowerCase(); // remove whitespace in property 5152 right = Type.createFunction(right, this, '', true); 5153 5154 // Debug 5155 if (!Type.exists(this.elementsByName[name])) { 5156 JXG.debug('debug conditions: |' + name + '| undefined'); 5157 } else { 5158 // plaintext += 'el = this.objects[\'' + el.id + '\'];\n'; 5159 5160 switch (property) { 5161 case 'x': 5162 functions.push(xyFun(this, el, right, 2)); 5163 break; 5164 case 'y': 5165 functions.push(xyFun(this, el, right, 1)); 5166 break; 5167 case 'visible': 5168 functions.push(visFun(this, el, right)); 5169 break; 5170 case 'position': 5171 functions.push(posFun(this, el, right)); 5172 break; 5173 case 'stroke': 5174 functions.push(colFun(this, el, right, 'stroke')); 5175 break; 5176 case 'style': 5177 functions.push(styleFun(this, el, right)); 5178 break; 5179 case 'strokewidth': 5180 functions.push(colFun(this, el, right, 'strokewidth')); 5181 break; 5182 case 'fill': 5183 functions.push(colFun(this, el, right, 'fill')); 5184 break; 5185 case 'label': 5186 break; 5187 default: 5188 JXG.debug( 5189 'property "' + 5190 property + 5191 '" in conditions not yet implemented:' + 5192 right 5193 ); 5194 break; 5195 } 5196 } 5197 str = str.slice(j + 7); // cut off '</data>' 5198 i = str.indexOf('<data>'); 5199 j = str.indexOf('<' + '/data>'); 5200 } 5201 5202 this.updateConditions = function () { 5203 var i; 5204 5205 for (i = 0; i < functions.length; i++) { 5206 functions[i](); 5207 } 5208 5209 this.prepareUpdate().updateElements(); 5210 return true; 5211 }; 5212 this.updateConditions(); 5213 }, 5214 5215 /** 5216 * Computes the commands in the conditions-section of the gxt file. 5217 * It is evaluated after an update, before the unsuspendRedraw. 5218 * The function is generated in 5219 * @see JXG.Board#addConditions 5220 * @private 5221 */ 5222 updateConditions: function () { 5223 return false; 5224 }, 5225 5226 /** 5227 * Calculates adequate snap sizes. 5228 * @returns {JXG.Board} Reference to the board. 5229 */ 5230 calculateSnapSizes: function () { 5231 var p1, p2, 5232 bbox = this.getBoundingBox(), 5233 gridStep = Type.evaluate(this.options.grid.majorStep), 5234 gridX = Type.evaluate(this.options.grid.gridX), 5235 gridY = Type.evaluate(this.options.grid.gridY), 5236 x, y; 5237 5238 if (!Type.isArray(gridStep)) { 5239 gridStep = [gridStep, gridStep]; 5240 } 5241 if (gridStep.length < 2) { 5242 gridStep = [gridStep[0], gridStep[0]]; 5243 } 5244 if (Type.exists(gridX)) { 5245 gridStep[0] = gridX; 5246 } 5247 if (Type.exists(gridY)) { 5248 gridStep[1] = gridY; 5249 } 5250 5251 if (gridStep[0] === 'auto') { 5252 gridStep[0] = 1; 5253 } else { 5254 gridStep[0] = Type.parseNumber(gridStep[0], Math.abs(bbox[1] - bbox[3]), 1 / this.unitX); 5255 } 5256 if (gridStep[1] === 'auto') { 5257 gridStep[1] = 1; 5258 } else { 5259 gridStep[1] = Type.parseNumber(gridStep[1], Math.abs(bbox[0] - bbox[2]), 1 / this.unitY); 5260 } 5261 5262 p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this); 5263 p2 = new Coords( 5264 Const.COORDS_BY_USER, 5265 [gridStep[0], gridStep[1]], 5266 this 5267 ); 5268 x = p1.scrCoords[1] - p2.scrCoords[1]; 5269 y = p1.scrCoords[2] - p2.scrCoords[2]; 5270 5271 this.options.grid.snapSizeX = gridStep[0]; 5272 while (Math.abs(x) > 25) { 5273 this.options.grid.snapSizeX *= 2; 5274 x /= 2; 5275 } 5276 5277 this.options.grid.snapSizeY = gridStep[1]; 5278 while (Math.abs(y) > 25) { 5279 this.options.grid.snapSizeY *= 2; 5280 y /= 2; 5281 } 5282 5283 return this; 5284 }, 5285 5286 /** 5287 * Apply update on all objects with the new zoom-factors. Clears all traces. 5288 * @returns {JXG.Board} Reference to the board. 5289 */ 5290 applyZoom: function () { 5291 this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate(); 5292 5293 return this; 5294 }, 5295 5296 /** 5297 * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5298 * The zoom operation is centered at x, y. 5299 * @param {Number} [x] 5300 * @param {Number} [y] 5301 * @returns {JXG.Board} Reference to the board 5302 */ 5303 zoomIn: function (x, y) { 5304 var bb = this.getBoundingBox(), 5305 zX = Type.evaluate(this.attr.zoom.factorx), 5306 zY = Type.evaluate(this.attr.zoom.factory), 5307 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX), 5308 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY), 5309 lr = 0.5, 5310 tr = 0.5, 5311 ma = Type.evaluate(this.attr.zoom.max), 5312 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated 5313 5314 if ( 5315 (this.zoomX > ma && zX > 1.0) || 5316 (this.zoomY > ma && zY > 1.0) || 5317 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices 5318 (this.zoomY < mi && zY < 1.0) 5319 ) { 5320 return this; 5321 } 5322 5323 if (Type.isNumber(x) && Type.isNumber(y)) { 5324 lr = (x - bb[0]) / (bb[2] - bb[0]); 5325 tr = (bb[1] - y) / (bb[1] - bb[3]); 5326 } 5327 5328 this.setBoundingBox( 5329 [ 5330 bb[0] + dX * lr, 5331 bb[1] - dY * tr, 5332 bb[2] - dX * (1 - lr), 5333 bb[3] + dY * (1 - tr) 5334 ], 5335 this.keepaspectratio, 5336 'update' 5337 ); 5338 return this.applyZoom(); 5339 }, 5340 5341 /** 5342 * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 5343 * The zoom operation is centered at x, y. 5344 * 5345 * @param {Number} [x] 5346 * @param {Number} [y] 5347 * @returns {JXG.Board} Reference to the board 5348 */ 5349 zoomOut: function (x, y) { 5350 var bb = this.getBoundingBox(), 5351 zX = Type.evaluate(this.attr.zoom.factorx), 5352 zY = Type.evaluate(this.attr.zoom.factory), 5353 dX = (bb[2] - bb[0]) * (1.0 - zX), 5354 dY = (bb[1] - bb[3]) * (1.0 - zY), 5355 lr = 0.5, 5356 tr = 0.5, 5357 mi = Type.evaluate(this.attr.zoom.eps) || Type.evaluate(this.attr.zoom.min) || 0.001; // this.attr.zoom.eps is deprecated 5358 5359 if (this.zoomX < mi || this.zoomY < mi) { 5360 return this; 5361 } 5362 5363 if (Type.isNumber(x) && Type.isNumber(y)) { 5364 lr = (x - bb[0]) / (bb[2] - bb[0]); 5365 tr = (bb[1] - y) / (bb[1] - bb[3]); 5366 } 5367 5368 this.setBoundingBox( 5369 [ 5370 bb[0] + dX * lr, 5371 bb[1] - dY * tr, 5372 bb[2] - dX * (1 - lr), 5373 bb[3] + dY * (1 - tr) 5374 ], 5375 this.keepaspectratio, 5376 'update' 5377 ); 5378 5379 return this.applyZoom(); 5380 }, 5381 5382 /** 5383 * Reset the zoom level to the original zoom level from initBoard(); 5384 * Additionally, if the board as been initialized with a boundingBox (which is the default), 5385 * restore the viewport to the original viewport during initialization. Otherwise, 5386 * (i.e. if the board as been initialized with unitX/Y and originX/Y), 5387 * just set the zoom level to 100%. 5388 * 5389 * @returns {JXG.Board} Reference to the board 5390 */ 5391 zoom100: function () { 5392 var bb, dX, dY; 5393 5394 if (Type.exists(this.attr.boundingbox)) { 5395 this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset'); 5396 } else { 5397 // Board has been set up with unitX/Y and originX/Y 5398 bb = this.getBoundingBox(); 5399 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5; 5400 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5; 5401 this.setBoundingBox( 5402 [bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], 5403 this.keepaspectratio, 5404 'reset' 5405 ); 5406 } 5407 return this.applyZoom(); 5408 }, 5409 5410 /** 5411 * Zooms the board so every visible point is shown. Keeps aspect ratio. 5412 * @returns {JXG.Board} Reference to the board 5413 */ 5414 zoomAllPoints: function () { 5415 var el, 5416 border, 5417 borderX, 5418 borderY, 5419 pEl, 5420 minX = 0, 5421 maxX = 0, 5422 minY = 0, 5423 maxY = 0, 5424 len = this.objectsList.length; 5425 5426 for (el = 0; el < len; el++) { 5427 pEl = this.objectsList[el]; 5428 5429 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) { 5430 if (pEl.coords.usrCoords[1] < minX) { 5431 minX = pEl.coords.usrCoords[1]; 5432 } else if (pEl.coords.usrCoords[1] > maxX) { 5433 maxX = pEl.coords.usrCoords[1]; 5434 } 5435 if (pEl.coords.usrCoords[2] > maxY) { 5436 maxY = pEl.coords.usrCoords[2]; 5437 } else if (pEl.coords.usrCoords[2] < minY) { 5438 minY = pEl.coords.usrCoords[2]; 5439 } 5440 } 5441 } 5442 5443 border = 50; 5444 borderX = border / this.unitX; 5445 borderY = border / this.unitY; 5446 5447 this.setBoundingBox( 5448 [minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], 5449 this.keepaspectratio, 5450 'update' 5451 ); 5452 5453 return this.applyZoom(); 5454 }, 5455 5456 /** 5457 * Reset the bounding box and the zoom level to 100% such that a given set of elements is 5458 * within the board's viewport. 5459 * @param {Array} elements A set of elements given by id, reference, or name. 5460 * @returns {JXG.Board} Reference to the board. 5461 */ 5462 zoomElements: function (elements) { 5463 var i, e, 5464 box, 5465 newBBox = [Infinity, -Infinity, -Infinity, Infinity], 5466 cx, cy, 5467 dx, dy, 5468 d; 5469 5470 if (!Type.isArray(elements) || elements.length === 0) { 5471 return this; 5472 } 5473 5474 for (i = 0; i < elements.length; i++) { 5475 e = this.select(elements[i]); 5476 5477 box = e.bounds(); 5478 if (Type.isArray(box)) { 5479 if (box[0] < newBBox[0]) { 5480 newBBox[0] = box[0]; 5481 } 5482 if (box[1] > newBBox[1]) { 5483 newBBox[1] = box[1]; 5484 } 5485 if (box[2] > newBBox[2]) { 5486 newBBox[2] = box[2]; 5487 } 5488 if (box[3] < newBBox[3]) { 5489 newBBox[3] = box[3]; 5490 } 5491 } 5492 } 5493 5494 if (Type.isArray(newBBox)) { 5495 cx = 0.5 * (newBBox[0] + newBBox[2]); 5496 cy = 0.5 * (newBBox[1] + newBBox[3]); 5497 dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5; 5498 dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5; 5499 d = Math.max(dx, dy); 5500 this.setBoundingBox( 5501 [cx - d, cy + d, cx + d, cy - d], 5502 this.keepaspectratio, 5503 'update' 5504 ); 5505 } 5506 5507 return this; 5508 }, 5509 5510 /** 5511 * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>. 5512 * @param {Number} fX 5513 * @param {Number} fY 5514 * @returns {JXG.Board} Reference to the board. 5515 */ 5516 setZoom: function (fX, fY) { 5517 var oX = this.attr.zoom.factorx, 5518 oY = this.attr.zoom.factory; 5519 5520 this.attr.zoom.factorx = fX / this.zoomX; 5521 this.attr.zoom.factory = fY / this.zoomY; 5522 5523 this.zoomIn(); 5524 5525 this.attr.zoom.factorx = oX; 5526 this.attr.zoom.factory = oY; 5527 5528 return this; 5529 }, 5530 5531 /** 5532 * Inner, recursive method of removeObject. 5533 * 5534 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5535 * The element(s) is/are given by name, id or a reference. 5536 * @param {Boolean} [saveMethod=false] If saveMethod=true, the algorithm runs through all elements 5537 * and tests if the element to be deleted is a child element. If this is the case, it will be 5538 * removed from the list of child elements. If saveMethod=false (default), the element 5539 * is removed from the lists of child elements of all its ancestors. 5540 * The latter should be much faster. 5541 * @returns {JXG.Board} Reference to the board 5542 * @private 5543 */ 5544 _removeObj: function (object, saveMethod) { 5545 var el, o, i; 5546 5547 if (Type.isArray(object)) { 5548 for (i = 0; i < object.length; i++) { 5549 this._removeObj(object[i], saveMethod); 5550 } 5551 5552 return this; 5553 } 5554 5555 object = this.select(object); 5556 5557 // If the object which is about to be removed is unknown or a string, do nothing. 5558 // it is a string if a string was given and could not be resolved to an element. 5559 if (!Type.exists(object) || Type.isString(object)) { 5560 return this; 5561 } 5562 5563 try { 5564 // remove all children. 5565 for (el in object.childElements) { 5566 if (object.childElements.hasOwnProperty(el)) { 5567 object.childElements[el].board._removeObj(object.childElements[el]); 5568 } 5569 } 5570 5571 // Remove all children in elements like turtle 5572 for (el in object.objects) { 5573 if (object.objects.hasOwnProperty(el)) { 5574 object.objects[el].board._removeObj(object.objects[el]); 5575 } 5576 } 5577 5578 // Remove the element from the childElement list and the descendant list of all elements. 5579 if (saveMethod) { 5580 // Running through all objects has quadratic complexity if many objects are deleted. 5581 for (el in this.objects) { 5582 if (this.objects.hasOwnProperty(el)) { 5583 if ( 5584 Type.exists(this.objects[el].childElements) && 5585 Type.exists( 5586 this.objects[el].childElements.hasOwnProperty(object.id) 5587 ) 5588 ) { 5589 delete this.objects[el].childElements[object.id]; 5590 delete this.objects[el].descendants[object.id]; 5591 } 5592 } 5593 } 5594 } else if (Type.exists(object.ancestors)) { 5595 // Running through the ancestors should be much more efficient. 5596 for (el in object.ancestors) { 5597 if (object.ancestors.hasOwnProperty(el)) { 5598 if ( 5599 Type.exists(object.ancestors[el].childElements) && 5600 Type.exists( 5601 object.ancestors[el].childElements.hasOwnProperty(object.id) 5602 ) 5603 ) { 5604 delete object.ancestors[el].childElements[object.id]; 5605 delete object.ancestors[el].descendants[object.id]; 5606 } 5607 } 5608 } 5609 } 5610 5611 // remove the object itself from our control structures 5612 if (object._pos > -1) { 5613 this.objectsList.splice(object._pos, 1); 5614 // Quadratic complexity for reindexing the positions: 5615 for (i = object._pos; i < this.objectsList.length; i++) { 5616 o = this.objectsList[i]; 5617 if (o._pos > -1) { 5618 o._pos--; 5619 } 5620 } 5621 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) { 5622 JXG.debug( 5623 'Board.removeObject: object ' + object.id + ' not found in list.' 5624 ); 5625 } 5626 5627 delete this.objects[object.id]; 5628 delete this.elementsByName[object.name]; 5629 5630 if (object.visProp && object.evalVisProp('trace')) { 5631 object.clearTrace(); 5632 } 5633 5634 // the object deletion itself is handled by the object. 5635 if (Type.exists(object.remove)) { 5636 object.remove(); 5637 } 5638 } catch (e) { 5639 JXG.debug(object.id + ': Could not be removed: ' + e); 5640 } 5641 5642 return this; 5643 }, 5644 5645 /** 5646 * Removes object from board and from the renderer object. 5647 * <p> 5648 * <b>Performance hints:</b> It is recommended to use the JSXGraph object's id. 5649 * If many elements are removed, it is best to either 5650 * <ul> 5651 * <li> remove the whole array if the elements are contained in an array instead 5652 * of looping through the array OR 5653 * <li> call <tt>board.suspendUpdate()</tt> 5654 * before looping through the elements to be removed and call 5655 * <tt>board.unsuspendUpdate()</tt> after the loop. Further, it is advisable to loop 5656 * in reverse order, i.e. remove the object in reverse order of their creation time. 5657 * </ul> 5658 * @param {JXG.GeometryElement|Array} object The object to remove or array of objects to be removed. 5659 * The element(s) is/are given by name, id or a reference. 5660 * @param {Boolean} saveMethod If true, the algorithm runs through all elements 5661 * and tests if the element to be deleted is a child element. If yes, it will be 5662 * removed from the list of child elements. If false (default), the element 5663 * is removed from the lists of child elements of all its ancestors. 5664 * This should be much faster. 5665 * @returns {JXG.Board} Reference to the board 5666 */ 5667 removeObject: function (object, saveMethod) { 5668 var i; 5669 5670 this.renderer.suspendRedraw(this); 5671 if (Type.isArray(object)) { 5672 for (i = 0; i < object.length; i++) { 5673 this._removeObj(object[i], saveMethod); 5674 } 5675 } else { 5676 this._removeObj(object, saveMethod); 5677 } 5678 this.renderer.unsuspendRedraw(); 5679 5680 this.update(); 5681 return this; 5682 }, 5683 5684 /** 5685 * Removes the ancestors of an object an the object itself from board and renderer. 5686 * @param {JXG.GeometryElement} object The object to remove. 5687 * @returns {JXG.Board} Reference to the board 5688 */ 5689 removeAncestors: function (object) { 5690 var anc; 5691 5692 for (anc in object.ancestors) { 5693 if (object.ancestors.hasOwnProperty(anc)) { 5694 this.removeAncestors(object.ancestors[anc]); 5695 } 5696 } 5697 5698 this.removeObject(object); 5699 5700 return this; 5701 }, 5702 5703 /** 5704 * Initialize some objects which are contained in every GEONExT construction by default, 5705 * but are not contained in the gxt files. 5706 * @returns {JXG.Board} Reference to the board 5707 */ 5708 initGeonextBoard: function () { 5709 var p1, p2, p3; 5710 5711 p1 = this.create('point', [0, 0], { 5712 id: this.id + 'g00e0', 5713 name: 'Ursprung', 5714 withLabel: false, 5715 visible: false, 5716 fixed: true 5717 }); 5718 5719 p2 = this.create('point', [1, 0], { 5720 id: this.id + 'gX0e0', 5721 name: 'Punkt_1_0', 5722 withLabel: false, 5723 visible: false, 5724 fixed: true 5725 }); 5726 5727 p3 = this.create('point', [0, 1], { 5728 id: this.id + 'gY0e0', 5729 name: 'Punkt_0_1', 5730 withLabel: false, 5731 visible: false, 5732 fixed: true 5733 }); 5734 5735 this.create('line', [p1, p2], { 5736 id: this.id + 'gXLe0', 5737 name: 'X-Achse', 5738 withLabel: false, 5739 visible: false 5740 }); 5741 5742 this.create('line', [p1, p3], { 5743 id: this.id + 'gYLe0', 5744 name: 'Y-Achse', 5745 withLabel: false, 5746 visible: false 5747 }); 5748 5749 return this; 5750 }, 5751 5752 /** 5753 * Change the height and width of the board's container. 5754 * After doing so, {@link JXG.JSXGraph.setBoundingBox} is called using 5755 * the actual size of the bounding box and the actual value of keepaspectratio. 5756 * If setBoundingbox() should not be called automatically, 5757 * call resizeContainer with dontSetBoundingBox == true. 5758 * @param {Number} canvasWidth New width of the container. 5759 * @param {Number} canvasHeight New height of the container. 5760 * @param {Boolean} [dontset=false] If true do not set the CSS width and height of the DOM element. 5761 * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(), but keep view centered around original visible center. 5762 * @returns {JXG.Board} Reference to the board 5763 */ 5764 resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) { 5765 var box, 5766 oldWidth, oldHeight, 5767 oX, oY; 5768 5769 oldWidth = this.canvasWidth; 5770 oldHeight = this.canvasHeight; 5771 5772 if (!dontSetBoundingBox) { 5773 box = this.getBoundingBox(); // This is the actual bounding box. 5774 } 5775 5776 // this.canvasWidth = Math.max(parseFloat(canvasWidth), Mat.eps); 5777 // this.canvasHeight = Math.max(parseFloat(canvasHeight), Mat.eps); 5778 this.canvasWidth = parseFloat(canvasWidth); 5779 this.canvasHeight = parseFloat(canvasHeight); 5780 5781 if (!dontset) { 5782 this.containerObj.style.width = this.canvasWidth + 'px'; 5783 this.containerObj.style.height = this.canvasHeight + 'px'; 5784 } 5785 this.renderer.resize(this.canvasWidth, this.canvasHeight); 5786 5787 if (!dontSetBoundingBox) { 5788 this.setBoundingBox(box, this.keepaspectratio, 'keep'); 5789 } else { 5790 oX = (this.canvasWidth - oldWidth) * 0.5; 5791 oY = (this.canvasHeight - oldHeight) * 0.5; 5792 5793 this.moveOrigin( 5794 this.origin.scrCoords[1] + oX, 5795 this.origin.scrCoords[2] + oY 5796 ); 5797 } 5798 5799 return this; 5800 }, 5801 5802 /** 5803 * Lists the dependencies graph in a new HTML-window. 5804 * @returns {JXG.Board} Reference to the board 5805 */ 5806 showDependencies: function () { 5807 var el, t, c, f, i; 5808 5809 t = '<p>\n'; 5810 for (el in this.objects) { 5811 if (this.objects.hasOwnProperty(el)) { 5812 i = 0; 5813 for (c in this.objects[el].childElements) { 5814 if (this.objects[el].childElements.hasOwnProperty(c)) { 5815 i += 1; 5816 } 5817 } 5818 if (i >= 0) { 5819 t += '<strong>' + this.objects[el].id + ':<' + '/strong> '; 5820 } 5821 5822 for (c in this.objects[el].childElements) { 5823 if (this.objects[el].childElements.hasOwnProperty(c)) { 5824 t += 5825 this.objects[el].childElements[c].id + 5826 '(' + 5827 this.objects[el].childElements[c].name + 5828 ')' + 5829 ', '; 5830 } 5831 } 5832 t += '<p>\n'; 5833 } 5834 } 5835 t += '<' + '/p>\n'; 5836 f = window.open(); 5837 f.document.open(); 5838 f.document.write(t); 5839 f.document.close(); 5840 return this; 5841 }, 5842 5843 /** 5844 * Lists the XML code of the construction in a new HTML-window. 5845 * @returns {JXG.Board} Reference to the board 5846 */ 5847 showXML: function () { 5848 var f = window.open(''); 5849 f.document.open(); 5850 f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>'); 5851 f.document.close(); 5852 return this; 5853 }, 5854 5855 /** 5856 * Sets for all objects the needsUpdate flag to 'true'. 5857 * @param{JXG.GeometryElement} [drag=undefined] Optional element that is dragged. 5858 * @returns {JXG.Board} Reference to the board 5859 */ 5860 prepareUpdate: function (drag) { 5861 var el, i, 5862 pEl, 5863 len = this.objectsList.length; 5864 5865 /* 5866 if (this.attr.updatetype === 'hierarchical') { 5867 return this; 5868 } 5869 */ 5870 5871 for (el = 0; el < len; el++) { 5872 pEl = this.objectsList[el]; 5873 if (this._change3DView || 5874 (Type.exists(drag) && drag.elType === 'view3d_slider') 5875 ) { 5876 // The 3D view has changed. No elements are recomputed, 5877 // only 3D elements are projected to the new view. 5878 pEl.needsUpdate = 5879 pEl.visProp.element3d || 5880 pEl.elType === 'view3d' || 5881 pEl.elType === 'view3d_slider' || 5882 this.needsFullUpdate; 5883 5884 // Special case sphere3d in central projection: 5885 // We have to update the defining points of the ellipse 5886 if (pEl.visProp.element3d && 5887 pEl.visProp.element3d.type === Const.OBJECT_TYPE_SPHERE3D 5888 ) { 5889 for (i = 0; i < pEl.parents.length; i++) { 5890 this.objects[pEl.parents[i]].needsUpdate = true; 5891 } 5892 } 5893 } else { 5894 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5895 } 5896 } 5897 5898 for (el in this.groups) { 5899 if (this.groups.hasOwnProperty(el)) { 5900 pEl = this.groups[el]; 5901 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 5902 } 5903 } 5904 5905 return this; 5906 }, 5907 5908 /** 5909 * Runs through all elements and calls their update() method. 5910 * @param {JXG.GeometryElement} drag Element that caused the update. 5911 * @returns {JXG.Board} Reference to the board 5912 */ 5913 updateElements: function (drag) { 5914 var el, pEl; 5915 //var childId, i = 0; 5916 5917 drag = this.select(drag); 5918 5919 /* 5920 if (Type.exists(drag)) { 5921 for (el = 0; el < this.objectsList.length; el++) { 5922 pEl = this.objectsList[el]; 5923 if (pEl.id === drag.id) { 5924 i = el; 5925 break; 5926 } 5927 } 5928 } 5929 */ 5930 for (el = 0; el < this.objectsList.length; el++) { 5931 pEl = this.objectsList[el]; 5932 if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) { 5933 pEl.updateSize(); 5934 } 5935 5936 // For updates of an element we distinguish if the dragged element is updated or 5937 // other elements are updated. 5938 // The difference lies in the treatment of gliders and points based on transformations. 5939 pEl.update(!Type.exists(drag) || pEl.id !== drag.id).updateVisibility(); 5940 } 5941 5942 // update groups last 5943 for (el in this.groups) { 5944 if (this.groups.hasOwnProperty(el)) { 5945 this.groups[el].update(drag); 5946 } 5947 } 5948 5949 return this; 5950 }, 5951 5952 /** 5953 * Runs through all elements and calls their update() method. 5954 * @returns {JXG.Board} Reference to the board 5955 */ 5956 updateRenderer: function () { 5957 var el, 5958 len = this.objectsList.length, 5959 autoPositionLabelList = [], 5960 currentIndex, randomIndex; 5961 5962 if (!this.renderer) { 5963 return; 5964 } 5965 5966 /* 5967 objs = this.objectsList.slice(0); 5968 objs.sort(function (a, b) { 5969 if (a.visProp.layer < b.visProp.layer) { 5970 return -1; 5971 } else if (a.visProp.layer === b.visProp.layer) { 5972 return b.lastDragTime.getTime() - a.lastDragTime.getTime(); 5973 } else { 5974 return 1; 5975 } 5976 }); 5977 */ 5978 5979 if (this.renderer.type === 'canvas') { 5980 this.updateRendererCanvas(); 5981 } else { 5982 for (el = 0; el < len; el++) { 5983 if (this.objectsList[el].visProp.islabel && this.objectsList[el].visProp.autoposition) { 5984 autoPositionLabelList.push(el); 5985 } else { 5986 this.objectsList[el].updateRenderer(); 5987 } 5988 } 5989 5990 currentIndex = autoPositionLabelList.length; 5991 5992 // Randomize the order of the labels 5993 while (currentIndex !== 0) { 5994 randomIndex = Math.floor(Math.random() * currentIndex); 5995 currentIndex--; 5996 [autoPositionLabelList[currentIndex], autoPositionLabelList[randomIndex]] = [autoPositionLabelList[randomIndex], autoPositionLabelList[currentIndex]]; 5997 } 5998 5999 for (el = 0; el < autoPositionLabelList.length; el++) { 6000 this.objectsList[autoPositionLabelList[el]].updateRenderer(); 6001 } 6002 /* 6003 for (el = autoPositionLabelList.length - 1; el >= 0; el--) { 6004 this.objectsList[autoPositionLabelList[el]].updateRenderer(); 6005 } 6006 */ 6007 } 6008 return this; 6009 }, 6010 6011 /** 6012 * Runs through all elements and calls their update() method. 6013 * This is a special version for the CanvasRenderer. 6014 * Here, we have to do our own layer handling. 6015 * @returns {JXG.Board} Reference to the board 6016 */ 6017 updateRendererCanvas: function () { 6018 var el, pEl, 6019 olen = this.objectsList.length, 6020 // i, minim, lay, 6021 // layers = this.options.layer, 6022 // len = this.options.layer.numlayers, 6023 // last = Number.NEGATIVE_INFINITY.toExponential, 6024 depth_order_layers = [], 6025 objects_sorted, 6026 6027 /** 6028 * Function to sort elements for depth ordering in canvas renderer. 6029 * Only relevant for elements having a zIndex. 6030 * Sort the elements for the canvas rendering according to 6031 * their layer, _pos, depthOrder (with this priority). 6032 * @param {JXG.GeometryObject} a 6033 * @param {JXG.GeometryObject} b 6034 * @returns Number 6035 * @private 6036 */ 6037 _compareDepth = function(a, b) { 6038 if (a.visProp.layer !== b.visProp.layer) { 6039 // For elements in different layers, the element in the 6040 // higher layer is in front. 6041 return a.visProp.layer - b.visProp.layer; 6042 } 6043 6044 // From here on, both objects are in the same layer. 6045 6046 if (depth_order_layers.indexOf(a.visProp.layer) === -1) { 6047 // The layer is not depth ordered. 6048 return a._pos - b._pos; 6049 } 6050 6051 // From here on, both objects are in the same layer 6052 // and the layer is depth ordered. 6053 6054 // The objects are in the same layer and the layer is depth ordered 6055 // But neither element is the 2D element of a 3D element. 6056 if (!a.visProp.element3d && !b.visProp.element3d) { 6057 return a._pos - b._pos; 6058 } 6059 6060 if (a.visProp.element3d && !b.visProp.element3d) { 6061 return -1; 6062 } 6063 6064 if (!a.visProp.element3d && b.visProp.element3d) { 6065 return 1; 6066 } 6067 6068 // Finqally, both elements are 2D elements of a 3D element. 6069 return a.visProp.element3d.zIndex - b.visProp.element3d.zIndex; 6070 }; 6071 6072 // Only one view3d element is supported. Get the depth order layers and 6073 // update the zIndices of the 3D elements. 6074 for (el = 0; el < olen; el++) { 6075 pEl = this.objectsList[el]; 6076 if (pEl.elType === 'view3d' && 6077 pEl.evalVisProp('depthorder.enabled') 6078 ) { 6079 depth_order_layers = pEl.evalVisProp('depthorder.layers'); 6080 pEl.updateRenderer(); 6081 break; 6082 } 6083 } 6084 6085 // objects_sorted = this.objectsList.toSorted(_compareDepth); 6086 6087 // 3D elements are not rendered, but their subelements element2D 6088 objects_sorted = this.objectsList.filter(function(e) { return !e.is3D; }).toSorted(_compareDepth); 6089 olen = objects_sorted.length; 6090 for (el = 0; el < olen; el++) { 6091 if ( 6092 objects_sorted[el].visPropCalc.visible && 6093 objects_sorted[el].type !== Const.OBJECT_TYPE_FACE3D // For these, updateRenderer is triggered in polyhedron3d.updateRenderer 6094 ) { 6095 objects_sorted[el].prepareUpdate().updateRenderer(); 6096 } 6097 } 6098 6099 return this; 6100 }, 6101 6102 /** 6103 * Please use {@link JXG.Board.on} instead. 6104 * @param {Function} hook A function to be called by the board after an update occurred. 6105 * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>. 6106 * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the 6107 * board object the hook is attached to. 6108 * @returns {Number} Id of the hook, required to remove the hook from the board. 6109 * @deprecated 6110 */ 6111 addHook: function (hook, m, context) { 6112 JXG.deprecated('Board.addHook()', 'Board.on()'); 6113 m = Type.def(m, 'update'); 6114 6115 context = Type.def(context, this); 6116 6117 this.hooks.push([m, hook]); 6118 this.on(m, hook, context); 6119 6120 return this.hooks.length - 1; 6121 }, 6122 6123 /** 6124 * Alias of {@link JXG.Board.on}. 6125 */ 6126 addEvent: JXG.shortcut(JXG.Board.prototype, 'on'), 6127 6128 /** 6129 * Please use {@link JXG.Board.off} instead. 6130 * @param {Number|function} id The number you got when you added the hook or a reference to the event handler. 6131 * @returns {JXG.Board} Reference to the board 6132 * @deprecated 6133 */ 6134 removeHook: function (id) { 6135 JXG.deprecated('Board.removeHook()', 'Board.off()'); 6136 if (this.hooks[id]) { 6137 this.off(this.hooks[id][0], this.hooks[id][1]); 6138 this.hooks[id] = null; 6139 } 6140 6141 return this; 6142 }, 6143 6144 /** 6145 * Alias of {@link JXG.Board.off}. 6146 */ 6147 removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'), 6148 6149 /** 6150 * Runs through all hooked functions and calls them. 6151 * @returns {JXG.Board} Reference to the board 6152 * @deprecated 6153 */ 6154 updateHooks: function (m) { 6155 var arg = Array.prototype.slice.call(arguments, 0); 6156 6157 JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()'); 6158 6159 arg[0] = Type.def(arg[0], 'update'); 6160 this.triggerEventHandlers([arg[0]], arguments); 6161 6162 return this; 6163 }, 6164 6165 /** 6166 * Adds a dependent board to this board. 6167 * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred. 6168 * @returns {JXG.Board} Reference to the board 6169 */ 6170 addChild: function (board) { 6171 if (Type.exists(board) && Type.exists(board.containerObj)) { 6172 this.dependentBoards.push(board); 6173 this.update(); 6174 } 6175 return this; 6176 }, 6177 6178 /** 6179 * Deletes a board from the list of dependent boards. 6180 * @param {JXG.Board} board Reference to the board which will be removed. 6181 * @returns {JXG.Board} Reference to the board 6182 */ 6183 removeChild: function (board) { 6184 var i; 6185 6186 for (i = this.dependentBoards.length - 1; i >= 0; i--) { 6187 if (this.dependentBoards[i] === board) { 6188 this.dependentBoards.splice(i, 1); 6189 } 6190 } 6191 return this; 6192 }, 6193 6194 /** 6195 * Runs through most elements and calls their update() method and update the conditions. 6196 * @param {JXG.GeometryElement} [drag] Element that caused the update. 6197 * @returns {JXG.Board} Reference to the board 6198 */ 6199 update: function (drag) { 6200 var i, len, b, insert, storeActiveEl; 6201 6202 if (this.inUpdate || this.isSuspendedUpdate) { 6203 return this; 6204 } 6205 this.inUpdate = true; 6206 6207 if ( 6208 this.attr.minimizereflow === 'all' && 6209 this.containerObj && 6210 this.renderer.type !== 'vml' 6211 ) { 6212 storeActiveEl = this.document.activeElement; // Store focus element 6213 insert = this.renderer.removeToInsertLater(this.containerObj); 6214 } 6215 6216 if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') { 6217 storeActiveEl = this.document.activeElement; 6218 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot); 6219 } 6220 6221 this.prepareUpdate(drag).updateElements(drag).updateConditions(); 6222 6223 this.renderer.suspendRedraw(this); 6224 this.updateRenderer(); 6225 this.renderer.unsuspendRedraw(); 6226 this.triggerEventHandlers(['update'], []); 6227 6228 if (insert) { 6229 insert(); 6230 storeActiveEl.focus(); // Restore focus element 6231 } 6232 6233 // To resolve dependencies between boards 6234 // for (var board in JXG.boards) { 6235 len = this.dependentBoards.length; 6236 for (i = 0; i < len; i++) { 6237 b = this.dependentBoards[i]; 6238 if (Type.exists(b) && b !== this) { 6239 b.updateQuality = this.updateQuality; 6240 b.prepareUpdate().updateElements().updateConditions(); 6241 b.renderer.suspendRedraw(this); 6242 b.updateRenderer(); 6243 b.renderer.unsuspendRedraw(); 6244 b.triggerEventHandlers(['update'], []); 6245 } 6246 } 6247 6248 this.inUpdate = false; 6249 return this; 6250 }, 6251 6252 /** 6253 * Runs through all elements and calls their update() method and update the conditions. 6254 * This is necessary after zooming and changing the bounding box. 6255 * @returns {JXG.Board} Reference to the board 6256 */ 6257 fullUpdate: function () { 6258 this.needsFullUpdate = true; 6259 this.update(); 6260 this.needsFullUpdate = false; 6261 return this; 6262 }, 6263 6264 /** 6265 * Adds a grid to the board according to the settings given in board.options. 6266 * @returns {JXG.Board} Reference to the board. 6267 */ 6268 addGrid: function () { 6269 this.create('grid', []); 6270 6271 return this; 6272 }, 6273 6274 /** 6275 * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or 6276 * more of the grids. 6277 * @returns {JXG.Board} Reference to the board object. 6278 */ 6279 removeGrids: function () { 6280 var i; 6281 6282 for (i = 0; i < this.grids.length; i++) { 6283 this.removeObject(this.grids[i]); 6284 } 6285 6286 this.grids.length = 0; 6287 this.update(); // required for canvas renderer 6288 6289 return this; 6290 }, 6291 6292 /** 6293 * Creates a new geometric element of type elementType. 6294 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'. 6295 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two 6296 * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 6297 * methods for a list of possible parameters. 6298 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 6299 * Common attributes are name, visible, strokeColor. 6300 * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing 6301 * two or more elements. 6302 */ 6303 create: function (elementType, parents, attributes) { 6304 var el, i; 6305 6306 elementType = elementType.toLowerCase(); 6307 6308 if (!Type.exists(parents)) { 6309 parents = []; 6310 } 6311 6312 if (!Type.exists(attributes)) { 6313 attributes = {}; 6314 } 6315 6316 for (i = 0; i < parents.length; i++) { 6317 if ( 6318 Type.isString(parents[i]) && 6319 !(elementType === 'text' && i === 2) && 6320 !(elementType === 'solidofrevolution3d' && i === 2) && 6321 !(elementType === 'text3d' && (i === 2 || i === 4)) && 6322 !( 6323 (elementType === 'input' || 6324 elementType === 'checkbox' || 6325 elementType === 'button') && 6326 (i === 2 || i === 3) 6327 ) && 6328 !(elementType === 'curve' /*&& i > 0*/) && // Allow curve plots with jessiecode, parents[0] is the 6329 // variable name 6330 !(elementType === 'functiongraph') && // Prevent problems with function terms like 'x', 'y' 6331 !(elementType === 'implicitcurve') 6332 ) { 6333 if (i > 0 && parents[0].elType === 'view3d') { 6334 // 3D elements are based on 3D elements, only 6335 parents[i] = parents[0].select(parents[i]); 6336 } else { 6337 parents[i] = this.select(parents[i]); 6338 } 6339 } 6340 } 6341 6342 if (Type.isFunction(JXG.elements[elementType])) { 6343 el = JXG.elements[elementType](this, parents, attributes); 6344 } else { 6345 throw new Error('JSXGraph: create: Unknown element type given: ' + elementType); 6346 } 6347 6348 if (!Type.exists(el)) { 6349 JXG.debug('JSXGraph: create: failure creating ' + elementType); 6350 return el; 6351 } 6352 6353 if (el.prepareUpdate && el.update && el.updateRenderer) { 6354 el.fullUpdate(); 6355 } 6356 return el; 6357 }, 6358 6359 /** 6360 * Deprecated name for {@link JXG.Board.create}. 6361 * @deprecated 6362 */ 6363 createElement: function () { 6364 JXG.deprecated('Board.createElement()', 'Board.create()'); 6365 return this.create.apply(this, arguments); 6366 }, 6367 6368 /** 6369 * Delete the elements drawn as part of a trace of an element. 6370 * @returns {JXG.Board} Reference to the board 6371 */ 6372 clearTraces: function () { 6373 var el; 6374 6375 for (el = 0; el < this.objectsList.length; el++) { 6376 this.objectsList[el].clearTrace(); 6377 } 6378 6379 this.numTraces = 0; 6380 return this; 6381 }, 6382 6383 /** 6384 * Stop updates of the board. 6385 * @returns {JXG.Board} Reference to the board 6386 */ 6387 suspendUpdate: function () { 6388 if (!this.inUpdate) { 6389 this.isSuspendedUpdate = true; 6390 } 6391 return this; 6392 }, 6393 6394 /** 6395 * Enable updates of the board. 6396 * @returns {JXG.Board} Reference to the board 6397 */ 6398 unsuspendUpdate: function () { 6399 if (this.isSuspendedUpdate) { 6400 this.isSuspendedUpdate = false; 6401 this.fullUpdate(); 6402 } 6403 return this; 6404 }, 6405 6406 /** 6407 * Set the bounding box of the board. 6408 * @param {Array} bbox New bounding box [x1,y1,x2,y2] 6409 * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but 6410 * the resulting viewport may be larger. 6411 * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset' 6412 * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0). 6413 * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing. 6414 * @returns {JXG.Board} Reference to the board 6415 */ 6416 setBoundingBox: function (bbox, keepaspectratio, setZoom) { 6417 var h, w, ux, uy, 6418 offX = 0, 6419 offY = 0, 6420 zoom_ratio = 1, 6421 ratio, dx, dy, prev_w, prev_h, 6422 dim = Env.getDimensions(this.containerObj, this.document); 6423 6424 if (!Type.isArray(bbox)) { 6425 return this; 6426 } 6427 6428 if ( 6429 bbox[0] < this.maxboundingbox[0] - Mat.eps || 6430 bbox[1] > this.maxboundingbox[1] + Mat.eps || 6431 bbox[2] > this.maxboundingbox[2] + Mat.eps || 6432 bbox[3] < this.maxboundingbox[3] - Mat.eps 6433 ) { 6434 return this; 6435 } 6436 6437 if (!Type.exists(setZoom)) { 6438 setZoom = 'reset'; 6439 } 6440 6441 ux = this.unitX; 6442 uy = this.unitY; 6443 this.canvasWidth = parseFloat(dim.width); // parseInt(dim.width, 10); 6444 this.canvasHeight = parseFloat(dim.height); // parseInt(dim.height, 10); 6445 w = this.canvasWidth; 6446 h = this.canvasHeight; 6447 if (keepaspectratio) { 6448 if (this.keepaspectratio) { 6449 ratio = ux / uy; // Keep this ratio if keepaspectratio was true 6450 if (isNaN(ratio)) { 6451 ratio = 1.0; 6452 } 6453 } else { 6454 ratio = 1.0; 6455 } 6456 if (setZoom === 'keep') { 6457 zoom_ratio = this.zoomX / this.zoomY; 6458 } 6459 dx = bbox[2] - bbox[0]; 6460 dy = bbox[1] - bbox[3]; 6461 prev_w = ux * dx; 6462 prev_h = uy * dy; 6463 if (w >= h) { 6464 if (prev_w >= prev_h) { 6465 this.unitY = h / dy; 6466 this.unitX = this.unitY * ratio; 6467 } else { 6468 // Switch dominating interval 6469 this.unitY = h / Math.abs(dx) * Mat.sign(dy) / zoom_ratio; 6470 this.unitX = this.unitY * ratio; 6471 } 6472 } else { 6473 if (prev_h > prev_w) { 6474 this.unitX = w / dx; 6475 this.unitY = this.unitX / ratio; 6476 } else { 6477 // Switch dominating interval 6478 this.unitX = w / Math.abs(dy) * Mat.sign(dx) * zoom_ratio; 6479 this.unitY = this.unitX / ratio; 6480 } 6481 } 6482 // Add the additional units in equal portions left and right 6483 offX = (w / this.unitX - dx) * 0.5; 6484 // Add the additional units in equal portions above and below 6485 offY = (h / this.unitY - dy) * 0.5; 6486 this.keepaspectratio = true; 6487 } else { 6488 this.unitX = w / (bbox[2] - bbox[0]); 6489 this.unitY = h / (bbox[1] - bbox[3]); 6490 this.keepaspectratio = false; 6491 } 6492 6493 this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY)); 6494 6495 if (setZoom === 'update') { 6496 this.zoomX *= this.unitX / ux; 6497 this.zoomY *= this.unitY / uy; 6498 } else if (setZoom === 'reset') { 6499 this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0; 6500 this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0; 6501 } 6502 6503 return this; 6504 }, 6505 6506 /** 6507 * Get the bounding box of the board. 6508 * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner 6509 */ 6510 getBoundingBox: function () { 6511 var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this).usrCoords, 6512 lr = new Coords( 6513 Const.COORDS_BY_SCREEN, 6514 [this.canvasWidth, this.canvasHeight], 6515 this 6516 ).usrCoords; 6517 return [ul[1], ul[2], lr[1], lr[2]]; 6518 }, 6519 6520 /** 6521 * Sets the value of attribute <tt>key</tt> to <tt>value</tt>. 6522 * @param {String} key The attribute's name. 6523 * @param value The new value 6524 * @private 6525 */ 6526 _set: function (key, value) { 6527 key = key.toLocaleLowerCase(); 6528 6529 if ( 6530 value !== null && 6531 Type.isObject(value) && 6532 !Type.exists(value.id) && 6533 !Type.exists(value.name) 6534 ) { 6535 // value is of type {prop: val, prop: val,...} 6536 // Convert these attributes to lowercase, too 6537 // this.attr[key] = {}; 6538 // for (el in value) { 6539 // if (value.hasOwnProperty(el)) { 6540 // this.attr[key][el.toLocaleLowerCase()] = value[el]; 6541 // } 6542 // } 6543 Type.mergeAttr(this.attr[key], value); 6544 } else { 6545 this.attr[key] = value; 6546 } 6547 }, 6548 6549 /** 6550 * Sets an arbitrary number of attributes. This method has one or more 6551 * parameters of the following types: 6552 * <ul> 6553 * <li> object: {key1:value1,key2:value2,...} 6554 * <li> string: 'key:value' 6555 * <li> array: ['key', value] 6556 * </ul> 6557 * Some board attributes are immutable, like e.g. the renderer type. 6558 * 6559 * @param {Object} attributes An object with attributes 6560 * @param {Boolean} [force=false] if true the attributes are set regardless of the previous setting was identical. 6561 * @returns {JXG.Board} Reference to the board 6562 * 6563 * @example 6564 * const board = JXG.JSXGraph.initBoard('jxgbox', { 6565 * boundingbox: [-5, 5, 5, -5], 6566 * keepAspectRatio: false, 6567 * axis:true, 6568 * showFullscreen: true, 6569 * showScreenshot: true, 6570 * showCopyright: false 6571 * }); 6572 * 6573 * board.setAttribute({ 6574 * animationDelay: 10, 6575 * boundingbox: [-10, 5, 10, -5], 6576 * defaultAxes: { 6577 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6578 * }, 6579 * description: 'test', 6580 * fullscreen: { 6581 * scale: 0.5 6582 * }, 6583 * intl: { 6584 * enabled: true, 6585 * locale: 'de-DE' 6586 * } 6587 * }); 6588 * 6589 * board.setAttribute({ 6590 * selection: { 6591 * enabled: true, 6592 * fillColor: 'blue' 6593 * }, 6594 * showInfobox: false, 6595 * zoomX: 0.5, 6596 * zoomY: 2, 6597 * fullscreen: { symbol: 'x' }, 6598 * screenshot: { symbol: 'y' }, 6599 * showCopyright: true, 6600 * showFullscreen: false, 6601 * showScreenshot: false, 6602 * showZoom: false, 6603 * showNavigation: false 6604 * }); 6605 * board.setAttribute('showCopyright:false'); 6606 * 6607 * var p = board.create('point', [1, 1], {size: 10, 6608 * label: { 6609 * fontSize: 24, 6610 * highlightStrokeOpacity: 0.1, 6611 * offset: [5, 0] 6612 * } 6613 * }); 6614 * 6615 * 6616 * </pre><div id="JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc" class="jxgbox" style="width: 300px; height: 300px;"></div> 6617 * <script type="text/javascript"> 6618 * (function() { 6619 * const board = JXG.JSXGraph.initBoard('JXGea7b8e09-beac-4d95-9a0c-5fc1c761ffbc', { 6620 * boundingbox: [-5, 5, 5, -5], 6621 * keepAspectRatio: false, 6622 * axis:true, 6623 * showFullscreen: true, 6624 * showScreenshot: true, 6625 * showCopyright: false 6626 * }); 6627 * 6628 * board.setAttribute({ 6629 * animationDelay: 10, 6630 * boundingbox: [-10, 5, 10, -5], 6631 * defaultAxes: { 6632 * x: { strokeColor: 'blue', ticks: { strokeColor: 'blue'}} 6633 * }, 6634 * description: 'test', 6635 * fullscreen: { 6636 * scale: 0.5 6637 * }, 6638 * intl: { 6639 * enabled: true, 6640 * locale: 'de-DE' 6641 * } 6642 * }); 6643 * 6644 * board.setAttribute({ 6645 * selection: { 6646 * enabled: true, 6647 * fillColor: 'blue' 6648 * }, 6649 * showInfobox: false, 6650 * zoomX: 0.5, 6651 * zoomY: 2, 6652 * fullscreen: { symbol: 'x' }, 6653 * screenshot: { symbol: 'y' }, 6654 * showCopyright: true, 6655 * showFullscreen: false, 6656 * showScreenshot: false, 6657 * showZoom: false, 6658 * showNavigation: false 6659 * }); 6660 * 6661 * board.setAttribute('showCopyright:false'); 6662 * 6663 * var p = board.create('point', [1, 1], {size: 10, 6664 * label: { 6665 * fontSize: 24, 6666 * highlightStrokeOpacity: 0.1, 6667 * offset: [5, 0] 6668 * } 6669 * }); 6670 * 6671 * 6672 * })(); 6673 * 6674 * </script><pre> 6675 * 6676 * 6677 */ 6678 setAttribute: function (attr, force) { 6679 var i, arg, pair, 6680 key, value, oldvalue,// j, le, 6681 node, lst, e, 6682 attributes = {}; 6683 6684 // Normalize the user input 6685 for (i = 0; i < arguments.length; i++) { 6686 arg = arguments[i]; 6687 if (Type.isString(arg)) { 6688 // pairRaw is string of the form 'key:value' 6689 pair = arg.split(":"); 6690 attributes[Type.trim(pair[0])] = Type.trim(pair[1]); 6691 } else if (!Type.isArray(arg)) { 6692 // pairRaw consists of objects of the form {key1:value1,key2:value2,...} 6693 JXG.extend(attributes, arg); 6694 } else { 6695 // pairRaw consists of array [key,value] 6696 attributes[arg[0]] = arg[1]; 6697 } 6698 } 6699 6700 for (i in attributes) { 6701 if (attributes.hasOwnProperty(i)) { 6702 key = i.replace(/\s+/g, "").toLowerCase(); 6703 value = attributes[i]; 6704 } 6705 value = (value.toLowerCase && value.toLowerCase() === 'false') 6706 ? false 6707 : value; 6708 oldvalue = this.attr[key]; 6709 if (!force && oldvalue === value) { 6710 continue; 6711 } 6712 switch (key) { 6713 case 'axis': 6714 if (Type.exists(this.defaultAxes)) { 6715 this.defaultAxes.x.setAttribute({ visible: value }); 6716 this.defaultAxes.y.setAttribute({ visible: value }); 6717 this.attr[key] = value; 6718 } else { 6719 if (value === true) { 6720 // create the default axis 6721 this.defaultAxes = {}; 6722 this.defaultAxes.x = this.create("axis", [[0, 0], [1, 0]]); 6723 this.defaultAxes.y = this.create("axis", [[0, 0], [0, 1]]); 6724 this.attr[key] = true; 6725 } 6726 } 6727 break; 6728 case 'cssstyle': 6729 lst = Type.css2js(value); 6730 node = this.containerObj; 6731 // node = this.renderer.svgRoot; 6732 for (e in lst) if (lst.hasOwnProperty(e)) { 6733 pair = lst[e]; 6734 node.style[pair.key] = pair.val; 6735 } 6736 6737 this._set(key, value); 6738 break; 6739 case 'boundingbox': 6740 this.setBoundingBox(value, this.keepaspectratio); 6741 this._set(key, value); 6742 break; 6743 case 'defaultaxes': 6744 if (Type.exists(this.defaultAxes.x) && Type.exists(value.x)) { 6745 this.defaultAxes.x.setAttribute(value.x); 6746 } 6747 if (Type.exists(this.defaultAxes.y) && Type.exists(value.y)) { 6748 this.defaultAxes.y.setAttribute(value.y); 6749 } 6750 break; 6751 case 'title': 6752 this.document.getElementById(this.container + '_ARIAlabel') 6753 .innerText = value; 6754 this._set(key, value); 6755 break; 6756 case 'keepaspectratio': 6757 this._set(key, value); 6758 this.setBoundingBox(this.getBoundingBox(), value, 'keep'); 6759 break; 6760 6761 // /* eslint-disable no-fallthrough */ 6762 case 'document': 6763 case 'maxboundingbox': 6764 this[key] = value; 6765 this._set(key, value); 6766 break; 6767 6768 case 'zoomx': 6769 case 'zoomy': 6770 this[key] = value; 6771 this._set(key, value); 6772 this.setZoom(this.attr.zoomx, this.attr.zoomy); 6773 break; 6774 6775 case 'registerevents': 6776 case 'renderer': 6777 // immutable, i.e. ignored 6778 break; 6779 6780 case 'fullscreen': 6781 case 'screenshot': 6782 node = this.containerObj.ownerDocument.getElementById( 6783 this.container + '_navigation_' + key); 6784 if (node && Type.exists(value.symbol)) { 6785 node.innerText = Type.evaluate(value.symbol); 6786 } 6787 this._set(key, value); 6788 break; 6789 6790 case 'selection': 6791 value.visible = false; 6792 value.withLines = false; 6793 value.vertices = { visible: false }; 6794 this._set(key, value); 6795 break; 6796 6797 case 'showcopyright': 6798 if (this.renderer.type === 'svg') { 6799 node = this.containerObj.ownerDocument.getElementById( 6800 this.renderer.uniqName('licenseText') 6801 ); 6802 if (node) { 6803 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none'); 6804 } else if (Type.evaluate(value)) { 6805 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 6806 } 6807 } 6808 this._set(key, value); 6809 break; 6810 6811 case 'showlogo': 6812 if (this.renderer.type === 'svg') { 6813 node = this.containerObj.ownerDocument.getElementById( 6814 this.renderer.uniqName('licenseLogo') 6815 ); 6816 if (node) { 6817 node.style.display = ((Type.evaluate(value)) ? 'inline' : 'none'); 6818 } else if (Type.evaluate(value)) { 6819 this.renderer.displayLogo(Const.licenseLogo, parseInt(this.options.text.fontSize, 10)); 6820 } 6821 } 6822 this._set(key, value); 6823 break; 6824 6825 default: 6826 if (Type.exists(this.attr[key])) { 6827 this._set(key, value); 6828 } 6829 break; 6830 // /* eslint-enable no-fallthrough */ 6831 } 6832 } 6833 6834 // Redraw navbar to handle the remaining show* attributes 6835 node = this.containerObj.ownerDocument.getElementById(this.container + "_navigationbar"); 6836 if (Type.exists(node)) { 6837 node.remove(); 6838 this.renderer.drawNavigationBar(this, this.attr.navbar); 6839 } 6840 6841 this.triggerEventHandlers(["attribute"], [attributes, this]); 6842 this.fullUpdate(); 6843 6844 return this; 6845 }, 6846 6847 /** 6848 * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the 6849 * animated elements. This function tells the board about new elements to animate. 6850 * @param {JXG.GeometryElement} element The element which is to be animated. 6851 * @returns {JXG.Board} Reference to the board 6852 */ 6853 addAnimation: function (element) { 6854 var that = this; 6855 6856 this.animationObjects[element.id] = element; 6857 6858 if (!this.animationIntervalCode) { 6859 this.animationIntervalCode = window.setInterval(function () { 6860 that.animate(); 6861 }, element.board.attr.animationdelay); 6862 } 6863 6864 return this; 6865 }, 6866 6867 /** 6868 * Cancels all running animations. 6869 * @returns {JXG.Board} Reference to the board 6870 */ 6871 stopAllAnimation: function () { 6872 var el; 6873 6874 for (el in this.animationObjects) { 6875 if ( 6876 this.animationObjects.hasOwnProperty(el) && 6877 Type.exists(this.animationObjects[el]) 6878 ) { 6879 this.animationObjects[el] = null; 6880 delete this.animationObjects[el]; 6881 } 6882 } 6883 6884 window.clearInterval(this.animationIntervalCode); 6885 delete this.animationIntervalCode; 6886 6887 return this; 6888 }, 6889 6890 /** 6891 * General purpose animation function. This currently only supports moving points from one place to another. This 6892 * is faster than managing the animation per point, especially if there is more than one animated point at the same time. 6893 * @returns {JXG.Board} Reference to the board 6894 */ 6895 animate: function () { 6896 var props, 6897 el, 6898 o, 6899 newCoords, 6900 r, 6901 p, 6902 c, 6903 cbtmp, 6904 count = 0, 6905 obj = null; 6906 6907 for (el in this.animationObjects) { 6908 if ( 6909 this.animationObjects.hasOwnProperty(el) && 6910 Type.exists(this.animationObjects[el]) 6911 ) { 6912 count += 1; 6913 o = this.animationObjects[el]; 6914 6915 if (o.animationPath) { 6916 if (Type.isFunction(o.animationPath)) { 6917 newCoords = o.animationPath( 6918 new Date().getTime() - o.animationStart 6919 ); 6920 } else { 6921 newCoords = o.animationPath.pop(); 6922 } 6923 6924 if ( 6925 !Type.exists(newCoords) || 6926 (!Type.isArray(newCoords) && isNaN(newCoords)) 6927 ) { 6928 delete o.animationPath; 6929 } else { 6930 o.setPositionDirectly(Const.COORDS_BY_USER, newCoords); 6931 o.fullUpdate(); 6932 obj = o; 6933 } 6934 } 6935 if (o.animationData) { 6936 c = 0; 6937 6938 for (r in o.animationData) { 6939 if (o.animationData.hasOwnProperty(r)) { 6940 p = o.animationData[r].pop(); 6941 6942 if (!Type.exists(p)) { 6943 delete o.animationData[p]; 6944 } else { 6945 c += 1; 6946 props = {}; 6947 props[r] = p; 6948 o.setAttribute(props); 6949 } 6950 } 6951 } 6952 6953 if (c === 0) { 6954 delete o.animationData; 6955 } 6956 } 6957 6958 if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) { 6959 this.animationObjects[el] = null; 6960 delete this.animationObjects[el]; 6961 6962 if (Type.exists(o.animationCallback)) { 6963 cbtmp = o.animationCallback; 6964 o.animationCallback = null; 6965 cbtmp(); 6966 } 6967 } 6968 } 6969 } 6970 6971 if (count === 0) { 6972 window.clearInterval(this.animationIntervalCode); 6973 delete this.animationIntervalCode; 6974 } else { 6975 this.update(obj); 6976 } 6977 6978 return this; 6979 }, 6980 6981 /** 6982 * Migrate the dependency properties of the point src 6983 * to the point dest and delete the point src. 6984 * For example, a circle around the point src 6985 * receives the new center dest. The old center src 6986 * will be deleted. 6987 * @param {JXG.Point} src Original point which will be deleted 6988 * @param {JXG.Point} dest New point with the dependencies of src. 6989 * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the 6990 * dest element. 6991 * @returns {JXG.Board} Reference to the board 6992 */ 6993 migratePoint: function (src, dest, copyName) { 6994 var child, 6995 childId, 6996 prop, 6997 found, 6998 i, 6999 srcLabelId, 7000 srcHasLabel = false; 7001 7002 src = this.select(src); 7003 dest = this.select(dest); 7004 7005 if (Type.exists(src.label)) { 7006 srcLabelId = src.label.id; 7007 srcHasLabel = true; 7008 this.removeObject(src.label); 7009 } 7010 7011 for (childId in src.childElements) { 7012 if (src.childElements.hasOwnProperty(childId)) { 7013 child = src.childElements[childId]; 7014 found = false; 7015 7016 for (prop in child) { 7017 if (child.hasOwnProperty(prop)) { 7018 if (child[prop] === src) { 7019 child[prop] = dest; 7020 found = true; 7021 } 7022 } 7023 } 7024 7025 if (found) { 7026 delete src.childElements[childId]; 7027 } 7028 7029 for (i = 0; i < child.parents.length; i++) { 7030 if (child.parents[i] === src.id) { 7031 child.parents[i] = dest.id; 7032 } 7033 } 7034 7035 dest.addChild(child); 7036 } 7037 } 7038 7039 // The destination object should receive the name 7040 // and the label of the originating (src) object 7041 if (copyName) { 7042 if (srcHasLabel) { 7043 delete dest.childElements[srcLabelId]; 7044 delete dest.descendants[srcLabelId]; 7045 } 7046 7047 if (dest.label) { 7048 this.removeObject(dest.label); 7049 } 7050 7051 delete this.elementsByName[dest.name]; 7052 dest.name = src.name; 7053 if (srcHasLabel) { 7054 dest.createLabel(); 7055 } 7056 } 7057 7058 this.removeObject(src); 7059 7060 if (Type.exists(dest.name) && dest.name !== '') { 7061 this.elementsByName[dest.name] = dest; 7062 } 7063 7064 this.fullUpdate(); 7065 7066 return this; 7067 }, 7068 7069 /** 7070 * Initializes color blindness simulation. 7071 * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'. 7072 * @returns {JXG.Board} Reference to the board 7073 */ 7074 emulateColorblindness: function (deficiency) { 7075 var e, o; 7076 7077 if (!Type.exists(deficiency)) { 7078 deficiency = 'none'; 7079 } 7080 7081 if (this.currentCBDef === deficiency) { 7082 return this; 7083 } 7084 7085 for (e in this.objects) { 7086 if (this.objects.hasOwnProperty(e)) { 7087 o = this.objects[e]; 7088 7089 if (deficiency !== 'none') { 7090 if (this.currentCBDef === 'none') { 7091 // this could be accomplished by JXG.extend, too. But do not use 7092 // JXG.deepCopy as this could result in an infinite loop because in 7093 // visProp there could be geometry elements which contain the board which 7094 // contains all objects which contain board etc. 7095 o.visPropOriginal = { 7096 strokecolor: o.visProp.strokecolor, 7097 fillcolor: o.visProp.fillcolor, 7098 highlightstrokecolor: o.visProp.highlightstrokecolor, 7099 highlightfillcolor: o.visProp.highlightfillcolor 7100 }; 7101 } 7102 o.setAttribute({ 7103 strokecolor: Color.rgb2cb( 7104 o.eval(o.visPropOriginal.strokecolor), 7105 deficiency 7106 ), 7107 fillcolor: Color.rgb2cb( 7108 o.eval(o.visPropOriginal.fillcolor), 7109 deficiency 7110 ), 7111 highlightstrokecolor: Color.rgb2cb( 7112 o.eval(o.visPropOriginal.highlightstrokecolor), 7113 deficiency 7114 ), 7115 highlightfillcolor: Color.rgb2cb( 7116 o.eval(o.visPropOriginal.highlightfillcolor), 7117 deficiency 7118 ) 7119 }); 7120 } else if (Type.exists(o.visPropOriginal)) { 7121 JXG.extend(o.visProp, o.visPropOriginal); 7122 } 7123 } 7124 } 7125 this.currentCBDef = deficiency; 7126 this.update(); 7127 7128 return this; 7129 }, 7130 7131 /** 7132 * Select a single or multiple elements at once. 7133 * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will 7134 * be used as a filter to return multiple elements at once filtered by the properties of the object. 7135 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 7136 * The advanced filters consisting of objects or functions are ignored. 7137 * @returns {JXG.GeometryElement|JXG.Composition} 7138 * @example 7139 * // select the element with name A 7140 * board.select('A'); 7141 * 7142 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 7143 * board.select({ 7144 * strokeColor: 'red' 7145 * }); 7146 * 7147 * // select all points on or below the x axis and make them black. 7148 * board.select({ 7149 * elementClass: JXG.OBJECT_CLASS_POINT, 7150 * Y: function (v) { 7151 * return v <= 0; 7152 * } 7153 * }).setAttribute({color: 'black'}); 7154 * 7155 * // select all elements 7156 * board.select(function (el) { 7157 * return true; 7158 * }); 7159 */ 7160 select: function (str, onlyByIdOrName) { 7161 var flist, 7162 olist, 7163 i, 7164 l, 7165 s = str; 7166 7167 if (s === null) { 7168 return s; 7169 } 7170 7171 // It's a string, most likely an id or a name. 7172 if (Type.isString(s) && s !== '') { 7173 // Search by ID 7174 if (Type.exists(this.objects[s])) { 7175 s = this.objects[s]; 7176 // Search by name 7177 } else if (Type.exists(this.elementsByName[s])) { 7178 s = this.elementsByName[s]; 7179 // Search by group ID 7180 } else if (Type.exists(this.groups[s])) { 7181 s = this.groups[s]; 7182 } 7183 7184 // It's a function or an object, but not an element 7185 } else if ( 7186 !onlyByIdOrName && 7187 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) 7188 ) { 7189 flist = Type.filterElements(this.objectsList, s); 7190 7191 olist = {}; 7192 l = flist.length; 7193 for (i = 0; i < l; i++) { 7194 olist[flist[i].id] = flist[i]; 7195 } 7196 s = new Composition(olist); 7197 7198 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 7199 } else if ( 7200 Type.isObject(s) && 7201 Type.exists(s.id) && 7202 !Type.exists(this.objects[s.id]) 7203 ) { 7204 s = null; 7205 } 7206 7207 return s; 7208 }, 7209 7210 /** 7211 * Checks if the given point is inside the boundingbox. 7212 * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object. 7213 * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object. 7214 * @returns {Boolean} 7215 */ 7216 hasPoint: function (x, y) { 7217 var px = x, 7218 py = y, 7219 bbox = this.getBoundingBox(); 7220 7221 if (Type.exists(x) && Type.isArray(x.usrCoords)) { 7222 px = x.usrCoords[1]; 7223 py = x.usrCoords[2]; 7224 } 7225 7226 return !!( 7227 Type.isNumber(px) && 7228 Type.isNumber(py) && 7229 bbox[0] < px && 7230 px < bbox[2] && 7231 bbox[1] > py && 7232 py > bbox[3] 7233 ); 7234 }, 7235 7236 /** 7237 * Update CSS transformations of type scaling. It is used to correct the mouse position 7238 * in {@link JXG.Board.getMousePosition}. 7239 * The inverse transformation matrix is updated on each mouseDown and touchStart event. 7240 * 7241 * It is up to the user to call this method after an update of the CSS transformation 7242 * in the DOM. 7243 */ 7244 updateCSSTransforms: function () { 7245 var obj = this.containerObj, 7246 o = obj, 7247 o2 = obj; 7248 7249 this.cssTransMat = Env.getCSSTransformMatrix(o); 7250 7251 // Newer variant of walking up the tree. 7252 // We walk up all parent nodes and collect possible CSS transforms. 7253 // Works also for ShadowDOM 7254 if (Type.exists(o.getRootNode)) { 7255 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 7256 while (o) { 7257 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7258 o = o.parentNode === o.getRootNode() ? o.parentNode.host : o.parentNode; 7259 } 7260 this.cssTransMat = Mat.inverse(this.cssTransMat); 7261 } else { 7262 /* 7263 * This is necessary for IE11 7264 */ 7265 o = o.offsetParent; 7266 while (o) { 7267 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7268 7269 o2 = o2.parentNode; 7270 while (o2 !== o) { 7271 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 7272 o2 = o2.parentNode; 7273 } 7274 o = o.offsetParent; 7275 } 7276 this.cssTransMat = Mat.inverse(this.cssTransMat); 7277 } 7278 return this; 7279 }, 7280 7281 /** 7282 * Start selection mode. This function can either be triggered from outside or by 7283 * a down event together with correct key pressing. The default keys are 7284 * shift+ctrl. But this can be changed in the options. 7285 * 7286 * Starting from out side can be realized for example with a button like this: 7287 * <pre> 7288 * <button onclick='board.startSelectionMode()'>Start</button> 7289 * </pre> 7290 * @example 7291 * // 7292 * // Set a new bounding box from the selection rectangle 7293 * // 7294 * var board = JXG.JSXGraph.initBoard('jxgbox', { 7295 * boundingBox:[-3,2,3,-2], 7296 * keepAspectRatio: false, 7297 * axis:true, 7298 * selection: { 7299 * enabled: true, 7300 * needShift: false, 7301 * needCtrl: true, 7302 * withLines: false, 7303 * vertices: { 7304 * visible: false 7305 * }, 7306 * fillColor: '#ffff00', 7307 * } 7308 * }); 7309 * 7310 * var f = function f(x) { return Math.cos(x); }, 7311 * curve = board.create('functiongraph', [f]); 7312 * 7313 * board.on('stopselecting', function(){ 7314 * var box = board.stopSelectionMode(), 7315 * 7316 * // bbox has the coordinates of the selection rectangle. 7317 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7318 * // are homogeneous coordinates. 7319 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7320 * 7321 * // Set a new bounding box 7322 * board.setBoundingBox(bbox, false); 7323 * }); 7324 * 7325 * 7326 * </pre><div class='jxgbox' id='JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723' style='width: 300px; height: 300px;'></div> 7327 * <script type='text/javascript'> 7328 * (function() { 7329 * // 7330 * // Set a new bounding box from the selection rectangle 7331 * // 7332 * var board = JXG.JSXGraph.initBoard('JXG11eff3a6-8c50-11e5-b01d-901b0e1b8723', { 7333 * boundingBox:[-3,2,3,-2], 7334 * keepAspectRatio: false, 7335 * axis:true, 7336 * selection: { 7337 * enabled: true, 7338 * needShift: false, 7339 * needCtrl: true, 7340 * withLines: false, 7341 * vertices: { 7342 * visible: false 7343 * }, 7344 * fillColor: '#ffff00', 7345 * } 7346 * }); 7347 * 7348 * var f = function f(x) { return Math.cos(x); }, 7349 * curve = board.create('functiongraph', [f]); 7350 * 7351 * board.on('stopselecting', function(){ 7352 * var box = board.stopSelectionMode(), 7353 * 7354 * // bbox has the coordinates of the selection rectangle. 7355 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 7356 * // are homogeneous coordinates. 7357 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 7358 * 7359 * // Set a new bounding box 7360 * board.setBoundingBox(bbox, false); 7361 * }); 7362 * })(); 7363 * 7364 * </script><pre> 7365 * 7366 */ 7367 startSelectionMode: function () { 7368 this.selectingMode = true; 7369 this.selectionPolygon.setAttribute({ visible: true }); 7370 this.selectingBox = [ 7371 [0, 0], 7372 [0, 0] 7373 ]; 7374 this._setSelectionPolygonFromBox(); 7375 this.selectionPolygon.fullUpdate(); 7376 }, 7377 7378 /** 7379 * Finalize the selection: disable selection mode and return the coordinates 7380 * of the selection rectangle. 7381 * @returns {Array} Coordinates of the selection rectangle. The array 7382 * contains two {@link JXG.Coords} objects. One the upper left corner and 7383 * the second for the lower right corner. 7384 */ 7385 stopSelectionMode: function () { 7386 this.selectingMode = false; 7387 this.selectionPolygon.setAttribute({ visible: false }); 7388 return [ 7389 this.selectionPolygon.vertices[0].coords, 7390 this.selectionPolygon.vertices[2].coords 7391 ]; 7392 }, 7393 7394 /** 7395 * Start the selection of a region. 7396 * @private 7397 * @param {Array} pos Screen coordiates of the upper left corner of the 7398 * selection rectangle. 7399 */ 7400 _startSelecting: function (pos) { 7401 this.isSelecting = true; 7402 this.selectingBox = [ 7403 [pos[0], pos[1]], 7404 [pos[0], pos[1]] 7405 ]; 7406 this._setSelectionPolygonFromBox(); 7407 }, 7408 7409 /** 7410 * Update the selection rectangle during a move event. 7411 * @private 7412 * @param {Array} pos Screen coordiates of the move event 7413 */ 7414 _moveSelecting: function (pos) { 7415 if (this.isSelecting) { 7416 this.selectingBox[1] = [pos[0], pos[1]]; 7417 this._setSelectionPolygonFromBox(); 7418 this.selectionPolygon.fullUpdate(); 7419 } 7420 }, 7421 7422 /** 7423 * Update the selection rectangle during an up event. Stop selection. 7424 * @private 7425 * @param {Object} evt Event object 7426 */ 7427 _stopSelecting: function (evt) { 7428 var pos = this.getMousePosition(evt); 7429 7430 this.isSelecting = false; 7431 this.selectingBox[1] = [pos[0], pos[1]]; 7432 this._setSelectionPolygonFromBox(); 7433 }, 7434 7435 /** 7436 * Update the Selection rectangle. 7437 * @private 7438 */ 7439 _setSelectionPolygonFromBox: function () { 7440 var A = this.selectingBox[0], 7441 B = this.selectingBox[1]; 7442 7443 this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7444 A[0], 7445 A[1] 7446 ]); 7447 this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7448 A[0], 7449 B[1] 7450 ]); 7451 this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7452 B[0], 7453 B[1] 7454 ]); 7455 this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [ 7456 B[0], 7457 A[1] 7458 ]); 7459 }, 7460 7461 /** 7462 * Test if a down event should start a selection. Test if the 7463 * required keys are pressed. If yes, {@link JXG.Board.startSelectionMode} is called. 7464 * @param {Object} evt Event object 7465 */ 7466 _testForSelection: function (evt) { 7467 if (this._isRequiredKeyPressed(evt, 'selection')) { 7468 if (!Type.exists(this.selectionPolygon)) { 7469 this._createSelectionPolygon(this.attr); 7470 } 7471 this.startSelectionMode(); 7472 } 7473 }, 7474 7475 /** 7476 * Create the internal selection polygon, which will be available as board.selectionPolygon. 7477 * @private 7478 * @param {Object} attr board attributes, e.g. the subobject board.attr. 7479 * @returns {Object} pointer to the board to enable chaining. 7480 */ 7481 _createSelectionPolygon: function (attr) { 7482 var selectionattr; 7483 7484 if (!Type.exists(this.selectionPolygon)) { 7485 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection'); 7486 if (selectionattr.enabled === true) { 7487 this.selectionPolygon = this.create( 7488 'polygon', 7489 [ 7490 [0, 0], 7491 [0, 0], 7492 [0, 0], 7493 [0, 0] 7494 ], 7495 selectionattr 7496 ); 7497 } 7498 } 7499 7500 return this; 7501 }, 7502 7503 /** 7504 * Reset the sketchcurves in board.sketches[] to length 0 and add the position 7505 * of the event as first point of the sketch curve. Called at down events. 7506 * <p> 7507 * Sets board.isSketching[i] = true where i depends on the finger (1st or 2nd). 7508 * 7509 * @private 7510 * @param {Object} evt Event object 7511 * @see JXG.Board#addToSketchCurve 7512 * @see JXG.Board#finalizeSketchCurve 7513 */ 7514 initSketchCurve: function(evt) { 7515 var i, c; 7516 // Init sketchcurves 7517 if (this.mode !== this.BOARD_MODE_MOVE_ORIGIN) { 7518 // Add coords to sketch curves 7519 // Only first and second finger are stored 7520 c = this.getUsrCoordsOfMouse(evt); 7521 i = (evt.isPrimary) ? 0 : 1; 7522 if (Type.exists(this.sketches[i])) { 7523 this.sketches[i].dataX = [c[0]]; 7524 this.sketches[i].dataY = [c[1]]; 7525 this.isSketching[i] = true; 7526 } 7527 } 7528 }, 7529 7530 /** 7531 * Add the position of the event to the sketchcurve i in board.sketches[]. 7532 * Called at move events. 7533 * Point is only added if board.isSketching[i] = true. 7534 * 7535 * @private 7536 * @param {Object} evt Event object 7537 * @see JXG.Board#initSketchCurve 7538 * @see JXG.Board#finalizeSketchCurve 7539 */ 7540 addToSketchCurve: function(evt) { 7541 var i, c, len; 7542 7543 // Add coords to sketchcurves 7544 // Only first and second finger are stored 7545 i = (evt.isPrimary) ? 0 : 1; 7546 if (this.attr.sketches.enabled && this.isSketching[i] === true) { 7547 if (Type.exists(this.sketches[i])) { 7548 c = this.getUsrCoordsOfMouse(evt); 7549 this.sketches[i].dataX.push(c[0]); 7550 this.sketches[i].dataY.push(c[1]); 7551 7552 len = this.sketches[i].evalVisProp('maxlength'); 7553 if (len !== null && this.sketches[i].dataX.length > len) { 7554 this.sketches[i].dataX = this.sketches[i].dataX.slice(-len); 7555 this.sketches[i].dataY = this.sketches[i].dataY.slice(-len); 7556 } 7557 if (this.sketches[i].evalVisProp('visible')) { 7558 this.update(); 7559 } 7560 } 7561 } 7562 }, 7563 7564 /** 7565 * Ends adding points to the sketchcurve i in board.sketches[]. 7566 * Called at up events. 7567 * Sets board.isSketching[i] = false. 7568 * Empties the curve if deleteOnUp==true; 7569 * 7570 * @private 7571 * @param {Object} evt Event object 7572 * @see JXG.Board#initSketchCurve 7573 * @see JXG.Board#addToSketchCurve 7574 */ 7575 finalizeSketchCurve: function(evt) { 7576 var i; 7577 7578 // Stop sketching into this.sketches 7579 i = (evt.isPrimary) ? 0 : 1; 7580 if (this.attr.sketches.enabled) { 7581 if (Type.exists(this.sketches[i])) { 7582 this.isSketching[i] = false; 7583 if (this.sketches[i].evalVisProp('deleteOnUp')) { 7584 this.sketches[i].dataX = []; 7585 this.sketches[i].dataY = []; 7586 } 7587 } 7588 } 7589 }, 7590 7591 /* ************************** 7592 * EVENT DEFINITION 7593 * for documentation purposes 7594 * ************************** */ 7595 7596 //region Event handler documentation 7597 7598 /** 7599 * @event 7600 * @description Whenever the {@link JXG.Board#setAttribute} is called. 7601 * @name JXG.Board#attribute 7602 * @param {Event} e The browser's event object. 7603 */ 7604 __evt__attribute: function (e) { }, 7605 7606 /** 7607 * @event 7608 * @description Whenever the user starts to touch or click the board. 7609 * @name JXG.Board#down 7610 * @param {Event} e The browser's event object. 7611 */ 7612 __evt__down: function (e) { }, 7613 7614 /** 7615 * @event 7616 * @description Whenever the user starts to click on the board. 7617 * @name JXG.Board#mousedown 7618 * @param {Event} e The browser's event object. 7619 */ 7620 __evt__mousedown: function (e) { }, 7621 7622 /** 7623 * @event 7624 * @description Whenever the user taps the pen on the board. 7625 * @name JXG.Board#pendown 7626 * @param {Event} e The browser's event object. 7627 */ 7628 __evt__pendown: function (e) { }, 7629 7630 /** 7631 * @event 7632 * @description Whenever the user starts to click on the board with a 7633 * device sending pointer events. 7634 * @name JXG.Board#pointerdown 7635 * @param {Event} e The browser's event object. 7636 */ 7637 __evt__pointerdown: function (e) { }, 7638 7639 /** 7640 * @event 7641 * @description Whenever the user starts to touch the board. 7642 * @name JXG.Board#touchstart 7643 * @param {Event} e The browser's event object. 7644 */ 7645 __evt__touchstart: function (e) { }, 7646 7647 /** 7648 * @event 7649 * @description Whenever the user stops to touch or click the board. 7650 * @name JXG.Board#up 7651 * @param {Event} e The browser's event object. 7652 */ 7653 __evt__up: function (e) { }, 7654 7655 /** 7656 * @event 7657 * @description Whenever the user releases the mousebutton over the board. 7658 * @name JXG.Board#mouseup 7659 * @param {Event} e The browser's event object. 7660 */ 7661 __evt__mouseup: function (e) { }, 7662 7663 /** 7664 * @event 7665 * @description Whenever the user releases the mousebutton over the board with a 7666 * device sending pointer events. 7667 * @name JXG.Board#pointerup 7668 * @param {Event} e The browser's event object. 7669 */ 7670 __evt__pointerup: function (e) { }, 7671 7672 /** 7673 * @event 7674 * @description Whenever the user stops touching the board. 7675 * @name JXG.Board#touchend 7676 * @param {Event} e The browser's event object. 7677 */ 7678 __evt__touchend: function (e) { }, 7679 7680 /** 7681 * @event 7682 * @description Whenever the user clicks on the board. 7683 * @name JXG.Board#click 7684 * @see JXG.Board#clickDelay 7685 * @param {Event} e The browser's event object. 7686 */ 7687 __evt__click: function (e) { }, 7688 7689 /** 7690 * @event 7691 * @description Whenever the user double clicks on the board. 7692 * This event works on desktop browser, but is undefined 7693 * on mobile browsers. 7694 * @name JXG.Board#dblclick 7695 * @see JXG.Board#clickDelay 7696 * @see JXG.Board#dblClickSuppressClick 7697 * @param {Event} e The browser's event object. 7698 */ 7699 __evt__dblclick: function (e) { }, 7700 7701 /** 7702 * @event 7703 * @description Whenever the user clicks on the board with a mouse device. 7704 * @name JXG.Board#mouseclick 7705 * @param {Event} e The browser's event object. 7706 */ 7707 __evt__mouseclick: function (e) { }, 7708 7709 /** 7710 * @event 7711 * @description Whenever the user double clicks on the board with a mouse device. 7712 * @name JXG.Board#mousedblclick 7713 * @see JXG.Board#clickDelay 7714 * @param {Event} e The browser's event object. 7715 */ 7716 __evt__mousedblclick: function (e) { }, 7717 7718 /** 7719 * @event 7720 * @description Whenever the user clicks on the board with a pointer device. 7721 * @name JXG.Board#pointerclick 7722 * @param {Event} e The browser's event object. 7723 */ 7724 __evt__pointerclick: function (e) { }, 7725 7726 /** 7727 * @event 7728 * @description Whenever the user double clicks on the board with a pointer device. 7729 * This event works on desktop browser, but is undefined 7730 * on mobile browsers. 7731 * @name JXG.Board#pointerdblclick 7732 * @see JXG.Board#clickDelay 7733 * @param {Event} e The browser's event object. 7734 */ 7735 __evt__pointerdblclick: function (e) { }, 7736 7737 /** 7738 * @event 7739 * @description This event is fired whenever the user is moving the finger or mouse pointer over the board. 7740 * @name JXG.Board#move 7741 * @param {Event} e The browser's event object. 7742 * @param {Number} mode The mode the board currently is in 7743 * @see JXG.Board#mode 7744 */ 7745 __evt__move: function (e, mode) { }, 7746 7747 /** 7748 * @event 7749 * @description This event is fired whenever the user is moving the mouse over the board. 7750 * @name JXG.Board#mousemove 7751 * @param {Event} e The browser's event object. 7752 * @param {Number} mode The mode the board currently is in 7753 * @see JXG.Board#mode 7754 */ 7755 __evt__mousemove: function (e, mode) { }, 7756 7757 /** 7758 * @event 7759 * @description This event is fired whenever the user is moving the pen over the board. 7760 * @name JXG.Board#penmove 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__penmove: function (e, mode) { }, 7766 7767 /** 7768 * @event 7769 * @description This event is fired whenever the user is moving the mouse over the board with a 7770 * device sending pointer events. 7771 * @name JXG.Board#pointermove 7772 * @param {Event} e The browser's event object. 7773 * @param {Number} mode The mode the board currently is in 7774 * @see JXG.Board#mode 7775 */ 7776 __evt__pointermove: function (e, mode) { }, 7777 7778 /** 7779 * @event 7780 * @description This event is fired whenever the user is moving the finger over the board. 7781 * @name JXG.Board#touchmove 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__touchmove: function (e, mode) { }, 7787 7788 /** 7789 * @event 7790 * @description This event is fired whenever the user is moving an element over the board by 7791 * pressing arrow keys on a keyboard. 7792 * @name JXG.Board#keymove 7793 * @param {Event} e The browser's event object. 7794 * @param {Number} mode The mode the board currently is in 7795 * @see JXG.Board#mode 7796 */ 7797 __evt__keymove: function (e, mode) { }, 7798 7799 /** 7800 * @event 7801 * @description Whenever an element is highlighted this event is fired. 7802 * @name JXG.Board#hit 7803 * @param {Event} e The browser's event object. 7804 * @param {JXG.GeometryElement} el The hit element. 7805 * @param target 7806 * 7807 * @example 7808 * var c = board.create('circle', [[1, 1], 2]); 7809 * board.on('hit', function(evt, el) { 7810 * console.log('JSXGraph example: Hit element', el); 7811 * }); 7812 * 7813 * </pre><div id='JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7814 * <script type='text/javascript'> 7815 * (function() { 7816 * var board = JXG.JSXGraph.initBoard('JXG19eb31ac-88e6-11e8-bcb5-901b0e1b8723', 7817 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 7818 * var c = board.create('circle', [[1, 1], 2]); 7819 * board.on('hit', function(evt, el) { 7820 * console.log('JSXGraph example: Hit element', el); 7821 * }); 7822 * 7823 * })(); 7824 * 7825 * </script><pre> 7826 */ 7827 __evt__hit: function (e, el, target) { }, 7828 7829 /** 7830 * @event 7831 * @description Whenever an element is highlighted this event is fired. 7832 * @name JXG.Board#mousehit 7833 * @see JXG.Board#hit 7834 * @param {Event} e The browser's event object. 7835 * @param {JXG.GeometryElement} el The hit element. 7836 * @param target 7837 */ 7838 __evt__mousehit: function (e, el, target) { }, 7839 7840 /** 7841 * @event 7842 * @description This board is updated. 7843 * @name JXG.Board#update 7844 */ 7845 __evt__update: function () { }, 7846 7847 /** 7848 * @event 7849 * @description The bounding box of the board has changed. 7850 * @name JXG.Board#boundingbox 7851 */ 7852 __evt__boundingbox: function () { }, 7853 7854 /** 7855 * @event 7856 * @description Select a region is started during a down event or by calling 7857 * {@link JXG.Board.startSelectionMode} 7858 * @name JXG.Board#startselecting 7859 */ 7860 __evt__startselecting: function () { }, 7861 7862 /** 7863 * @event 7864 * @description Select a region is started during a down event 7865 * from a device sending mouse events or by calling 7866 * {@link JXG.Board.startSelectionMode}. 7867 * @name JXG.Board#mousestartselecting 7868 */ 7869 __evt__mousestartselecting: function () { }, 7870 7871 /** 7872 * @event 7873 * @description Select a region is started during a down event 7874 * from a device sending pointer events or by calling 7875 * {@link JXG.Board.startSelectionMode}. 7876 * @name JXG.Board#pointerstartselecting 7877 */ 7878 __evt__pointerstartselecting: function () { }, 7879 7880 /** 7881 * @event 7882 * @description Select a region is started during a down event 7883 * from a device sending touch events or by calling 7884 * {@link JXG.Board.startSelectionMode}. 7885 * @name JXG.Board#touchstartselecting 7886 */ 7887 __evt__touchstartselecting: function () { }, 7888 7889 /** 7890 * @event 7891 * @description Selection of a region is stopped during an up event. 7892 * @name JXG.Board#stopselecting 7893 */ 7894 __evt__stopselecting: function () { }, 7895 7896 /** 7897 * @event 7898 * @description Selection of a region is stopped during an up event 7899 * from a device sending mouse events. 7900 * @name JXG.Board#mousestopselecting 7901 */ 7902 __evt__mousestopselecting: function () { }, 7903 7904 /** 7905 * @event 7906 * @description Selection of a region is stopped during an up event 7907 * from a device sending pointer events. 7908 * @name JXG.Board#pointerstopselecting 7909 */ 7910 __evt__pointerstopselecting: function () { }, 7911 7912 /** 7913 * @event 7914 * @description Selection of a region is stopped during an up event 7915 * from a device sending touch events. 7916 * @name JXG.Board#touchstopselecting 7917 */ 7918 __evt__touchstopselecting: function () { }, 7919 7920 /** 7921 * @event 7922 * @description A move event while selecting of a region is active. 7923 * @name JXG.Board#moveselecting 7924 */ 7925 __evt__moveselecting: function () { }, 7926 7927 /** 7928 * @event 7929 * @description A move event while selecting of a region is active 7930 * from a device sending mouse events. 7931 * @name JXG.Board#mousemoveselecting 7932 */ 7933 __evt__mousemoveselecting: function () { }, 7934 7935 /** 7936 * @event 7937 * @description Select a region is started during a down event 7938 * from a device sending mouse events. 7939 * @name JXG.Board#pointermoveselecting 7940 */ 7941 __evt__pointermoveselecting: function () { }, 7942 7943 /** 7944 * @event 7945 * @description Select a region is started during a down event 7946 * from a device sending touch events. 7947 * @name JXG.Board#touchmoveselecting 7948 */ 7949 __evt__touchmoveselecting: function () { }, 7950 7951 /** 7952 * @ignore 7953 */ 7954 __evt: function () { }, 7955 7956 //endregion 7957 7958 /** 7959 * Expand the JSXGraph construction to fullscreen. 7960 * In order to preserve the proportions of the JSXGraph element, 7961 * a wrapper div is created which is set to fullscreen. 7962 * This function is called when fullscreen mode is triggered 7963 * <b>and</b> when it is closed. 7964 * <p> 7965 * The wrapping div has the CSS class 'jxgbox_wrap_private' which is 7966 * defined in the file 'jsxgraph.css' 7967 * <p> 7968 * This feature is not available on iPhones (as of December 2021). 7969 * 7970 * @param {String} id (Optional) id of the div element which is brought to fullscreen. 7971 * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick 7972 * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied. 7973 * 7974 * @return {JXG.Board} Reference to the board 7975 * 7976 * @example 7977 * <div id='jxgbox' class='jxgbox' style='width:500px; height:200px;'></div> 7978 * <button onClick='board.toFullscreen()'>Fullscreen</button> 7979 * 7980 * <script language='Javascript' type='text/javascript'> 7981 * var board = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,5,5,-5]}); 7982 * var p = board.create('point', [0, 1]); 7983 * </script> 7984 * 7985 * </pre><div id='JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723' class='jxgbox' style='width: 300px; height: 300px;'></div> 7986 * <script type='text/javascript'> 7987 * var board_d5bab8b6; 7988 * (function() { 7989 * var board = JXG.JSXGraph.initBoard('JXGd5bab8b6-fd40-11e8-ab14-901b0e1b8723', 7990 * {boundingbox:[-5,5,5,-5], axis: true, showcopyright: false, shownavigation: false}); 7991 * var p = board.create('point', [0, 1]); 7992 * board_d5bab8b6 = board; 7993 * })(); 7994 * </script> 7995 * <button onClick='board_d5bab8b6.toFullscreen()'>Fullscreen</button> 7996 * <pre> 7997 * 7998 * @example 7999 * <div id='outer' style='max-width: 500px; margin: 0 auto;'> 8000 * <div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'></div> 8001 * </div> 8002 * <button onClick='board.toFullscreen('outer')'>Fullscreen</button> 8003 * 8004 * <script language='Javascript' type='text/javascript'> 8005 * var board = JXG.JSXGraph.initBoard('jxgbox', { 8006 * axis:true, 8007 * boundingbox:[-5,5,5,-5], 8008 * fullscreen: { id: 'outer' }, 8009 * showFullscreen: true 8010 * }); 8011 * var p = board.create('point', [-2, 3], {}); 8012 * </script> 8013 * 8014 * </pre><div id='JXG7103f6b_outer' style='max-width: 500px; margin: 0 auto;'> 8015 * <div id='JXG7103f6be-6993-4ff8-8133-c78e50a8afac' class='jxgbox' style='height: 0; padding-bottom: 100%;'></div> 8016 * </div> 8017 * <button onClick='board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')'>Fullscreen</button> 8018 * <script type='text/javascript'> 8019 * var board_JXG7103f6be; 8020 * (function() { 8021 * var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac', 8022 * {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true, 8023 * showcopyright: false, shownavigation: false}); 8024 * var p = board.create('point', [-2, 3], {}); 8025 * board_JXG7103f6be = board; 8026 * })(); 8027 * 8028 * </script><pre> 8029 * 8030 * 8031 */ 8032 toFullscreen: function (id) { 8033 var wrap_id, 8034 wrap_node, 8035 inner_node, 8036 dim, 8037 doc = this.document, 8038 fullscreenElement; 8039 8040 id = id || this.container; 8041 this._fullscreen_inner_id = id; 8042 inner_node = doc.getElementById(id); 8043 wrap_id = 'fullscreenwrap_' + id; 8044 8045 if (!Type.exists(inner_node._cssFullscreenStore)) { 8046 // Store the actual, absolute size of the div 8047 // This is used in scaleJSXGraphDiv 8048 dim = this.containerObj.getBoundingClientRect(); 8049 inner_node._cssFullscreenStore = { 8050 w: dim.width, 8051 h: dim.height 8052 }; 8053 } 8054 8055 // Wrap a div around the JSXGraph div. 8056 // It is removed when fullscreen mode is closed. 8057 if (doc.getElementById(wrap_id)) { 8058 wrap_node = doc.getElementById(wrap_id); 8059 } else { 8060 wrap_node = document.createElement('div'); 8061 wrap_node.classList.add('JXG_wrap_private'); 8062 wrap_node.setAttribute('id', wrap_id); 8063 inner_node.parentNode.insertBefore(wrap_node, inner_node); 8064 wrap_node.appendChild(inner_node); 8065 } 8066 8067 // Trigger fullscreen mode 8068 wrap_node.requestFullscreen = 8069 wrap_node.requestFullscreen || 8070 wrap_node.webkitRequestFullscreen || 8071 wrap_node.mozRequestFullScreen || 8072 wrap_node.msRequestFullscreen; 8073 8074 if (doc.fullscreenElement !== undefined) { 8075 fullscreenElement = doc.fullscreenElement; 8076 } else if (doc.webkitFullscreenElement !== undefined) { 8077 fullscreenElement = doc.webkitFullscreenElement; 8078 } else { 8079 fullscreenElement = doc.msFullscreenElement; 8080 } 8081 8082 if (fullscreenElement === null) { 8083 // Start fullscreen mode 8084 if (wrap_node.requestFullscreen) { 8085 wrap_node.requestFullscreen(); 8086 this.startFullscreenResizeObserver(wrap_node); 8087 } 8088 } else { 8089 this.stopFullscreenResizeObserver(wrap_node); 8090 if (Type.exists(document.exitFullscreen)) { 8091 document.exitFullscreen(); 8092 } else if (Type.exists(document.webkitExitFullscreen)) { 8093 document.webkitExitFullscreen(); 8094 } 8095 } 8096 8097 return this; 8098 }, 8099 8100 /** 8101 * If fullscreen mode is toggled, the possible CSS transformations 8102 * which are applied to the JSXGraph canvas have to be reread. 8103 * Otherwise the position of upper left corner is wrongly interpreted. 8104 * 8105 * @param {Object} evt fullscreen event object (unused) 8106 */ 8107 fullscreenListener: function (evt) { 8108 var inner_id, 8109 inner_node, 8110 fullscreenElement, 8111 doc = this.document; 8112 8113 inner_id = this._fullscreen_inner_id; 8114 if (!Type.exists(inner_id)) { 8115 return; 8116 } 8117 8118 if (doc.fullscreenElement !== undefined) { 8119 fullscreenElement = doc.fullscreenElement; 8120 } else if (doc.webkitFullscreenElement !== undefined) { 8121 fullscreenElement = doc.webkitFullscreenElement; 8122 } else { 8123 fullscreenElement = doc.msFullscreenElement; 8124 } 8125 8126 inner_node = doc.getElementById(inner_id); 8127 // If full screen mode is started we have to remove CSS margin around the JSXGraph div. 8128 // Otherwise, the positioning of the fullscreen div will be false. 8129 // When leaving the fullscreen mode, the margin is put back in. 8130 if (fullscreenElement) { 8131 // Just entered fullscreen mode 8132 8133 // Store the original data. 8134 // Further, the CSS margin has to be removed when in fullscreen mode, 8135 // and must be restored later. 8136 // 8137 // Obsolete: 8138 // It is used in AbstractRenderer.updateText to restore the scaling matrix 8139 // which is removed by MathJax. 8140 inner_node._cssFullscreenStore.id = fullscreenElement.id; 8141 inner_node._cssFullscreenStore.isFullscreen = true; 8142 inner_node._cssFullscreenStore.margin = inner_node.style.margin; 8143 inner_node._cssFullscreenStore.width = inner_node.style.width; 8144 inner_node._cssFullscreenStore.height = inner_node.style.height; 8145 inner_node._cssFullscreenStore.transform = inner_node.style.transform; 8146 // Be sure to replace relative width / height units by absolute units 8147 inner_node.style.width = inner_node._cssFullscreenStore.w + 'px'; 8148 inner_node.style.height = inner_node._cssFullscreenStore.h + 'px'; 8149 inner_node.style.margin = ''; 8150 8151 // Do the shifting and scaling via CSS properties 8152 // We do this after fullscreen mode has been established to get the correct size 8153 // of the JSXGraph div. 8154 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 8155 Type.evaluate(this.attr.fullscreen.scale)); 8156 8157 // Clear this.doc.fullscreenElement, because Safari doesn't to it and 8158 // when leaving full screen mode it is still set. 8159 fullscreenElement = null; 8160 } else if (Type.exists(inner_node._cssFullscreenStore)) { 8161 // Just left the fullscreen mode 8162 8163 inner_node._cssFullscreenStore.isFullscreen = false; 8164 inner_node.style.margin = inner_node._cssFullscreenStore.margin; 8165 inner_node.style.width = inner_node._cssFullscreenStore.width; 8166 inner_node.style.height = inner_node._cssFullscreenStore.height; 8167 inner_node.style.transform = inner_node._cssFullscreenStore.transform; 8168 inner_node._cssFullscreenStore = null; 8169 8170 // Remove the wrapper div 8171 inner_node.parentElement.replaceWith(inner_node); 8172 } 8173 8174 this.updateCSSTransforms(); 8175 }, 8176 8177 /** 8178 * Start resize observer to handle 8179 * orientation changes in fullscreen mode. 8180 * 8181 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 8182 * around the JSXGraph div. 8183 * @returns {JXG.Board} Reference to the board 8184 * @private 8185 * @see JXG.Board#toFullscreen 8186 * 8187 */ 8188 startFullscreenResizeObserver: function(node) { 8189 var that = this; 8190 8191 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 8192 return this; 8193 } 8194 8195 this.resizeObserver = new ResizeObserver(function (entries) { 8196 var inner_id, 8197 fullscreenElement, 8198 doc = that.document; 8199 8200 if (!that._isResizing) { 8201 that._isResizing = true; 8202 window.setTimeout(function () { 8203 try { 8204 inner_id = that._fullscreen_inner_id; 8205 if (doc.fullscreenElement !== undefined) { 8206 fullscreenElement = doc.fullscreenElement; 8207 } else if (doc.webkitFullscreenElement !== undefined) { 8208 fullscreenElement = doc.webkitFullscreenElement; 8209 } else { 8210 fullscreenElement = doc.msFullscreenElement; 8211 } 8212 if (fullscreenElement !== null) { 8213 Env.scaleJSXGraphDiv(fullscreenElement.id, inner_id, doc, 8214 Type.evaluate(that.attr.fullscreen.scale)); 8215 } 8216 } catch (err) { 8217 that.stopFullscreenResizeObserver(node); 8218 } finally { 8219 that._isResizing = false; 8220 } 8221 }, that.attr.resize.throttle); 8222 } 8223 }); 8224 this.resizeObserver.observe(node); 8225 return this; 8226 }, 8227 8228 /** 8229 * Remove resize observer to handle orientation changes in fullscreen mode. 8230 * @param {Object} node DOM object which is in fullscreen mode. It is the wrapper element 8231 * around the JSXGraph div. 8232 * @returns {JXG.Board} Reference to the board 8233 * @private 8234 * @see JXG.Board#toFullscreen 8235 */ 8236 stopFullscreenResizeObserver: function(node) { 8237 if (!Env.isBrowser || !this.attr.resize || !this.attr.resize.enabled) { 8238 return this; 8239 } 8240 8241 if (Type.exists(this.resizeObserver)) { 8242 this.resizeObserver.unobserve(node); 8243 } 8244 return this; 8245 }, 8246 8247 /** 8248 * Add user activity to the array 'board.userLog'. 8249 * 8250 * @param {String} type Event type, e.g. 'drag' 8251 * @param {Object} obj JSXGraph element object 8252 * 8253 * @see JXG.Board#userLog 8254 * @return {JXG.Board} Reference to the board 8255 */ 8256 addLogEntry: function (type, obj, pos) { 8257 var t, id, 8258 last = this.userLog.length - 1; 8259 8260 if (Type.exists(obj.elementClass)) { 8261 id = obj.id; 8262 } 8263 if (Type.evaluate(this.attr.logging.enabled)) { 8264 t = (new Date()).getTime(); 8265 if (last >= 0 && 8266 this.userLog[last].type === type && 8267 this.userLog[last].id === id && 8268 // Distinguish consecutive drag events of 8269 // the same element 8270 t - this.userLog[last].end < 500) { 8271 8272 this.userLog[last].end = t; 8273 this.userLog[last].endpos = pos; 8274 } else { 8275 this.userLog.push({ 8276 type: type, 8277 id: id, 8278 start: t, 8279 startpos: pos, 8280 end: t, 8281 endpos: pos, 8282 bbox: this.getBoundingBox(), 8283 canvas: [this.canvasWidth, this.canvasHeight], 8284 zoom: [this.zoomX, this.zoomY] 8285 }); 8286 } 8287 } 8288 return this; 8289 }, 8290 8291 /** 8292 * Function to animate a curve rolling on another curve. 8293 * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls 8294 * @param {Curve} c2 JSXGraph curve which rolls on c1. 8295 * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the 8296 * rolling process 8297 * @param {Number} stepsize Increase in t in each step for the curve c1 8298 * @param {Number} direction 8299 * @param {Number} time Delay time for setInterval() 8300 * @param {Array} pointlist Array of points which are rolled in each step. This list should contain 8301 * all points which define c2 and gliders on c2. 8302 * 8303 * @example 8304 * 8305 * // Line which will be the floor to roll upon. 8306 * var line = board.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 8307 * // Center of the rolling circle 8308 * var C = board.create('point',[0,2],{name:'C'}); 8309 * // Starting point of the rolling circle 8310 * var P = board.create('point',[0,1],{name:'P', trace:true}); 8311 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 8312 * var circle = board.create('curve',[ 8313 * function (t){var d = P.Dist(C), 8314 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8315 * t += beta; 8316 * return C.X()+d*Math.cos(t); 8317 * }, 8318 * function (t){var d = P.Dist(C), 8319 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8320 * t += beta; 8321 * return C.Y()+d*Math.sin(t); 8322 * }, 8323 * 0,2*Math.PI], 8324 * {strokeWidth:6, strokeColor:'green'}); 8325 * 8326 * // Point on circle 8327 * var B = board.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 8328 * var roll = board.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 8329 * roll.start() // Start the rolling, to be stopped by roll.stop() 8330 * 8331 * </pre><div class='jxgbox' id='JXGe5e1b53c-a036-4a46-9e35-190d196beca5' style='width: 300px; height: 300px;'></div> 8332 * <script type='text/javascript'> 8333 * var brd = JXG.JSXGraph.initBoard('JXGe5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false}); 8334 * // Line which will be the floor to roll upon. 8335 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 8336 * // Center of the rolling circle 8337 * var C = brd.create('point',[0,2],{name:'C'}); 8338 * // Starting point of the rolling circle 8339 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 8340 * // Circle defined as a curve. The circle 'starts' at P, i.e. circle(0) = P 8341 * var circle = brd.create('curve',[ 8342 * function (t){var d = P.Dist(C), 8343 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8344 * t += beta; 8345 * return C.X()+d*Math.cos(t); 8346 * }, 8347 * function (t){var d = P.Dist(C), 8348 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 8349 * t += beta; 8350 * return C.Y()+d*Math.sin(t); 8351 * }, 8352 * 0,2*Math.PI], 8353 * {strokeWidth:6, strokeColor:'green'}); 8354 * 8355 * // Point on circle 8356 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 8357 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 8358 * roll.start() // Start the rolling, to be stopped by roll.stop() 8359 * </script><pre> 8360 */ 8361 createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) { 8362 var brd = this, 8363 Roulette = function () { 8364 var alpha = 0, 8365 Tx = 0, 8366 Ty = 0, 8367 t1 = start_c1, 8368 t2 = Numerics.root( 8369 function (t) { 8370 var c1x = c1.X(t1), 8371 c1y = c1.Y(t1), 8372 c2x = c2.X(t), 8373 c2y = c2.Y(t); 8374 8375 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y); 8376 }, 8377 [0, Math.PI * 2] 8378 ), 8379 t1_new = 0.0, 8380 t2_new = 0.0, 8381 c1dist, 8382 rotation = brd.create( 8383 'transform', 8384 [ 8385 function () { 8386 return alpha; 8387 } 8388 ], 8389 { type: 'rotate' } 8390 ), 8391 rotationLocal = brd.create( 8392 'transform', 8393 [ 8394 function () { 8395 return alpha; 8396 }, 8397 function () { 8398 return c1.X(t1); 8399 }, 8400 function () { 8401 return c1.Y(t1); 8402 } 8403 ], 8404 { type: 'rotate' } 8405 ), 8406 translate = brd.create( 8407 'transform', 8408 [ 8409 function () { 8410 return Tx; 8411 }, 8412 function () { 8413 return Ty; 8414 } 8415 ], 8416 { type: 'translate' } 8417 ), 8418 // arc length via Simpson's rule. 8419 arclen = function (c, a, b) { 8420 var cpxa = Numerics.D(c.X)(a), 8421 cpya = Numerics.D(c.Y)(a), 8422 cpxb = Numerics.D(c.X)(b), 8423 cpyb = Numerics.D(c.Y)(b), 8424 cpxab = Numerics.D(c.X)((a + b) * 0.5), 8425 cpyab = Numerics.D(c.Y)((a + b) * 0.5), 8426 fa = Mat.hypot(cpxa, cpya), 8427 fb = Mat.hypot(cpxb, cpyb), 8428 fab = Mat.hypot(cpxab, cpyab); 8429 8430 return ((fa + 4 * fab + fb) * (b - a)) / 6; 8431 }, 8432 exactDist = function (t) { 8433 return c1dist - arclen(c2, t2, t); 8434 }, 8435 beta = Math.PI / 18, 8436 beta9 = beta * 9, 8437 interval = null; 8438 8439 this.rolling = function () { 8440 var h, g, hp, gp, z; 8441 8442 t1_new = t1 + direction * stepsize; 8443 8444 // arc length between c1(t1) and c1(t1_new) 8445 c1dist = arclen(c1, t1, t1_new); 8446 8447 // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist. 8448 t2_new = Numerics.root(exactDist, t2); 8449 8450 // c1(t) as complex number 8451 h = new Complex(c1.X(t1_new), c1.Y(t1_new)); 8452 8453 // c2(t) as complex number 8454 g = new Complex(c2.X(t2_new), c2.Y(t2_new)); 8455 8456 hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new)); 8457 gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new)); 8458 8459 // z is angle between the tangents of c1 at t1_new, and c2 at t2_new 8460 z = Complex.C.div(hp, gp); 8461 8462 alpha = Math.atan2(z.imaginary, z.real); 8463 // Normalizing the quotient 8464 z.div(Complex.C.abs(z)); 8465 z.mult(g); 8466 Tx = h.real - z.real; 8467 8468 // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new); 8469 Ty = h.imaginary - z.imaginary; 8470 8471 // -(10-90) degrees: make corners roll smoothly 8472 if (alpha < -beta && alpha > -beta9) { 8473 alpha = -beta; 8474 rotationLocal.applyOnce(pointlist); 8475 } else if (alpha > beta && alpha < beta9) { 8476 alpha = beta; 8477 rotationLocal.applyOnce(pointlist); 8478 } else { 8479 rotation.applyOnce(pointlist); 8480 translate.applyOnce(pointlist); 8481 t1 = t1_new; 8482 t2 = t2_new; 8483 } 8484 brd.update(); 8485 }; 8486 8487 this.start = function () { 8488 if (time > 0) { 8489 interval = window.setInterval(this.rolling, time); 8490 } 8491 return this; 8492 }; 8493 8494 this.stop = function () { 8495 window.clearInterval(interval); 8496 return this; 8497 }; 8498 return this; 8499 }; 8500 return new Roulette(); 8501 } 8502 } 8503 ); 8504 8505 export default JXG.Board; 8506