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