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