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