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