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