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