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