1 /* 2 Copyright 2008-2016 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Alfred Wassermann 7 8 This file is part of JSXGraph. 9 10 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 11 12 You can redistribute it and/or modify it under the terms of the 13 14 * GNU Lesser General Public License as published by 15 the Free Software Foundation, either version 3 of the License, or 16 (at your option) any later version 17 OR 18 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 19 20 JSXGraph is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 GNU Lesser General Public License for more details. 24 25 You should have received a copy of the GNU Lesser General Public License and 26 the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/> 27 and <http://opensource.org/licenses/MIT/>. 28 */ 29 30 31 /*global JXG: true, define: true, console: true, window: true*/ 32 /*jslint nomen: true, plusplus: true*/ 33 34 /* depends: 35 jxg 36 options 37 math/math 38 math/geometry 39 math/numerics 40 base/coords 41 base/constants 42 base/element 43 parser/geonext 44 utils/type 45 elements: 46 transform 47 */ 48 49 /** 50 * @fileoverview The geometry object CoordsElement is defined in this file. 51 * This object provides the coordinate handling of points, images and texts. 52 */ 53 54 define([ 55 'jxg', 'options', 'math/math', 'math/geometry', 'math/numerics', 'math/statistics', 'base/coords', 'base/constants', 'base/element', 56 'parser/geonext', 'utils/type', 'base/transformation' 57 ], function (JXG, Options, Mat, Geometry, Numerics, Statistics, Coords, Const, GeometryElement, GeonextParser, Type, Transform) { 58 59 "use strict"; 60 61 /** 62 * An element containing coords is the basic geometric element. Based on points lines and circles can be constructed which can be intersected 63 * which in turn are points again which can be used to construct new lines, circles, polygons, etc. This class holds methods for 64 * all kind of coordinate elements like points, texts and images. 65 * @class Creates a new coords element object. Do not use this constructor to create an element. 66 * 67 * @private 68 * @augments JXG.GeometryElement 69 * @param {Array} coordinates An array with the affine user coordinates of the point. 70 * {@link JXG.Options#elements}, and - optionally - a name and an id. 71 */ 72 JXG.CoordsElement = function (coordinates, isLabel) { 73 var i; 74 75 if (!Type.exists(coordinates)) { 76 coordinates = [1, 0, 0]; 77 } 78 79 for (i = 0; i < coordinates.length; ++i) { 80 coordinates[i] = parseFloat(coordinates[i]); 81 } 82 83 /** 84 * Coordinates of the element. 85 * @type JXG.Coords 86 * @private 87 */ 88 this.coords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 89 this.initialCoords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 90 91 /** 92 * Relative position on a slide element (line, circle, curve) if element is a glider on this element. 93 * @type Number 94 * @private 95 */ 96 this.position = null; 97 98 /** 99 * Determines whether the element slides on a polygon if point is a glider. 100 * @type boolean 101 * @default false 102 * @private 103 */ 104 this.onPolygon = false; 105 106 /** 107 * When used as a glider this member stores the object, where to glide on. 108 * To set the object to glide on use the method 109 * {@link JXG.Point#makeGlider} and DO NOT set this property directly 110 * as it will break the dependency tree. 111 * @type JXG.GeometryElement 112 * @name Glider#slideObject 113 */ 114 this.slideObject = null; 115 116 /** 117 * List of elements the element is bound to, i.e. the element glides on. 118 * Only the last entry is active. 119 * Use {@link JXG.Point#popSlideObject} to remove the currently active slideObject. 120 */ 121 this.slideObjects = []; 122 123 /** 124 * A {@link JXG.CoordsElement#updateGlider} call is usually followed 125 * by a general {@link JXG.Board#update} which calls 126 * {@link JXG.CoordsElement#updateGliderFromParent}. 127 * To prevent double updates, {@link JXG.CoordsElement#needsUpdateFromParent} 128 * is set to false in updateGlider() and reset to true in the following call to 129 * {@link JXG.CoordsElement#updateGliderFromParent} 130 * @type {Boolean} 131 */ 132 this.needsUpdateFromParent = true; 133 134 /** 135 * Dummy function for unconstrained points or gliders. 136 * @private 137 */ 138 this.updateConstraint = function () { 139 return this; 140 }; 141 142 /** 143 * Stores the groups of this element in an array of Group. 144 * @type array 145 * @see JXG.Group 146 * @private 147 */ 148 this.groups = []; 149 150 /* 151 * Do we need this? 152 */ 153 this.Xjc = null; 154 this.Yjc = null; 155 156 // documented in GeometryElement 157 this.methodMap = Type.deepCopy(this.methodMap, { 158 move: 'moveTo', 159 moveTo: 'moveTo', 160 moveAlong: 'moveAlong', 161 visit: 'visit', 162 glide: 'makeGlider', 163 makeGlider: 'makeGlider', 164 intersect: 'makeIntersection', 165 makeIntersection: 'makeIntersection', 166 X: 'X', 167 Y: 'Y', 168 free: 'free', 169 setPosition: 'setGliderPosition', 170 setGliderPosition: 'setGliderPosition', 171 addConstraint: 'addConstraint', 172 dist: 'Dist', 173 onPolygon: 'onPolygon' 174 }); 175 176 /* 177 * this.element may have been set by the object constructor. 178 */ 179 if (Type.exists(this.element)) { 180 this.addAnchor(coordinates, isLabel); 181 } 182 this.isDraggable = true; 183 184 }; 185 186 JXG.extend(JXG.CoordsElement.prototype, /** @lends JXG.CoordsElement.prototype */ { 187 /** 188 * Updates the coordinates of the element. 189 * @private 190 */ 191 updateCoords: function (fromParent) { 192 if (!this.needsUpdate) { 193 return this; 194 } 195 196 if (!Type.exists(fromParent)) { 197 fromParent = false; 198 } 199 200 /* 201 * We need to calculate the new coordinates no matter of the elements visibility because 202 * a child could be visible and depend on the coordinates of the element/point (e.g. perpendicular). 203 * 204 * Check if the element is a glider and calculate new coords in dependency of this.slideObject. 205 * This function is called with fromParent==true in case it is a glider element for example if 206 * the defining elements of the line or circle have been changed. 207 */ 208 if (this.type === Const.OBJECT_TYPE_GLIDER) { 209 if (fromParent) { 210 this.updateGliderFromParent(); 211 } else { 212 this.updateGlider(); 213 } 214 } 215 216 if (!this.visProp.frozen) { 217 this.updateConstraint(); 218 } 219 this.updateTransform(); 220 221 return this; 222 }, 223 224 /** 225 * Update of glider in case of dragging the glider or setting the postion of the glider. 226 * The relative position of the glider has to be updated. 227 * 228 * In case of a glider on a line: 229 * If the second point is an ideal point, then -1 < this.position < 1, 230 * this.position==+/-1 equals point2, this.position==0 equals point1 231 * 232 * If the first point is an ideal point, then 0 < this.position < 2 233 * this.position==0 or 2 equals point1, this.position==1 equals point2 234 * 235 * @private 236 */ 237 updateGlider: function () { 238 var i, p1c, p2c, d, v, poly, cc, pos, sgn, 239 alpha, beta, 240 delta = 2.0 * Math.PI, 241 angle, 242 cp, c, invMat, newCoords, newPos, 243 doRound = false, 244 slide = this.slideObject; 245 246 this.needsUpdateFromParent = false; 247 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 248 if (this.visProp.isgeonext) { 249 delta = 1.0; 250 } 251 //this.coords.setCoordinates(Const.COORDS_BY_USER, 252 // Geometry.projectPointToCircle(this, slide, this.board).usrCoords, false); 253 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 254 newPos = Geometry.rad([slide.center.X() + 1.0, slide.center.Y()], slide.center, this) / delta; 255 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 256 /* 257 * onPolygon==true: the point is a slider on a segment and this segment is one of the 258 * "borders" of a polygon. 259 * This is a GEONExT feature. 260 */ 261 if (this.onPolygon) { 262 p1c = slide.point1.coords.usrCoords; 263 p2c = slide.point2.coords.usrCoords; 264 i = 1; 265 d = p2c[i] - p1c[i]; 266 267 if (Math.abs(d) < Mat.eps) { 268 i = 2; 269 d = p2c[i] - p1c[i]; 270 } 271 272 cc = Geometry.projectPointToLine(this, slide, this.board); 273 pos = (cc.usrCoords[i] - p1c[i]) / d; 274 poly = slide.parentPolygon; 275 276 if (pos < 0) { 277 for (i = 0; i < poly.borders.length; i++) { 278 if (slide === poly.borders[i]) { 279 slide = poly.borders[(i - 1 + poly.borders.length) % poly.borders.length]; 280 break; 281 } 282 } 283 } else if (pos > 1.0) { 284 for (i = 0; i < poly.borders.length; i++) { 285 if (slide === poly.borders[i]) { 286 slide = poly.borders[(i + 1 + poly.borders.length) % poly.borders.length]; 287 break; 288 } 289 } 290 } 291 292 // If the slide object has changed, save the change to the glider. 293 if (slide.id !== this.slideObject.id) { 294 this.slideObject = slide; 295 } 296 } 297 298 p1c = slide.point1.coords; 299 p2c = slide.point2.coords; 300 301 // Distance between the two defining points 302 d = p1c.distance(Const.COORDS_BY_USER, p2c); 303 304 // The defining points are identical 305 if (d < Mat.eps) { 306 //this.coords.setCoordinates(Const.COORDS_BY_USER, p1c); 307 newCoords = p1c; 308 doRound = true; 309 newPos = 0.0; 310 } else { 311 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToLine(this, slide, this.board).usrCoords, false); 312 newCoords = Geometry.projectPointToLine(this, slide, this.board); 313 p1c = p1c.usrCoords.slice(0); 314 p2c = p2c.usrCoords.slice(0); 315 316 // The second point is an ideal point 317 if (Math.abs(p2c[0]) < Mat.eps) { 318 i = 1; 319 d = p2c[i]; 320 321 if (Math.abs(d) < Mat.eps) { 322 i = 2; 323 d = p2c[i]; 324 } 325 326 d = (newCoords.usrCoords[i] - p1c[i]) / d; 327 sgn = (d >= 0) ? 1 : -1; 328 d = Math.abs(d); 329 newPos = sgn * d / (d + 1); 330 331 // The first point is an ideal point 332 } else if (Math.abs(p1c[0]) < Mat.eps) { 333 i = 1; 334 d = p1c[i]; 335 336 if (Math.abs(d) < Mat.eps) { 337 i = 2; 338 d = p1c[i]; 339 } 340 341 d = (newCoords.usrCoords[i] - p2c[i]) / d; 342 343 // 1.0 - d/(1-d); 344 if (d < 0.0) { 345 newPos = (1 - 2.0 * d) / (1.0 - d); 346 } else { 347 newPos = 1 / (d + 1); 348 } 349 } else { 350 i = 1; 351 d = p2c[i] - p1c[i]; 352 353 if (Math.abs(d) < Mat.eps) { 354 i = 2; 355 d = p2c[i] - p1c[i]; 356 } 357 newPos = (newCoords.usrCoords[i] - p1c[i]) / d; 358 } 359 } 360 361 // Snap the glider point of the slider into its appropiate position 362 // First, recalculate the new value of this.position 363 // Second, call update(fromParent==true) to make the positioning snappier. 364 if (this.visProp.snapwidth > 0.0 && Math.abs(this._smax - this._smin) >= Mat.eps) { 365 newPos = Math.max(Math.min(newPos, 1), 0); 366 367 v = newPos * (this._smax - this._smin) + this._smin; 368 v = Math.round(v / this.visProp.snapwidth) * this.visProp.snapwidth; 369 newPos = (v - this._smin) / (this._smax - this._smin); 370 this.update(true); 371 } 372 373 p1c = slide.point1.coords; 374 if (!slide.visProp.straightfirst && Math.abs(p1c.usrCoords[0]) > Mat.eps && newPos < 0) { 375 //this.coords.setCoordinates(Const.COORDS_BY_USER, p1c); 376 newCoords = p1c; 377 doRound = true; 378 newPos = 0; 379 } 380 381 p2c = slide.point2.coords; 382 if (!slide.visProp.straightlast && Math.abs(p2c.usrCoords[0]) > Mat.eps && newPos > 1) { 383 //this.coords.setCoordinates(Const.COORDS_BY_USER, p2c); 384 newCoords = p2c; 385 doRound = true; 386 newPos = 1; 387 } 388 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 389 // In case, the point is a constrained glider. 390 // side-effect: this.position is overwritten 391 this.updateConstraint(); 392 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToTurtle(this, slide, this.board).usrCoords, false); 393 newCoords = Geometry.projectPointToTurtle(this, slide, this.board); 394 newPos = this.position; // save position for the overwriting below 395 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 396 if ((slide.type === Const.OBJECT_TYPE_ARC || 397 slide.type === Const.OBJECT_TYPE_SECTOR)) { 398 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 399 400 angle = Geometry.rad(slide.radiuspoint, slide.center, this); 401 alpha = 0.0; 402 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 403 newPos = angle; 404 405 if ((slide.visProp.selection === 'minor' && beta > Math.PI) || 406 (slide.visProp.selection === 'major' && beta < Math.PI)) { 407 alpha = beta; 408 beta = 2 * Math.PI; 409 } 410 411 // Correct the position if we are outside of the sector/arc 412 if (angle < alpha || angle > beta) { 413 newPos = beta; 414 415 if ((angle < alpha && angle > alpha * 0.5) || (angle > beta && angle > beta * 0.5 + Math.PI)) { 416 newPos = alpha; 417 } 418 419 this.needsUpdateFromParent = true; 420 this.updateGliderFromParent(); 421 } 422 423 delta = beta - alpha; 424 if (this.visProp.isgeonext) { 425 delta = 1.0; 426 } 427 if (Math.abs(delta) > Mat.eps) { 428 newPos /= delta; 429 } 430 } else { 431 // In case, the point is a constrained glider. 432 this.updateConstraint(); 433 434 if (slide.transformations.length > 0) { 435 slide.updateTransformMatrix(); 436 invMat = Mat.inverse(slide.transformMat); 437 c = Mat.matVecMult(invMat, this.coords.usrCoords); 438 439 cp = (new Coords(Const.COORDS_BY_USER, c, this.board)).usrCoords; 440 c = Geometry.projectCoordsToCurve(cp[1], cp[2], this.position || 0, slide, this.board); 441 442 newCoords = c[0]; 443 newPos = c[1]; 444 } else { 445 // side-effect: this.position is overwritten 446 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToCurve(this, slide, this.board).usrCoords, false); 447 newCoords = Geometry.projectPointToCurve(this, slide, this.board); 448 newPos = this.position; // save position for the overwriting below 449 } 450 } 451 } else if (Type.isPoint(slide)) { 452 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToPoint(this, slide, this.board).usrCoords, false); 453 newCoords = Geometry.projectPointToPoint(this, slide, this.board); 454 newPos = this.position; // save position for the overwriting below 455 } 456 457 this.coords.setCoordinates(Const.COORDS_BY_USER, newCoords.usrCoords, doRound); 458 this.position = newPos; 459 }, 460 461 /** 462 * Update of a glider in case a parent element has been updated. That means the 463 * relative position of the glider stays the same. 464 * @private 465 */ 466 updateGliderFromParent: function () { 467 var p1c, p2c, r, lbda, c, 468 slide = this.slideObject, 469 baseangle, alpha, angle, beta, 470 delta = 2.0 * Math.PI, 471 newPos; 472 473 if (!this.needsUpdateFromParent) { 474 this.needsUpdateFromParent = true; 475 return; 476 } 477 478 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 479 r = slide.Radius(); 480 if (this.visProp.isgeonext) { 481 delta = 1.0; 482 } 483 c = [ 484 slide.center.X() + r * Math.cos(this.position * delta), 485 slide.center.Y() + r * Math.sin(this.position * delta) 486 ]; 487 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 488 p1c = slide.point1.coords.usrCoords; 489 p2c = slide.point2.coords.usrCoords; 490 491 // If one of the defining points of the line does not exist, 492 // the glider should disappear 493 if ((p1c[0] === 0 && p1c[1] === 0 && p1c[2] === 0) || 494 (p2c[0] === 0 && p2c[1] === 0 && p2c[2] === 0)) { 495 c = [0, 0, 0]; 496 // The second point is an ideal point 497 } else if (Math.abs(p2c[0]) < Mat.eps) { 498 lbda = Math.min(Math.abs(this.position), 1 - Mat.eps); 499 lbda /= (1.0 - lbda); 500 501 if (this.position < 0) { 502 lbda = -lbda; 503 } 504 505 c = [ 506 p1c[0] + lbda * p2c[0], 507 p1c[1] + lbda * p2c[1], 508 p1c[2] + lbda * p2c[2] 509 ]; 510 // The first point is an ideal point 511 } else if (Math.abs(p1c[0]) < Mat.eps) { 512 lbda = Math.max(this.position, Mat.eps); 513 lbda = Math.min(lbda, 2 - Mat.eps); 514 515 if (lbda > 1) { 516 lbda = (lbda - 1) / (lbda - 2); 517 } else { 518 lbda = (1 - lbda) / lbda; 519 } 520 521 c = [ 522 p2c[0] + lbda * p1c[0], 523 p2c[1] + lbda * p1c[1], 524 p2c[2] + lbda * p1c[2] 525 ]; 526 } else { 527 lbda = this.position; 528 c = [ 529 p1c[0] + lbda * (p2c[0] - p1c[0]), 530 p1c[1] + lbda * (p2c[1] - p1c[1]), 531 p1c[2] + lbda * (p2c[2] - p1c[2]) 532 ]; 533 } 534 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 535 this.coords.setCoordinates(Const.COORDS_BY_USER, [slide.Z(this.position), slide.X(this.position), slide.Y(this.position)]); 536 // In case, the point is a constrained glider. 537 // side-effect: this.position is overwritten: 538 this.updateConstraint(); 539 c = Geometry.projectPointToTurtle(this, slide, this.board).usrCoords; 540 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 541 this.coords.setCoordinates(Const.COORDS_BY_USER, [slide.Z(this.position), slide.X(this.position), slide.Y(this.position)]); 542 543 if (slide.type === Const.OBJECT_TYPE_ARC || slide.type === Const.OBJECT_TYPE_SECTOR) { 544 baseangle = Geometry.rad([slide.center.X() + 1, slide.center.Y()], slide.center, slide.radiuspoint); 545 546 alpha = 0.0; 547 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 548 549 if ((slide.visProp.selection === 'minor' && beta > Math.PI) || 550 (slide.visProp.selection === 'major' && beta < Math.PI)) { 551 alpha = beta; 552 beta = 2 * Math.PI; 553 } 554 555 delta = beta - alpha; 556 if (this.visProp.isgeonext) { 557 delta = 1.0; 558 } 559 angle = this.position * delta; 560 561 // Correct the position if we are outside of the sector/arc 562 if (angle < alpha || angle > beta) { 563 angle = beta; 564 565 if ((angle < alpha && angle > alpha * 0.5) || 566 (angle > beta && angle > beta * 0.5 + Math.PI)) { 567 angle = alpha; 568 } 569 570 this.position = angle; 571 if (Math.abs(delta) > Mat.eps) { 572 this.position /= delta; 573 } 574 } 575 576 r = slide.Radius(); 577 c = [ 578 slide.center.X() + r * Math.cos(this.position * delta + baseangle), 579 slide.center.Y() + r * Math.sin(this.position * delta + baseangle) 580 ]; 581 } else { 582 // In case, the point is a constrained glider. 583 // side-effect: this.position is overwritten 584 this.updateConstraint(); 585 c = Geometry.projectPointToCurve(this, slide, this.board).usrCoords; 586 } 587 588 } else if (Type.isPoint(slide)) { 589 c = Geometry.projectPointToPoint(this, slide, this.board).usrCoords; 590 } 591 592 this.coords.setCoordinates(Const.COORDS_BY_USER, c, false); 593 }, 594 595 updateRendererGeneric: function (rendererMethod) { 596 var wasReal; 597 598 if (!this.needsUpdate) { 599 return this; 600 } 601 602 /* Call the renderer only if point is visible. */ 603 if (this.visProp.visible) { 604 wasReal = this.isReal; 605 this.isReal = (!isNaN(this.coords.usrCoords[1] + this.coords.usrCoords[2])); 606 //Homogeneous coords: ideal point 607 this.isReal = (Math.abs(this.coords.usrCoords[0]) > Mat.eps) ? this.isReal : false; 608 609 if (this.isReal) { 610 if (wasReal !== this.isReal) { 611 this.board.renderer.show(this); 612 613 if (this.hasLabel && this.label.visProp.visible) { 614 this.board.renderer.show(this.label); 615 } 616 } 617 this.board.renderer[rendererMethod](this); 618 } else { 619 if (wasReal !== this.isReal) { 620 this.board.renderer.hide(this); 621 622 if (this.hasLabel && this.label.visProp.visible) { 623 this.board.renderer.hide(this.label); 624 } 625 } 626 } 627 } 628 629 /* Update the label if visible. */ 630 if (this.hasLabel && this.visProp.visible && this.label && this.label.visProp.visible && this.isReal) { 631 this.label.update(); 632 this.board.renderer.updateText(this.label); 633 } 634 635 this.needsUpdate = false; 636 637 return this; 638 }, 639 640 /** 641 * Getter method for x, this is used by for CAS-points to access point coordinates. 642 * @returns {Number} User coordinate of point in x direction. 643 */ 644 X: function () { 645 return this.coords.usrCoords[1]; 646 }, 647 648 /** 649 * Getter method for y, this is used by CAS-points to access point coordinates. 650 * @returns {Number} User coordinate of point in y direction. 651 */ 652 Y: function () { 653 return this.coords.usrCoords[2]; 654 }, 655 656 /** 657 * Getter method for z, this is used by CAS-points to access point coordinates. 658 * @returns {Number} User coordinate of point in z direction. 659 */ 660 Z: function () { 661 return this.coords.usrCoords[0]; 662 }, 663 664 /** 665 * New evaluation of the function term. 666 * This is required for CAS-points: Their XTerm() method is 667 * overwritten in {@link JXG.CoordsElement#addConstraint}. 668 * 669 * @returns {Number} User coordinate of point in x direction. 670 * @private 671 */ 672 XEval: function () { 673 return this.coords.usrCoords[1]; 674 }, 675 676 /** 677 * New evaluation of the function term. 678 * This is required for CAS-points: Their YTerm() method is overwritten 679 * in {@link JXG.CoordsElement#addConstraint}. 680 * 681 * @returns {Number} User coordinate of point in y direction. 682 * @private 683 */ 684 YEval: function () { 685 return this.coords.usrCoords[2]; 686 }, 687 688 /** 689 * New evaluation of the function term. 690 * This is required for CAS-points: Their ZTerm() method is overwritten in 691 * {@link JXG.CoordsElement#addConstraint}. 692 * 693 * @returns {Number} User coordinate of point in z direction. 694 * @private 695 */ 696 ZEval: function () { 697 return this.coords.usrCoords[0]; 698 }, 699 700 /** 701 * Getter method for the distance to a second point, this is required for CAS-elements. 702 * Here, function inlining seems to be worthwile (for plotting). 703 * @param {JXG.Point} point2 The point to which the distance shall be calculated. 704 * @returns {Number} Distance in user coordinate to the given point 705 */ 706 Dist: function (point2) { 707 if (this.isReal && point2.isReal) { 708 return this.coords.distance(Const.COORDS_BY_USER, point2.coords); 709 } 710 return NaN; 711 }, 712 713 /** 714 * Alias for {@link JXG.Element#handleSnapToGrid} 715 * @param {Boolean} force force snapping independent from what the snaptogrid attribute says 716 * @returns {JXG.Point} Reference to this element 717 */ 718 snapToGrid: function (force) { 719 return this.handleSnapToGrid(force); 720 }, 721 722 /** 723 * Let a point snap to the nearest point in distance of 724 * {@link JXG.Point#attractorDistance}. 725 * The function uses the coords object of the point as 726 * its actual position. 727 * @param {Boolean} force force snapping independent from what the snaptogrid attribute says 728 * @returns {JXG.Point} Reference to this element 729 */ 730 handleSnapToPoints: function (force) { 731 var i, pEl, pCoords, 732 d = 0, 733 len, 734 dMax = Infinity, 735 c = null, 736 len2, j, ignore = false; 737 738 len = this.board.objectsList.length; 739 740 if (this.visProp.ignoredsnaptopoints) { 741 len2 = this.visProp.ignoredsnaptopoints.length; 742 } 743 744 if (this.visProp.snaptopoints || force) { 745 for (i = 0; i < len; i++) { 746 pEl = this.board.objectsList[i]; 747 748 if (this.visProp.ignoredsnaptopoints) { 749 ignore = false; 750 for (j = 0; j < len2; j++) { 751 if (pEl == this.board.select(this.visProp.ignoredsnaptopoints[j])) { 752 ignore = true; 753 break; 754 } 755 } 756 if (ignore) { 757 continue; 758 } 759 } 760 761 if (Type.isPoint(pEl) && pEl !== this && pEl.visProp.visible) { 762 pCoords = Geometry.projectPointToPoint(this, pEl, this.board); 763 if (this.visProp.attractorunit === 'screen') { 764 d = pCoords.distance(Const.COORDS_BY_SCREEN, this.coords); 765 } else { 766 d = pCoords.distance(Const.COORDS_BY_USER, this.coords); 767 } 768 769 if (d < this.visProp.attractordistance && d < dMax) { 770 dMax = d; 771 c = pCoords; 772 } 773 } 774 } 775 776 if (c !== null) { 777 this.coords.setCoordinates(Const.COORDS_BY_USER, c.usrCoords); 778 } 779 } 780 781 return this; 782 }, 783 784 /** 785 * Alias for {@link JXG.CoordsElement#handleSnapToPoints}. 786 * 787 * @param {Boolean} force force snapping independent from what the snaptogrid attribute says 788 * @returns {JXG.Point} Reference to this element 789 */ 790 snapToPoints: function (force) { 791 return this.handleSnapToPoints(force); 792 }, 793 794 /** 795 * A point can change its type from free point to glider 796 * and vice versa. If it is given an array of attractor elements 797 * (attribute attractors) and the attribute attractorDistance 798 * then the point will be made a glider if it less than attractorDistance 799 * apart from one of its attractor elements. 800 * If attractorDistance is equal to zero, the point stays in its 801 * current form. 802 * @returns {JXG.Point} Reference to this element 803 */ 804 handleAttractors: function () { 805 var i, el, projCoords, 806 d = 0.0, 807 projection, 808 len = this.visProp.attractors.length; 809 810 if (this.visProp.attractordistance === 0.0) { 811 return; 812 } 813 814 for (i = 0; i < len; i++) { 815 el = this.board.select(this.visProp.attractors[i]); 816 817 if (Type.exists(el) && el !== this) { 818 if (Type.isPoint(el)) { 819 projCoords = Geometry.projectPointToPoint(this, el, this.board); 820 } else if (el.elementClass === Const.OBJECT_CLASS_LINE) { 821 projection = Geometry.projectCoordsToSegment( 822 this.coords.usrCoords, 823 el.point1.coords.usrCoords, 824 el.point2.coords.usrCoords); 825 if (!el.visProp.straightfirst && projection[1] < 0.0) { 826 projCoords = el.point1.coords; 827 } else if (!el.visProp.straightlast && projection[1] > 1.0) { 828 projCoords = el.point2.coords; 829 } else { 830 projCoords = new Coords(Const.COORDS_BY_USER, projection[0], this.board); 831 } 832 } else if (el.elementClass === Const.OBJECT_CLASS_CIRCLE) { 833 projCoords = Geometry.projectPointToCircle(this, el, this.board); 834 } else if (el.elementClass === Const.OBJECT_CLASS_CURVE) { 835 projCoords = Geometry.projectPointToCurve(this, el, this.board); 836 } else if (el.type === Const.OBJECT_TYPE_TURTLE) { 837 projCoords = Geometry.projectPointToTurtle(this, el, this.board); 838 } 839 840 if (this.visProp.attractorunit === 'screen') { 841 d = projCoords.distance(Const.COORDS_BY_SCREEN, this.coords); 842 } else { 843 d = projCoords.distance(Const.COORDS_BY_USER, this.coords); 844 } 845 846 if (d < this.visProp.attractordistance) { 847 if (!(this.type === Const.OBJECT_TYPE_GLIDER && this.slideObject === el)) { 848 this.makeGlider(el); 849 } 850 851 break; // bind the point to the first attractor in its list. 852 } else { 853 if (el === this.slideObject && d >= this.visProp.snatchdistance) { 854 this.popSlideObject(); 855 } 856 } 857 } 858 } 859 860 return this; 861 }, 862 863 /** 864 * Sets coordinates and calls the point's update() method. 865 * @param {Number} method The type of coordinates used here. 866 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 867 * @param {Array} coords coordinates <tt>([z], x, y)</tt> in screen/user units 868 * @returns {JXG.Point} this element 869 */ 870 setPositionDirectly: function (method, coords) { 871 var i, c, dc, 872 oldCoords = this.coords, 873 newCoords; 874 875 if (this.relativeCoords) { 876 c = new Coords(method, coords, this.board); 877 if (this.visProp.islabel) { 878 dc = Statistics.subtract(c.scrCoords, oldCoords.scrCoords); 879 this.relativeCoords.scrCoords[1] += dc[1]; 880 this.relativeCoords.scrCoords[2] += dc[2]; 881 } else { 882 dc = Statistics.subtract(c.usrCoords, oldCoords.usrCoords); 883 this.relativeCoords.usrCoords[1] += dc[1]; 884 this.relativeCoords.usrCoords[2] += dc[2]; 885 } 886 887 return this; 888 } 889 890 this.coords.setCoordinates(method, coords); 891 this.handleSnapToGrid(); 892 this.handleSnapToPoints(); 893 this.handleAttractors(); 894 895 // Update the initial coordinates. This is needed for free points 896 // that have a transformation bound to it. 897 for (i = this.transformations.length - 1; i >= 0; i--) { 898 if (method === Const.COORDS_BY_SCREEN) { 899 newCoords = (new Coords(method, coords, this.board)).usrCoords; 900 } else { 901 if (coords.length === 2) { 902 coords = [1].concat(coords); 903 } 904 newCoords = coords; 905 } 906 this.initialCoords.setCoordinates(Const.COORDS_BY_USER, Mat.matVecMult(Mat.inverse(this.transformations[i].matrix), newCoords)); 907 } 908 this.prepareUpdate().update(); 909 910 // If the user suspends the board updates we need to recalculate the relative position of 911 // the point on the slide object. this is done in updateGlider() which is NOT called during the 912 // update process triggered by unsuspendUpdate. 913 if (this.board.isSuspendedUpdate && this.type === Const.OBJECT_TYPE_GLIDER) { 914 this.updateGlider(); 915 } 916 917 return this; 918 }, 919 920 /** 921 * Translates the point by <tt>tv = (x, y)</tt>. 922 * @param {Number} method The type of coordinates used here. 923 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 924 * @param {Array} tv (x, y) 925 * @returns {JXG.Point} 926 */ 927 setPositionByTransform: function (method, tv) { 928 var t; 929 930 tv = new Coords(method, tv, this.board); 931 t = this.board.create('transform', tv.usrCoords.slice(1), {type: 'translate'}); 932 933 if (this.transformations.length > 0 && 934 this.transformations[this.transformations.length - 1].isNumericMatrix) { 935 this.transformations[this.transformations.length - 1].melt(t); 936 } else { 937 this.addTransform(this, t); 938 } 939 940 this.prepareUpdate().update(); 941 942 return this; 943 }, 944 945 /** 946 * Sets coordinates and calls the point's update() method. 947 * @param {Number} method The type of coordinates used here. 948 * Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 949 * @param {Array} coords coordinates in screen/user units 950 * @returns {JXG.Point} 951 */ 952 setPosition: function (method, coords) { 953 return this.setPositionDirectly(method, coords); 954 }, 955 956 /** 957 * Sets the position of a glider relative to the defining elements 958 * of the {@link JXG.Point#slideObject}. 959 * @param {Number} x 960 * @returns {JXG.Point} Reference to the point element. 961 */ 962 setGliderPosition: function (x) { 963 if (this.type === Const.OBJECT_TYPE_GLIDER) { 964 this.position = x; 965 this.board.update(); 966 } 967 968 return this; 969 }, 970 971 /** 972 * Convert the point to glider and update the construction. 973 * To move the point visual onto the glider, a call of board update is necessary. 974 * @param {String|Object} slide The object the point will be bound to. 975 */ 976 makeGlider: function (slide) { 977 var slideobj = this.board.select(slide), 978 onPolygon = false, 979 min, 980 i, 981 dist; 982 983 if (slideobj.type === Const.OBJECT_TYPE_POLYGON){ 984 // Search for the closest side of the polygon. 985 min = Number.MAX_VALUE; 986 for (i = 0; i < slideobj.borders.length; i++){ 987 dist = JXG.Math.Geometry.distPointLine(this.coords.usrCoords, slideobj.borders[i].stdform); 988 if (dist < min){ 989 min = dist; 990 slide = slideobj.borders[i]; 991 } 992 } 993 slideobj = this.board.select(slide); 994 onPolygon = true; 995 } 996 997 /* Gliders on Ticks are forbidden */ 998 if (!Type.exists(slideobj)) { 999 throw new Error("JSXGraph: slide object undefined."); 1000 } else if (slideobj.type === Const.OBJECT_TYPE_TICKS) { 1001 throw new Error("JSXGraph: gliders on ticks are not possible."); 1002 } 1003 1004 this.slideObject = this.board.select(slide); 1005 this.slideObjects.push(this.slideObject); 1006 this.addParents(slide); 1007 1008 this.type = Const.OBJECT_TYPE_GLIDER; 1009 this.elType = 'glider'; 1010 this.visProp.snapwidth = -1; // By default, deactivate snapWidth 1011 this.slideObject.addChild(this); 1012 this.isDraggable = true; 1013 this.onPolygon = onPolygon; 1014 1015 this.generatePolynomial = function () { 1016 return this.slideObject.generatePolynomial(this); 1017 }; 1018 1019 // Determine the initial value of this.position 1020 this.updateGlider(); 1021 this.needsUpdateFromParent = true; 1022 this.updateGliderFromParent(); 1023 1024 return this; 1025 }, 1026 1027 /** 1028 * Remove the last slideObject. If there are more than one elements the point is bound to, 1029 * the second last element is the new active slideObject. 1030 */ 1031 popSlideObject: function () { 1032 if (this.slideObjects.length > 0) { 1033 this.slideObjects.pop(); 1034 1035 // It may not be sufficient to remove the point from 1036 // the list of childElement. For complex dependencies 1037 // one may have to go to the list of ancestor and descendants. A.W. 1038 // yes indeed, see #51 on github bugtracker 1039 //delete this.slideObject.childElements[this.id]; 1040 this.slideObject.removeChild(this); 1041 1042 if (this.slideObjects.length === 0) { 1043 this.type = this._org_type; 1044 if (this.type === Const.OBJECT_TYPE_POINT) { 1045 this.elType = 'point'; 1046 } else if (this.elementClass === Const.OBJECT_CLASS_TEXT) { 1047 this.elType = 'text'; 1048 } else if (this.type === Const.OBJECT_TYPE_IMAGE) { 1049 this.elType = 'image'; 1050 } 1051 1052 this.slideObject = null; 1053 } else { 1054 this.slideObject = this.slideObjects[this.slideObjects.length - 1]; 1055 } 1056 } 1057 }, 1058 1059 /** 1060 * Converts a calculated element into a free element, 1061 * i.e. it will delete all ancestors and transformations and, 1062 * if the element is currently a glider, will remove the slideObject reference. 1063 */ 1064 free: function () { 1065 var ancestorId, ancestor, child; 1066 1067 if (this.type !== Const.OBJECT_TYPE_GLIDER) { 1068 // remove all transformations 1069 this.transformations.length = 0; 1070 1071 if (!this.isDraggable) { 1072 this.isDraggable = true; 1073 1074 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1075 this.type = Const.OBJECT_TYPE_POINT; 1076 this.elType = 'point'; 1077 } 1078 1079 this.XEval = function () { 1080 return this.coords.usrCoords[1]; 1081 }; 1082 1083 this.YEval = function () { 1084 return this.coords.usrCoords[2]; 1085 }; 1086 1087 this.ZEval = function () { 1088 return this.coords.usrCoords[0]; 1089 }; 1090 1091 this.Xjc = null; 1092 this.Yjc = null; 1093 } else { 1094 return; 1095 } 1096 } 1097 1098 // a free point does not depend on anything. And instead of running through tons of descendants and ancestor 1099 // structures, where we eventually are going to visit a lot of objects twice or thrice with hard to read and 1100 // comprehend code, just run once through all objects and delete all references to this point and its label. 1101 for (ancestorId in this.board.objects) { 1102 if (this.board.objects.hasOwnProperty(ancestorId)) { 1103 ancestor = this.board.objects[ancestorId]; 1104 1105 if (ancestor.descendants) { 1106 delete ancestor.descendants[this.id]; 1107 delete ancestor.childElements[this.id]; 1108 1109 if (this.hasLabel) { 1110 delete ancestor.descendants[this.label.id]; 1111 delete ancestor.childElements[this.label.id]; 1112 } 1113 } 1114 } 1115 } 1116 1117 // A free point does not depend on anything. Remove all ancestors. 1118 this.ancestors = {}; // only remove the reference 1119 1120 // Completely remove all slideObjects of the element 1121 this.slideObject = null; 1122 this.slideObjects = []; 1123 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1124 this.type = Const.OBJECT_TYPE_POINT; 1125 this.elType = 'point'; 1126 } else if (this.elementClass === Const.OBJECT_CLASS_TEXT) { 1127 this.type = this._org_type; 1128 this.elType = 'text'; 1129 } else if (this.elementClass === Const.OBJECT_CLASS_OTHER) { 1130 this.type = this._org_type; 1131 this.elType = 'image'; 1132 } 1133 }, 1134 1135 /** 1136 * Convert the point to CAS point and call update(). 1137 * @param {Array} terms [[zterm], xterm, yterm] defining terms for the z, x and y coordinate. 1138 * The z-coordinate is optional and it is used for homogeneous coordinates. 1139 * The coordinates may be either <ul> 1140 * <li>a JavaScript function,</li> 1141 * <li>a string containing GEONExT syntax. This string will be converted into a JavaScript 1142 * function here,</li> 1143 * <li>a Number</li> 1144 * <li>a pointer to a slider object. This will be converted into a call of the Value()-method 1145 * of this slider.</li> 1146 * </ul> 1147 * @see JXG.GeonextParser#geonext2JS 1148 */ 1149 addConstraint: function (terms) { 1150 var fs, i, v, t, 1151 newfuncs = [], 1152 what = ['X', 'Y'], 1153 1154 makeConstFunction = function (z) { 1155 return function () { 1156 return z; 1157 }; 1158 }, 1159 1160 makeSliderFunction = function (a) { 1161 return function () { 1162 return a.Value(); 1163 }; 1164 }; 1165 1166 if (this.elementClass === Const.OBJECT_CLASS_POINT) { 1167 this.type = Const.OBJECT_TYPE_CAS; 1168 } 1169 1170 this.isDraggable = false; 1171 1172 for (i = 0; i < terms.length; i++) { 1173 v = terms[i]; 1174 1175 if (Type.isString(v)) { 1176 // Convert GEONExT syntax into JavaScript syntax 1177 //t = JXG.GeonextParser.geonext2JS(v, this.board); 1178 //newfuncs[i] = new Function('','return ' + t + ';'); 1179 //v = GeonextParser.replaceNameById(v, this.board); 1180 newfuncs[i] = this.board.jc.snippet(v, true, null, true); 1181 1182 if (terms.length === 2) { 1183 this[what[i] + 'jc'] = terms[i]; 1184 } 1185 } else if (Type.isFunction(v)) { 1186 newfuncs[i] = v; 1187 } else if (Type.isNumber(v)) { 1188 newfuncs[i] = makeConstFunction(v); 1189 // Slider 1190 } else if (Type.isObject(v) && Type.isFunction(v.Value)) { 1191 newfuncs[i] = makeSliderFunction(v); 1192 } 1193 1194 newfuncs[i].origin = v; 1195 } 1196 1197 // Intersection function 1198 if (terms.length === 1) { 1199 this.updateConstraint = function () { 1200 var c = newfuncs[0](); 1201 1202 // Array 1203 if (Type.isArray(c)) { 1204 this.coords.setCoordinates(Const.COORDS_BY_USER, c); 1205 // Coords object 1206 } else { 1207 this.coords = c; 1208 } 1209 }; 1210 // Euclidean coordinates 1211 } else if (terms.length === 2) { 1212 this.XEval = newfuncs[0]; 1213 this.YEval = newfuncs[1]; 1214 1215 this.setParents([newfuncs[0].origin, newfuncs[1].origin]); 1216 1217 this.updateConstraint = function () { 1218 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.XEval(), this.YEval()]); 1219 }; 1220 // Homogeneous coordinates 1221 } else { 1222 this.ZEval = newfuncs[0]; 1223 this.XEval = newfuncs[1]; 1224 this.YEval = newfuncs[2]; 1225 1226 this.setParents([newfuncs[0].origin, newfuncs[1].origin, newfuncs[2].origin]); 1227 1228 this.updateConstraint = function () { 1229 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.ZEval(), this.XEval(), this.YEval()]); 1230 }; 1231 } 1232 1233 /** 1234 * We have to do an update. Otherwise, elements relying on this point will receive NaN. 1235 */ 1236 this.prepareUpdate().update(); 1237 1238 if (!this.board.isSuspendedUpdate) { 1239 this.updateRenderer(); 1240 } 1241 1242 return this; 1243 }, 1244 1245 /** 1246 * In case there is an attribute "anchor", the element is bound to 1247 * this anchor element. 1248 * This is handled with this.relativeCoords. If the element is a label 1249 * relativeCoords are given in scrCoords, otherwise in usrCoords. 1250 * @param{Array} coordinates Offset from th anchor element. These are the values for this.relativeCoords. 1251 * In case of a label, coordinates are screen coordinates. Otherwise, coordinates are user coordinates. 1252 * @param{Boolean} isLabel Yes/no 1253 * @private 1254 */ 1255 addAnchor: function (coordinates, isLabel) { 1256 if (isLabel) { 1257 this.relativeCoords = new Coords(Const.COORDS_BY_SCREEN, coordinates.slice(0, 2), this.board); 1258 } else { 1259 this.relativeCoords = new Coords(Const.COORDS_BY_USER, coordinates, this.board); 1260 } 1261 this.element.addChild(this); 1262 if (isLabel) { 1263 this.addParents(this.element); 1264 } 1265 1266 this.XEval = function () { 1267 var sx, coords, anchor; 1268 1269 if (this.visProp.islabel) { 1270 sx = parseFloat(this.visProp.offset[0]); 1271 anchor = this.element.getLabelAnchor(); 1272 coords = new Coords(Const.COORDS_BY_SCREEN, 1273 [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1], 0], this.board); 1274 1275 return coords.usrCoords[1]; 1276 } 1277 1278 anchor = this.element.getTextAnchor(); 1279 return this.relativeCoords.usrCoords[1] + anchor.usrCoords[1]; 1280 }; 1281 1282 this.YEval = function () { 1283 var sy, coords, anchor; 1284 1285 if (this.visProp.islabel) { 1286 sy = -parseFloat(this.visProp.offset[1]); 1287 anchor = this.element.getLabelAnchor(); 1288 coords = new Coords(Const.COORDS_BY_SCREEN, 1289 [0, sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], this.board); 1290 1291 return coords.usrCoords[2]; 1292 } 1293 1294 anchor = this.element.getTextAnchor(); 1295 return this.relativeCoords.usrCoords[2] + anchor.usrCoords[2]; 1296 }; 1297 1298 this.ZEval = Type.createFunction(1, this.board, ''); 1299 1300 this.updateConstraint = function () { 1301 this.coords.setCoordinates(Const.COORDS_BY_USER, [this.ZEval(), this.XEval(), this.YEval()]); 1302 }; 1303 1304 this.coords = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this.board); 1305 }, 1306 1307 /** 1308 * Applies the transformations of the element. 1309 * This method applies to text and images. Point transformations are handled differently. 1310 * @returns {JXG.CoordsElement} Reference to this object. 1311 */ 1312 updateTransform: function () { 1313 var i; 1314 1315 if (this.transformations.length === 0) { 1316 return this; 1317 } 1318 1319 for (i = 0; i < this.transformations.length; i++) { 1320 this.transformations[i].update(); 1321 } 1322 1323 return this; 1324 }, 1325 1326 /** 1327 * Add transformations to this point. 1328 * @param {JXG.GeometryElement} el 1329 * @param {JXG.Transformation|Array} transform Either one {@link JXG.Transformation} 1330 * or an array of {@link JXG.Transformation}s. 1331 * @returns {JXG.Point} Reference to this point object. 1332 */ 1333 addTransform: function (el, transform) { 1334 var i, 1335 list = Type.isArray(transform) ? transform : [transform], 1336 len = list.length; 1337 1338 // There is only one baseElement possible 1339 if (this.transformations.length === 0) { 1340 this.baseElement = el; 1341 } 1342 1343 for (i = 0; i < len; i++) { 1344 this.transformations.push(list[i]); 1345 } 1346 1347 return this; 1348 }, 1349 1350 /** 1351 * Animate the point. 1352 * @param {Number} direction The direction the glider is animated. Can be +1 or -1. 1353 * @param {Number} stepCount The number of steps. 1354 * @name Glider#startAnimation 1355 * @see Glider#stopAnimation 1356 * @function 1357 */ 1358 startAnimation: function (direction, stepCount) { 1359 var that = this; 1360 1361 if ((this.type === Const.OBJECT_TYPE_GLIDER) && !Type.exists(this.intervalCode)) { 1362 this.intervalCode = window.setInterval(function () { 1363 that._anim(direction, stepCount); 1364 }, 250); 1365 1366 if (!Type.exists(this.intervalCount)) { 1367 this.intervalCount = 0; 1368 } 1369 } 1370 return this; 1371 }, 1372 1373 /** 1374 * Stop animation. 1375 * @name Glider#stopAnimation 1376 * @see Glider#startAnimation 1377 * @function 1378 */ 1379 stopAnimation: function () { 1380 if (Type.exists(this.intervalCode)) { 1381 window.clearInterval(this.intervalCode); 1382 delete this.intervalCode; 1383 } 1384 1385 return this; 1386 }, 1387 1388 /** 1389 * Starts an animation which moves the point along a given path in given time. 1390 * @param {Array|function} path The path the point is moved on. 1391 * This can be either an array of arrays or containing x and y values of the points of 1392 * the path, or an array of points, or a function taking the amount of elapsed time since the animation 1393 * has started and returns an array containing a x and a y value or NaN. 1394 * In case of NaN the animation stops. 1395 * @param {Number} time The time in milliseconds in which to finish the animation 1396 * @param {Object} [options] Optional settings for the animation. 1397 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1398 * @param {Boolean} [options.interpolate=true] If <tt>path</tt> is an array moveAlong() 1399 * will interpolate the path 1400 * using {@link JXG.Math.Numerics.Neville}. Set this flag to false if you don't want to use interpolation. 1401 * @returns {JXG.Point} Reference to the point. 1402 */ 1403 moveAlong: function (path, time, options) { 1404 options = options || {}; 1405 1406 var i, neville, 1407 interpath = [], 1408 p = [], 1409 delay = this.board.attr.animationdelay, 1410 steps = time / delay, 1411 len, pos, part, 1412 1413 makeFakeFunction = function (i, j) { 1414 return function () { 1415 return path[i][j]; 1416 }; 1417 }; 1418 1419 if (Type.isArray(path)) { 1420 len = path.length; 1421 for (i = 0; i < len; i++) { 1422 if (Type.isPoint(path[i])) { 1423 p[i] = path[i]; 1424 } else { 1425 p[i] = { 1426 elementClass: Const.OBJECT_CLASS_POINT, 1427 X: makeFakeFunction(i, 0), 1428 Y: makeFakeFunction(i, 1) 1429 }; 1430 } 1431 } 1432 1433 time = time || 0; 1434 if (time === 0) { 1435 this.setPosition(Const.COORDS_BY_USER, [p[p.length - 1].X(), p[p.length - 1].Y()]); 1436 return this.board.update(this); 1437 } 1438 1439 if (!Type.exists(options.interpolate) || options.interpolate) { 1440 neville = Numerics.Neville(p); 1441 for (i = 0; i < steps; i++) { 1442 interpath[i] = []; 1443 interpath[i][0] = neville[0]((steps - i) / steps * neville[3]()); 1444 interpath[i][1] = neville[1]((steps - i) / steps * neville[3]()); 1445 } 1446 } else { 1447 len = path.length - 1; 1448 for (i = 0; i < steps; ++i) { 1449 pos = Math.floor(i / steps * len); 1450 part = i / steps * len - pos; 1451 1452 interpath[i] = []; 1453 interpath[i][0] = (1.0 - part) * p[pos].X() + part * p[pos + 1].X(); 1454 interpath[i][1] = (1.0 - part) * p[pos].Y() + part * p[pos + 1].Y(); 1455 } 1456 interpath.push([p[len].X(), p[len].Y()]); 1457 interpath.reverse(); 1458 /* 1459 for (i = 0; i < steps; i++) { 1460 interpath[i] = []; 1461 interpath[i][0] = path[Math.floor((steps - i) / steps * (path.length - 1))][0]; 1462 interpath[i][1] = path[Math.floor((steps - i) / steps * (path.length - 1))][1]; 1463 } 1464 */ 1465 } 1466 1467 this.animationPath = interpath; 1468 } else if (Type.isFunction(path)) { 1469 this.animationPath = path; 1470 this.animationStart = new Date().getTime(); 1471 } 1472 1473 this.animationCallback = options.callback; 1474 this.board.addAnimation(this); 1475 1476 return this; 1477 }, 1478 1479 /** 1480 * Starts an animated point movement towards the given coordinates <tt>where</tt>. 1481 * The animation is done after <tt>time</tt> milliseconds. 1482 * If the second parameter is not given or is equal to 0, setPosition() is called, see #setPosition. 1483 * @param {Array} where Array containing the x and y coordinate of the target location. 1484 * @param {Number} [time] Number of milliseconds the animation should last. 1485 * @param {Object} [options] Optional settings for the animation 1486 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1487 * @param {String} [options.effect='<>'] animation effects like speed fade in and out. possible values are 1488 * '<>' for speed increase on start and slow down at the end (default) and '--' for constant speed during 1489 * the whole animation. 1490 * @returns {JXG.Point} Reference to itself. 1491 * @see #animate 1492 */ 1493 moveTo: function (where, time, options) { 1494 options = options || {}; 1495 where = new Coords(Const.COORDS_BY_USER, where, this.board); 1496 1497 var i, 1498 delay = this.board.attr.animationdelay, 1499 steps = Math.ceil(time / delay), 1500 coords = [], 1501 X = this.coords.usrCoords[1], 1502 Y = this.coords.usrCoords[2], 1503 dX = (where.usrCoords[1] - X), 1504 dY = (where.usrCoords[2] - Y), 1505 1506 /** @ignore */ 1507 stepFun = function (i) { 1508 if (options.effect && options.effect === '<>') { 1509 return Math.pow(Math.sin((i / steps) * Math.PI / 2), 2); 1510 } 1511 return i / steps; 1512 }; 1513 1514 if (!Type.exists(time) || time === 0 || (Math.abs(where.usrCoords[0] - this.coords.usrCoords[0]) > Mat.eps)) { 1515 this.setPosition(Const.COORDS_BY_USER, where.usrCoords); 1516 return this.board.update(this); 1517 } 1518 1519 // In case there is no callback and we are already at the endpoint we can stop here 1520 if (!Type.exists(options.callback) && Math.abs(dX) < Mat.eps && Math.abs(dY) < Mat.eps) { 1521 return this; 1522 } 1523 1524 for (i = steps; i >= 0; i--) { 1525 coords[steps - i] = [where.usrCoords[0], X + dX * stepFun(i), Y + dY * stepFun(i)]; 1526 } 1527 1528 this.animationPath = coords; 1529 this.animationCallback = options.callback; 1530 this.board.addAnimation(this); 1531 1532 return this; 1533 }, 1534 1535 /** 1536 * Starts an animated point movement towards the given coordinates <tt>where</tt>. After arriving at 1537 * <tt>where</tt> the point moves back to where it started. The animation is done after <tt>time</tt> 1538 * milliseconds. 1539 * @param {Array} where Array containing the x and y coordinate of the target location. 1540 * @param {Number} time Number of milliseconds the animation should last. 1541 * @param {Object} [options] Optional settings for the animation 1542 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1543 * @param {String} [options.effect='<>'] animation effects like speed fade in and out. possible values are 1544 * '<>' for speed increase on start and slow down at the end (default) and '--' for constant speed during 1545 * the whole animation. 1546 * @param {Number} [options.repeat=1] How often this animation should be repeated. 1547 * @returns {JXG.Point} Reference to itself. 1548 * @see #animate 1549 */ 1550 visit: function (where, time, options) { 1551 where = new Coords(Const.COORDS_BY_USER, where, this.board); 1552 1553 var i, j, steps, 1554 delay = this.board.attr.animationdelay, 1555 coords = [], 1556 X = this.coords.usrCoords[1], 1557 Y = this.coords.usrCoords[2], 1558 dX = (where.usrCoords[1] - X), 1559 dY = (where.usrCoords[2] - Y), 1560 1561 /** @ignore */ 1562 stepFun = function (i) { 1563 var x = (i < steps / 2 ? 2 * i / steps : 2 * (steps - i) / steps); 1564 1565 if (options.effect && options.effect === '<>') { 1566 return Math.pow(Math.sin(x * Math.PI / 2), 2); 1567 } 1568 1569 return x; 1570 }; 1571 1572 // support legacy interface where the third parameter was the number of repeats 1573 if (Type.isNumber(options)) { 1574 options = {repeat: options}; 1575 } else { 1576 options = options || {}; 1577 if (!Type.exists(options.repeat)) { 1578 options.repeat = 1; 1579 } 1580 } 1581 1582 steps = Math.ceil(time / (delay * options.repeat)); 1583 1584 for (j = 0; j < options.repeat; j++) { 1585 for (i = steps; i >= 0; i--) { 1586 coords[j * (steps + 1) + steps - i] = [where.usrCoords[0], X + dX * stepFun(i), Y + dY * stepFun(i)]; 1587 } 1588 } 1589 this.animationPath = coords; 1590 this.animationCallback = options.callback; 1591 this.board.addAnimation(this); 1592 1593 return this; 1594 }, 1595 1596 /** 1597 * Animates a glider. Is called by the browser after startAnimation is called. 1598 * @param {Number} direction The direction the glider is animated. 1599 * @param {Number} stepCount The number of steps. 1600 * @see #startAnimation 1601 * @see #stopAnimation 1602 * @private 1603 */ 1604 _anim: function (direction, stepCount) { 1605 var distance, slope, dX, dY, alpha, startPoint, newX, radius, 1606 factor = 1; 1607 1608 this.intervalCount += 1; 1609 if (this.intervalCount > stepCount) { 1610 this.intervalCount = 0; 1611 } 1612 1613 if (this.slideObject.elementClass === Const.OBJECT_CLASS_LINE) { 1614 distance = this.slideObject.point1.coords.distance(Const.COORDS_BY_SCREEN, this.slideObject.point2.coords); 1615 slope = this.slideObject.getSlope(); 1616 if (slope !== Infinity) { 1617 alpha = Math.atan(slope); 1618 dX = Math.round((this.intervalCount / stepCount) * distance * Math.cos(alpha)); 1619 dY = Math.round((this.intervalCount / stepCount) * distance * Math.sin(alpha)); 1620 } else { 1621 dX = 0; 1622 dY = Math.round((this.intervalCount / stepCount) * distance); 1623 } 1624 1625 if (direction < 0) { 1626 startPoint = this.slideObject.point2; 1627 1628 if (this.slideObject.point2.coords.scrCoords[1] - this.slideObject.point1.coords.scrCoords[1] > 0) { 1629 factor = -1; 1630 } else if (this.slideObject.point2.coords.scrCoords[1] - this.slideObject.point1.coords.scrCoords[1] === 0) { 1631 if (this.slideObject.point2.coords.scrCoords[2] - this.slideObject.point1.coords.scrCoords[2] > 0) { 1632 factor = -1; 1633 } 1634 } 1635 } else { 1636 startPoint = this.slideObject.point1; 1637 1638 if (this.slideObject.point1.coords.scrCoords[1] - this.slideObject.point2.coords.scrCoords[1] > 0) { 1639 factor = -1; 1640 } else if (this.slideObject.point1.coords.scrCoords[1] - this.slideObject.point2.coords.scrCoords[1] === 0) { 1641 if (this.slideObject.point1.coords.scrCoords[2] - this.slideObject.point2.coords.scrCoords[2] > 0) { 1642 factor = -1; 1643 } 1644 } 1645 } 1646 1647 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [ 1648 startPoint.coords.scrCoords[1] + factor * dX, 1649 startPoint.coords.scrCoords[2] + factor * dY 1650 ]); 1651 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CURVE) { 1652 if (direction > 0) { 1653 newX = Math.round(this.intervalCount / stepCount * this.board.canvasWidth); 1654 } else { 1655 newX = Math.round((stepCount - this.intervalCount) / stepCount * this.board.canvasWidth); 1656 } 1657 1658 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [newX, 0]); 1659 this.coords = Geometry.projectPointToCurve(this, this.slideObject, this.board); 1660 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1661 if (direction < 0) { 1662 alpha = this.intervalCount / stepCount * 2 * Math.PI; 1663 } else { 1664 alpha = (stepCount - this.intervalCount) / stepCount * 2 * Math.PI; 1665 } 1666 1667 radius = this.slideObject.Radius(); 1668 1669 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 1670 this.slideObject.center.coords.usrCoords[1] + radius * Math.cos(alpha), 1671 this.slideObject.center.coords.usrCoords[2] + radius * Math.sin(alpha) 1672 ]); 1673 } 1674 1675 this.board.update(this); 1676 return this; 1677 }, 1678 1679 // documented in GeometryElement 1680 getTextAnchor: function () { 1681 return this.coords; 1682 }, 1683 1684 // documented in GeometryElement 1685 getLabelAnchor: function () { 1686 return this.coords; 1687 }, 1688 1689 // documented in element.js 1690 getParents: function () { 1691 var p = [this.Z(), this.X(), this.Y()]; 1692 1693 if (this.parents.length !== 0) { 1694 p = this.parents; 1695 } 1696 1697 if (this.type === Const.OBJECT_TYPE_GLIDER) { 1698 p = [this.X(), this.Y(), this.slideObject.id]; 1699 } 1700 1701 return p; 1702 } 1703 1704 }); 1705 1706 /** 1707 * Generic method to create point, text or image. 1708 * Determines the type of the construction, i.e. free, or constrained by function, 1709 * transformation or of glider type. 1710 * @param{Object} Callback Object type, e.g. JXG.Point, JXG.Text or JXG.Image 1711 * @param{Object} board Link to the board object 1712 * @param{Array} coords Array with coordinates. This may be: array of numbers, function 1713 * returning an array of numbers, array of functions returning a number, object and transformation. 1714 * If the attribute "slideObject" exists, a glider element is constructed. 1715 * @param{Object} attr Attributes object 1716 * @param{Object} arg1 Optional argument 1: in case of text this is the text content, 1717 * in case of an image this is the url. 1718 * @param{Array} arg2 Optional argument 2: in case of image this is an array containing the size of 1719 * the image. 1720 * @returns{Object} returns the created object or false. 1721 */ 1722 JXG.CoordsElement.create = function (Callback, board, coords, attr, arg1, arg2) { 1723 var el, isConstrained = false, i; 1724 1725 for (i = 0; i < coords.length; i++) { 1726 if (Type.isFunction(coords[i]) || Type.isString(coords[i])) { 1727 isConstrained = true; 1728 } 1729 } 1730 1731 if (!isConstrained) { 1732 if (Type.isNumber(coords[0]) && Type.isNumber(coords[1])) { 1733 el = new Callback(board, coords, attr, arg1, arg2); 1734 1735 if (Type.exists(attr.slideobject)) { 1736 el.makeGlider(attr.slideobject); 1737 } else { 1738 // Free element 1739 el.baseElement = el; 1740 } 1741 el.isDraggable = true; 1742 } else if (Type.isObject(coords[0]) && 1743 (Type.isObject(coords[1]) || // Transformation 1744 (Type.isArray(coords[1]) && coords[1].length > 0 && Type.isObject(coords[1][0])) 1745 )) { // Array of transformations 1746 1747 // Transformation 1748 el = new Callback(board, [0, 0], attr, arg1, arg2); 1749 el.addTransform(coords[0], coords[1]); 1750 el.isDraggable = false; 1751 } else { 1752 return false; 1753 } 1754 } else { 1755 el = new Callback(board, [0, 0], attr, arg1, arg2); 1756 el.addConstraint(coords); 1757 } 1758 1759 el.handleSnapToGrid(); 1760 el.handleSnapToPoints(); 1761 el.handleAttractors(); 1762 1763 el.addParents(coords); 1764 return el; 1765 }; 1766 1767 return JXG.CoordsElement; 1768 1769 }); 1770