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