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