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