1 /* 2 Copyright 2008-2017 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 33 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, window: true, document: true, init: true, translateASCIIMath: true, google: true*/ 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', 'parser/geonext', 'utils/color', 'utils/type', 'utils/event', 'utils/env', 'base/transformation', 69 'base/point', 'base/line', 'base/text', 'element/composition', 'base/composition' 70 ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, GeonextParser, Color, Type, 71 EventEmitter, Env, Transform, Point, Line, Text, Composition, EComposition) { 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 * {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 low quality, e.g. graphs are evaluated at a lesser amount of points. 124 * @type Number 125 * @constant 126 * @see JXG.Board#updateQuality 127 */ 128 this.BOARD_QUALITY_LOW = 0x1; 129 130 /** 131 * Update is made with high quality, e.g. graphs are evaluated at much more points. 132 * @type Number 133 * @constant 134 * @see JXG.Board#updateQuality 135 */ 136 this.BOARD_QUALITY_HIGH = 0x2; 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_MODE_ZOOM = 0x0011; 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 (typeof 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 */ 178 this.renderer = renderer; 179 180 /** 181 * Grids keeps track of all grids attached to this board. 182 */ 183 this.grids = []; 184 185 /** 186 * Some standard options 187 * @type JXG.Options 188 */ 189 this.options = Type.deepCopy(Options); 190 this.attr = attributes; 191 192 /** 193 * Dimension of the board. 194 * @default 2 195 * @type Number 196 */ 197 this.dimension = 2; 198 199 this.jc = new JessieCode(); 200 this.jc.use(this); 201 202 /** 203 * Coordinates of the boards origin. This a object with the two properties 204 * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords 205 * stores the boards origin in homogeneous screen coordinates. 206 * @type Object 207 */ 208 this.origin = {}; 209 this.origin.usrCoords = [1, 0, 0]; 210 this.origin.scrCoords = [1, origin[0], origin[1]]; 211 212 /** 213 * Zoom factor in X direction. It only stores the zoom factor to be able 214 * to get back to 100% in zoom100(). 215 * @type Number 216 */ 217 this.zoomX = zoomX; 218 219 /** 220 * Zoom factor in Y direction. It only stores the zoom factor to be able 221 * to get back to 100% in zoom100(). 222 * @type Number 223 */ 224 this.zoomY = zoomY; 225 226 /** 227 * The number of pixels which represent one unit in user-coordinates in x direction. 228 * @type Number 229 */ 230 this.unitX = unitX * this.zoomX; 231 232 /** 233 * The number of pixels which represent one unit in user-coordinates in y direction. 234 * @type Number 235 */ 236 this.unitY = unitY * this.zoomY; 237 238 /** 239 * Keep aspect ratio if bounding box is set and the width/height ratio differs from the 240 * width/height ratio of the canvas. 241 */ 242 this.keepaspectratio = false; 243 244 /** 245 * Canvas width. 246 * @type Number 247 */ 248 this.canvasWidth = canvasWidth; 249 250 /** 251 * Canvas Height 252 * @type Number 253 */ 254 this.canvasHeight = canvasHeight; 255 256 // If the given id is not valid, generate an unique id 257 if (Type.exists(id) && id !== '' && Env.isBrowser && !Type.exists(this.document.getElementById(id))) { 258 this.id = id; 259 } else { 260 this.id = this.generateId(); 261 } 262 263 EventEmitter.eventify(this); 264 265 this.hooks = []; 266 267 /** 268 * An array containing all other boards that are updated after this board has been updated. 269 * @type Array 270 * @see JXG.Board#addChild 271 * @see JXG.Board#removeChild 272 */ 273 this.dependentBoards = []; 274 275 /** 276 * During the update process this is set to false to prevent an endless loop. 277 * @default false 278 * @type Boolean 279 */ 280 this.inUpdate = false; 281 282 /** 283 * 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. 284 * @type Object 285 */ 286 this.objects = {}; 287 288 /** 289 * An array containing all geometric objects on the board in the order of construction. 290 * @type {Array} 291 */ 292 this.objectsList = []; 293 294 /** 295 * 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. 296 * @type Object 297 */ 298 this.groups = {}; 299 300 /** 301 * Stores all the objects that are currently running an animation. 302 * @type Object 303 */ 304 this.animationObjects = {}; 305 306 /** 307 * An associative array containing all highlighted elements belonging to the board. 308 * @type Object 309 */ 310 this.highlightedObjects = {}; 311 312 /** 313 * Number of objects ever created on this board. This includes every object, even invisible and deleted ones. 314 * @type Number 315 */ 316 this.numObjects = 0; 317 318 /** 319 * 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. 320 * @type Object 321 */ 322 this.elementsByName = {}; 323 324 /** 325 * The board mode the board is currently in. Possible values are 326 * <ul> 327 * <li>JXG.Board.BOARD_MODE_NONE</li> 328 * <li>JXG.Board.BOARD_MODE_DRAG</li> 329 * <li>JXG.Board.BOARD_MODE_MOVE_ORIGIN</li> 330 * </ul> 331 * @type Number 332 */ 333 this.mode = this.BOARD_MODE_NONE; 334 335 /** 336 * The update quality of the board. In most cases this is set to {@link JXG.Board#BOARD_QUALITY_HIGH}. 337 * If {@link JXG.Board#mode} equals {@link JXG.Board#BOARD_MODE_DRAG} this is set to 338 * {@link JXG.Board#BOARD_QUALITY_LOW} to speed up the update process by e.g. reducing the number of 339 * evaluation points when plotting functions. Possible values are 340 * <ul> 341 * <li>BOARD_QUALITY_LOW</li> 342 * <li>BOARD_QUALITY_HIGH</li> 343 * </ul> 344 * @type Number 345 * @see JXG.Board#mode 346 */ 347 this.updateQuality = this.BOARD_QUALITY_HIGH; 348 349 /** 350 * If true updates are skipped. 351 * @type Boolean 352 */ 353 this.isSuspendedRedraw = false; 354 355 this.calculateSnapSizes(); 356 357 /** 358 * The distance from the mouse to the dragged object in x direction when the user clicked the mouse button. 359 * @type Number 360 * @see JXG.Board#drag_dy 361 * @see JXG.Board#drag_obj 362 */ 363 this.drag_dx = 0; 364 365 /** 366 * The distance from the mouse to the dragged object in y direction when the user clicked the mouse button. 367 * @type Number 368 * @see JXG.Board#drag_dx 369 * @see JXG.Board#drag_obj 370 */ 371 this.drag_dy = 0; 372 373 /** 374 * The last position where a drag event has been fired. 375 * @type Array 376 * @see JXG.Board#moveObject 377 */ 378 this.drag_position = [0, 0]; 379 380 /** 381 * References to the object that is dragged with the mouse on the board. 382 * @type {@link JXG.GeometryElement}. 383 * @see {JXG.Board#touches} 384 */ 385 this.mouse = {}; 386 387 /** 388 * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events. 389 * @type Array 390 * @see {JXG.Board#mouse} 391 */ 392 this.touches = []; 393 394 /** 395 * A string containing the XML text of the construction. This is set in {@link JXG.FileReader#parseString}. 396 * Only useful if a construction is read from a GEONExT-, Intergeo-, Geogebra-, or Cinderella-File. 397 * @type String 398 */ 399 this.xmlString = ''; 400 401 /** 402 * Cached result of getCoordsTopLeftCorner for touch/mouseMove-Events to save some DOM operations. 403 * @type Array 404 */ 405 this.cPos = []; 406 407 /** 408 * Contains the last time (epoch, msec) since the last touchMove event which was not thrown away or since 409 * touchStart because Android's Webkit browser fires too much of them. 410 * @type Number 411 */ 412 // this.touchMoveLast = 0; 413 414 /** 415 * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away. 416 * @type Number 417 */ 418 this.positionAccessLast = 0; 419 420 /** 421 * Collects all elements that triggered a mouse down event. 422 * @type Array 423 */ 424 this.downObjects = []; 425 426 if (this.attr.showcopyright) { 427 this.renderer.displayCopyright(Const.licenseText, parseInt(this.options.text.fontSize, 10)); 428 } 429 430 /** 431 * Full updates are needed after zoom and axis translates. This saves some time during an update. 432 * @default false 433 * @type Boolean 434 */ 435 this.needsFullUpdate = false; 436 437 /** 438 * If reducedUpdate is set to true then only the dragged element and few (e.g. 2) following 439 * elements are updated during mouse move. On mouse up the whole construction is 440 * updated. This enables us to be fast even on very slow devices. 441 * @type Boolean 442 * @default false 443 */ 444 this.reducedUpdate = false; 445 446 /** 447 * The current color blindness deficiency is stored in this property. If color blindness is not emulated 448 * at the moment, it's value is 'none'. 449 */ 450 this.currentCBDef = 'none'; 451 452 /** 453 * If GEONExT constructions are displayed, then this property should be set to true. 454 * At the moment there should be no difference. But this may change. 455 * This is set in {@link JXG.GeonextReader#readGeonext}. 456 * @type Boolean 457 * @default false 458 * @see JXG.GeonextReader#readGeonext 459 */ 460 this.geonextCompatibilityMode = false; 461 462 if (this.options.text.useASCIIMathML && translateASCIIMath) { 463 init(); 464 } else { 465 this.options.text.useASCIIMathML = false; 466 } 467 468 /** 469 * A flag which tells if the board registers mouse events. 470 * @type Boolean 471 * @default false 472 */ 473 this.hasMouseHandlers = false; 474 475 /** 476 * A flag which tells if the board registers touch events. 477 * @type Boolean 478 * @default false 479 */ 480 this.hasTouchHandlers = false; 481 482 /** 483 * A flag which stores if the board registered pointer events. 484 * @type {Boolean} 485 * @default false 486 */ 487 this.hasPointerHandlers = false; 488 489 /** 490 * A flag which tells if the board the JXG.Board#mouseUpListener is currently registered. 491 * @type Boolean 492 * @default false 493 */ 494 this.hasMouseUp = false; 495 496 /** 497 * A flag which tells if the board the JXG.Board#touchEndListener is currently registered. 498 * @type Boolean 499 * @default false 500 */ 501 this.hasTouchEnd = false; 502 503 /** 504 * A flag which tells us if the board has a pointerUp event registered at the moment. 505 * @type {Boolean} 506 * @default false 507 */ 508 this.hasPointerUp = false; 509 510 /** 511 * Offset for large coords elements like images 512 * @type {Array} 513 * @private 514 * @default [0, 0] 515 */ 516 this._drag_offset = [0, 0]; 517 518 this._board_touches = []; 519 520 /** 521 * A flag which tells us if the board is in the selecting mode 522 * @type {Boolean} 523 * @default false 524 */ 525 this.selectingMode = false; 526 527 /** 528 * A flag which tells us if the user is selecting 529 * @type {Boolean} 530 * @default false 531 */ 532 this.isSelecting = false; 533 534 /** 535 * A bounding box for the selection 536 * @type {Array} 537 * @default [ [0,0], [0,0] ] 538 */ 539 this.selectingBox = [[0, 0], [0, 0]]; 540 541 if (this.attr.registerevents) { 542 this.addEventHandlers(); 543 } 544 545 this.methodMap = { 546 update: 'update', 547 fullUpdate: 'fullUpdate', 548 on: 'on', 549 off: 'off', 550 trigger: 'trigger', 551 setView: 'setBoundingBox', 552 setBoundingBox: 'setBoundingBox', 553 migratePoint: 'migratePoint', 554 colorblind: 'emulateColorblindness', 555 suspendUpdate: 'suspendUpdate', 556 unsuspendUpdate: 'unsuspendUpdate', 557 clearTraces: 'clearTraces', 558 left: 'clickLeftArrow', 559 right: 'clickRightArrow', 560 up: 'clickUpArrow', 561 down: 'clickDownArrow', 562 zoomIn: 'zoomIn', 563 zoomOut: 'zoomOut', 564 zoom100: 'zoom100', 565 zoomElements: 'zoomElements', 566 remove: 'removeObject', 567 removeObject: 'removeObject' 568 }; 569 }; 570 571 JXG.extend(JXG.Board.prototype, /** @lends JXG.Board.prototype */ { 572 573 /** 574 * Generates an unique name for the given object. The result depends on the objects type, if the 575 * object is a {@link JXG.Point}, capital characters are used, if it is of type {@link JXG.Line} 576 * only lower case characters are used. If object is of type {@link JXG.Polygon}, a bunch of lower 577 * case characters prefixed with P_ are used. If object is of type {@link JXG.Circle} the name is 578 * generated using lower case characters. prefixed with k_ is used. In any other case, lower case 579 * chars prefixed with s_ is used. 580 * @param {Object} object Reference of an JXG.GeometryElement that is to be named. 581 * @returns {String} Unique name for the object. 582 */ 583 generateName: function (object) { 584 var possibleNames, i, 585 maxNameLength = this.attr.maxnamelength, 586 pre = '', 587 post = '', 588 indices = [], 589 name = ''; 590 591 if (object.type === Const.OBJECT_TYPE_TICKS) { 592 return ''; 593 } 594 595 if (Type.isPoint(object)) { 596 // points have capital letters 597 possibleNames = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 598 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 599 } else if (object.type === Const.OBJECT_TYPE_ANGLE) { 600 possibleNames = ['', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 601 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 602 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω']; 603 } else { 604 // all other elements get lowercase labels 605 possibleNames = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 606 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; 607 } 608 609 if (!Type.isPoint(object) && 610 object.elementClass !== Const.OBJECT_CLASS_LINE && 611 object.type !== Const.OBJECT_TYPE_ANGLE) { 612 if (object.type === Const.OBJECT_TYPE_POLYGON) { 613 pre = 'P_{'; 614 } else if (object.elementClass === Const.OBJECT_CLASS_CIRCLE) { 615 pre = 'k_{'; 616 } else if (object.elementClass === Const.OBJECT_CLASS_TEXT) { 617 pre = 't_{'; 618 } else { 619 pre = 's_{'; 620 } 621 post = '}'; 622 } 623 624 for (i = 0; i < maxNameLength; i++) { 625 indices[i] = 0; 626 } 627 628 while (indices[maxNameLength - 1] < possibleNames.length) { 629 for (indices[0] = 1; indices[0] < possibleNames.length; indices[0]++) { 630 name = pre; 631 632 for (i = maxNameLength; i > 0; i--) { 633 name += possibleNames[indices[i - 1]]; 634 } 635 636 if (!Type.exists(this.elementsByName[name + post])) { 637 return name + post; 638 } 639 640 } 641 indices[0] = possibleNames.length; 642 643 for (i = 1; i < maxNameLength; i++) { 644 if (indices[i - 1] === possibleNames.length) { 645 indices[i - 1] = 1; 646 indices[i] += 1; 647 } 648 } 649 } 650 651 return ''; 652 }, 653 654 /** 655 * Generates unique id for a board. The result is randomly generated and prefixed with 'jxgBoard'. 656 * @returns {String} Unique id for a board. 657 */ 658 generateId: function () { 659 var r = 1; 660 661 // as long as we don't have a unique id generate a new one 662 while (Type.exists(JXG.boards['jxgBoard' + r])) { 663 r = Math.round(Math.random() * 65535); 664 } 665 666 return ('jxgBoard' + r); 667 }, 668 669 /** 670 * Composes an id for an element. If the ID is empty ('' or null) a new ID is generated, depending on the 671 * object type. As a side effect {@link JXG.Board#numObjects} 672 * is updated. 673 * @param {Object} obj Reference of an geometry object that needs an id. 674 * @param {Number} type Type of the object. 675 * @returns {String} Unique id for an element. 676 */ 677 setId: function (obj, type) { 678 var num = this.numObjects, 679 elId = obj.id; 680 681 this.numObjects += 1; 682 683 // If no id is provided or id is empty string, a new one is chosen 684 if (elId === '' || !Type.exists(elId)) { 685 elId = this.id + type + num; 686 while (Type.exists(this.objects[elId])) { 687 randomNumber = Math.round(Math.random() * 65535); 688 elId = this.id + type + num + '-' + randomNumber; 689 } 690 } 691 692 obj.id = elId; 693 this.objects[elId] = obj; 694 obj._pos = this.objectsList.length; 695 this.objectsList[this.objectsList.length] = obj; 696 697 return elId; 698 }, 699 700 /** 701 * After construction of the object the visibility is set 702 * and the label is constructed if necessary. 703 * @param {Object} obj The object to add. 704 */ 705 finalizeAdding: function (obj) { 706 if (Type.evaluate(obj.visProp.visible) === false) { 707 this.renderer.display(obj, false); 708 } 709 }, 710 711 finalizeLabel: function (obj) { 712 if (obj.hasLabel && 713 !Type.evaluate(obj.label.visProp.islabel) && 714 Type.evaluate(obj.label.visProp.visible) === false) { 715 this.renderer.display(obj.label, false); 716 } 717 }, 718 719 /********************************************************** 720 * 721 * Event Handler helpers 722 * 723 **********************************************************/ 724 725 /** 726 * Calculates mouse coordinates relative to the boards container. 727 * @returns {Array} Array of coordinates relative the boards container top left corner. 728 */ 729 getCoordsTopLeftCorner: function () { 730 var cPos, doc, crect, 731 docElement = this.document.documentElement || this.document.body.parentNode, 732 docBody = this.document.body, 733 container = this.containerObj, 734 viewport, content, 735 zoom, o; 736 737 /** 738 * During drags and origin moves the container element is usually not changed. 739 * Check the position of the upper left corner at most every 1000 msecs 740 */ 741 if (this.cPos.length > 0 && 742 (this.mode === this.BOARD_MODE_DRAG || this.mode === this.BOARD_MODE_MOVE_ORIGIN || 743 (new Date()).getTime() - this.positionAccessLast < 1000)) { 744 return this.cPos; 745 } 746 this.positionAccessLast = (new Date()).getTime(); 747 748 // Check if getBoundingClientRect exists. If so, use this as this covers *everything* 749 // even CSS3D transformations etc. 750 // Supported by all browsers but IE 6, 7. 751 752 if (container.getBoundingClientRect) { 753 crect = container.getBoundingClientRect(); 754 755 756 zoom = 1.0; 757 // Recursively search for zoom style entries. 758 // This is necessary for reveal.js on webkit. 759 // It fails if the user does zooming 760 o = container; 761 while (o && Type.exists(o.parentNode)) { 762 if (Type.exists(o.style) && Type.exists(o.style.zoom) && o.style.zoom !== '') { 763 zoom *= parseFloat(o.style.zoom); 764 } 765 o = o.parentNode; 766 } 767 cPos = [crect.left * zoom, crect.top * zoom]; 768 769 // add border width 770 cPos[0] += Env.getProp(container, 'border-left-width'); 771 cPos[1] += Env.getProp(container, 'border-top-width'); 772 773 // vml seems to ignore paddings 774 if (this.renderer.type !== 'vml') { 775 // add padding 776 cPos[0] += Env.getProp(container, 'padding-left'); 777 cPos[1] += Env.getProp(container, 'padding-top'); 778 } 779 780 this.cPos = cPos.slice(); 781 return this.cPos; 782 } 783 784 // 785 // OLD CODE 786 // IE 6-7 only: 787 // 788 cPos = Env.getOffset(container); 789 doc = this.document.documentElement.ownerDocument; 790 791 if (!this.containerObj.currentStyle && doc.defaultView) { // Non IE 792 // this is for hacks like this one used in wordpress for the admin bar: 793 // html { margin-top: 28px } 794 // seems like it doesn't work in IE 795 796 cPos[0] += Env.getProp(docElement, 'margin-left'); 797 cPos[1] += Env.getProp(docElement, 'margin-top'); 798 799 cPos[0] += Env.getProp(docElement, 'border-left-width'); 800 cPos[1] += Env.getProp(docElement, 'border-top-width'); 801 802 cPos[0] += Env.getProp(docElement, 'padding-left'); 803 cPos[1] += Env.getProp(docElement, 'padding-top'); 804 } 805 806 if (docBody) { 807 cPos[0] += Env.getProp(docBody, 'left'); 808 cPos[1] += Env.getProp(docBody, 'top'); 809 } 810 811 // Google Translate offers widgets for web authors. These widgets apparently tamper with the clientX 812 // and clientY coordinates of the mouse events. The minified sources seem to be the only publicly 813 // available version so we're doing it the hacky way: Add a fixed offset. 814 // see https://groups.google.com/d/msg/google-translate-general/H2zj0TNjjpY/jw6irtPlCw8J 815 if (typeof google === 'object' && google.translate) { 816 cPos[0] += 10; 817 cPos[1] += 25; 818 } 819 820 // add border width 821 cPos[0] += Env.getProp(container, 'border-left-width'); 822 cPos[1] += Env.getProp(container, 'border-top-width'); 823 824 // vml seems to ignore paddings 825 if (this.renderer.type !== 'vml') { 826 // add padding 827 cPos[0] += Env.getProp(container, 'padding-left'); 828 cPos[1] += Env.getProp(container, 'padding-top'); 829 } 830 831 cPos[0] += this.attr.offsetx; 832 cPos[1] += this.attr.offsety; 833 834 this.cPos = cPos.slice(); 835 return this.cPos; 836 }, 837 838 /** 839 * Get the position of the mouse in screen coordinates, relative to the upper left corner 840 * of the host tag. 841 * @param {Event} e Event object given by the browser. 842 * @param {Number} [i] Only use in case of touch events. This determines which finger to use and should not be set 843 * for mouseevents. 844 * @returns {Array} Contains the mouse coordinates in user coordinates, ready for {@link JXG.Coords} 845 */ 846 getMousePosition: function (e, i) { 847 var cPos = this.getCoordsTopLeftCorner(), 848 absPos, 849 v; 850 851 // position of mouse cursor relative to containers position of container 852 absPos = Env.getPosition(e, i, this.document); 853 854 /** 855 * In case there has been no down event before. 856 */ 857 if (!Type.exists(this.cssTransMat)) { 858 this.updateCSSTransforms(); 859 } 860 v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]]; 861 v = Mat.matVecMult(this.cssTransMat, v); 862 v[1] /= v[0]; 863 v[2] /= v[0]; 864 return [v[1], v[2]]; 865 866 // Method without CSS transformation 867 /* 868 return [absPos[0] - cPos[0], absPos[1] - cPos[1]]; 869 */ 870 }, 871 872 /** 873 * Initiate moving the origin. This is used in mouseDown and touchStart listeners. 874 * @param {Number} x Current mouse/touch coordinates 875 * @param {Number} y Current mouse/touch coordinates 876 */ 877 initMoveOrigin: function (x, y) { 878 this.drag_dx = x - this.origin.scrCoords[1]; 879 this.drag_dy = y - this.origin.scrCoords[2]; 880 881 this.mode = this.BOARD_MODE_MOVE_ORIGIN; 882 this.updateQuality = this.BOARD_QUALITY_LOW; 883 }, 884 885 /** 886 * Collects all elements below the current mouse pointer and fulfilling the following constraints: 887 * <ul><li>isDraggable</li><li>visible</li><li>not fixed</li><li>not frozen</li></ul> 888 * @param {Number} x Current mouse/touch coordinates 889 * @param {Number} y current mouse/touch coordinates 890 * @param {Object} evt An event object 891 * @param {String} type What type of event? 'touch' or 'mouse'. 892 * @returns {Array} A list of geometric elements. 893 */ 894 initMoveObject: function (x, y, evt, type) { 895 var pEl, 896 el, 897 collect = [], 898 offset = [], 899 haspoint, 900 len = this.objectsList.length, 901 dragEl = {visProp: {layer: -10000}}; 902 903 //for (el in this.objects) { 904 for (el = 0; el < len; el++) { 905 pEl = this.objectsList[el]; 906 haspoint = pEl.hasPoint && pEl.hasPoint(x, y); 907 908 if (pEl.visPropCalc.visible && haspoint) { 909 pEl.triggerEventHandlers([type + 'down', 'down'], [evt]); 910 this.downObjects.push(pEl); 911 } 912 913 if (((this.geonextCompatibilityMode && 914 (Type.isPoint(pEl) || 915 pEl.elementClass === Const.OBJECT_CLASS_TEXT)) || 916 !this.geonextCompatibilityMode) && 917 pEl.isDraggable && 918 pEl.visPropCalc.visible && 919 (!Type.evaluate(pEl.visProp.fixed)) && /*(!pEl.visProp.frozen) &&*/ 920 haspoint) { 921 // Elements in the highest layer get priority. 922 if (pEl.visProp.layer > dragEl.visProp.layer || 923 (pEl.visProp.layer === dragEl.visProp.layer && 924 pEl.lastDragTime.getTime() >= dragEl.lastDragTime.getTime() 925 )) { 926 // If an element and its label have the focus 927 // simultaneously, the element is taken. 928 // This only works if we assume that every browser runs 929 // through this.objects in the right order, i.e. an element A 930 // added before element B turns up here before B does. 931 if (!this.attr.ignorelabels || 932 (!Type.exists(dragEl.label) || pEl !== dragEl.label)) { 933 dragEl = pEl; 934 collect.push(dragEl); 935 936 // Save offset for large coords elements. 937 if (Type.exists(dragEl.coords)) { 938 offset.push(Statistics.subtract(dragEl.coords.scrCoords.slice(1), [x, y])); 939 } else { 940 offset.push([0, 0]); 941 } 942 943 // we can't drop out of this loop because of the event handling system 944 //if (this.attr.takefirst) { 945 // return collect; 946 //} 947 } 948 } 949 } 950 } 951 952 if (collect.length > 0) { 953 this.mode = this.BOARD_MODE_DRAG; 954 } 955 956 // A one-element array is returned. 957 if (this.attr.takefirst) { 958 collect.length = 1; 959 this._drag_offset = offset[0]; 960 } else { 961 collect = collect.slice(-1); 962 this._drag_offset = offset[offset.length - 1]; 963 } 964 965 if (!this._drag_offset) { 966 this._drag_offset = [0, 0]; 967 } 968 969 // Move drag element to the top of the layer 970 if (this.renderer.type === 'svg' && 971 Type.exists(collect[0]) && 972 Type.evaluate(collect[0].visProp.dragtotopoflayer) && 973 collect.length === 1 && 974 Type.exists(collect[0].rendNode)) { 975 976 collect[0].rendNode.parentNode.appendChild(collect[0].rendNode); 977 } 978 979 return collect; 980 }, 981 982 /** 983 * Moves an object. 984 * @param {Number} x Coordinate 985 * @param {Number} y Coordinate 986 * @param {Object} o The touch object that is dragged: {JXG.Board#mouse} or {JXG.Board#touches}. 987 * @param {Object} evt The event object. 988 * @param {String} type Mouse or touch event? 989 */ 990 moveObject: function (x, y, o, evt, type) { 991 var newPos = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(x, y), this), 992 drag, 993 dragScrCoords, newDragScrCoords; 994 995 if (!(o && o.obj)) { 996 return; 997 } 998 drag = o.obj; 999 1000 // Save updates for very small movements of coordsElements, see below 1001 if (drag.coords) { 1002 dragScrCoords = drag.coords.scrCoords.slice(); 1003 } 1004 1005 /* 1006 * Save the position. 1007 */ 1008 this.drag_position = [newPos.scrCoords[1], newPos.scrCoords[2]]; 1009 this.drag_position = Statistics.add(this.drag_position, this._drag_offset); 1010 // 1011 // We have to distinguish between CoordsElements and other elements like lines. 1012 // The latter need the difference between two move events. 1013 if (Type.exists(drag.coords)) { 1014 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, this.drag_position); 1015 } else { 1016 this.showInfobox(false); 1017 // Hide infobox in case the user has touched an intersection point 1018 // and drags the underlying line now. 1019 1020 if (!isNaN(o.targets[0].Xprev + o.targets[0].Yprev)) { 1021 drag.setPositionDirectly(Const.COORDS_BY_SCREEN, 1022 [newPos.scrCoords[1], newPos.scrCoords[2]], 1023 [o.targets[0].Xprev, o.targets[0].Yprev] 1024 ); 1025 } 1026 // Remember the actual position for the next move event. Then we are able to 1027 // compute the difference vector. 1028 o.targets[0].Xprev = newPos.scrCoords[1]; 1029 o.targets[0].Yprev = newPos.scrCoords[2]; 1030 } 1031 // This may be necessary for some gliders 1032 drag.prepareUpdate().update(false).updateRenderer(); 1033 this.updateInfobox(drag); 1034 drag.prepareUpdate().update(true).updateRenderer(); 1035 if (drag.coords) { 1036 newDragScrCoords = drag.coords.scrCoords; 1037 } 1038 1039 // No updates for very small movements of coordsElements 1040 if (!drag.coords || 1041 dragScrCoords[1] !== newDragScrCoords[1] || dragScrCoords[2] !== newDragScrCoords[2]) { 1042 1043 drag.triggerEventHandlers([type + 'drag', 'drag'], [evt]); 1044 this.update(); 1045 } 1046 drag.highlight(true); 1047 1048 drag.lastDragTime = new Date(); 1049 }, 1050 1051 /** 1052 * Moves elements in multitouch mode. 1053 * @param {Array} p1 x,y coordinates of first touch 1054 * @param {Array} p2 x,y coordinates of second touch 1055 * @param {Object} o The touch object that is dragged: {JXG.Board#touches}. 1056 * @param {Object} evt The event object that lead to this movement. 1057 */ 1058 twoFingerMove: function (p1, p2, o, evt) { 1059 var np1c, np2c, drag; 1060 if (Type.exists(o) && Type.exists(o.obj)) { 1061 drag = o.obj; 1062 } else { 1063 return; 1064 } 1065 1066 // New finger position 1067 np1c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this); 1068 np2c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this); 1069 1070 if (drag.elementClass === Const.OBJECT_CLASS_LINE || 1071 drag.type === Const.OBJECT_TYPE_POLYGON) { 1072 this.twoFingerTouchObject(np1c, np2c, o, drag); 1073 } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1074 this.twoFingerTouchCircle(np1c, np2c, o, drag); 1075 } 1076 drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]); 1077 1078 o.targets[0].Xprev = np1c.scrCoords[1]; 1079 o.targets[0].Yprev = np1c.scrCoords[2]; 1080 o.targets[1].Xprev = np2c.scrCoords[1]; 1081 o.targets[1].Yprev = np2c.scrCoords[2]; 1082 }, 1083 1084 /** 1085 * Moves a line or polygon with two fingers 1086 * @param {JXG.Coords} np1c x,y coordinates of first touch 1087 * @param {JXG.Coords} np2c x,y coordinates of second touch 1088 * @param {object} o The touch object that is dragged: {JXG.Board#touches}. 1089 * @param {object} drag The object that is dragged: 1090 */ 1091 twoFingerTouchObject: function (np1c, np2c, o, drag) { 1092 var np1, np2, op1, op2, 1093 nmid, omid, nd, od, 1094 d, 1095 S, alpha, t1, t2, t3, t4, t5, 1096 ar, i, len; 1097 1098 if (Type.exists(o.targets[0]) && 1099 Type.exists(o.targets[1]) && 1100 !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) { 1101 1102 np1 = np1c.usrCoords; 1103 np2 = np2c.usrCoords; 1104 // Previous finger position 1105 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords; 1106 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords; 1107 1108 // Affine mid points of the old and new positions 1109 omid = [1, (op1[1] + op2[1]) * 0.5, (op1[2] + op2[2]) * 0.5]; 1110 nmid = [1, (np1[1] + np2[1]) * 0.5, (np1[2] + np2[2]) * 0.5]; 1111 1112 // Old and new directions 1113 od = Mat.crossProduct(op1, op2); 1114 nd = Mat.crossProduct(np1, np2); 1115 S = Mat.crossProduct(od, nd); 1116 1117 // If parallel, translate otherwise rotate 1118 if (Math.abs(S[0]) < Mat.eps) { 1119 return; 1120 } 1121 1122 S[1] /= S[0]; 1123 S[2] /= S[0]; 1124 alpha = Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1)); 1125 t1 = this.create('transform', [alpha, S[1], S[2]], {type: 'rotate'}); 1126 1127 // Old midpoint of fingers after first transformation: 1128 t1.update(); 1129 omid = Mat.matVecMult(t1.matrix, omid); 1130 omid[1] /= omid[0]; 1131 omid[2] /= omid[0]; 1132 1133 // Shift to the new mid point 1134 t2 = this.create('transform', [nmid[1] - omid[1], nmid[2] - omid[2]], {type: 'translate'}); 1135 t2.update(); 1136 //omid = Mat.matVecMult(t2.matrix, omid); 1137 1138 t1.melt(t2); 1139 if (Type.evaluate(drag.visProp.scalable)) { 1140 // Scale 1141 d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2); 1142 t3 = this.create('transform', [-nmid[1], -nmid[2]], {type: 'translate'}); 1143 t4 = this.create('transform', [d, d], {type: 'scale'}); 1144 t5 = this.create('transform', [nmid[1], nmid[2]], {type: 'translate'}); 1145 t1.melt(t3).melt(t4).melt(t5); 1146 } 1147 1148 1149 if (drag.elementClass === Const.OBJECT_CLASS_LINE) { 1150 ar = []; 1151 if (drag.point1.draggable()) { 1152 ar.push(drag.point1); 1153 } 1154 if (drag.point2.draggable()) { 1155 ar.push(drag.point2); 1156 } 1157 t1.applyOnce(ar); 1158 } else if (drag.type === Const.OBJECT_TYPE_POLYGON) { 1159 ar = []; 1160 len = drag.vertices.length - 1; 1161 for (i = 0; i < len; ++i) { 1162 if (drag.vertices[i].draggable()) { 1163 ar.push(drag.vertices[i]); 1164 } 1165 } 1166 t1.applyOnce(ar); 1167 } 1168 1169 this.update(); 1170 drag.highlight(true); 1171 } 1172 }, 1173 1174 /* 1175 * Moves a circle with two fingers 1176 * @param {JXG.Coords} np1c x,y coordinates of first touch 1177 * @param {JXG.Coords} np2c x,y coordinates of second touch 1178 * @param {object} o The touch object that is dragged: {JXG.Board#touches}. 1179 * @param {object} drag The object that is dragged: 1180 */ 1181 twoFingerTouchCircle: function (np1c, np2c, o, drag) { 1182 var np1, np2, op1, op2, 1183 d, alpha, t1, t2, t3, t4, t5; 1184 1185 if (drag.method === 'pointCircle' || 1186 drag.method === 'pointLine') { 1187 return; 1188 } 1189 1190 if (Type.exists(o.targets[0]) && 1191 Type.exists(o.targets[1]) && 1192 !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) { 1193 1194 np1 = np1c.usrCoords; 1195 np2 = np2c.usrCoords; 1196 // Previous finger position 1197 op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords; 1198 op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords; 1199 1200 // Shift by the movement of the first finger 1201 t1 = this.create('transform', [np1[1] - op1[1], np1[2] - op1[2]], {type: 'translate'}); 1202 alpha = Geometry.rad(op2.slice(1), np1.slice(1), np2.slice(1)); 1203 1204 // Rotate and scale by the movement of the second finger 1205 t2 = this.create('transform', [-np1[1], -np1[2]], {type: 'translate'}); 1206 t3 = this.create('transform', [alpha], {type: 'rotate'}); 1207 t1.melt(t2).melt(t3); 1208 1209 if (Type.evaluate(drag.visProp.scalable)) { 1210 d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2); 1211 t4 = this.create('transform', [d, d], {type: 'scale'}); 1212 t1.melt(t4); 1213 } 1214 t5 = this.create('transform', [np1[1], np1[2]], {type: 'translate'}); 1215 t1.melt(t5); 1216 1217 if (drag.center.draggable()) { 1218 t1.applyOnce([drag.center]); 1219 } 1220 1221 if (drag.method === 'twoPoints') { 1222 if (drag.point2.draggable()) { 1223 t1.applyOnce([drag.point2]); 1224 } 1225 } else if (drag.method === 'pointRadius') { 1226 if (Type.isNumber(drag.updateRadius.origin)) { 1227 drag.setRadius(drag.radius * d); 1228 } 1229 } 1230 this.update(drag.center); 1231 drag.highlight(true); 1232 } 1233 }, 1234 1235 highlightElements: function (x, y, evt, target) { 1236 var el, pEl, pId, 1237 overObjects = {}, 1238 len = this.objectsList.length; 1239 1240 // Elements below the mouse pointer which are not highlighted yet will be highlighted. 1241 for (el = 0; el < len; el++) { 1242 pEl = this.objectsList[el]; 1243 pId = pEl.id; 1244 if (Type.exists(pEl.hasPoint) && pEl.visPropCalc.visible && pEl.hasPoint(x, y)) { 1245 // this is required in any case because otherwise the box won't be shown until the point is dragged 1246 this.updateInfobox(pEl); 1247 1248 if (!Type.exists(this.highlightedObjects[pId])) { // highlight only if not highlighted 1249 overObjects[pId] = pEl; 1250 pEl.highlight(); 1251 this.triggerEventHandlers(['mousehit', 'hit'], [evt, pEl, target]); 1252 } 1253 1254 if (pEl.mouseover) { 1255 pEl.triggerEventHandlers(['mousemove', 'move'], [evt]); 1256 } else { 1257 pEl.triggerEventHandlers(['mouseover', 'over'], [evt]); 1258 pEl.mouseover = true; 1259 } 1260 } 1261 } 1262 1263 for (el = 0; el < len; el++) { 1264 pEl = this.objectsList[el]; 1265 pId = pEl.id; 1266 if (pEl.mouseover) { 1267 if (!overObjects[pId]) { 1268 pEl.triggerEventHandlers(['mouseout', 'out'], [evt]); 1269 pEl.mouseover = false; 1270 } 1271 } 1272 } 1273 }, 1274 1275 /** 1276 * Helper function which returns a reasonable starting point for the object being dragged. 1277 * Formerly known as initXYstart(). 1278 * @private 1279 * @param {JXG.GeometryElement} obj The object to be dragged 1280 * @param {Array} targets Array of targets. It is changed by this function. 1281 */ 1282 saveStartPos: function (obj, targets) { 1283 var xy = [], i, len; 1284 1285 if (obj.type === Const.OBJECT_TYPE_TICKS) { 1286 xy.push([1, NaN, NaN]); 1287 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE) { 1288 xy.push(obj.point1.coords.usrCoords); 1289 xy.push(obj.point2.coords.usrCoords); 1290 } else if (obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1291 xy.push(obj.center.coords.usrCoords); 1292 if (obj.method === 'twoPoints') { 1293 xy.push(obj.point2.coords.usrCoords); 1294 } 1295 } else if (obj.type === Const.OBJECT_TYPE_POLYGON) { 1296 len = obj.vertices.length - 1; 1297 for (i = 0; i < len; i++) { 1298 xy.push(obj.vertices[i].coords.usrCoords); 1299 } 1300 } else if (obj.type === Const.OBJECT_TYPE_SECTOR) { 1301 xy.push(obj.point1.coords.usrCoords); 1302 xy.push(obj.point2.coords.usrCoords); 1303 xy.push(obj.point3.coords.usrCoords); 1304 } else if (Type.isPoint(obj) || obj.type === Const.OBJECT_TYPE_GLIDER) { 1305 xy.push(obj.coords.usrCoords); 1306 } else if (obj.elementClass === Const.OBJECT_CLASS_CURVE) { 1307 if (Type.exists(obj.parents)) { 1308 len = obj.parents.length; 1309 for (i = 0; i < len; i++) { 1310 xy.push(this.select(obj.parents[i]).coords.usrCoords); 1311 } 1312 } 1313 } else { 1314 try { 1315 xy.push(obj.coords.usrCoords); 1316 } catch (e) { 1317 JXG.debug('JSXGraph+ saveStartPos: obj.coords.usrCoords not available: ' + e); 1318 } 1319 } 1320 1321 len = xy.length; 1322 for (i = 0; i < len; i++) { 1323 targets.Zstart.push(xy[i][0]); 1324 targets.Xstart.push(xy[i][1]); 1325 targets.Ystart.push(xy[i][2]); 1326 } 1327 }, 1328 1329 mouseOriginMoveStart: function (evt) { 1330 var r, pos; 1331 1332 r = this._isRequiredKeyPressed(evt, 'pan'); 1333 if (r) { 1334 pos = this.getMousePosition(evt); 1335 this.initMoveOrigin(pos[0], pos[1]); 1336 } 1337 1338 return r; 1339 }, 1340 1341 mouseOriginMove: function (evt) { 1342 var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN), 1343 pos; 1344 1345 if (r) { 1346 pos = this.getMousePosition(evt); 1347 this.moveOrigin(pos[0], pos[1], true); 1348 } 1349 1350 return r; 1351 }, 1352 1353 /** 1354 * Start moving the origin with one finger. 1355 * @private 1356 * @param {Object} evt Event from touchStartListener 1357 * @return {Boolean} returns if the origin is moved. 1358 */ 1359 touchOriginMoveStart: function (evt) { 1360 var touches = evt[JXG.touchProperty], 1361 r, pos; 1362 1363 r = this.attr.pan.enabled && 1364 !this.attr.pan.needtwofingers && 1365 touches.length == 1; 1366 1367 if (r) { 1368 pos = this.getMousePosition(evt, 0); 1369 this.initMoveOrigin(pos[0], pos[1]); 1370 } 1371 1372 return r; 1373 }, 1374 1375 /** 1376 * Move the origin with one finger 1377 * @private 1378 * @param {Object} evt Event from touchMoveListener 1379 * @return {Boolean} returns if the origin is moved. 1380 */ 1381 touchOriginMove: function (evt) { 1382 var r = (this.mode === this.BOARD_MODE_MOVE_ORIGIN), 1383 pos; 1384 1385 if (r) { 1386 pos = this.getMousePosition(evt, 0); 1387 this.moveOrigin(pos[0], pos[1], true); 1388 } 1389 1390 return r; 1391 }, 1392 1393 /** 1394 * Stop moving the origin with one finger 1395 * @return {null} null 1396 * @private 1397 */ 1398 originMoveEnd: function () { 1399 this.updateQuality = this.BOARD_QUALITY_HIGH; 1400 this.mode = this.BOARD_MODE_NONE; 1401 }, 1402 1403 /********************************************************** 1404 * 1405 * Event Handler 1406 * 1407 **********************************************************/ 1408 1409 /** 1410 * Add all possible event handlers to the board object 1411 */ 1412 addEventHandlers: function () { 1413 if (Env.supportsPointerEvents()) { 1414 this.addPointerEventHandlers(); 1415 } else { 1416 this.addMouseEventHandlers(); 1417 this.addTouchEventHandlers(); 1418 } 1419 //if (Env.isBrowser) { 1420 //Env.addEvent(window, 'resize', this.update, this); 1421 //} 1422 1423 // This one produces errors on IE 1424 //Env.addEvent(this.containerObj, 'contextmenu', function (e) { e.preventDefault(); return false;}, this); 1425 // This one works on IE, Firefox and Chromium with default configurations. On some Safari 1426 // or Opera versions the user must explicitly allow the deactivation of the context menu. 1427 if (this.containerObj !== null) { 1428 this.containerObj.oncontextmenu = function (e) { 1429 if (Type.exists(e)) { 1430 e.preventDefault(); 1431 } 1432 return false; 1433 }; 1434 } 1435 1436 }, 1437 1438 /** 1439 * Registers the MSPointer* event handlers. 1440 */ 1441 addPointerEventHandlers: function () { 1442 if (!this.hasPointerHandlers && Env.isBrowser) { 1443 if (window.navigator.pointerEnabled) { // IE11+ 1444 Env.addEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1445 Env.addEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this); 1446 } else { 1447 Env.addEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1448 Env.addEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this); 1449 } 1450 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1451 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1452 1453 if (this.containerObj !== null) { 1454 // This is needed for capturing touch events. 1455 // It is also in jsxgraph.css, but one never knows... 1456 this.containerObj.style.touchAction = 'none'; 1457 } 1458 1459 this.hasPointerHandlers = true; 1460 } 1461 }, 1462 1463 /** 1464 * Registers mouse move, down and wheel event handlers. 1465 */ 1466 addMouseEventHandlers: function () { 1467 if (!this.hasMouseHandlers && Env.isBrowser) { 1468 Env.addEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1469 Env.addEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this); 1470 1471 Env.addEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1472 Env.addEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1473 1474 this.hasMouseHandlers = true; 1475 } 1476 }, 1477 1478 /** 1479 * Register touch start and move and gesture start and change event handlers. 1480 * @param {Boolean} appleGestures If set to false the gesturestart and gesturechange event handlers 1481 * will not be registered. 1482 */ 1483 addTouchEventHandlers: function (appleGestures) { 1484 if (!this.hasTouchHandlers && Env.isBrowser) { 1485 Env.addEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1486 Env.addEvent(this.containerObj, 'touchmove', this.touchMoveListener, this); 1487 1488 /* 1489 if (!Type.exists(appleGestures) || appleGestures) { 1490 // Gesture listener are called in touchStart and touchMove. 1491 //Env.addEvent(this.containerObj, 'gesturestart', this.gestureStartListener, this); 1492 //Env.addEvent(this.containerObj, 'gesturechange', this.gestureChangeListener, this); 1493 } 1494 */ 1495 1496 this.hasTouchHandlers = true; 1497 } 1498 }, 1499 1500 /** 1501 * Remove MSPointer* Event handlers. 1502 */ 1503 removePointerEventHandlers: function () { 1504 if (this.hasPointerHandlers && Env.isBrowser) { 1505 if (window.navigator.pointerEnabled) { // IE11+ 1506 Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this); 1507 Env.removeEvent(this.containerObj, 'pointermove', this.pointerMoveListener, this); 1508 } else { 1509 Env.removeEvent(this.containerObj, 'MSPointerDown', this.pointerDownListener, this); 1510 Env.removeEvent(this.containerObj, 'MSPointerMove', this.pointerMoveListener, this); 1511 } 1512 1513 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1514 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1515 1516 if (this.hasPointerUp) { 1517 if (window.navigator.pointerEnabled) { // IE11+ 1518 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 1519 } else { 1520 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 1521 } 1522 this.hasPointerUp = false; 1523 } 1524 1525 this.hasPointerHandlers = false; 1526 } 1527 }, 1528 1529 /** 1530 * De-register mouse event handlers. 1531 */ 1532 removeMouseEventHandlers: function () { 1533 if (this.hasMouseHandlers && Env.isBrowser) { 1534 Env.removeEvent(this.containerObj, 'mousedown', this.mouseDownListener, this); 1535 Env.removeEvent(this.containerObj, 'mousemove', this.mouseMoveListener, this); 1536 1537 if (this.hasMouseUp) { 1538 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 1539 this.hasMouseUp = false; 1540 } 1541 1542 Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this); 1543 Env.removeEvent(this.containerObj, 'DOMMouseScroll', this.mouseWheelListener, this); 1544 1545 this.hasMouseHandlers = false; 1546 } 1547 }, 1548 1549 /** 1550 * Remove all registered touch event handlers. 1551 */ 1552 removeTouchEventHandlers: function () { 1553 if (this.hasTouchHandlers && Env.isBrowser) { 1554 Env.removeEvent(this.containerObj, 'touchstart', this.touchStartListener, this); 1555 Env.removeEvent(this.containerObj, 'touchmove', this.touchMoveListener, this); 1556 1557 if (this.hasTouchEnd) { 1558 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 1559 this.hasTouchEnd = false; 1560 } 1561 1562 this.hasTouchHandlers = false; 1563 } 1564 }, 1565 1566 /** 1567 * Remove all event handlers from the board object 1568 */ 1569 removeEventHandlers: function () { 1570 this.removeMouseEventHandlers(); 1571 this.removeTouchEventHandlers(); 1572 this.removePointerEventHandlers(); 1573 }, 1574 1575 /** 1576 * Handler for click on left arrow in the navigation bar 1577 * @returns {JXG.Board} Reference to the board 1578 */ 1579 clickLeftArrow: function () { 1580 this.moveOrigin(this.origin.scrCoords[1] + this.canvasWidth * 0.1, this.origin.scrCoords[2]); 1581 return this; 1582 }, 1583 1584 /** 1585 * Handler for click on right arrow in the navigation bar 1586 * @returns {JXG.Board} Reference to the board 1587 */ 1588 clickRightArrow: function () { 1589 this.moveOrigin(this.origin.scrCoords[1] - this.canvasWidth * 0.1, this.origin.scrCoords[2]); 1590 return this; 1591 }, 1592 1593 /** 1594 * Handler for click on up arrow in the navigation bar 1595 * @returns {JXG.Board} Reference to the board 1596 */ 1597 clickUpArrow: function () { 1598 this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] - this.canvasHeight * 0.1); 1599 return this; 1600 }, 1601 1602 /** 1603 * Handler for click on down arrow in the navigation bar 1604 * @returns {JXG.Board} Reference to the board 1605 */ 1606 clickDownArrow: function () { 1607 this.moveOrigin(this.origin.scrCoords[1], this.origin.scrCoords[2] + this.canvasHeight * 0.1); 1608 return this; 1609 }, 1610 1611 /** 1612 * Triggered on iOS/Safari while the user inputs a gesture (e.g. pinch) and is used to zoom into the board. 1613 * Works on iOS/Safari and Android. 1614 * @param {Event} evt Browser event object 1615 * @returns {Boolean} 1616 */ 1617 gestureChangeListener: function (evt) { 1618 var c, 1619 // Save zoomFactors 1620 zx = this.attr.zoom.factorx, 1621 zy = this.attr.zoom.factory, 1622 factor, 1623 dist, 1624 dx, dy, theta, cx, cy, bound; 1625 1626 if (this.mode !== this.BOARD_MODE_ZOOM) { 1627 return true; 1628 } 1629 evt.preventDefault(); 1630 1631 c = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt, 0), this); 1632 dist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY], 1633 [evt.touches[1].clientX, evt.touches[1].clientY], 2); 1634 1635 // Android pinch to zoom 1636 if (evt.scale === undefined) { 1637 // evt.scale is undefined in Android 1638 evt.scale = dist / this.prevDist; 1639 } 1640 1641 factor = evt.scale / this.prevScale; 1642 this.prevScale = evt.scale; 1643 1644 // pan detected 1645 if (this.attr.pan.enabled && 1646 this.attr.pan.needtwofingers && 1647 Math.abs(evt.scale - 1.0) < 0.4 && 1648 this._num_pan >= 0.8 * this._num_zoom) { 1649 1650 this._num_pan++; 1651 this.moveOrigin(c.scrCoords[1], c.scrCoords[2], true); 1652 } else if (this.attr.zoom.enabled && 1653 Math.abs(factor - 1.0) < 0.5) { 1654 1655 this._num_zoom++; 1656 1657 if (this.attr.zoom.pinchhorizontal || this.attr.zoom.pinchvertical) { 1658 dx = Math.abs(evt.touches[0].clientX - evt.touches[1].clientX); 1659 dy = Math.abs(evt.touches[0].clientY - evt.touches[1].clientY); 1660 theta = Math.abs(Math.atan2(dy, dx)); 1661 bound = Math.PI * this.attr.zoom.pinchsensitivity / 90.0; 1662 } 1663 1664 if (this.attr.zoom.pinchhorizontal && theta < bound) { 1665 this.attr.zoom.factorx = factor; 1666 this.attr.zoom.factory = 1.0; 1667 cx = 0; 1668 cy = 0; 1669 } else if (this.attr.zoom.pinchvertical && Math.abs(theta - Math.PI * 0.5) < bound) { 1670 this.attr.zoom.factorx = 1.0; 1671 this.attr.zoom.factory = factor; 1672 cx = 0; 1673 cy = 0; 1674 } else { 1675 this.attr.zoom.factorx = factor; 1676 this.attr.zoom.factory = factor; 1677 cx = c.usrCoords[1]; 1678 cy = c.usrCoords[2]; 1679 } 1680 1681 this.zoomIn(cx, cy); 1682 1683 // Restore zoomFactors 1684 this.attr.zoom.factorx = zx; 1685 this.attr.zoom.factory = zy; 1686 } 1687 1688 return false; 1689 }, 1690 1691 /** 1692 * Called by iOS/Safari as soon as the user starts a gesture. Works natively on iOS/Safari, 1693 * on Android we emulate it. 1694 * @param {Event} evt 1695 * @returns {Boolean} 1696 */ 1697 gestureStartListener: function (evt) { 1698 var pos; 1699 1700 evt.preventDefault(); 1701 this.prevScale = 1.0; 1702 // Android pinch to zoom 1703 this.prevDist = Geometry.distance([evt.touches[0].clientX, evt.touches[0].clientY], 1704 [evt.touches[1].clientX, evt.touches[1].clientY], 2); 1705 1706 // If pinch-to-zoom is interpreted as panning 1707 // we have to prepare move origin 1708 pos = this.getMousePosition(evt, 0); 1709 this.initMoveOrigin(pos[0], pos[1]); 1710 1711 this._num_zoom = this._num_pan = 0; 1712 this.mode = this.BOARD_MODE_ZOOM; 1713 return false; 1714 }, 1715 1716 /** 1717 * Test if the required key combination is pressed for wheel zoom, move origin and 1718 * selection 1719 * @private 1720 * @param {Object} evt Mouse or pen event 1721 * @param {String} action String containing the action: 'zoom', 'pan', 'selection'. 1722 * Corresponds to the attribute subobject. 1723 * @return {Boolean} true or false. 1724 */ 1725 _isRequiredKeyPressed: function (evt, action) { 1726 var obj = this.attr[action]; 1727 if (!obj.enabled) { 1728 return false; 1729 } 1730 1731 if (((obj.needshift && evt.shiftKey) || (!obj.needshift && !evt.shiftKey)) && 1732 ((obj.needctrl && evt.ctrlKey) || (!obj.needctrl && !evt.ctrlKey)) 1733 ) { 1734 return true; 1735 } 1736 1737 return false; 1738 }, 1739 1740 /** 1741 * pointer-Events 1742 */ 1743 1744 _pointerAddBoardTouches: function (evt) { 1745 var i, found; 1746 1747 for (i = 0, found = false; i < this._board_touches.length; i++) { 1748 if (this._board_touches[i].pointerId === evt.pointerId) { 1749 this._board_touches[i].clientX = evt.clientX; 1750 this._board_touches[i].clientY = evt.clientY; 1751 found = true; 1752 break; 1753 } 1754 } 1755 1756 if (!found) { 1757 this._board_touches.push({ 1758 pointerId: evt.pointerId, 1759 clientX: evt.clientX, 1760 clientY: evt.clientY 1761 }); 1762 } 1763 1764 return this; 1765 }, 1766 1767 _pointerRemoveBoardTouches: function (evt) { 1768 var i; 1769 for (i = 0; i < this._board_touches.length; i++) { 1770 if (this._board_touches[i].pointerId === evt.pointerId) { 1771 this._board_touches.splice(i, 1); 1772 break; 1773 } 1774 } 1775 1776 return this; 1777 }, 1778 1779 /** 1780 * This method is called by the browser when a pointing device is pressed on the screen. 1781 * @param {Event} evt The browsers event object. 1782 * @param {Object} object If the object to be dragged is already known, it can be submitted via this parameter 1783 * @returns {Boolean} ... 1784 */ 1785 pointerDownListener: function (evt, object) { 1786 var i, j, k, pos, elements, sel, 1787 eps = this.options.precision.touch, 1788 found, target, result; 1789 1790 if (!this.hasPointerUp) { 1791 if (window.navigator.pointerEnabled) { // IE11+ 1792 Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this); 1793 } else { 1794 Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 1795 } 1796 this.hasPointerUp = true; 1797 } 1798 1799 if (this.hasMouseHandlers) { 1800 this.removeMouseEventHandlers(); 1801 } 1802 1803 if (this.hasTouchHandlers) { 1804 this.removeTouchEventHandlers(); 1805 } 1806 1807 // prevent accidental selection of text 1808 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 1809 this.document.selection.empty(); 1810 } else if (window.getSelection) { 1811 sel = window.getSelection(); 1812 if (sel.removeAllRanges) { 1813 try { 1814 sel.removeAllRanges(); 1815 } catch (e) {} 1816 } 1817 } 1818 1819 // Touch or pen device 1820 if (Env.isBrowser && 1821 (evt.pointerType === 'touch' || // New 1822 (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) // Old 1823 ) { 1824 this.options.precision.hasPoint = eps; 1825 } 1826 1827 // This should be easier than the touch events. Every pointer device gets its own pointerId, e.g. the mouse 1828 // always has id 1, fingers and pens get unique ids every time a pointerDown event is fired and they will 1829 // keep this id until a pointerUp event is fired. What we have to do here is: 1830 // 1. collect all elements under the current pointer 1831 // 2. run through the touches control structure 1832 // a. look for the object collected in step 1. 1833 // b. if an object is found, check the number of pointers. If appropriate, add the pointer. 1834 1835 pos = this.getMousePosition(evt); 1836 1837 // selection 1838 this._testForSelection(evt); 1839 if (this.selectingMode) { 1840 this._startSelecting(pos); 1841 this.triggerEventHandlers(['touchstartselecting', 'pointerstartselecting', 'startselecting'], [evt]); 1842 return; // don't continue as a normal click 1843 } 1844 1845 if (object) { 1846 elements = [ object ]; 1847 this.mode = this.BOARD_MODE_DRAG; 1848 } else { 1849 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 1850 } 1851 1852 // if no draggable object can be found, get out here immediately 1853 if (elements.length > 0) { 1854 // check touches structure 1855 target = elements[elements.length - 1]; 1856 found = false; 1857 for (i = 0; i < this.touches.length; i++) { 1858 // the target is already in our touches array, try to add the pointer to the existing touch 1859 if (this.touches[i].obj === target) { 1860 j = i; 1861 k = this.touches[i].targets.push({ 1862 num: evt.pointerId, 1863 X: pos[0], 1864 Y: pos[1], 1865 Xprev: NaN, 1866 Yprev: NaN, 1867 Xstart: [], 1868 Ystart: [], 1869 Zstart: [] 1870 }) - 1; 1871 1872 found = true; 1873 break; 1874 } 1875 } 1876 1877 if (!found) { 1878 k = 0; 1879 j = this.touches.push({ 1880 obj: target, 1881 targets: [{ 1882 num: evt.pointerId, 1883 X: pos[0], 1884 Y: pos[1], 1885 Xprev: NaN, 1886 Yprev: NaN, 1887 Xstart: [], 1888 Ystart: [], 1889 Zstart: [] 1890 }] 1891 }) - 1; 1892 } 1893 1894 this.dehighlightAll(); 1895 target.highlight(true); 1896 1897 this.saveStartPos(target, this.touches[j].targets[k]); 1898 1899 // prevent accidental text selection 1900 // this could get us new trouble: input fields, links and drop down boxes placed as text 1901 // on the board don't work anymore. 1902 if (evt && evt.preventDefault) { 1903 evt.preventDefault(); 1904 } else if (window.event) { 1905 window.event.returnValue = false; 1906 } 1907 } 1908 1909 if (this.touches.length > 0) { 1910 evt.preventDefault(); 1911 evt.stopPropagation(); 1912 } 1913 1914 this.options.precision.hasPoint = this.options.precision.mouse; 1915 1916 if (Env.isBrowser && evt.pointerType !== 'touch') { 1917 if (this.mode === this.BOARD_MODE_NONE) { 1918 this.mouseOriginMoveStart(evt); 1919 } 1920 } else { 1921 this._pointerAddBoardTouches(evt); 1922 evt.touches = this._board_touches; 1923 1924 // See touchStartListener 1925 if (this.mode === this.BOARD_MODE_NONE && this.touchOriginMoveStart(evt)) { 1926 } else if ((this.mode === this.BOARD_MODE_NONE || 1927 this.mode === this.BOARD_MODE_MOVE_ORIGIN) && 1928 evt.touches.length == 2) { 1929 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 1930 this.originMoveEnd(); 1931 } 1932 1933 this.gestureStartListener(evt); 1934 } 1935 } 1936 1937 this.triggerEventHandlers(['touchstart', 'down', 'pointerdown', 'MSPointerDown'], [evt]); 1938 1939 //return result; 1940 return false; 1941 }, 1942 1943 /** 1944 * Called periodically by the browser while the user moves a pointing device across the screen. 1945 * @param {Event} evt 1946 * @returns {Boolean} 1947 */ 1948 pointerMoveListener: function (evt) { 1949 var i, j, pos; 1950 1951 if (this.mode !== this.BOARD_MODE_DRAG) { 1952 this.dehighlightAll(); 1953 this.showInfobox(false); 1954 } 1955 1956 if (this.mode !== this.BOARD_MODE_NONE) { 1957 evt.preventDefault(); 1958 evt.stopPropagation(); 1959 } 1960 1961 // Touch or pen device 1962 if (Env.isBrowser && 1963 (evt.pointerType === 'touch' || // New 1964 (window.navigator.msMaxTouchPoints && window.navigator.msMaxTouchPoints > 1)) // Old 1965 ) { 1966 this.options.precision.hasPoint = this.options.precision.touch; 1967 } 1968 this.updateQuality = this.BOARD_QUALITY_LOW; 1969 1970 // selection 1971 if (this.selectingMode) { 1972 pos = this.getMousePosition(evt); 1973 this._moveSelecting(pos); 1974 this.triggerEventHandlers(['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], [evt, this.mode]); 1975 } else if (!this.mouseOriginMove(evt)) { 1976 if (this.mode === this.BOARD_MODE_DRAG) { 1977 // Runs through all elements which are touched by at least one finger. 1978 for (i = 0; i < this.touches.length; i++) { 1979 for (j = 0; j < this.touches[i].targets.length; j++) { 1980 if (this.touches[i].targets[j].num === evt.pointerId) { 1981 // Touch by one finger: this is possible for all elements that can be dragged 1982 if (this.touches[i].targets.length === 1) { 1983 this.touches[i].targets[j].X = evt.pageX; 1984 this.touches[i].targets[j].Y = evt.pageY; 1985 pos = this.getMousePosition(evt); 1986 this.moveObject(pos[0], pos[1], this.touches[i], evt, 'touch'); 1987 // Touch by two fingers: moving lines 1988 } else if (this.touches[i].targets.length === 2 && 1989 this.touches[i].targets[0].num > -1 && this.touches[i].targets[1].num > -1) { 1990 1991 this.touches[i].targets[j].X = evt.pageX; 1992 this.touches[i].targets[j].Y = evt.pageY; 1993 1994 this.twoFingerMove( 1995 this.getMousePosition({ 1996 clientX: this.touches[i].targets[0].X, 1997 clientY: this.touches[i].targets[0].Y 1998 }), 1999 this.getMousePosition({ 2000 clientX: this.touches[i].targets[1].X, 2001 clientY: this.touches[i].targets[1].Y 2002 }), 2003 this.touches[i], 2004 evt 2005 ); 2006 } 2007 2008 // there is only one pointer in the evt object, there's no point in looking further 2009 break; 2010 } 2011 } 2012 } 2013 } else { 2014 if (evt.pointerType == 'touch') { 2015 this._pointerAddBoardTouches(evt); 2016 if (this._board_touches.length == 2) { 2017 evt.touches = this._board_touches; 2018 this.gestureChangeListener(evt); 2019 } 2020 } else { 2021 pos = this.getMousePosition(evt); 2022 this.highlightElements(pos[0], pos[1], evt, -1); 2023 } 2024 } 2025 } 2026 2027 // Hiding the infobox is commented out, since it prevents showing the infobox 2028 // on IE 11+ on 'over' 2029 //if (this.mode !== this.BOARD_MODE_DRAG) { 2030 //this.showInfobox(false); 2031 //} 2032 2033 this.options.precision.hasPoint = this.options.precision.mouse; 2034 this.triggerEventHandlers(['touchmove', 'move', 'pointermove', 'MSPointerMove'], [evt, this.mode]); 2035 2036 return this.mode === this.BOARD_MODE_NONE; 2037 }, 2038 2039 /** 2040 * Triggered as soon as the user stops touching the device with at least one finger. 2041 * @param {Event} evt 2042 * @returns {Boolean} 2043 */ 2044 pointerUpListener: function (evt) { 2045 var i, j, found; 2046 2047 this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 2048 this.showInfobox(false); 2049 2050 if (evt) { 2051 for (i = 0; i < this.touches.length; i++) { 2052 for (j = 0; j < this.touches[i].targets.length; j++) { 2053 if (this.touches[i].targets[j].num === evt.pointerId) { 2054 this.touches[i].targets.splice(j, 1); 2055 2056 if (this.touches[i].targets.length === 0) { 2057 this.touches.splice(i, 1); 2058 } 2059 2060 break; 2061 } 2062 } 2063 } 2064 } 2065 2066 // selection 2067 if (this.selectingMode) { 2068 this._stopSelecting(evt); 2069 this.triggerEventHandlers(['touchstopselecting', 'pointerstopselecting', 'stopselecting'], [evt]); 2070 } else { 2071 for (i = this.downObjects.length - 1; i > -1; i--) { 2072 found = false; 2073 for (j = 0; j < this.touches.length; j++) { 2074 if (this.touches[j].obj.id === this.downObjects[i].id) { 2075 found = true; 2076 } 2077 } 2078 if (!found) { 2079 this.downObjects[i].triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]); 2080 this.downObjects[i].snapToGrid(); 2081 this.downObjects[i].snapToPoints(); 2082 this.downObjects.splice(i, 1); 2083 } 2084 } 2085 } 2086 2087 this._pointerRemoveBoardTouches(evt); 2088 2089 // if (this.touches.length === 0) { 2090 if (this._board_touches.length === 0) { 2091 if (this.hasPointerUp) { 2092 if (window.navigator.pointerEnabled) { // IE11+ 2093 Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this); 2094 } else { 2095 Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this); 2096 } 2097 this.hasPointerUp = false; 2098 } 2099 2100 this.dehighlightAll(); 2101 this.updateQuality = this.BOARD_QUALITY_HIGH; 2102 this.mode = this.BOARD_MODE_NONE; 2103 2104 this.originMoveEnd(); 2105 this.update(); 2106 } 2107 2108 2109 return true; 2110 }, 2111 2112 /** 2113 * Touch-Events 2114 */ 2115 2116 /** 2117 * This method is called by the browser when a finger touches the surface of the touch-device. 2118 * @param {Event} evt The browsers event object. 2119 * @returns {Boolean} ... 2120 */ 2121 touchStartListener: function (evt) { 2122 var i, pos, elements, j, k, time, 2123 eps = this.options.precision.touch, 2124 obj, found, targets, 2125 evtTouches = evt[JXG.touchProperty], 2126 target; 2127 2128 if (!this.hasTouchEnd) { 2129 Env.addEvent(this.document, 'touchend', this.touchEndListener, this); 2130 this.hasTouchEnd = true; 2131 } 2132 2133 // Do not remove mouseHandlers, since Chrome on win tablets sends mouseevents if used with pen. 2134 //if (this.hasMouseHandlers) { this.removeMouseEventHandlers(); } 2135 2136 // prevent accidental selection of text 2137 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2138 this.document.selection.empty(); 2139 } else if (window.getSelection) { 2140 window.getSelection().removeAllRanges(); 2141 } 2142 2143 // multitouch 2144 this.options.precision.hasPoint = this.options.precision.touch; 2145 2146 // This is the most critical part. first we should run through the existing touches and collect all targettouches that don't belong to our 2147 // 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 2148 // 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 2149 // 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 2150 // stretching. as a last step we're going through the rest of the targettouches and initiate new move operations: 2151 // * points have higher priority over other elements. 2152 // * 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 2153 // this element and add them. 2154 // ADDENDUM 11/10/11: 2155 // (1) run through the touches control object, 2156 // (2) try to find the targetTouches for every touch. on touchstart only new touches are added, hence we can find a targettouch 2157 // for every target in our touches objects 2158 // (3) if one of the targettouches was bound to a touches targets array, mark it 2159 // (4) run through the targettouches. if the targettouch is marked, continue. otherwise check for elements below the targettouch: 2160 // (a) if no element could be found: mark the target touches and continue 2161 // --- in the following cases, "init" means: 2162 // (i) check if the element is already used in another touches element, if so, mark the targettouch and continue 2163 // (ii) if not, init a new touches element, add the targettouch to the touches property and mark it 2164 // (b) if the element is a point, init 2165 // (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 2166 // (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 2167 // add both to the touches array and mark them. 2168 for (i = 0; i < evtTouches.length; i++) { 2169 evtTouches[i].jxg_isused = false; 2170 } 2171 2172 for (i = 0; i < this.touches.length; i++) { 2173 for (j = 0; j < this.touches[i].targets.length; j++) { 2174 this.touches[i].targets[j].num = -1; 2175 eps = this.options.precision.touch; 2176 2177 do { 2178 for (k = 0; k < evtTouches.length; k++) { 2179 // find the new targettouches 2180 if (Math.abs(Math.pow(evtTouches[k].screenX - this.touches[i].targets[j].X, 2) + 2181 Math.pow(evtTouches[k].screenY - this.touches[i].targets[j].Y, 2)) < eps * eps) { 2182 this.touches[i].targets[j].num = k; 2183 2184 this.touches[i].targets[j].X = evtTouches[k].screenX; 2185 this.touches[i].targets[j].Y = evtTouches[k].screenY; 2186 evtTouches[k].jxg_isused = true; 2187 break; 2188 } 2189 } 2190 2191 eps *= 2; 2192 2193 } while (this.touches[i].targets[j].num === -1 && eps < this.options.precision.touchMax); 2194 2195 if (this.touches[i].targets[j].num === -1) { 2196 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.'); 2197 JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax); 2198 this.touches[i].targets.splice(i, 1); 2199 } 2200 2201 } 2202 } 2203 2204 // we just re-mapped the targettouches to our existing touches list. 2205 // now we have to initialize some touches from additional targettouches 2206 for (i = 0; i < evtTouches.length; i++) { 2207 if (!evtTouches[i].jxg_isused) { 2208 2209 pos = this.getMousePosition(evt, i); 2210 // selection 2211 // this._testForSelection(evt); // we do not have shift or ctrl keys yet. 2212 if (this.selectingMode) { 2213 this._startSelecting(pos); 2214 this.triggerEventHandlers(['touchstartselecting', 'startselecting'], [evt]); 2215 evt.preventDefault(); 2216 evt.stopPropagation(); 2217 this.options.precision.hasPoint = this.options.precision.mouse; 2218 return this.touches.length > 0; // don't continue as a normal click 2219 } 2220 2221 elements = this.initMoveObject(pos[0], pos[1], evt, 'touch'); 2222 if (elements.length !== 0) { 2223 obj = elements[elements.length - 1]; 2224 2225 if (Type.isPoint(obj) || 2226 obj.elementClass === Const.OBJECT_CLASS_TEXT || 2227 obj.type === Const.OBJECT_TYPE_TICKS || 2228 obj.type === Const.OBJECT_TYPE_IMAGE) { 2229 // it's a point, so it's single touch, so we just push it to our touches 2230 targets = [{ num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] }]; 2231 2232 // For the UNDO/REDO of object moves 2233 this.saveStartPos(obj, targets[0]); 2234 2235 this.touches.push({ obj: obj, targets: targets }); 2236 obj.highlight(true); 2237 2238 } else if (obj.elementClass === Const.OBJECT_CLASS_LINE || 2239 obj.elementClass === Const.OBJECT_CLASS_CIRCLE || 2240 obj.elementClass === Const.OBJECT_CLASS_CURVE || 2241 obj.type === Const.OBJECT_TYPE_POLYGON) { 2242 found = false; 2243 2244 // first check if this geometric object is already captured in this.touches 2245 for (j = 0; j < this.touches.length; j++) { 2246 if (obj.id === this.touches[j].obj.id) { 2247 found = true; 2248 // only add it, if we don't have two targets in there already 2249 if (this.touches[j].targets.length === 1) { 2250 target = { num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] }; 2251 2252 // For the UNDO/REDO of object moves 2253 this.saveStartPos(obj, target); 2254 this.touches[j].targets.push(target); 2255 } 2256 2257 evtTouches[i].jxg_isused = true; 2258 } 2259 } 2260 2261 // we couldn't find it in touches, so we just init a new touches 2262 // IF there is a second touch targetting this line, we will find it later on, and then add it to 2263 // the touches control object. 2264 if (!found) { 2265 targets = [{ num: i, X: evtTouches[i].screenX, Y: evtTouches[i].screenY, Xprev: NaN, Yprev: NaN, Xstart: [], Ystart: [], Zstart: [] }]; 2266 2267 // For the UNDO/REDO of object moves 2268 this.saveStartPos(obj, targets[0]); 2269 this.touches.push({ obj: obj, targets: targets }); 2270 obj.highlight(true); 2271 } 2272 } 2273 } 2274 2275 evtTouches[i].jxg_isused = true; 2276 } 2277 } 2278 2279 if (this.touches.length > 0) { 2280 evt.preventDefault(); 2281 evt.stopPropagation(); 2282 } 2283 2284 // Touch events on empty areas of the board are handled here: 2285 // 1. case: one finger. If allowed, this triggers pan with one finger 2286 if (this.mode === this.BOARD_MODE_NONE && this.touchOriginMoveStart(evt)) { 2287 } else if (evtTouches.length == 2 && 2288 (this.mode === this.BOARD_MODE_NONE || 2289 this.mode === this.BOARD_MODE_MOVE_ORIGIN /*|| 2290 (this.mode === this.BOARD_MODE_DRAG && this.touches.length == 1) */ 2291 )) { 2292 // 2. case: two fingers: pinch to zoom or pan with two fingers needed. 2293 // This happens when the second finger hits the device. First, the 2294 // "one finger pan mode" has to be cancelled. 2295 if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) { 2296 this.originMoveEnd(); 2297 } 2298 this.gestureStartListener(evt); 2299 } 2300 2301 this.options.precision.hasPoint = this.options.precision.mouse; 2302 this.triggerEventHandlers(['touchstart', 'down'], [evt]); 2303 2304 return false; 2305 //return this.touches.length > 0; 2306 }, 2307 2308 /** 2309 * Called periodically by the browser while the user moves his fingers across the device. 2310 * @param {Event} evt 2311 * @returns {Boolean} 2312 */ 2313 touchMoveListener: function (evt) { 2314 var i, pos1, pos2, time, 2315 evtTouches = evt[JXG.touchProperty]; 2316 2317 if (this.mode !== this.BOARD_MODE_NONE) { 2318 evt.preventDefault(); 2319 evt.stopPropagation(); 2320 } 2321 2322 // Reduce update frequency for Android devices 2323 // if (false && Env.isWebkitAndroid()) { 2324 // time = new Date(); 2325 // time = time.getTime(); 2326 // 2327 // if (time - this.touchMoveLast < 80) { 2328 // this.updateQuality = this.BOARD_QUALITY_HIGH; 2329 // this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 2330 // 2331 // return false; 2332 // } 2333 // 2334 // this.touchMoveLast = time; 2335 // } 2336 2337 if (this.mode !== this.BOARD_MODE_DRAG) { 2338 this.showInfobox(false); 2339 } 2340 2341 this.options.precision.hasPoint = this.options.precision.touch; 2342 this.updateQuality = this.BOARD_QUALITY_LOW; 2343 2344 // selection 2345 if (this.selectingMode) { 2346 for (i = 0; i < evtTouches.length; i++) { 2347 if (!evtTouches[i].jxg_isused) { 2348 pos1 = this.getMousePosition(evt, i); 2349 this._moveSelecting(pos1); 2350 this.triggerEventHandlers(['touchmoves', 'moveselecting'], [evt, this.mode]); 2351 break; 2352 } 2353 } 2354 } else { 2355 if (!this.touchOriginMove(evt)) { 2356 if (this.mode === this.BOARD_MODE_DRAG) { 2357 // Runs over through all elements which are touched 2358 // by at least one finger. 2359 for (i = 0; i < this.touches.length; i++) { 2360 // Touch by one finger: this is possible for all elements that can be dragged 2361 if (this.touches[i].targets.length === 1) { 2362 if (evtTouches[this.touches[i].targets[0].num]) { 2363 pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num); 2364 if (pos1[0] < 0 || pos1[0] > this.canvasWidth || pos1[1] < 0 || pos1[1] > this.canvasHeight) { 2365 return; 2366 } 2367 this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX; 2368 this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY; 2369 this.moveObject(pos1[0], pos1[1], this.touches[i], evt, 'touch'); 2370 } 2371 // Touch by two fingers: moving lines 2372 } else if (this.touches[i].targets.length === 2 && 2373 this.touches[i].targets[0].num > -1 && 2374 this.touches[i].targets[1].num > -1) { 2375 if (evtTouches[this.touches[i].targets[0].num] && evtTouches[this.touches[i].targets[1].num]) { 2376 pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num); 2377 pos2 = this.getMousePosition(evt, this.touches[i].targets[1].num); 2378 if (pos1[0] < 0 || pos1[0] > this.canvasWidth || pos1[1] < 0 || pos1[1] > this.canvasHeight || 2379 pos2[0] < 0 || pos2[0] > this.canvasWidth || pos2[1] < 0 || pos2[1] > this.canvasHeight) { 2380 return; 2381 } 2382 this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX; 2383 this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY; 2384 this.touches[i].targets[1].X = evtTouches[this.touches[i].targets[1].num].screenX; 2385 this.touches[i].targets[1].Y = evtTouches[this.touches[i].targets[1].num].screenY; 2386 this.twoFingerMove(pos1, pos2, this.touches[i], evt); 2387 } 2388 } 2389 } 2390 } else { 2391 if (evtTouches.length == 2) { 2392 this.gestureChangeListener(evt); 2393 } 2394 } 2395 } 2396 } 2397 2398 if (this.mode !== this.BOARD_MODE_DRAG) { 2399 this.showInfobox(false); 2400 } 2401 2402 /* 2403 this.updateQuality = this.BOARD_QUALITY_HIGH; is set in touchEnd 2404 */ 2405 this.options.precision.hasPoint = this.options.precision.mouse; 2406 this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]); 2407 2408 return this.mode === this.BOARD_MODE_NONE; 2409 }, 2410 2411 /** 2412 * Triggered as soon as the user stops touching the device with at least one finger. 2413 * @param {Event} evt 2414 * @returns {Boolean} 2415 */ 2416 touchEndListener: function (evt) { 2417 var i, j, k, 2418 eps = this.options.precision.touch, 2419 tmpTouches = [], found, foundNumber, 2420 evtTouches = evt && evt[JXG.touchProperty]; 2421 2422 this.triggerEventHandlers(['touchend', 'up'], [evt]); 2423 this.showInfobox(false); 2424 2425 // selection 2426 if (this.selectingMode) { 2427 this._stopSelecting(evt); 2428 this.triggerEventHandlers(['touchstopselecting', 'stopselecting'], [evt]); 2429 } else if (evtTouches && evtTouches.length > 0) { 2430 for (i = 0; i < this.touches.length; i++) { 2431 tmpTouches[i] = this.touches[i]; 2432 } 2433 this.touches.length = 0; 2434 2435 // try to convert the operation, e.g. if a lines is rotated and translated with two fingers and one finger is lifted, 2436 // convert the operation to a simple one-finger-translation. 2437 // ADDENDUM 11/10/11: 2438 // see addendum to touchStartListener from 11/10/11 2439 // (1) run through the tmptouches 2440 // (2) check the touches.obj, if it is a 2441 // (a) point, try to find the targettouch, if found keep it and mark the targettouch, else drop the touch. 2442 // (b) line with 2443 // (i) one target: try to find it, if found keep it mark the targettouch, else drop the touch. 2444 // (ii) two targets: if none can be found, drop the touch. if one can be found, remove the other target. mark all found targettouches 2445 // (c) circle with [proceed like in line] 2446 2447 // init the targettouches marker 2448 for (i = 0; i < evtTouches.length; i++) { 2449 evtTouches[i].jxg_isused = false; 2450 } 2451 2452 for (i = 0; i < tmpTouches.length; i++) { 2453 // could all targets of the current this.touches.obj be assigned to targettouches? 2454 found = false; 2455 foundNumber = 0; 2456 2457 for (j = 0; j < tmpTouches[i].targets.length; j++) { 2458 tmpTouches[i].targets[j].found = false; 2459 for (k = 0; k < evtTouches.length; k++) { 2460 if (Math.abs(Math.pow(evtTouches[k].screenX - tmpTouches[i].targets[j].X, 2) + Math.pow(evtTouches[k].screenY - tmpTouches[i].targets[j].Y, 2)) < eps * eps) { 2461 tmpTouches[i].targets[j].found = true; 2462 tmpTouches[i].targets[j].num = k; 2463 tmpTouches[i].targets[j].X = evtTouches[k].screenX; 2464 tmpTouches[i].targets[j].Y = evtTouches[k].screenY; 2465 foundNumber += 1; 2466 break; 2467 } 2468 } 2469 } 2470 2471 if (Type.isPoint(tmpTouches[i].obj)) { 2472 found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found); 2473 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) { 2474 found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found) || (tmpTouches[i].targets[1] && tmpTouches[i].targets[1].found); 2475 } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) { 2476 found = foundNumber === 1 || foundNumber === 3; 2477 } 2478 2479 // if we found this object to be still dragged by the user, add it back to this.touches 2480 if (found) { 2481 this.touches.push({ 2482 obj: tmpTouches[i].obj, 2483 targets: [] 2484 }); 2485 2486 for (j = 0; j < tmpTouches[i].targets.length; j++) { 2487 if (tmpTouches[i].targets[j].found) { 2488 this.touches[this.touches.length - 1].targets.push({ 2489 num: tmpTouches[i].targets[j].num, 2490 X: tmpTouches[i].targets[j].screenX, 2491 Y: tmpTouches[i].targets[j].screenY, 2492 Xprev: NaN, 2493 Yprev: NaN, 2494 Xstart: tmpTouches[i].targets[j].Xstart, 2495 Ystart: tmpTouches[i].targets[j].Ystart, 2496 Zstart: tmpTouches[i].targets[j].Zstart 2497 }); 2498 } 2499 } 2500 2501 } else { 2502 tmpTouches[i].obj.noHighlight(); 2503 } 2504 } 2505 2506 } else { 2507 this.touches.length = 0; 2508 } 2509 2510 for (i = this.downObjects.length - 1; i > -1; i--) { 2511 found = false; 2512 for (j = 0; j < this.touches.length; j++) { 2513 if (this.touches[j].obj.id === this.downObjects[i].id) { 2514 found = true; 2515 } 2516 } 2517 if (!found) { 2518 this.downObjects[i].triggerEventHandlers(['touchup', 'up'], [evt]); 2519 this.downObjects[i].snapToGrid(); 2520 this.downObjects[i].snapToPoints(); 2521 this.downObjects.splice(i, 1); 2522 } 2523 } 2524 2525 if (!evtTouches || evtTouches.length === 0) { 2526 2527 if (this.hasTouchEnd) { 2528 Env.removeEvent(this.document, 'touchend', this.touchEndListener, this); 2529 this.hasTouchEnd = false; 2530 } 2531 2532 this.dehighlightAll(); 2533 this.updateQuality = this.BOARD_QUALITY_HIGH; 2534 2535 this.originMoveEnd(); 2536 this.update(); 2537 } 2538 2539 return true; 2540 }, 2541 2542 /** 2543 * This method is called by the browser when the mouse button is clicked. 2544 * @param {Event} evt The browsers event object. 2545 * @returns {Boolean} True if no element is found under the current mouse pointer, false otherwise. 2546 */ 2547 mouseDownListener: function (evt) { 2548 var pos, elements, result; 2549 2550 // prevent accidental selection of text 2551 if (this.document.selection && Type.isFunction(this.document.selection.empty)) { 2552 this.document.selection.empty(); 2553 } else if (window.getSelection) { 2554 window.getSelection().removeAllRanges(); 2555 } 2556 2557 if (!this.hasMouseUp) { 2558 Env.addEvent(this.document, 'mouseup', this.mouseUpListener, this); 2559 this.hasMouseUp = true; 2560 } else { 2561 // In case this.hasMouseUp==true, it may be that there was a 2562 // mousedown event before which was not followed by an mouseup event. 2563 // This seems to happen with interactive whiteboard pens sometimes. 2564 return; 2565 } 2566 2567 pos = this.getMousePosition(evt); 2568 2569 // selection 2570 this._testForSelection(evt); 2571 if (this.selectingMode) { 2572 this._startSelecting(pos); 2573 this.triggerEventHandlers(['mousestartselecting', 'startselecting'], [evt]); 2574 return; // don't continue as a normal click 2575 } 2576 2577 elements = this.initMoveObject(pos[0], pos[1], evt, 'mouse'); 2578 2579 // if no draggable object can be found, get out here immediately 2580 if (elements.length === 0) { 2581 this.mode = this.BOARD_MODE_NONE; 2582 result = true; 2583 } else { 2584 this.mouse = { 2585 obj: null, 2586 targets: [{ 2587 X: pos[0], 2588 Y: pos[1], 2589 Xprev: NaN, 2590 Yprev: NaN 2591 }] 2592 }; 2593 this.mouse.obj = elements[elements.length - 1]; 2594 2595 this.dehighlightAll(); 2596 this.mouse.obj.highlight(true); 2597 2598 this.mouse.targets[0].Xstart = []; 2599 this.mouse.targets[0].Ystart = []; 2600 this.mouse.targets[0].Zstart = []; 2601 2602 this.saveStartPos(this.mouse.obj, this.mouse.targets[0]); 2603 2604 // prevent accidental text selection 2605 // this could get us new trouble: input fields, links and drop down boxes placed as text 2606 // on the board don't work anymore. 2607 if (evt && evt.preventDefault) { 2608 evt.preventDefault(); 2609 } else if (window.event) { 2610 window.event.returnValue = false; 2611 } 2612 } 2613 2614 if (this.mode === this.BOARD_MODE_NONE) { 2615 result = this.mouseOriginMoveStart(evt); 2616 } 2617 2618 this.triggerEventHandlers(['mousedown', 'down'], [evt]); 2619 2620 return result; 2621 }, 2622 2623 /** 2624 * This method is called by the browser when the mouse is moved. 2625 * @param {Event} evt The browsers event object. 2626 */ 2627 mouseMoveListener: function (evt) { 2628 var pos; 2629 2630 pos = this.getMousePosition(evt); 2631 2632 this.updateQuality = this.BOARD_QUALITY_LOW; 2633 2634 if (this.mode !== this.BOARD_MODE_DRAG) { 2635 this.dehighlightAll(); 2636 this.showInfobox(false); 2637 } 2638 2639 // we have to check for four cases: 2640 // * user moves origin 2641 // * user drags an object 2642 // * user just moves the mouse, here highlight all elements at 2643 // the current mouse position 2644 // * the user is selecting 2645 2646 // selection 2647 if (this.selectingMode) { 2648 this._moveSelecting(pos); 2649 this.triggerEventHandlers(['mousemoveselecting', 'moveselecting'], [evt, this.mode]); 2650 } else if (!this.mouseOriginMove(evt)) { 2651 if (this.mode === this.BOARD_MODE_DRAG) { 2652 this.moveObject(pos[0], pos[1], this.mouse, evt, 'mouse'); 2653 } else { // BOARD_MODE_NONE 2654 this.highlightElements(pos[0], pos[1], evt, -1); 2655 } 2656 this.triggerEventHandlers(['mousemove', 'move'], [evt, this.mode]); 2657 } 2658 this.updateQuality = this.BOARD_QUALITY_HIGH; 2659 }, 2660 2661 /** 2662 * This method is called by the browser when the mouse button is released. 2663 * @param {Event} evt 2664 */ 2665 mouseUpListener: function (evt) { 2666 var i; 2667 2668 if (this.selectingMode === false) { 2669 this.triggerEventHandlers(['mouseup', 'up'], [evt]); 2670 } 2671 2672 // redraw with high precision 2673 this.updateQuality = this.BOARD_QUALITY_HIGH; 2674 2675 if (this.mouse && this.mouse.obj) { 2676 // The parameter is needed for lines with snapToGrid enabled 2677 this.mouse.obj.snapToGrid(this.mouse.targets[0]); 2678 this.mouse.obj.snapToPoints(); 2679 } 2680 2681 this.originMoveEnd(); 2682 this.dehighlightAll(); 2683 this.update(); 2684 2685 // selection 2686 if (this.selectingMode) { 2687 this._stopSelecting(evt); 2688 this.triggerEventHandlers(['mousestopselecting', 'stopselecting'], [evt]); 2689 } else { 2690 for (i = 0; i < this.downObjects.length; i++) { 2691 this.downObjects[i].triggerEventHandlers(['mouseup', 'up'], [evt]); 2692 } 2693 } 2694 2695 this.downObjects.length = 0; 2696 2697 if (this.hasMouseUp) { 2698 Env.removeEvent(this.document, 'mouseup', this.mouseUpListener, this); 2699 this.hasMouseUp = false; 2700 } 2701 2702 // release dragged mouse object 2703 this.mouse = null; 2704 }, 2705 2706 /** 2707 * Handler for mouse wheel events. Used to zoom in and out of the board. 2708 * @param {Event} evt 2709 * @returns {Boolean} 2710 */ 2711 mouseWheelListener: function (evt) { 2712 if (!this.attr.zoom.wheel || !this._isRequiredKeyPressed(evt, 'zoom')) { 2713 return true; 2714 } 2715 2716 evt = evt || window.event; 2717 var wd = evt.detail ? -evt.detail : evt.wheelDelta / 40, 2718 pos = new Coords(Const.COORDS_BY_SCREEN, this.getMousePosition(evt), this); 2719 2720 if (wd > 0) { 2721 this.zoomIn(pos.usrCoords[1], pos.usrCoords[2]); 2722 } else { 2723 this.zoomOut(pos.usrCoords[1], pos.usrCoords[2]); 2724 } 2725 2726 this.triggerEventHandlers(['mousewheel'], [evt]); 2727 2728 evt.preventDefault(); 2729 return false; 2730 }, 2731 2732 /********************************************************** 2733 * 2734 * End of Event Handlers 2735 * 2736 **********************************************************/ 2737 2738 /** 2739 * Initialize the info box object which is used to display 2740 * the coordinates of points near the mouse pointer, 2741 * @returns {JXG.Board} Reference to the board 2742 */ 2743 initInfobox: function () { 2744 var attr = Type.copyAttributes({}, this.options, 'infobox'); 2745 2746 attr.id = this.id + '_infobox'; 2747 this.infobox = this.create('text', [0, 0, '0,0'], attr); 2748 2749 this.infobox.distanceX = -20; 2750 this.infobox.distanceY = 25; 2751 // this.infobox.needsUpdateSize = false; // That is not true, but it speeds drawing up. 2752 2753 this.infobox.dump = false; 2754 2755 this.showInfobox(false); 2756 return this; 2757 }, 2758 2759 /** 2760 * Updates and displays a little info box to show coordinates of current selected points. 2761 * @param {JXG.GeometryElement} el A GeometryElement 2762 * @returns {JXG.Board} Reference to the board 2763 */ 2764 updateInfobox: function (el) { 2765 var x, y, xc, yc, 2766 vpinfoboxdigits; 2767 2768 if (!Type.evaluate(el.visProp.showinfobox)) { 2769 return this; 2770 } 2771 2772 if (Type.isPoint(el)) { 2773 xc = el.coords.usrCoords[1]; 2774 yc = el.coords.usrCoords[2]; 2775 2776 vpinfoboxdigits = Type.evaluate(el.visProp.infoboxdigits); 2777 this.infobox.setCoords(xc + this.infobox.distanceX / this.unitX, 2778 yc + this.infobox.distanceY / this.unitY); 2779 2780 if (typeof el.infoboxText !== 'string') { 2781 if (vpinfoboxdigits === 'auto') { 2782 x = Type.autoDigits(xc); 2783 y = Type.autoDigits(yc); 2784 } else if (Type.isNumber(vpinfoboxdigits)) { 2785 x = Type.toFixed(xc, vpinfoboxdigits); 2786 y = Type.toFixed(yc, vpinfoboxdigits); 2787 } else { 2788 x = xc; 2789 y = yc; 2790 } 2791 2792 this.highlightInfobox(x, y, el); 2793 } else { 2794 this.highlightCustomInfobox(el.infoboxText, el); 2795 } 2796 2797 this.showInfobox(true); 2798 } 2799 return this; 2800 }, 2801 2802 /** 2803 * Set infobox visible / invisible. 2804 * 2805 * It uses its property hiddenByParent to memorize its status. 2806 * In this way, many DOM access can be avoided. 2807 * 2808 * @param {Boolean} val true for visible, false for invisible 2809 * @return {JXG.Board} Reference to the board. 2810 */ 2811 showInfobox: function(val) { 2812 if (this.infobox.hiddenByParent == val) { 2813 this.infobox.hiddenByParent = !val; 2814 this.infobox.prepareUpdate().updateVisibility(val).updateRenderer(); 2815 } 2816 return this; 2817 }, 2818 2819 /** 2820 * Changes the text of the info box to show the given coordinates. 2821 * @param {Number} x 2822 * @param {Number} y 2823 * @param {JXG.GeometryElement} [el] The element the mouse is pointing at 2824 * @returns {JXG.Board} Reference to the board. 2825 */ 2826 highlightInfobox: function (x, y, el) { 2827 this.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 2828 return this; 2829 }, 2830 2831 /** 2832 * Changes the text of the info box to what is provided via text. 2833 * @param {String} text 2834 * @param {JXG.GeometryElement} [el] 2835 * @returns {JXG.Board} Reference to the board. 2836 */ 2837 highlightCustomInfobox: function (text, el) { 2838 this.infobox.setText(text); 2839 return this; 2840 }, 2841 2842 /** 2843 * Remove highlighting of all elements. 2844 * @returns {JXG.Board} Reference to the board. 2845 */ 2846 dehighlightAll: function () { 2847 var el, pEl, needsDehighlight = false; 2848 2849 for (el in this.highlightedObjects) { 2850 if (this.highlightedObjects.hasOwnProperty(el)) { 2851 pEl = this.highlightedObjects[el]; 2852 2853 if (this.hasMouseHandlers || this.hasPointerHandlers) { 2854 pEl.noHighlight(); 2855 } 2856 2857 needsDehighlight = true; 2858 2859 // In highlightedObjects should only be objects which fulfill all these conditions 2860 // And in case of complex elements, like a turtle based fractal, it should be faster to 2861 // just de-highlight the element instead of checking hasPoint... 2862 // if ((!Type.exists(pEl.hasPoint)) || !pEl.hasPoint(x, y) || !pEl.visPropCalc.visible) 2863 } 2864 } 2865 2866 this.highlightedObjects = {}; 2867 2868 // We do not need to redraw during dehighlighting in CanvasRenderer 2869 // because we are redrawing anyhow 2870 // -- We do need to redraw during dehighlighting. Otherwise objects won't be dehighlighted until 2871 // another object is highlighted. 2872 if (this.renderer.type === 'canvas' && needsDehighlight) { 2873 this.prepareUpdate(); 2874 this.renderer.suspendRedraw(this); 2875 this.updateRenderer(); 2876 this.renderer.unsuspendRedraw(); 2877 } 2878 2879 return this; 2880 }, 2881 2882 /** 2883 * Returns the input parameters in an array. This method looks pointless and it really is, but it had a purpose 2884 * once. 2885 * @param {Number} x X coordinate in screen coordinates 2886 * @param {Number} y Y coordinate in screen coordinates 2887 * @returns {Array} Coordinates of the mouse in screen coordinates. 2888 */ 2889 getScrCoordsOfMouse: function (x, y) { 2890 return [x, y]; 2891 }, 2892 2893 /** 2894 * This method calculates the user coords of the current mouse coordinates. 2895 * @param {Event} evt Event object containing the mouse coordinates. 2896 * @returns {Array} Coordinates of the mouse in screen coordinates. 2897 */ 2898 getUsrCoordsOfMouse: function (evt) { 2899 var cPos = this.getCoordsTopLeftCorner(), 2900 absPos = Env.getPosition(evt, null, this.document), 2901 x = absPos[0] - cPos[0], 2902 y = absPos[1] - cPos[1], 2903 newCoords = new Coords(Const.COORDS_BY_SCREEN, [x, y], this); 2904 2905 return newCoords.usrCoords.slice(1); 2906 }, 2907 2908 /** 2909 * Collects all elements under current mouse position plus current user coordinates of mouse cursor. 2910 * @param {Event} evt Event object containing the mouse coordinates. 2911 * @returns {Array} Array of elements at the current mouse position plus current user coordinates of mouse. 2912 */ 2913 getAllUnderMouse: function (evt) { 2914 var elList = this.getAllObjectsUnderMouse(evt); 2915 elList.push(this.getUsrCoordsOfMouse(evt)); 2916 2917 return elList; 2918 }, 2919 2920 /** 2921 * Collects all elements under current mouse position. 2922 * @param {Event} evt Event object containing the mouse coordinates. 2923 * @returns {Array} Array of elements at the current mouse position. 2924 */ 2925 getAllObjectsUnderMouse: function (evt) { 2926 var cPos = this.getCoordsTopLeftCorner(), 2927 absPos = Env.getPosition(evt, null, this.document), 2928 dx = absPos[0] - cPos[0], 2929 dy = absPos[1] - cPos[1], 2930 elList = [], 2931 el, 2932 pEl, 2933 len = this.objectsList.length; 2934 2935 for (el = 0; el < len; el++) { 2936 pEl = this.objectsList[el]; 2937 if (pEl.visPropCalc.visible && pEl.hasPoint && pEl.hasPoint(dx, dy)) { 2938 elList[elList.length] = pEl; 2939 } 2940 } 2941 2942 return elList; 2943 }, 2944 2945 /** 2946 * Update the coords object of all elements which possess this 2947 * property. This is necessary after changing the viewport. 2948 * @returns {JXG.Board} Reference to this board. 2949 **/ 2950 updateCoords: function () { 2951 var el, ob, len = this.objectsList.length; 2952 2953 for (ob = 0; ob < len; ob++) { 2954 el = this.objectsList[ob]; 2955 2956 if (Type.exists(el.coords)) { 2957 if (Type.evaluate(el.visProp.frozen)) { 2958 el.coords.screen2usr(); 2959 } else { 2960 el.coords.usr2screen(); 2961 } 2962 } 2963 } 2964 return this; 2965 }, 2966 2967 /** 2968 * Moves the origin and initializes an update of all elements. 2969 * @param {Number} x 2970 * @param {Number} y 2971 * @param {Boolean} [diff=false] 2972 * @returns {JXG.Board} Reference to this board. 2973 */ 2974 moveOrigin: function (x, y, diff) { 2975 if (Type.exists(x) && Type.exists(y)) { 2976 this.origin.scrCoords[1] = x; 2977 this.origin.scrCoords[2] = y; 2978 2979 if (diff) { 2980 this.origin.scrCoords[1] -= this.drag_dx; 2981 this.origin.scrCoords[2] -= this.drag_dy; 2982 } 2983 } 2984 2985 this.updateCoords().clearTraces().fullUpdate(); 2986 this.triggerEventHandlers(['boundingbox']); 2987 2988 return this; 2989 }, 2990 2991 /** 2992 * Add conditional updates to the elements. 2993 * @param {String} str String containing coniditional update in geonext syntax 2994 */ 2995 addConditions: function (str) { 2996 var term, m, left, right, name, el, property, 2997 functions = [], 2998 plaintext = 'var el, x, y, c, rgbo;\n', 2999 i = str.indexOf('<data>'), 3000 j = str.indexOf('<' + '/data>'), 3001 3002 xyFun = function (board, el, f, what) { 3003 return function () { 3004 var e, t; 3005 3006 e = board.select(el.id); 3007 t = e.coords.usrCoords[what]; 3008 3009 if (what === 2) { 3010 e.setPositionDirectly(Const.COORDS_BY_USER, [f(), t]); 3011 } else { 3012 e.setPositionDirectly(Const.COORDS_BY_USER, [t, f()]); 3013 } 3014 e.prepareUpdate().update(); 3015 }; 3016 }, 3017 3018 visFun = function (board, el, f) { 3019 return function () { 3020 var e, v; 3021 3022 e = board.select(el.id); 3023 v = f(); 3024 3025 e.setAttribute({visible: v}); 3026 }; 3027 }, 3028 3029 colFun = function (board, el, f, what) { 3030 return function () { 3031 var e, v; 3032 3033 e = board.select(el.id); 3034 v = f(); 3035 3036 if (what === 'strokewidth') { 3037 e.visProp.strokewidth = v; 3038 } else { 3039 v = Color.rgba2rgbo(v); 3040 e.visProp[what + 'color'] = v[0]; 3041 e.visProp[what + 'opacity'] = v[1]; 3042 } 3043 }; 3044 }, 3045 3046 posFun = function (board, el, f) { 3047 return function () { 3048 var e = board.select(el.id); 3049 3050 e.position = f(); 3051 }; 3052 }, 3053 3054 styleFun = function (board, el, f) { 3055 return function () { 3056 var e = board.select(el.id); 3057 3058 e.setStyle(f()); 3059 }; 3060 }; 3061 3062 if (i < 0) { 3063 return; 3064 } 3065 3066 while (i >= 0) { 3067 term = str.slice(i + 6, j); // throw away <data> 3068 m = term.indexOf('='); 3069 left = term.slice(0, m); 3070 right = term.slice(m + 1); 3071 m = left.indexOf('.'); // Dies erzeugt Probleme bei Variablennamen der Form " Steuern akt." 3072 name = left.slice(0, m); //.replace(/\s+$/,''); // do NOT cut out name (with whitespace) 3073 el = this.elementsByName[Type.unescapeHTML(name)]; 3074 3075 property = left.slice(m + 1).replace(/\s+/g, '').toLowerCase(); // remove whitespace in property 3076 right = Type.createfunction (right, this, '', true); 3077 3078 // Debug 3079 if (!Type.exists(this.elementsByName[name])) { 3080 JXG.debug("debug conditions: |" + name + "| undefined"); 3081 } else { 3082 plaintext += "el = this.objects[\"" + el.id + "\"];\n"; 3083 3084 switch (property) { 3085 case 'x': 3086 functions.push(xyFun(this, el, right, 2)); 3087 break; 3088 case 'y': 3089 functions.push(xyFun(this, el, right, 1)); 3090 break; 3091 case 'visible': 3092 functions.push(visFun(this, el, right)); 3093 break; 3094 case 'position': 3095 functions.push(posFun(this, el, right)); 3096 break; 3097 case 'stroke': 3098 functions.push(colFun(this, el, right, 'stroke')); 3099 break; 3100 case 'style': 3101 functions.push(styleFun(this, el, right)); 3102 break; 3103 case 'strokewidth': 3104 functions.push(colFun(this, el, right, 'strokewidth')); 3105 break; 3106 case 'fill': 3107 functions.push(colFun(this, el, right, 'fill')); 3108 break; 3109 case 'label': 3110 break; 3111 default: 3112 JXG.debug("property '" + property + "' in conditions not yet implemented:" + right); 3113 break; 3114 } 3115 } 3116 str = str.slice(j + 7); // cut off "</data>" 3117 i = str.indexOf('<data>'); 3118 j = str.indexOf('<' + '/data>'); 3119 } 3120 3121 this.updateConditions = function () { 3122 var i; 3123 3124 for (i = 0; i < functions.length; i++) { 3125 functions[i](); 3126 } 3127 3128 this.prepareUpdate().updateElements(); 3129 return true; 3130 }; 3131 this.updateConditions(); 3132 }, 3133 3134 /** 3135 * Computes the commands in the conditions-section of the gxt file. 3136 * It is evaluated after an update, before the unsuspendRedraw. 3137 * The function is generated in 3138 * @see JXG.Board#addConditions 3139 * @private 3140 */ 3141 updateConditions: function () { 3142 return false; 3143 }, 3144 3145 /** 3146 * Calculates adequate snap sizes. 3147 * @returns {JXG.Board} Reference to the board. 3148 */ 3149 calculateSnapSizes: function () { 3150 var p1 = new Coords(Const.COORDS_BY_USER, [0, 0], this), 3151 p2 = new Coords(Const.COORDS_BY_USER, [this.options.grid.gridX, this.options.grid.gridY], this), 3152 x = p1.scrCoords[1] - p2.scrCoords[1], 3153 y = p1.scrCoords[2] - p2.scrCoords[2]; 3154 3155 this.options.grid.snapSizeX = this.options.grid.gridX; 3156 while (Math.abs(x) > 25) { 3157 this.options.grid.snapSizeX *= 2; 3158 x /= 2; 3159 } 3160 3161 this.options.grid.snapSizeY = this.options.grid.gridY; 3162 while (Math.abs(y) > 25) { 3163 this.options.grid.snapSizeY *= 2; 3164 y /= 2; 3165 } 3166 3167 return this; 3168 }, 3169 3170 /** 3171 * Apply update on all objects with the new zoom-factors. Clears all traces. 3172 * @returns {JXG.Board} Reference to the board. 3173 */ 3174 applyZoom: function () { 3175 this.updateCoords().calculateSnapSizes().clearTraces().fullUpdate(); 3176 3177 return this; 3178 }, 3179 3180 /** 3181 * Zooms into the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 3182 * The zoom operation is centered at x, y. 3183 * @param {Number} [x] 3184 * @param {Number} [y] 3185 * @returns {JXG.Board} Reference to the board 3186 */ 3187 zoomIn: function (x, y) { 3188 var bb = this.getBoundingBox(), 3189 zX = this.attr.zoom.factorx, 3190 zY = this.attr.zoom.factory, 3191 dX = (bb[2] - bb[0]) * (1.0 - 1.0 / zX), 3192 dY = (bb[1] - bb[3]) * (1.0 - 1.0 / zY), 3193 lr = 0.5, 3194 tr = 0.5, 3195 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 3196 3197 if ((this.zoomX > this.attr.zoom.max && zX > 1.0) || 3198 (this.zoomY > this.attr.zoom.max && zY > 1.0) || 3199 (this.zoomX < mi && zX < 1.0) || // zoomIn is used for all zooms on touch devices 3200 (this.zoomY < mi && zY < 1.0)) { 3201 return this; 3202 } 3203 3204 if (Type.isNumber(x) && Type.isNumber(y)) { 3205 lr = (x - bb[0]) / (bb[2] - bb[0]); 3206 tr = (bb[1] - y) / (bb[1] - bb[3]); 3207 } 3208 3209 this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false); 3210 this.zoomX *= zX; 3211 this.zoomY *= zY; 3212 return this.applyZoom(); 3213 }, 3214 3215 /** 3216 * Zooms out of the board by the factors board.attr.zoom.factorX and board.attr.zoom.factorY and applies the zoom. 3217 * The zoom operation is centered at x, y. 3218 * 3219 * @param {Number} [x] 3220 * @param {Number} [y] 3221 * @returns {JXG.Board} Reference to the board 3222 */ 3223 zoomOut: function (x, y) { 3224 var bb = this.getBoundingBox(), 3225 zX = this.attr.zoom.factorx, 3226 zY = this.attr.zoom.factory, 3227 dX = (bb[2] - bb[0]) * (1.0 - zX), 3228 dY = (bb[1] - bb[3]) * (1.0 - zY), 3229 lr = 0.5, 3230 tr = 0.5, 3231 mi = this.attr.zoom.eps || this.attr.zoom.min || 0.001; // this.attr.zoom.eps is deprecated 3232 3233 if (this.zoomX < mi || this.zoomY < mi) { 3234 return this; 3235 } 3236 3237 if (Type.isNumber(x) && Type.isNumber(y)) { 3238 lr = (x - bb[0]) / (bb[2] - bb[0]); 3239 tr = (bb[1] - y) / (bb[1] - bb[3]); 3240 } 3241 3242 this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false); 3243 this.zoomX /= zX; 3244 this.zoomY /= zY; 3245 3246 return this.applyZoom(); 3247 }, 3248 3249 /** 3250 * Resets zoom factor to 100%. 3251 * @returns {JXG.Board} Reference to the board 3252 */ 3253 zoom100: function () { 3254 var bb = this.getBoundingBox(), 3255 dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5, 3256 dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5; 3257 3258 this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], false); 3259 this.zoomX = 1.0; 3260 this.zoomY = 1.0; 3261 return this.applyZoom(); 3262 }, 3263 3264 /** 3265 * Zooms the board so every visible point is shown. Keeps aspect ratio. 3266 * @returns {JXG.Board} Reference to the board 3267 */ 3268 zoomAllPoints: function () { 3269 var el, border, borderX, borderY, pEl, 3270 minX = 0, 3271 maxX = 0, 3272 minY = 0, 3273 maxY = 0, 3274 len = this.objectsList.length; 3275 3276 for (el = 0; el < len; el++) { 3277 pEl = this.objectsList[el]; 3278 3279 if (Type.isPoint(pEl) && pEl.visPropCalc.visible) { 3280 if (pEl.coords.usrCoords[1] < minX) { 3281 minX = pEl.coords.usrCoords[1]; 3282 } else if (pEl.coords.usrCoords[1] > maxX) { 3283 maxX = pEl.coords.usrCoords[1]; 3284 } 3285 if (pEl.coords.usrCoords[2] > maxY) { 3286 maxY = pEl.coords.usrCoords[2]; 3287 } else if (pEl.coords.usrCoords[2] < minY) { 3288 minY = pEl.coords.usrCoords[2]; 3289 } 3290 } 3291 } 3292 3293 border = 50; 3294 borderX = border / this.unitX; 3295 borderY = border / this.unitY; 3296 3297 this.zoomX = 1.0; 3298 this.zoomY = 1.0; 3299 3300 this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], true); 3301 3302 return this.applyZoom(); 3303 }, 3304 3305 /** 3306 * Reset the bounding box and the zoom level to 100% such that a given set of elements is within the board's viewport. 3307 * @param {Array} elements A set of elements given by id, reference, or name. 3308 * @returns {JXG.Board} Reference to the board. 3309 */ 3310 zoomElements: function (elements) { 3311 var i, j, e, box, 3312 newBBox = [0, 0, 0, 0], 3313 dir = [1, -1, -1, 1]; 3314 3315 if (!Type.isArray(elements) || elements.length === 0) { 3316 return this; 3317 } 3318 3319 for (i = 0; i < elements.length; i++) { 3320 e = this.select(elements[i]); 3321 3322 box = e.bounds(); 3323 if (Type.isArray(box)) { 3324 if (Type.isArray(newBBox)) { 3325 for (j = 0; j < 4; j++) { 3326 if (dir[j] * box[j] < dir[j] * newBBox[j]) { 3327 newBBox[j] = box[j]; 3328 } 3329 } 3330 } else { 3331 newBBox = box; 3332 } 3333 } 3334 } 3335 3336 if (Type.isArray(newBBox)) { 3337 for (j = 0; j < 4; j++) { 3338 newBBox[j] -= dir[j]; 3339 } 3340 3341 this.zoomX = 1.0; 3342 this.zoomY = 1.0; 3343 this.setBoundingBox(newBBox, true); 3344 } 3345 3346 return this; 3347 }, 3348 3349 /** 3350 * Sets the zoom level to <tt>fX</tt> resp <tt>fY</tt>. 3351 * @param {Number} fX 3352 * @param {Number} fY 3353 * @returns {JXG.Board} Reference to the board. 3354 */ 3355 setZoom: function (fX, fY) { 3356 var oX = this.attr.zoom.factorx, 3357 oY = this.attr.zoom.factory; 3358 3359 this.attr.zoom.factorx = fX / this.zoomX; 3360 this.attr.zoom.factory = fY / this.zoomY; 3361 3362 this.zoomIn(); 3363 3364 this.attr.zoom.factorx = oX; 3365 this.attr.zoom.factory = oY; 3366 3367 return this; 3368 }, 3369 3370 /** 3371 * Removes object from board and renderer. 3372 * @param {JXG.GeometryElement} object The object to remove. 3373 * @returns {JXG.Board} Reference to the board 3374 */ 3375 removeObject: function (object) { 3376 var el, i; 3377 3378 if (Type.isArray(object)) { 3379 for (i = 0; i < object.length; i++) { 3380 this.removeObject(object[i]); 3381 } 3382 3383 return this; 3384 } 3385 3386 object = this.select(object); 3387 3388 // If the object which is about to be removed unknown or a string, do nothing. 3389 // it is a string if a string was given and could not be resolved to an element. 3390 if (!Type.exists(object) || Type.isString(object)) { 3391 return this; 3392 } 3393 3394 try { 3395 // remove all children. 3396 for (el in object.childElements) { 3397 if (object.childElements.hasOwnProperty(el)) { 3398 object.childElements[el].board.removeObject(object.childElements[el]); 3399 } 3400 } 3401 3402 // Remove all children in elements like turtle 3403 for (el in object.objects) { 3404 if (object.objects.hasOwnProperty(el)) { 3405 object.objects[el].board.removeObject(object.objects[el]); 3406 } 3407 } 3408 3409 for (el in this.objects) { 3410 if (this.objects.hasOwnProperty(el) && Type.exists(this.objects[el].childElements)) { 3411 delete this.objects[el].childElements[object.id]; 3412 delete this.objects[el].descendants[object.id]; 3413 } 3414 } 3415 3416 // remove the object itself from our control structures 3417 if (object._pos > -1) { 3418 this.objectsList.splice(object._pos, 1); 3419 for (el = object._pos; el < this.objectsList.length; el++) { 3420 this.objectsList[el]._pos--; 3421 } 3422 } else if (object.type !== Const.OBJECT_TYPE_TURTLE) { 3423 JXG.debug('Board.removeObject: object ' + object.id + ' not found in list.'); 3424 } 3425 3426 delete this.objects[object.id]; 3427 delete this.elementsByName[object.name]; 3428 3429 3430 if (object.visProp && Type.evaluate(object.visProp.trace)) { 3431 object.clearTrace(); 3432 } 3433 3434 // the object deletion itself is handled by the object. 3435 if (Type.exists(object.remove)) { 3436 object.remove(); 3437 } 3438 } catch (e) { 3439 JXG.debug(object.id + ': Could not be removed: ' + e); 3440 } 3441 3442 this.update(); 3443 3444 return this; 3445 }, 3446 3447 /** 3448 * Removes the ancestors of an object an the object itself from board and renderer. 3449 * @param {JXG.GeometryElement} object The object to remove. 3450 * @returns {JXG.Board} Reference to the board 3451 */ 3452 removeAncestors: function (object) { 3453 var anc; 3454 3455 for (anc in object.ancestors) { 3456 if (object.ancestors.hasOwnProperty(anc)) { 3457 this.removeAncestors(object.ancestors[anc]); 3458 } 3459 } 3460 3461 this.removeObject(object); 3462 3463 return this; 3464 }, 3465 3466 /** 3467 * Initialize some objects which are contained in every GEONExT construction by default, 3468 * but are not contained in the gxt files. 3469 * @returns {JXG.Board} Reference to the board 3470 */ 3471 initGeonextBoard: function () { 3472 var p1, p2, p3; 3473 3474 p1 = this.create('point', [0, 0], { 3475 id: this.id + 'g00e0', 3476 name: 'Ursprung', 3477 withLabel: false, 3478 visible: false, 3479 fixed: true 3480 }); 3481 3482 p2 = this.create('point', [1, 0], { 3483 id: this.id + 'gX0e0', 3484 name: 'Punkt_1_0', 3485 withLabel: false, 3486 visible: false, 3487 fixed: true 3488 }); 3489 3490 p3 = this.create('point', [0, 1], { 3491 id: this.id + 'gY0e0', 3492 name: 'Punkt_0_1', 3493 withLabel: false, 3494 visible: false, 3495 fixed: true 3496 }); 3497 3498 this.create('line', [p1, p2], { 3499 id: this.id + 'gXLe0', 3500 name: 'X-Achse', 3501 withLabel: false, 3502 visible: false 3503 }); 3504 3505 this.create('line', [p1, p3], { 3506 id: this.id + 'gYLe0', 3507 name: 'Y-Achse', 3508 withLabel: false, 3509 visible: false 3510 }); 3511 3512 return this; 3513 }, 3514 3515 /** 3516 * Change the height and width of the board's container. 3517 * After doing so, {@link JXG.JSXGraph#setBoundingBox} is called using 3518 * the actual size of the bounding box and the actual value of keepaspectratio. 3519 * If setBoundingbox() should not be called automatically, 3520 * call resizeContainer with dontSetBoundingBox == true. 3521 * @param {Number} canvasWidth New width of the container. 3522 * @param {Number} canvasHeight New height of the container. 3523 * @param {Boolean} [dontset=false] If true do not set the height of the DOM element. 3524 * @param {Boolean} [dontSetBoundingBox=false] If true do not call setBoundingBox(). 3525 * @returns {JXG.Board} Reference to the board 3526 */ 3527 resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) { 3528 var box; 3529 3530 if (!dontSetBoundingBox) { 3531 box = this.getBoundingBox(); 3532 } 3533 this.canvasWidth = parseInt(canvasWidth, 10); 3534 this.canvasHeight = parseInt(canvasHeight, 10); 3535 3536 if (!dontset) { 3537 this.containerObj.style.width = (this.canvasWidth) + 'px'; 3538 this.containerObj.style.height = (this.canvasHeight) + 'px'; 3539 } 3540 3541 this.renderer.resize(this.canvasWidth, this.canvasHeight); 3542 3543 if (!dontSetBoundingBox) { 3544 this.setBoundingBox(box, this.keepaspectratio); 3545 } 3546 3547 return this; 3548 }, 3549 3550 /** 3551 * Lists the dependencies graph in a new HTML-window. 3552 * @returns {JXG.Board} Reference to the board 3553 */ 3554 showDependencies: function () { 3555 var el, t, c, f, i; 3556 3557 t = '<p>\n'; 3558 for (el in this.objects) { 3559 if (this.objects.hasOwnProperty(el)) { 3560 i = 0; 3561 for (c in this.objects[el].childElements) { 3562 if (this.objects[el].childElements.hasOwnProperty(c)) { 3563 i += 1; 3564 } 3565 } 3566 if (i >= 0) { 3567 t += '<strong>' + this.objects[el].id + ':<' + '/strong> '; 3568 } 3569 3570 for (c in this.objects[el].childElements) { 3571 if (this.objects[el].childElements.hasOwnProperty(c)) { 3572 t += this.objects[el].childElements[c].id + '(' + this.objects[el].childElements[c].name + ')' + ', '; 3573 } 3574 } 3575 t += '<p>\n'; 3576 } 3577 } 3578 t += '<' + '/p>\n'; 3579 f = window.open(); 3580 f.document.open(); 3581 f.document.write(t); 3582 f.document.close(); 3583 return this; 3584 }, 3585 3586 /** 3587 * Lists the XML code of the construction in a new HTML-window. 3588 * @returns {JXG.Board} Reference to the board 3589 */ 3590 showXML: function () { 3591 var f = window.open(''); 3592 f.document.open(); 3593 f.document.write('<pre>' + Type.escapeHTML(this.xmlString) + '<' + '/pre>'); 3594 f.document.close(); 3595 return this; 3596 }, 3597 3598 /** 3599 * Sets for all objects the needsUpdate flag to "true". 3600 * @returns {JXG.Board} Reference to the board 3601 */ 3602 prepareUpdate: function () { 3603 var el, pEl, len = this.objectsList.length; 3604 3605 /* 3606 if (this.attr.updatetype === 'hierarchical') { 3607 return this; 3608 } 3609 */ 3610 3611 for (el = 0; el < len; el++) { 3612 pEl = this.objectsList[el]; 3613 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 3614 } 3615 3616 for (el in this.groups) { 3617 if (this.groups.hasOwnProperty(el)) { 3618 pEl = this.groups[el]; 3619 pEl.needsUpdate = pEl.needsRegularUpdate || this.needsFullUpdate; 3620 } 3621 } 3622 3623 return this; 3624 }, 3625 3626 /** 3627 * Runs through all elements and calls their update() method. 3628 * @param {JXG.GeometryElement} drag Element that caused the update. 3629 * @returns {JXG.Board} Reference to the board 3630 */ 3631 updateElements: function (drag) { 3632 var el, pEl; 3633 //var childId, i = 0; 3634 3635 drag = this.select(drag); 3636 3637 /* 3638 if (Type.exists(drag)) { 3639 for (el = 0; el < this.objectsList.length; el++) { 3640 pEl = this.objectsList[el]; 3641 if (pEl.id === drag.id) { 3642 i = el; 3643 break; 3644 } 3645 } 3646 } 3647 */ 3648 3649 for (el = 0; el < this.objectsList.length; el++) { 3650 pEl = this.objectsList[el]; 3651 // For updates of an element we distinguish if the dragged element is updated or 3652 // other elements are updated. 3653 // The difference lies in the treatment of gliders. 3654 pEl.update(!Type.exists(drag) || pEl.id !== drag.id) 3655 .updateVisibility(); 3656 } 3657 3658 // update groups last 3659 for (el in this.groups) { 3660 if (this.groups.hasOwnProperty(el)) { 3661 this.groups[el].update(drag); 3662 } 3663 } 3664 3665 return this; 3666 }, 3667 3668 /** 3669 * Runs through all elements and calls their update() method. 3670 * @returns {JXG.Board} Reference to the board 3671 */ 3672 updateRenderer: function () { 3673 var el, 3674 len = this.objectsList.length; 3675 3676 /* 3677 objs = this.objectsList.slice(0); 3678 objs.sort(function (a, b) { 3679 if (a.visProp.layer < b.visProp.layer) { 3680 return -1; 3681 } else if (a.visProp.layer === b.visProp.layer) { 3682 return b.lastDragTime.getTime() - a.lastDragTime.getTime(); 3683 } else { 3684 return 1; 3685 } 3686 }); 3687 */ 3688 3689 if (this.renderer.type === 'canvas') { 3690 this.updateRendererCanvas(); 3691 } else { 3692 for (el = 0; el < len; el++) { 3693 this.objectsList[el].updateRenderer(); 3694 } 3695 } 3696 return this; 3697 }, 3698 3699 /** 3700 * Runs through all elements and calls their update() method. 3701 * This is a special version for the CanvasRenderer. 3702 * Here, we have to do our own layer handling. 3703 * @returns {JXG.Board} Reference to the board 3704 */ 3705 updateRendererCanvas: function () { 3706 var el, pEl, i, mini, la, 3707 olen = this.objectsList.length, 3708 layers = this.options.layer, 3709 len = this.options.layer.numlayers, 3710 last = Number.NEGATIVE_INFINITY; 3711 3712 for (i = 0; i < len; i++) { 3713 mini = Number.POSITIVE_INFINITY; 3714 3715 for (la in layers) { 3716 if (layers.hasOwnProperty(la)) { 3717 if (layers[la] > last && layers[la] < mini) { 3718 mini = layers[la]; 3719 } 3720 } 3721 } 3722 3723 last = mini; 3724 3725 for (el = 0; el < olen; el++) { 3726 pEl = this.objectsList[el]; 3727 3728 if (pEl.visProp.layer === mini) { 3729 pEl.prepareUpdate().updateRenderer(); 3730 } 3731 } 3732 } 3733 return this; 3734 }, 3735 3736 /** 3737 * Please use {@link JXG.Board#on} instead. 3738 * @param {Function} hook A function to be called by the board after an update occurred. 3739 * @param {String} [m='update'] When the hook is to be called. Possible values are <i>mouseup</i>, <i>mousedown</i> and <i>update</i>. 3740 * @param {Object} [context=board] Determines the execution context the hook is called. This parameter is optional, default is the 3741 * board object the hook is attached to. 3742 * @returns {Number} Id of the hook, required to remove the hook from the board. 3743 * @deprecated 3744 */ 3745 addHook: function (hook, m, context) { 3746 JXG.deprecated('Board.addHook()', 'Board.on()'); 3747 m = Type.def(m, 'update'); 3748 3749 context = Type.def(context, this); 3750 3751 this.hooks.push([m, hook]); 3752 this.on(m, hook, context); 3753 3754 return this.hooks.length - 1; 3755 }, 3756 3757 /** 3758 * Alias of {@link JXG.Board#on}. 3759 */ 3760 addEvent: JXG.shortcut(JXG.Board.prototype, 'on'), 3761 3762 /** 3763 * Please use {@link JXG.Board#off} instead. 3764 * @param {Number|function} id The number you got when you added the hook or a reference to the event handler. 3765 * @returns {JXG.Board} Reference to the board 3766 * @deprecated 3767 */ 3768 removeHook: function (id) { 3769 JXG.deprecated('Board.removeHook()', 'Board.off()'); 3770 if (this.hooks[id]) { 3771 this.off(this.hooks[id][0], this.hooks[id][1]); 3772 this.hooks[id] = null; 3773 } 3774 3775 return this; 3776 }, 3777 3778 /** 3779 * Alias of {@link JXG.Board#off}. 3780 */ 3781 removeEvent: JXG.shortcut(JXG.Board.prototype, 'off'), 3782 3783 /** 3784 * Runs through all hooked functions and calls them. 3785 * @returns {JXG.Board} Reference to the board 3786 * @deprecated 3787 */ 3788 updateHooks: function (m) { 3789 var arg = Array.prototype.slice.call(arguments, 0); 3790 3791 JXG.deprecated('Board.updateHooks()', 'Board.triggerEventHandlers()'); 3792 3793 arg[0] = Type.def(arg[0], 'update'); 3794 this.triggerEventHandlers([arg[0]], arguments); 3795 3796 return this; 3797 }, 3798 3799 /** 3800 * Adds a dependent board to this board. 3801 * @param {JXG.Board} board A reference to board which will be updated after an update of this board occurred. 3802 * @returns {JXG.Board} Reference to the board 3803 */ 3804 addChild: function (board) { 3805 if (Type.exists(board) && Type.exists(board.containerObj)) { 3806 this.dependentBoards.push(board); 3807 this.update(); 3808 } 3809 return this; 3810 }, 3811 3812 /** 3813 * Deletes a board from the list of dependent boards. 3814 * @param {JXG.Board} board Reference to the board which will be removed. 3815 * @returns {JXG.Board} Reference to the board 3816 */ 3817 removeChild: function (board) { 3818 var i; 3819 3820 for (i = this.dependentBoards.length - 1; i >= 0; i--) { 3821 if (this.dependentBoards[i] === board) { 3822 this.dependentBoards.splice(i, 1); 3823 } 3824 } 3825 return this; 3826 }, 3827 3828 /** 3829 * Runs through most elements and calls their update() method and update the conditions. 3830 * @param {JXG.GeometryElement} [drag] Element that caused the update. 3831 * @returns {JXG.Board} Reference to the board 3832 */ 3833 update: function (drag) { 3834 var i, len, b, insert; 3835 3836 if (this.inUpdate || this.isSuspendedUpdate) { 3837 return this; 3838 } 3839 this.inUpdate = true; 3840 3841 if (this.attr.minimizereflow === 'all' && this.containerObj && this.renderer.type !== 'vml') { 3842 insert = this.renderer.removeToInsertLater(this.containerObj); 3843 } 3844 3845 if (this.attr.minimizereflow === 'svg' && this.renderer.type === 'svg') { 3846 insert = this.renderer.removeToInsertLater(this.renderer.svgRoot); 3847 } 3848 this.prepareUpdate().updateElements(drag).updateConditions(); 3849 3850 this.renderer.suspendRedraw(this); 3851 this.updateRenderer(); 3852 this.renderer.unsuspendRedraw(); 3853 this.triggerEventHandlers(['update'], []); 3854 3855 if (insert) { 3856 insert(); 3857 } 3858 3859 // To resolve dependencies between boards 3860 // for (var board in JXG.boards) { 3861 len = this.dependentBoards.length; 3862 for (i = 0; i < len; i++) { 3863 b = this.dependentBoards[i]; 3864 if (Type.exists(b) && b !== this) { 3865 b.updateQuality = this.updateQuality; 3866 b.prepareUpdate().updateElements().updateConditions(); 3867 b.renderer.suspendRedraw(); 3868 b.updateRenderer(); 3869 b.renderer.unsuspendRedraw(); 3870 b.triggerEventHandlers(['update'], []); 3871 } 3872 3873 } 3874 3875 this.inUpdate = false; 3876 return this; 3877 }, 3878 3879 /** 3880 * Runs through all elements and calls their update() method and update the conditions. 3881 * This is necessary after zooming and changing the bounding box. 3882 * @returns {JXG.Board} Reference to the board 3883 */ 3884 fullUpdate: function () { 3885 this.needsFullUpdate = true; 3886 this.update(); 3887 this.needsFullUpdate = false; 3888 return this; 3889 }, 3890 3891 /** 3892 * Adds a grid to the board according to the settings given in board.options. 3893 * @returns {JXG.Board} Reference to the board. 3894 */ 3895 addGrid: function () { 3896 this.create('grid', []); 3897 3898 return this; 3899 }, 3900 3901 /** 3902 * Removes all grids assigned to this board. Warning: This method also removes all objects depending on one or 3903 * more of the grids. 3904 * @returns {JXG.Board} Reference to the board object. 3905 */ 3906 removeGrids: function () { 3907 var i; 3908 3909 for (i = 0; i < this.grids.length; i++) { 3910 this.removeObject(this.grids[i]); 3911 } 3912 3913 this.grids.length = 0; 3914 this.update(); // required for canvas renderer 3915 3916 return this; 3917 }, 3918 3919 /** 3920 * Creates a new geometric element of type elementType. 3921 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point' or 'circle'. 3922 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a point or two 3923 * points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 3924 * methods for a list of possible parameters. 3925 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 3926 * Common attributes are name, visible, strokeColor. 3927 * @returns {Object} Reference to the created element. This is usually a GeometryElement, but can be an array containing 3928 * two or more elements. 3929 */ 3930 create: function (elementType, parents, attributes) { 3931 var el, i; 3932 3933 elementType = elementType.toLowerCase(); 3934 3935 if (!Type.exists(parents)) { 3936 parents = []; 3937 } 3938 3939 if (!Type.exists(attributes)) { 3940 attributes = {}; 3941 } 3942 3943 for (i = 0; i < parents.length; i++) { 3944 if (Type.isString(parents[i]) && 3945 !(elementType === 'text' && i === 2) && 3946 !((elementType === 'input' || elementType === 'checkbox' || elementType === 'button') && 3947 (i === 2 || i == 3)) 3948 ) { 3949 parents[i] = this.select(parents[i]); 3950 } 3951 } 3952 3953 if (Type.isFunction(JXG.elements[elementType])) { 3954 el = JXG.elements[elementType](this, parents, attributes); 3955 } else { 3956 throw new Error("JSXGraph: create: Unknown element type given: " + elementType); 3957 } 3958 3959 if (!Type.exists(el)) { 3960 JXG.debug("JSXGraph: create: failure creating " + elementType); 3961 return el; 3962 } 3963 3964 if (el.prepareUpdate && el.update && el.updateRenderer) { 3965 el.fullUpdate(); 3966 } 3967 return el; 3968 }, 3969 3970 /** 3971 * Deprecated name for {@link JXG.Board#create}. 3972 * @deprecated 3973 */ 3974 createElement: function () { 3975 JXG.deprecated('Board.createElement()', 'Board.create()'); 3976 return this.create.apply(this, arguments); 3977 }, 3978 3979 /** 3980 * Delete the elements drawn as part of a trace of an element. 3981 * @returns {JXG.Board} Reference to the board 3982 */ 3983 clearTraces: function () { 3984 var el; 3985 3986 for (el = 0; el < this.objectsList.length; el++) { 3987 this.objectsList[el].clearTrace(); 3988 } 3989 3990 this.numTraces = 0; 3991 return this; 3992 }, 3993 3994 /** 3995 * Stop updates of the board. 3996 * @returns {JXG.Board} Reference to the board 3997 */ 3998 suspendUpdate: function () { 3999 if (!this.inUpdate) { 4000 this.isSuspendedUpdate = true; 4001 } 4002 return this; 4003 }, 4004 4005 /** 4006 * Enable updates of the board. 4007 * @returns {JXG.Board} Reference to the board 4008 */ 4009 unsuspendUpdate: function () { 4010 if (this.isSuspendedUpdate) { 4011 this.isSuspendedUpdate = false; 4012 this.fullUpdate(); 4013 } 4014 return this; 4015 }, 4016 4017 /** 4018 * Set the bounding box of the board. 4019 * @param {Array} bbox New bounding box [x1,y1,x2,y2] 4020 * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but 4021 * the resulting viewport may be larger. 4022 * @returns {JXG.Board} Reference to the board 4023 */ 4024 setBoundingBox: function (bbox, keepaspectratio) { 4025 var h, w, 4026 dim = Env.getDimensions(this.container, this.document); 4027 4028 if (!Type.isArray(bbox)) { 4029 return this; 4030 } 4031 4032 this.plainBB = bbox; 4033 4034 this.canvasWidth = parseInt(dim.width, 10); 4035 this.canvasHeight = parseInt(dim.height, 10); 4036 w = this.canvasWidth; 4037 h = this.canvasHeight; 4038 4039 if (keepaspectratio) { 4040 this.unitX = w / (bbox[2] - bbox[0]); 4041 this.unitY = h / (bbox[1] - bbox[3]); 4042 if (Math.abs(this.unitX) < Math.abs(this.unitY)) { 4043 this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY); 4044 } else { 4045 this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX); 4046 } 4047 this.keepaspectratio = true; 4048 } else { 4049 this.unitX = w / (bbox[2] - bbox[0]); 4050 this.unitY = h / (bbox[1] - bbox[3]); 4051 this.keepaspectratio = false; 4052 } 4053 4054 this.moveOrigin(-this.unitX * bbox[0], this.unitY * bbox[1]); 4055 4056 return this; 4057 }, 4058 4059 /** 4060 * Get the bounding box of the board. 4061 * @returns {Array} bounding box [x1,y1,x2,y2] upper left corner, lower right corner 4062 */ 4063 getBoundingBox: function () { 4064 var ul = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this), 4065 lr = new Coords(Const.COORDS_BY_SCREEN, [this.canvasWidth, this.canvasHeight], this); 4066 4067 return [ul.usrCoords[1], ul.usrCoords[2], lr.usrCoords[1], lr.usrCoords[2]]; 4068 }, 4069 4070 /** 4071 * Adds an animation. Animations are controlled by the boards, so the boards need to be aware of the 4072 * animated elements. This function tells the board about new elements to animate. 4073 * @param {JXG.GeometryElement} element The element which is to be animated. 4074 * @returns {JXG.Board} Reference to the board 4075 */ 4076 addAnimation: function (element) { 4077 var that = this; 4078 4079 this.animationObjects[element.id] = element; 4080 4081 if (!this.animationIntervalCode) { 4082 this.animationIntervalCode = window.setInterval(function () { 4083 that.animate(); 4084 }, element.board.attr.animationdelay); 4085 } 4086 4087 return this; 4088 }, 4089 4090 /** 4091 * Cancels all running animations. 4092 * @returns {JXG.Board} Reference to the board 4093 */ 4094 stopAllAnimation: function () { 4095 var el; 4096 4097 for (el in this.animationObjects) { 4098 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) { 4099 this.animationObjects[el] = null; 4100 delete this.animationObjects[el]; 4101 } 4102 } 4103 4104 window.clearInterval(this.animationIntervalCode); 4105 delete this.animationIntervalCode; 4106 4107 return this; 4108 }, 4109 4110 /** 4111 * General purpose animation function. This currently only supports moving points from one place to another. This 4112 * is faster than managing the animation per point, especially if there is more than one animated point at the same time. 4113 * @returns {JXG.Board} Reference to the board 4114 */ 4115 animate: function () { 4116 var props, el, o, newCoords, r, p, c, cbtmp, 4117 count = 0, 4118 obj = null; 4119 4120 for (el in this.animationObjects) { 4121 if (this.animationObjects.hasOwnProperty(el) && Type.exists(this.animationObjects[el])) { 4122 count += 1; 4123 o = this.animationObjects[el]; 4124 4125 if (o.animationPath) { 4126 if (Type.isFunction(o.animationPath)) { 4127 newCoords = o.animationPath(new Date().getTime() - o.animationStart); 4128 } else { 4129 newCoords = o.animationPath.pop(); 4130 } 4131 4132 if ((!Type.exists(newCoords)) || (!Type.isArray(newCoords) && isNaN(newCoords))) { 4133 delete o.animationPath; 4134 } else { 4135 o.setPositionDirectly(Const.COORDS_BY_USER, newCoords); 4136 o.fullUpdate(); 4137 obj = o; 4138 } 4139 } 4140 if (o.animationData) { 4141 c = 0; 4142 4143 for (r in o.animationData) { 4144 if (o.animationData.hasOwnProperty(r)) { 4145 p = o.animationData[r].pop(); 4146 4147 if (!Type.exists(p)) { 4148 delete o.animationData[p]; 4149 } else { 4150 c += 1; 4151 props = {}; 4152 props[r] = p; 4153 o.setAttribute(props); 4154 } 4155 } 4156 } 4157 4158 if (c === 0) { 4159 delete o.animationData; 4160 } 4161 } 4162 4163 if (!Type.exists(o.animationData) && !Type.exists(o.animationPath)) { 4164 this.animationObjects[el] = null; 4165 delete this.animationObjects[el]; 4166 4167 if (Type.exists(o.animationCallback)) { 4168 cbtmp = o.animationCallback; 4169 o.animationCallback = null; 4170 cbtmp(); 4171 } 4172 } 4173 } 4174 } 4175 4176 if (count === 0) { 4177 window.clearInterval(this.animationIntervalCode); 4178 delete this.animationIntervalCode; 4179 } else { 4180 this.update(obj); 4181 } 4182 4183 return this; 4184 }, 4185 4186 /** 4187 * Migrate the dependency properties of the point src 4188 * to the point dest and delete the point src. 4189 * For example, a circle around the point src 4190 * receives the new center dest. The old center src 4191 * will be deleted. 4192 * @param {JXG.Point} src Original point which will be deleted 4193 * @param {JXG.Point} dest New point with the dependencies of src. 4194 * @param {Boolean} copyName Flag which decides if the name of the src element is copied to the 4195 * dest element. 4196 * @returns {JXG.Board} Reference to the board 4197 */ 4198 migratePoint: function (src, dest, copyName) { 4199 var child, childId, prop, found, i, srcLabelId, srcHasLabel = false; 4200 4201 src = this.select(src); 4202 dest = this.select(dest); 4203 4204 if (Type.exists(src.label)) { 4205 srcLabelId = src.label.id; 4206 srcHasLabel = true; 4207 this.removeObject(src.label); 4208 } 4209 4210 for (childId in src.childElements) { 4211 if (src.childElements.hasOwnProperty(childId)) { 4212 child = src.childElements[childId]; 4213 found = false; 4214 4215 for (prop in child) { 4216 if (child.hasOwnProperty(prop)) { 4217 if (child[prop] === src) { 4218 child[prop] = dest; 4219 found = true; 4220 } 4221 } 4222 } 4223 4224 if (found) { 4225 delete src.childElements[childId]; 4226 } 4227 4228 for (i = 0; i < child.parents.length; i++) { 4229 if (child.parents[i] === src.id) { 4230 child.parents[i] = dest.id; 4231 } 4232 } 4233 4234 dest.addChild(child); 4235 } 4236 } 4237 4238 // The destination object should receive the name 4239 // and the label of the originating (src) object 4240 if (copyName) { 4241 if (srcHasLabel) { 4242 delete dest.childElements[srcLabelId]; 4243 delete dest.descendants[srcLabelId]; 4244 } 4245 4246 if (dest.label) { 4247 this.removeObject(dest.label); 4248 } 4249 4250 delete this.elementsByName[dest.name]; 4251 dest.name = src.name; 4252 if (srcHasLabel) { 4253 dest.createLabel(); 4254 } 4255 } 4256 4257 this.removeObject(src); 4258 4259 if (Type.exists(dest.name) && dest.name !== '') { 4260 this.elementsByName[dest.name] = dest; 4261 } 4262 4263 this.fullUpdate(); 4264 4265 return this; 4266 }, 4267 4268 /** 4269 * Initializes color blindness simulation. 4270 * @param {String} deficiency Describes the color blindness deficiency which is simulated. Accepted values are 'protanopia', 'deuteranopia', and 'tritanopia'. 4271 * @returns {JXG.Board} Reference to the board 4272 */ 4273 emulateColorblindness: function (deficiency) { 4274 var e, o; 4275 4276 if (!Type.exists(deficiency)) { 4277 deficiency = 'none'; 4278 } 4279 4280 if (this.currentCBDef === deficiency) { 4281 return this; 4282 } 4283 4284 for (e in this.objects) { 4285 if (this.objects.hasOwnProperty(e)) { 4286 o = this.objects[e]; 4287 4288 if (deficiency !== 'none') { 4289 if (this.currentCBDef === 'none') { 4290 // this could be accomplished by JXG.extend, too. But do not use 4291 // JXG.deepCopy as this could result in an infinite loop because in 4292 // visProp there could be geometry elements which contain the board which 4293 // contains all objects which contain board etc. 4294 o.visPropOriginal = { 4295 strokecolor: o.visProp.strokecolor, 4296 fillcolor: o.visProp.fillcolor, 4297 highlightstrokecolor: o.visProp.highlightstrokecolor, 4298 highlightfillcolor: o.visProp.highlightfillcolor 4299 }; 4300 } 4301 o.setAttribute({ 4302 strokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.strokecolor), deficiency), 4303 fillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.fillcolor), deficiency), 4304 highlightstrokecolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightstrokecolor), deficiency), 4305 highlightfillcolor: Color.rgb2cb(Type.evaluate(o.visPropOriginal.highlightfillcolor), deficiency) 4306 }); 4307 } else if (Type.exists(o.visPropOriginal)) { 4308 JXG.extend(o.visProp, o.visPropOriginal); 4309 } 4310 } 4311 } 4312 this.currentCBDef = deficiency; 4313 this.update(); 4314 4315 return this; 4316 }, 4317 4318 /** 4319 * Select a single or multiple elements at once. 4320 * @param {String|Object|function} str The name, id or a reference to a JSXGraph element on this board. An object will 4321 * be used as a filter to return multiple elements at once filtered by the properties of the object. 4322 * @returns {JXG.GeometryElement|JXG.Composition} 4323 * @example 4324 * // select the element with name A 4325 * board.select('A'); 4326 * 4327 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 4328 * board.select({ 4329 * strokeColor: 'red' 4330 * }); 4331 * 4332 * // select all points on or below the x axis and make them black. 4333 * board.select({ 4334 * elementClass: JXG.OBJECT_CLASS_POINT, 4335 * Y: function (v) { 4336 * return v <= 0; 4337 * } 4338 * }).setAttribute({color: 'black'}); 4339 * 4340 * // select all elements 4341 * board.select(function (el) { 4342 * return true; 4343 * }); 4344 */ 4345 select: function (str) { 4346 var flist, olist, i, l, 4347 s = str; 4348 4349 if (s === null) { 4350 return s; 4351 } 4352 4353 // it's a string, most likely an id or a name. 4354 if (Type.isString(s) && s !== '') { 4355 // Search by ID 4356 if (Type.exists(this.objects[s])) { 4357 s = this.objects[s]; 4358 // Search by name 4359 } else if (Type.exists(this.elementsByName[s])) { 4360 s = this.elementsByName[s]; 4361 // Search by group ID 4362 } else if (Type.exists(this.groups[s])) { 4363 s = this.groups[s]; 4364 } 4365 // it's a function or an object, but not an element 4366 } else if (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) { 4367 4368 flist = Type.filterElements(this.objectsList, s); 4369 4370 olist = {}; 4371 l = flist.length; 4372 for (i = 0; i < l; i++) { 4373 olist[flist[i].id] = flist[i]; 4374 } 4375 s = new EComposition(olist); 4376 // it's an element which has been deleted (and still hangs around, e.g. in an attractor list 4377 } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) { 4378 s = null; 4379 } 4380 4381 return s; 4382 }, 4383 4384 /** 4385 * Checks if the given point is inside the boundingbox. 4386 * @param {Number|JXG.Coords} x User coordinate or {@link JXG.Coords} object. 4387 * @param {Number} [y] User coordinate. May be omitted in case <tt>x</tt> is a {@link JXG.Coords} object. 4388 * @returns {Boolean} 4389 */ 4390 hasPoint: function (x, y) { 4391 var px = x, 4392 py = y, 4393 bbox = this.getBoundingBox(); 4394 4395 if (Type.exists(x) && Type.isArray(x.usrCoords)) { 4396 px = x.usrCoords[1]; 4397 py = x.usrCoords[2]; 4398 } 4399 4400 return !!(Type.isNumber(px) && Type.isNumber(py) && 4401 bbox[0] < px && px < bbox[2] && bbox[1] > py && py > bbox[3]); 4402 }, 4403 4404 /** 4405 * Update CSS transformations of sclaing type. It is used to correct the mouse position 4406 * in {@link JXG.Board#getMousePosition}. 4407 * The inverse transformation matrix is updated on each mouseDown and touchStart event. 4408 * 4409 * It is up to the user to call this method after an update of the CSS transformation 4410 * in the DOM. 4411 */ 4412 updateCSSTransforms: function () { 4413 var obj = this.containerObj, 4414 o = obj, 4415 o2 = obj; 4416 4417 this.cssTransMat = Env.getCSSTransformMatrix(o); 4418 4419 /* 4420 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 4421 * if not to the body. In IE and if we are in an position:absolute environment 4422 * offsetParent walks up the DOM hierarchy. 4423 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 4424 * we need the parentNode steps. 4425 */ 4426 o = o.offsetParent; 4427 while (o) { 4428 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 4429 4430 o2 = o2.parentNode; 4431 while (o2 !== o) { 4432 this.cssTransMat = Mat.matMatMult(Env.getCSSTransformMatrix(o), this.cssTransMat); 4433 o2 = o2.parentNode; 4434 } 4435 4436 o = o.offsetParent; 4437 } 4438 this.cssTransMat = Mat.inverse(this.cssTransMat); 4439 4440 return this; 4441 }, 4442 4443 /** 4444 * Start selection mode. This function can either be triggered from outside or by 4445 * a down event together with correct key pressing. The default keys are 4446 * shift+ctrl. But this can be changed in the options. 4447 * 4448 * Starting from out side can be realized for example with a button like this: 4449 * <pre> 4450 * <button onclick="board.startSelectionMode()">Start</button> 4451 * </pre> 4452 * @example 4453 * // 4454 * // Set a new bounding box from the selection rectangle 4455 * // 4456 * var board = JXG.JSXGraph.initBoard('jxgbox', { 4457 * boundingBox:[-3,2,3,-2], 4458 * keepAspectRatio: false, 4459 * axis:true, 4460 * selection: { 4461 * enabled: true, 4462 * needShift: false, 4463 * needCtrl: true, 4464 * withLines: false, 4465 * vertices: { 4466 * visible: false 4467 * }, 4468 * fillColor: '#ffff00', 4469 * } 4470 * }); 4471 * 4472 * var f = function f(x) { return Math.cos(x); }, 4473 * curve = board.create('functiongraph', [f]); 4474 * 4475 * board.on('stopselecting', function(){ 4476 * var box = board.stopSelectionMode(), 4477 * 4478 * // bbox has the coordinates of the selection rectangle. 4479 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 4480 * // are homogeneous coordinates. 4481 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 4482 * 4483 * // Set a new bounding box 4484 * board.setBoundingBox(bbox, false); 4485 * }); 4486 * 4487 * 4488 * </pre><div class="jxgbox" id="11eff3a6-8c50-11e5-b01d-901b0e1b8723" style="width: 300px; height: 300px;"></div> 4489 * <script type="text/javascript"> 4490 * (function() { 4491 * var board = JXG.JSXGraph.initBoard('11eff3a6-8c50-11e5-b01d-901b0e1b8723', 4492 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 4493 * // 4494 * // Set a new bounding box from the selection rectangle 4495 * // 4496 * var board = JXG.JSXGraph.initBoard('11eff3a6-8c50-11e5-b01d-901b0e1b8723', { 4497 * boundingBox:[-3,2,3,-2], 4498 * keepAspectRatio: false, 4499 * axis:true, 4500 * selection: { 4501 * enabled: true, 4502 * needShift: false, 4503 * needCtrl: true, 4504 * withLines: false, 4505 * vertices: { 4506 * visible: false 4507 * }, 4508 * fillColor: '#ffff00', 4509 * } 4510 * }); 4511 * 4512 * var f = function f(x) { return Math.cos(x); }, 4513 * curve = board.create('functiongraph', [f]); 4514 * 4515 * board.on('stopselecting', function(){ 4516 * var box = board.stopSelectionMode(), 4517 * 4518 * // bbox has the coordinates of the selection rectangle. 4519 * // Attention: box[i].usrCoords have the form [1, x, y], i.e. 4520 * // are homogeneous coordinates. 4521 * bbox = box[0].usrCoords.slice(1).concat(box[1].usrCoords.slice(1)); 4522 * 4523 * // Set a new bounding box 4524 * board.setBoundingBox(bbox, false); 4525 * }); 4526 * })(); 4527 * 4528 * </script><pre> 4529 * 4530 */ 4531 startSelectionMode: function () { 4532 this.selectingMode = true; 4533 this.selectionPolygon.setAttribute({visible: true}); 4534 this.selectingBox = [[0, 0], [0, 0]]; 4535 this._setSelectionPolygonFromBox(); 4536 this.selectionPolygon.fullUpdate(); 4537 }, 4538 4539 /** 4540 * Finalize the selection: disable selection mode and return the coordinates 4541 * of the selection rectangle. 4542 * @returns {Array} Coordinates of the selection rectangle. The array 4543 * contains two {@link JXG.Coords} objects. One the upper left corner and 4544 * the second for the lower right corner. 4545 */ 4546 stopSelectionMode: function () { 4547 this.selectingMode = false; 4548 this.selectionPolygon.setAttribute({visible: false}); 4549 return [this.selectionPolygon.vertices[0].coords, this.selectionPolygon.vertices[2].coords]; 4550 }, 4551 4552 /** 4553 * Start the selection of a region. 4554 * @private 4555 * @param {Array} pos Screen coordiates of the upper left corner of the 4556 * selection rectangle. 4557 */ 4558 _startSelecting: function (pos) { 4559 this.isSelecting = true; 4560 this.selectingBox = [ [pos[0], pos[1]], [pos[0], pos[1]] ]; 4561 this._setSelectionPolygonFromBox(); 4562 }, 4563 4564 /** 4565 * Update the selection rectangle during a move event. 4566 * @private 4567 * @param {Array} pos Screen coordiates of the move event 4568 */ 4569 _moveSelecting: function (pos) { 4570 if (this.isSelecting) { 4571 this.selectingBox[1] = [pos[0], pos[1]]; 4572 this._setSelectionPolygonFromBox(); 4573 this.selectionPolygon.fullUpdate(); 4574 } 4575 }, 4576 4577 /** 4578 * Update the selection rectangle during an up event. Stop selection. 4579 * @private 4580 * @param {Object} evt Event object 4581 */ 4582 _stopSelecting: function (evt) { 4583 var pos = this.getMousePosition(evt); 4584 4585 this.isSelecting = false; 4586 this.selectingBox[1] = [pos[0], pos[1]]; 4587 this._setSelectionPolygonFromBox(); 4588 }, 4589 4590 /** 4591 * Update the Selection rectangle. 4592 * @private 4593 */ 4594 _setSelectionPolygonFromBox: function () { 4595 var A = this.selectingBox[0], 4596 B = this.selectingBox[1]; 4597 4598 this.selectionPolygon.vertices[0].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], A[1]]); 4599 this.selectionPolygon.vertices[1].setPositionDirectly(JXG.COORDS_BY_SCREEN, [A[0], B[1]]); 4600 this.selectionPolygon.vertices[2].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], B[1]]); 4601 this.selectionPolygon.vertices[3].setPositionDirectly(JXG.COORDS_BY_SCREEN, [B[0], A[1]]); 4602 }, 4603 4604 /** 4605 * Test if a down event should start a selection. Test if the 4606 * required keys are pressed. If yes, {@link JXG.Board#startSelectionMode} is called. 4607 * @param {Object} evt Event object 4608 */ 4609 _testForSelection: function (evt) { 4610 if (this._isRequiredKeyPressed(evt, 'selection')) { 4611 if (!Type.exists(this.selectionPolygon)) { 4612 this._createSelectionPolygon(this.attr); 4613 } 4614 this.startSelectionMode(); 4615 } 4616 }, 4617 4618 /** 4619 * Create the internal selection polygon, which will be available as board.selectionPolygon. 4620 * @private 4621 * @param {Object} attr board attributes, e.g. the subobject board.attr. 4622 * @returns {Object} pointer to the board to enable chaining. 4623 */ 4624 _createSelectionPolygon: function(attr) { 4625 var selectionattr; 4626 4627 if (!Type.exists(this.selectionPolygon)) { 4628 selectionattr = Type.copyAttributes(attr, Options, 'board', 'selection'); 4629 if (selectionattr.enabled === true) { 4630 this.selectionPolygon = this.create('polygon', [[0, 0], [0, 0], [0, 0], [0, 0]], selectionattr); 4631 } 4632 } 4633 4634 return this; 4635 }, 4636 4637 /* ************************** 4638 * EVENT DEFINITION 4639 * for documentation purposes 4640 * ************************** */ 4641 4642 //region Event handler documentation 4643 4644 /** 4645 * @event 4646 * @description Whenever the user starts to touch or click the board. 4647 * @name JXG.Board#down 4648 * @param {Event} e The browser's event object. 4649 */ 4650 __evt__down: function (e) { }, 4651 4652 /** 4653 * @event 4654 * @description Whenever the user starts to click on the board. 4655 * @name JXG.Board#mousedown 4656 * @param {Event} e The browser's event object. 4657 */ 4658 __evt__mousedown: function (e) { }, 4659 4660 /** 4661 * @event 4662 * @description Whenever the user starts to click on the board with a 4663 * device sending pointer events. 4664 * @name JXG.Board#pointerdown 4665 * @param {Event} e The browser's event object. 4666 */ 4667 __evt__pointerdown: function (e) { }, 4668 4669 /** 4670 * @event 4671 * @description Whenever the user starts to touch the board. 4672 * @name JXG.Board#touchstart 4673 * @param {Event} e The browser's event object. 4674 */ 4675 __evt__touchstart: function (e) { }, 4676 4677 /** 4678 * @event 4679 * @description Whenever the user stops to touch or click the board. 4680 * @name JXG.Board#up 4681 * @param {Event} e The browser's event object. 4682 */ 4683 __evt__up: function (e) { }, 4684 4685 /** 4686 * @event 4687 * @description Whenever the user releases the mousebutton over the board. 4688 * @name JXG.Board#mouseup 4689 * @param {Event} e The browser's event object. 4690 */ 4691 __evt__mouseup: function (e) { }, 4692 4693 /** 4694 * @event 4695 * @description Whenever the user releases the mousebutton over the board with a 4696 * device sending pointer events. 4697 * @name JXG.Board#pointerup 4698 * @param {Event} e The browser's event object. 4699 */ 4700 __evt__pointerup: function (e) { }, 4701 4702 /** 4703 * @event 4704 * @description Whenever the user stops touching the board. 4705 * @name JXG.Board#touchend 4706 * @param {Event} e The browser's event object. 4707 */ 4708 __evt__touchend: function (e) { }, 4709 4710 /** 4711 * @event 4712 * @description This event is fired whenever the user is moving the finger or mouse pointer over the board. 4713 * @name JXG.Board#move 4714 * @param {Event} e The browser's event object. 4715 * @param {Number} mode The mode the board currently is in 4716 * @see {JXG.Board#mode} 4717 */ 4718 __evt__move: function (e, mode) { }, 4719 4720 /** 4721 * @event 4722 * @description This event is fired whenever the user is moving the mouse over the board. 4723 * @name JXG.Board#mousemove 4724 * @param {Event} e The browser's event object. 4725 * @param {Number} mode The mode the board currently is in 4726 * @see {JXG.Board#mode} 4727 */ 4728 __evt__mousemove: function (e, mode) { }, 4729 4730 /** 4731 * @event 4732 * @description This event is fired whenever the user is moving the mouse over the board with a 4733 * device sending pointer events. 4734 * @name JXG.Board#pointermove 4735 * @param {Event} e The browser's event object. 4736 * @param {Number} mode The mode the board currently is in 4737 * @see {JXG.Board#mode} 4738 */ 4739 __evt__pointermove: function (e, mode) { }, 4740 4741 /** 4742 * @event 4743 * @description This event is fired whenever the user is moving the finger over the board. 4744 * @name JXG.Board#touchmove 4745 * @param {Event} e The browser's event object. 4746 * @param {Number} mode The mode the board currently is in 4747 * @see {JXG.Board#mode} 4748 */ 4749 __evt__touchmove: function (e, mode) { }, 4750 4751 /** 4752 * @event 4753 * @description Whenever an element is highlighted this event is fired. 4754 * @name JXG.Board#hit 4755 * @param {Event} e The browser's event object. 4756 * @param {JXG.GeometryElement} el The hit element. 4757 * @param target 4758 */ 4759 __evt__hit: function (e, el, target) { }, 4760 4761 /** 4762 * @event 4763 * @description Whenever an element is highlighted this event is fired. 4764 * @name JXG.Board#mousehit 4765 * @param {Event} e The browser's event object. 4766 * @param {JXG.GeometryElement} el The hit element. 4767 * @param target 4768 */ 4769 __evt__mousehit: function (e, el, target) { }, 4770 4771 /** 4772 * @event 4773 * @description This board is updated. 4774 * @name JXG.Board#update 4775 */ 4776 __evt__update: function () { }, 4777 4778 /** 4779 * @event 4780 * @description The bounding box of the board has changed. 4781 * @name JXG.Board#boundingbox 4782 */ 4783 __evt__boundingbox: function () { }, 4784 4785 /** 4786 * @event 4787 * @description Select a region is started during a down event or by calling 4788 * {@link JXG.Board#startSelectionMode} 4789 * @name JXG.Board#startselecting 4790 */ 4791 __evt__startselecting: function () { }, 4792 4793 /** 4794 * @event 4795 * @description Select a region is started during a down event 4796 * from a device sending mouse events or by calling 4797 * {@link JXG.Board#startSelectionMode}. 4798 * @name JXG.Board#mousestartselecting 4799 */ 4800 __evt__mousestartselecting: function () { }, 4801 4802 /** 4803 * @event 4804 * @description Select a region is started during a down event 4805 * from a device sending pointer events or by calling 4806 * {@link JXG.Board#startSelectionMode}. 4807 * @name JXG.Board#pointerstartselecting 4808 */ 4809 __evt__pointerstartselecting: function () { }, 4810 4811 /** 4812 * @event 4813 * @description Select a region is started during a down event 4814 * from a device sending touch events or by calling 4815 * {@link JXG.Board#startSelectionMode}. 4816 * @name JXG.Board#touchstartselecting 4817 */ 4818 __evt__touchstartselecting: function () { }, 4819 4820 /** 4821 * @event 4822 * @description Selection of a region is stopped during an up event. 4823 * @name JXG.Board#stopselecting 4824 */ 4825 __evt__stopselecting: function () { }, 4826 4827 /** 4828 * @event 4829 * @description Selection of a region is stopped during an up event 4830 * from a device sending mouse events. 4831 * @name JXG.Board#mousestopselecting 4832 */ 4833 __evt__mousestopselecting: function () { }, 4834 4835 /** 4836 * @event 4837 * @description Selection of a region is stopped during an up event 4838 * from a device sending pointer events. 4839 * @name JXG.Board#pointerstopselecting 4840 */ 4841 __evt__pointerstopselecting: function () { }, 4842 4843 /** 4844 * @event 4845 * @description Selection of a region is stopped during an up event 4846 * from a device sending touch events. 4847 * @name JXG.Board#touchstopselecting 4848 */ 4849 __evt__touchstopselecting: function () { }, 4850 4851 /** 4852 * @event 4853 * @description A move event while selecting of a region is active. 4854 * @name JXG.Board#moveselecting 4855 */ 4856 __evt__moveselecting: function () { }, 4857 4858 /** 4859 * @event 4860 * @description A move event while selecting of a region is active 4861 * from a device sending mouse events. 4862 * @name JXG.Board#mousemoveselecting 4863 */ 4864 __evt__mousemoveselecting: function () { }, 4865 4866 /** 4867 * @event 4868 * @description Select a region is started during a down event 4869 * from a device sending mouse events. 4870 * @name JXG.Board#pointermoveselecting 4871 */ 4872 __evt__pointermoveselecting: function () { }, 4873 4874 /** 4875 * @event 4876 * @description Select a region is started during a down event 4877 * from a device sending touch events. 4878 * @name JXG.Board#touchmoveselecting 4879 */ 4880 __evt__touchmoveselecting: function () { }, 4881 4882 /** 4883 * @ignore 4884 */ 4885 __evt: function () {}, 4886 4887 //endregion 4888 4889 /** 4890 * Function to animate a curve rolling on another curve. 4891 * @param {Curve} c1 JSXGraph curve building the floor where c2 rolls 4892 * @param {Curve} c2 JSXGraph curve which rolls on c1. 4893 * @param {number} start_c1 The parameter t such that c1(t) touches c2. This is the start position of the 4894 * rolling process 4895 * @param {Number} stepsize Increase in t in each step for the curve c1 4896 * @param {Number} direction 4897 * @param {Number} time Delay time for setInterval() 4898 * @param {Array} pointlist Array of points which are rolled in each step. This list should contain 4899 * all points which define c2 and gliders on c2. 4900 * 4901 * @example 4902 * 4903 * // Line which will be the floor to roll upon. 4904 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 4905 * // Center of the rolling circle 4906 * var C = brd.create('point',[0,2],{name:'C'}); 4907 * // Starting point of the rolling circle 4908 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 4909 * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P 4910 * var circle = brd.create('curve',[ 4911 * function (t){var d = P.Dist(C), 4912 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 4913 * t += beta; 4914 * return C.X()+d*Math.cos(t); 4915 * }, 4916 * function (t){var d = P.Dist(C), 4917 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 4918 * t += beta; 4919 * return C.Y()+d*Math.sin(t); 4920 * }, 4921 * 0,2*Math.PI], 4922 * {strokeWidth:6, strokeColor:'green'}); 4923 * 4924 * // Point on circle 4925 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 4926 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 4927 * roll.start() // Start the rolling, to be stopped by roll.stop() 4928 * 4929 * </pre><div class="jxgbox" id="e5e1b53c-a036-4a46-9e35-190d196beca5" style="width: 300px; height: 300px;"></div> 4930 * <script type="text/javascript"> 4931 * var brd = JXG.JSXGraph.initBoard('e5e1b53c-a036-4a46-9e35-190d196beca5', {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright:false, shownavigation: false}); 4932 * // Line which will be the floor to roll upon. 4933 * var line = brd.create('curve', [function (t) { return t;}, function (t){ return 1;}], {strokeWidth:6}); 4934 * // Center of the rolling circle 4935 * var C = brd.create('point',[0,2],{name:'C'}); 4936 * // Starting point of the rolling circle 4937 * var P = brd.create('point',[0,1],{name:'P', trace:true}); 4938 * // Circle defined as a curve. The circle "starts" at P, i.e. circle(0) = P 4939 * var circle = brd.create('curve',[ 4940 * function (t){var d = P.Dist(C), 4941 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 4942 * t += beta; 4943 * return C.X()+d*Math.cos(t); 4944 * }, 4945 * function (t){var d = P.Dist(C), 4946 * beta = JXG.Math.Geometry.rad([C.X()+1,C.Y()],C,P); 4947 * t += beta; 4948 * return C.Y()+d*Math.sin(t); 4949 * }, 4950 * 0,2*Math.PI], 4951 * {strokeWidth:6, strokeColor:'green'}); 4952 * 4953 * // Point on circle 4954 * var B = brd.create('glider',[0,2,circle],{name:'B', color:'blue',trace:false}); 4955 * var roll = brd.createRoulette(line, circle, 0, Math.PI/20, 1, 100, [C,P,B]); 4956 * roll.start() // Start the rolling, to be stopped by roll.stop() 4957 * </script><pre> 4958 */ 4959 createRoulette: function (c1, c2, start_c1, stepsize, direction, time, pointlist) { 4960 var brd = this, 4961 Roulette = function () { 4962 var alpha = 0, Tx = 0, Ty = 0, 4963 t1 = start_c1, 4964 t2 = Numerics.root( 4965 function (t) { 4966 var c1x = c1.X(t1), 4967 c1y = c1.Y(t1), 4968 c2x = c2.X(t), 4969 c2y = c2.Y(t); 4970 4971 return (c1x - c2x) * (c1x - c2x) + (c1y - c2y) * (c1y - c2y); 4972 }, 4973 [0, Math.PI * 2] 4974 ), 4975 t1_new = 0.0, t2_new = 0.0, 4976 c1dist, 4977 4978 rotation = brd.create('transform', [ 4979 function () { 4980 return alpha; 4981 } 4982 ], {type: 'rotate'}), 4983 4984 rotationLocal = brd.create('transform', [ 4985 function () { 4986 return alpha; 4987 }, 4988 function () { 4989 return c1.X(t1); 4990 }, 4991 function () { 4992 return c1.Y(t1); 4993 } 4994 ], {type: 'rotate'}), 4995 4996 translate = brd.create('transform', [ 4997 function () { 4998 return Tx; 4999 }, 5000 function () { 5001 return Ty; 5002 } 5003 ], {type: 'translate'}), 5004 5005 // arc length via Simpson's rule. 5006 arclen = function (c, a, b) { 5007 var cpxa = Numerics.D(c.X)(a), 5008 cpya = Numerics.D(c.Y)(a), 5009 cpxb = Numerics.D(c.X)(b), 5010 cpyb = Numerics.D(c.Y)(b), 5011 cpxab = Numerics.D(c.X)((a + b) * 0.5), 5012 cpyab = Numerics.D(c.Y)((a + b) * 0.5), 5013 5014 fa = Math.sqrt(cpxa * cpxa + cpya * cpya), 5015 fb = Math.sqrt(cpxb * cpxb + cpyb * cpyb), 5016 fab = Math.sqrt(cpxab * cpxab + cpyab * cpyab); 5017 5018 return (fa + 4 * fab + fb) * (b - a) / 6; 5019 }, 5020 5021 exactDist = function (t) { 5022 return c1dist - arclen(c2, t2, t); 5023 }, 5024 5025 beta = Math.PI / 18, 5026 beta9 = beta * 9, 5027 interval = null; 5028 5029 this.rolling = function () { 5030 var h, g, hp, gp, z; 5031 5032 t1_new = t1 + direction * stepsize; 5033 5034 // arc length between c1(t1) and c1(t1_new) 5035 c1dist = arclen(c1, t1, t1_new); 5036 5037 // find t2_new such that arc length between c2(t2) and c1(t2_new) equals c1dist. 5038 t2_new = Numerics.root(exactDist, t2); 5039 5040 // c1(t) as complex number 5041 h = new Complex(c1.X(t1_new), c1.Y(t1_new)); 5042 5043 // c2(t) as complex number 5044 g = new Complex(c2.X(t2_new), c2.Y(t2_new)); 5045 5046 hp = new Complex(Numerics.D(c1.X)(t1_new), Numerics.D(c1.Y)(t1_new)); 5047 gp = new Complex(Numerics.D(c2.X)(t2_new), Numerics.D(c2.Y)(t2_new)); 5048 5049 // z is angle between the tangents of c1 at t1_new, and c2 at t2_new 5050 z = Complex.C.div(hp, gp); 5051 5052 alpha = Math.atan2(z.imaginary, z.real); 5053 // Normalizing the quotient 5054 z.div(Complex.C.abs(z)); 5055 z.mult(g); 5056 Tx = h.real - z.real; 5057 5058 // T = h(t1_new)-g(t2_new)*h'(t1_new)/g'(t2_new); 5059 Ty = h.imaginary - z.imaginary; 5060 5061 // -(10-90) degrees: make corners roll smoothly 5062 if (alpha < -beta && alpha > -beta9) { 5063 alpha = -beta; 5064 rotationLocal.applyOnce(pointlist); 5065 } else if (alpha > beta && alpha < beta9) { 5066 alpha = beta; 5067 rotationLocal.applyOnce(pointlist); 5068 } else { 5069 rotation.applyOnce(pointlist); 5070 translate.applyOnce(pointlist); 5071 t1 = t1_new; 5072 t2 = t2_new; 5073 } 5074 brd.update(); 5075 }; 5076 5077 this.start = function () { 5078 if (time > 0) { 5079 interval = window.setInterval(this.rolling, time); 5080 } 5081 return this; 5082 }; 5083 5084 this.stop = function () { 5085 window.clearInterval(interval); 5086 return this; 5087 }; 5088 return this; 5089 }; 5090 return new Roulette(); 5091 } 5092 }); 5093 5094 return JXG.Board; 5095 }); 5096