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