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