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