1 /* 2 Copyright 2008-2017 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 (!Type.evaluate(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 ev_sw, ev_sel, 245 slide = this.slideObject; 246 247 this.needsUpdateFromParent = false; 248 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 249 if (Type.evaluate(this.visProp.isgeonext)) { 250 delta = 1.0; 251 } 252 //this.coords.setCoordinates(Const.COORDS_BY_USER, 253 // Geometry.projectPointToCircle(this, slide, this.board).usrCoords, false); 254 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 255 newPos = Geometry.rad([slide.center.X() + 1.0, slide.center.Y()], slide.center, this) / delta; 256 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 257 /* 258 * onPolygon==true: the point is a slider on a segment and this segment is one of the 259 * "borders" of a polygon. 260 * This is a GEONExT feature. 261 */ 262 if (this.onPolygon) { 263 p1c = slide.point1.coords.usrCoords; 264 p2c = slide.point2.coords.usrCoords; 265 i = 1; 266 d = p2c[i] - p1c[i]; 267 268 if (Math.abs(d) < Mat.eps) { 269 i = 2; 270 d = p2c[i] - p1c[i]; 271 } 272 273 cc = Geometry.projectPointToLine(this, slide, this.board); 274 pos = (cc.usrCoords[i] - p1c[i]) / d; 275 poly = slide.parentPolygon; 276 277 if (pos < 0) { 278 for (i = 0; i < poly.borders.length; i++) { 279 if (slide === poly.borders[i]) { 280 slide = poly.borders[(i - 1 + poly.borders.length) % poly.borders.length]; 281 break; 282 } 283 } 284 } else if (pos > 1.0) { 285 for (i = 0; i < poly.borders.length; i++) { 286 if (slide === poly.borders[i]) { 287 slide = poly.borders[(i + 1 + poly.borders.length) % poly.borders.length]; 288 break; 289 } 290 } 291 } 292 293 // If the slide object has changed, save the change to the glider. 294 if (slide.id !== this.slideObject.id) { 295 this.slideObject = slide; 296 } 297 } 298 299 p1c = slide.point1.coords; 300 p2c = slide.point2.coords; 301 302 // Distance between the two defining points 303 d = p1c.distance(Const.COORDS_BY_USER, p2c); 304 305 // The defining points are identical 306 if (d < Mat.eps) { 307 //this.coords.setCoordinates(Const.COORDS_BY_USER, p1c); 308 newCoords = p1c; 309 doRound = true; 310 newPos = 0.0; 311 } else { 312 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToLine(this, slide, this.board).usrCoords, false); 313 newCoords = Geometry.projectPointToLine(this, slide, this.board); 314 p1c = p1c.usrCoords.slice(0); 315 p2c = p2c.usrCoords.slice(0); 316 317 // The second point is an ideal point 318 if (Math.abs(p2c[0]) < Mat.eps) { 319 i = 1; 320 d = p2c[i]; 321 322 if (Math.abs(d) < Mat.eps) { 323 i = 2; 324 d = p2c[i]; 325 } 326 327 d = (newCoords.usrCoords[i] - p1c[i]) / d; 328 sgn = (d >= 0) ? 1 : -1; 329 d = Math.abs(d); 330 newPos = sgn * d / (d + 1); 331 332 // The first point is an ideal point 333 } else if (Math.abs(p1c[0]) < Mat.eps) { 334 i = 1; 335 d = p1c[i]; 336 337 if (Math.abs(d) < Mat.eps) { 338 i = 2; 339 d = p1c[i]; 340 } 341 342 d = (newCoords.usrCoords[i] - p2c[i]) / d; 343 344 // 1.0 - d/(1-d); 345 if (d < 0.0) { 346 newPos = (1 - 2.0 * d) / (1.0 - d); 347 } else { 348 newPos = 1 / (d + 1); 349 } 350 } else { 351 i = 1; 352 d = p2c[i] - p1c[i]; 353 354 if (Math.abs(d) < Mat.eps) { 355 i = 2; 356 d = p2c[i] - p1c[i]; 357 } 358 newPos = (newCoords.usrCoords[i] - p1c[i]) / d; 359 } 360 } 361 362 // Snap the glider point of the slider into its appropiate position 363 // First, recalculate the new value of this.position 364 // Second, call update(fromParent==true) to make the positioning snappier. 365 ev_sw = Type.evaluate(this.visProp.snapwidth); 366 if (Type.evaluate(ev_sw) > 0.0 && 367 Math.abs(this._smax - this._smin) >= Mat.eps) { 368 newPos = Math.max(Math.min(newPos, 1), 0); 369 370 v = newPos * (this._smax - this._smin) + this._smin; 371 v = Math.round(v / ev_sw) * ev_sw; 372 newPos = (v - this._smin) / (this._smax - this._smin); 373 this.update(true); 374 } 375 376 p1c = slide.point1.coords; 377 if (!Type.evaluate(slide.visProp.straightfirst) && 378 Math.abs(p1c.usrCoords[0]) > Mat.eps && newPos < 0) { 379 newCoords = p1c; 380 doRound = true; 381 newPos = 0; 382 } 383 384 p2c = slide.point2.coords; 385 if (!Type.evaluate(slide.visProp.straightlast) && 386 Math.abs(p2c.usrCoords[0]) > Mat.eps && newPos > 1) { 387 newCoords = p2c; 388 doRound = true; 389 newPos = 1; 390 } 391 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 392 // In case, the point is a constrained glider. 393 // side-effect: this.position is overwritten 394 this.updateConstraint(); 395 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToTurtle(this, slide, this.board).usrCoords, false); 396 newCoords = Geometry.projectPointToTurtle(this, slide, this.board); 397 newPos = this.position; // save position for the overwriting below 398 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 399 if ((slide.type === Const.OBJECT_TYPE_ARC || 400 slide.type === Const.OBJECT_TYPE_SECTOR)) { 401 newCoords = Geometry.projectPointToCircle(this, slide, this.board); 402 403 angle = Geometry.rad(slide.radiuspoint, slide.center, this); 404 alpha = 0.0; 405 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 406 newPos = angle; 407 408 ev_sw = Type.evaluate(slide.visProp.selection); 409 if ((ev_sw === 'minor' && beta > Math.PI) || 410 (ev_sw === 'major' && beta < Math.PI)) { 411 alpha = beta; 412 beta = 2 * Math.PI; 413 } 414 415 // Correct the position if we are outside of the sector/arc 416 if (angle < alpha || angle > beta) { 417 newPos = beta; 418 419 if ((angle < alpha && angle > alpha * 0.5) || (angle > beta && angle > beta * 0.5 + Math.PI)) { 420 newPos = alpha; 421 } 422 423 this.needsUpdateFromParent = true; 424 this.updateGliderFromParent(); 425 } 426 427 delta = beta - alpha; 428 if (this.visProp.isgeonext) { 429 delta = 1.0; 430 } 431 if (Math.abs(delta) > Mat.eps) { 432 newPos /= delta; 433 } 434 } else { 435 // In case, the point is a constrained glider. 436 this.updateConstraint(); 437 438 if (slide.transformations.length > 0) { 439 slide.updateTransformMatrix(); 440 invMat = Mat.inverse(slide.transformMat); 441 c = Mat.matVecMult(invMat, this.coords.usrCoords); 442 443 cp = (new Coords(Const.COORDS_BY_USER, c, this.board)).usrCoords; 444 c = Geometry.projectCoordsToCurve(cp[1], cp[2], this.position || 0, slide, this.board); 445 446 newCoords = c[0]; 447 newPos = c[1]; 448 } else { 449 // side-effect: this.position is overwritten 450 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToCurve(this, slide, this.board).usrCoords, false); 451 newCoords = Geometry.projectPointToCurve(this, slide, this.board); 452 newPos = this.position; // save position for the overwriting below 453 } 454 } 455 } else if (Type.isPoint(slide)) { 456 //this.coords.setCoordinates(Const.COORDS_BY_USER, Geometry.projectPointToPoint(this, slide, this.board).usrCoords, false); 457 newCoords = Geometry.projectPointToPoint(this, slide, this.board); 458 newPos = this.position; // save position for the overwriting below 459 } 460 461 this.coords.setCoordinates(Const.COORDS_BY_USER, newCoords.usrCoords, doRound); 462 this.position = newPos; 463 }, 464 465 /** 466 * Update of a glider in case a parent element has been updated. That means the 467 * relative position of the glider stays the same. 468 * @private 469 */ 470 updateGliderFromParent: function () { 471 var p1c, p2c, r, lbda, c, 472 slide = this.slideObject, 473 baseangle, alpha, angle, beta, 474 delta = 2.0 * Math.PI, 475 newPos; 476 477 if (!this.needsUpdateFromParent) { 478 this.needsUpdateFromParent = true; 479 return; 480 } 481 482 if (slide.elementClass === Const.OBJECT_CLASS_CIRCLE) { 483 r = slide.Radius(); 484 if (Type.evaluate(this.visProp.isgeonext)) { 485 delta = 1.0; 486 } 487 c = [ 488 slide.center.X() + r * Math.cos(this.position * delta), 489 slide.center.Y() + r * Math.sin(this.position * delta) 490 ]; 491 } else if (slide.elementClass === Const.OBJECT_CLASS_LINE) { 492 p1c = slide.point1.coords.usrCoords; 493 p2c = slide.point2.coords.usrCoords; 494 495 // If one of the defining points of the line does not exist, 496 // the glider should disappear 497 if ((p1c[0] === 0 && p1c[1] === 0 && p1c[2] === 0) || 498 (p2c[0] === 0 && p2c[1] === 0 && p2c[2] === 0)) { 499 c = [0, 0, 0]; 500 // The second point is an ideal point 501 } else if (Math.abs(p2c[0]) < Mat.eps) { 502 lbda = Math.min(Math.abs(this.position), 1 - Mat.eps); 503 lbda /= (1.0 - lbda); 504 505 if (this.position < 0) { 506 lbda = -lbda; 507 } 508 509 c = [ 510 p1c[0] + lbda * p2c[0], 511 p1c[1] + lbda * p2c[1], 512 p1c[2] + lbda * p2c[2] 513 ]; 514 // The first point is an ideal point 515 } else if (Math.abs(p1c[0]) < Mat.eps) { 516 lbda = Math.max(this.position, Mat.eps); 517 lbda = Math.min(lbda, 2 - Mat.eps); 518 519 if (lbda > 1) { 520 lbda = (lbda - 1) / (lbda - 2); 521 } else { 522 lbda = (1 - lbda) / lbda; 523 } 524 525 c = [ 526 p2c[0] + lbda * p1c[0], 527 p2c[1] + lbda * p1c[1], 528 p2c[2] + lbda * p1c[2] 529 ]; 530 } else { 531 lbda = this.position; 532 c = [ 533 p1c[0] + lbda * (p2c[0] - p1c[0]), 534 p1c[1] + lbda * (p2c[1] - p1c[1]), 535 p1c[2] + lbda * (p2c[2] - p1c[2]) 536 ]; 537 } 538 } else if (slide.type === Const.OBJECT_TYPE_TURTLE) { 539 this.coords.setCoordinates(Const.COORDS_BY_USER, [slide.Z(this.position), slide.X(this.position), slide.Y(this.position)]); 540 // In case, the point is a constrained glider. 541 // side-effect: this.position is overwritten: 542 this.updateConstraint(); 543 c = Geometry.projectPointToTurtle(this, slide, this.board).usrCoords; 544 } else if (slide.elementClass === Const.OBJECT_CLASS_CURVE) { 545 this.coords.setCoordinates(Const.COORDS_BY_USER, [slide.Z(this.position), slide.X(this.position), slide.Y(this.position)]); 546 547 if (slide.type === Const.OBJECT_TYPE_ARC || slide.type === Const.OBJECT_TYPE_SECTOR) { 548 baseangle = Geometry.rad([slide.center.X() + 1, slide.center.Y()], slide.center, slide.radiuspoint); 549 550 alpha = 0.0; 551 beta = Geometry.rad(slide.radiuspoint, slide.center, slide.anglepoint); 552 553 if ((slide.visProp.selection === 'minor' && beta > Math.PI) || 554 (slide.visProp.selection === 'major' && beta < Math.PI)) { 555 alpha = beta; 556 beta = 2 * Math.PI; 557 } 558 559 delta = beta - alpha; 560 if (ev_ig) { 561 delta = 1.0; 562 } 563 angle = this.position * delta; 564 565 // Correct the position if we are outside of the sector/arc 566 if (angle < alpha || angle > beta) { 567 angle = beta; 568 569 if ((angle < alpha && angle > alpha * 0.5) || 570 (angle > beta && angle > beta * 0.5 + Math.PI)) { 571 angle = alpha; 572 } 573 574 this.position = angle; 575 if (Math.abs(delta) > Mat.eps) { 576 this.position /= delta; 577 } 578 } 579 580 r = slide.Radius(); 581 c = [ 582 slide.center.X() + r * Math.cos(this.position * delta + baseangle), 583 slide.center.Y() + r * Math.sin(this.position * delta + baseangle) 584 ]; 585 } else { 586 // In case, the point is a constrained glider. 587 // side-effect: this.position is overwritten 588 this.updateConstraint(); 589 c = Geometry.projectPointToCurve(this, slide, this.board).usrCoords; 590 } 591 592 } else if (Type.isPoint(slide)) { 593 c = Geometry.projectPointToPoint(this, slide, this.board).usrCoords; 594 } 595 596 this.coords.setCoordinates(Const.COORDS_BY_USER, c, false); 597 }, 598 599 updateRendererGeneric: function (rendererMethod) { 600 //var wasReal; 601 602 if (!this.needsUpdate) { 603 return this; 604 } 605 606 if (this.visPropCalc.visible) { 607 //wasReal = this.isReal; 608 this.isReal = (!isNaN(this.coords.usrCoords[1] + this.coords.usrCoords[2])); 609 //Homogeneous coords: ideal point 610 this.isReal = (Math.abs(this.coords.usrCoords[0]) > Mat.eps) ? this.isReal : false; 611 612 if (/*wasReal &&*/ !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 || (Math.abs(where.usrCoords[0] - this.coords.usrCoords[0]) > Mat.eps)) { 1530 this.setPosition(Const.COORDS_BY_USER, where.usrCoords); 1531 return this.board.update(this); 1532 } 1533 1534 // In case there is no callback and we are already at the endpoint we can stop here 1535 if (!Type.exists(options.callback) && Math.abs(dX) < Mat.eps && Math.abs(dY) < Mat.eps) { 1536 return this; 1537 } 1538 1539 for (i = steps; i >= 0; i--) { 1540 coords[steps - i] = [where.usrCoords[0], X + dX * stepFun(i), Y + dY * stepFun(i)]; 1541 } 1542 1543 this.animationPath = coords; 1544 this.animationCallback = options.callback; 1545 this.board.addAnimation(this); 1546 1547 return this; 1548 }, 1549 1550 /** 1551 * Starts an animated point movement towards the given coordinates <tt>where</tt>. After arriving at 1552 * <tt>where</tt> the point moves back to where it started. The animation is done after <tt>time</tt> 1553 * milliseconds. 1554 * @param {Array} where Array containing the x and y coordinate of the target location. 1555 * @param {Number} time Number of milliseconds the animation should last. 1556 * @param {Object} [options] Optional settings for the animation 1557 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 1558 * @param {String} [options.effect='<>'] animation effects like speed fade in and out. possible values are 1559 * '<>' for speed increase on start and slow down at the end (default) and '--' for constant speed during 1560 * the whole animation. 1561 * @param {Number} [options.repeat=1] How often this animation should be repeated. 1562 * @returns {JXG.Point} Reference to itself. 1563 * @see #animate 1564 */ 1565 visit: function (where, time, options) { 1566 where = new Coords(Const.COORDS_BY_USER, where, this.board); 1567 1568 var i, j, steps, 1569 delay = this.board.attr.animationdelay, 1570 coords = [], 1571 X = this.coords.usrCoords[1], 1572 Y = this.coords.usrCoords[2], 1573 dX = (where.usrCoords[1] - X), 1574 dY = (where.usrCoords[2] - Y), 1575 1576 /** @ignore */ 1577 stepFun = function (i) { 1578 var x = (i < steps / 2 ? 2 * i / steps : 2 * (steps - i) / steps); 1579 1580 if (options.effect && options.effect === '<>') { 1581 return Math.pow(Math.sin(x * Math.PI / 2), 2); 1582 } 1583 1584 return x; 1585 }; 1586 1587 // support legacy interface where the third parameter was the number of repeats 1588 if (Type.isNumber(options)) { 1589 options = {repeat: options}; 1590 } else { 1591 options = options || {}; 1592 if (!Type.exists(options.repeat)) { 1593 options.repeat = 1; 1594 } 1595 } 1596 1597 steps = Math.ceil(time / (delay * options.repeat)); 1598 1599 for (j = 0; j < options.repeat; j++) { 1600 for (i = steps; i >= 0; i--) { 1601 coords[j * (steps + 1) + steps - i] = [where.usrCoords[0], X + dX * stepFun(i), Y + dY * stepFun(i)]; 1602 } 1603 } 1604 this.animationPath = coords; 1605 this.animationCallback = options.callback; 1606 this.board.addAnimation(this); 1607 1608 return this; 1609 }, 1610 1611 /** 1612 * Animates a glider. Is called by the browser after startAnimation is called. 1613 * @param {Number} direction The direction the glider is animated. 1614 * @param {Number} stepCount The number of steps. 1615 * @see #startAnimation 1616 * @see #stopAnimation 1617 * @private 1618 */ 1619 _anim: function (direction, stepCount) { 1620 var distance, slope, dX, dY, alpha, startPoint, newX, radius, 1621 factor = 1; 1622 1623 this.intervalCount += 1; 1624 if (this.intervalCount > stepCount) { 1625 this.intervalCount = 0; 1626 } 1627 1628 if (this.slideObject.elementClass === Const.OBJECT_CLASS_LINE) { 1629 distance = this.slideObject.point1.coords.distance(Const.COORDS_BY_SCREEN, this.slideObject.point2.coords); 1630 slope = this.slideObject.getSlope(); 1631 if (slope !== Infinity) { 1632 alpha = Math.atan(slope); 1633 dX = Math.round((this.intervalCount / stepCount) * distance * Math.cos(alpha)); 1634 dY = Math.round((this.intervalCount / stepCount) * distance * Math.sin(alpha)); 1635 } else { 1636 dX = 0; 1637 dY = Math.round((this.intervalCount / stepCount) * distance); 1638 } 1639 1640 if (direction < 0) { 1641 startPoint = this.slideObject.point2; 1642 1643 if (this.slideObject.point2.coords.scrCoords[1] - this.slideObject.point1.coords.scrCoords[1] > 0) { 1644 factor = -1; 1645 } else if (this.slideObject.point2.coords.scrCoords[1] - this.slideObject.point1.coords.scrCoords[1] === 0) { 1646 if (this.slideObject.point2.coords.scrCoords[2] - this.slideObject.point1.coords.scrCoords[2] > 0) { 1647 factor = -1; 1648 } 1649 } 1650 } else { 1651 startPoint = this.slideObject.point1; 1652 1653 if (this.slideObject.point1.coords.scrCoords[1] - this.slideObject.point2.coords.scrCoords[1] > 0) { 1654 factor = -1; 1655 } else if (this.slideObject.point1.coords.scrCoords[1] - this.slideObject.point2.coords.scrCoords[1] === 0) { 1656 if (this.slideObject.point1.coords.scrCoords[2] - this.slideObject.point2.coords.scrCoords[2] > 0) { 1657 factor = -1; 1658 } 1659 } 1660 } 1661 1662 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [ 1663 startPoint.coords.scrCoords[1] + factor * dX, 1664 startPoint.coords.scrCoords[2] + factor * dY 1665 ]); 1666 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CURVE) { 1667 if (direction > 0) { 1668 newX = Math.round(this.intervalCount / stepCount * this.board.canvasWidth); 1669 } else { 1670 newX = Math.round((stepCount - this.intervalCount) / stepCount * this.board.canvasWidth); 1671 } 1672 1673 this.coords.setCoordinates(Const.COORDS_BY_SCREEN, [newX, 0]); 1674 this.coords = Geometry.projectPointToCurve(this, this.slideObject, this.board); 1675 } else if (this.slideObject.elementClass === Const.OBJECT_CLASS_CIRCLE) { 1676 if (direction < 0) { 1677 alpha = this.intervalCount / stepCount * 2 * Math.PI; 1678 } else { 1679 alpha = (stepCount - this.intervalCount) / stepCount * 2 * Math.PI; 1680 } 1681 1682 radius = this.slideObject.Radius(); 1683 1684 this.coords.setCoordinates(Const.COORDS_BY_USER, [ 1685 this.slideObject.center.coords.usrCoords[1] + radius * Math.cos(alpha), 1686 this.slideObject.center.coords.usrCoords[2] + radius * Math.sin(alpha) 1687 ]); 1688 } 1689 1690 this.board.update(this); 1691 return this; 1692 }, 1693 1694 // documented in GeometryElement 1695 getTextAnchor: function () { 1696 return this.coords; 1697 }, 1698 1699 // documented in GeometryElement 1700 getLabelAnchor: function () { 1701 return this.coords; 1702 }, 1703 1704 // documented in element.js 1705 getParents: function () { 1706 var p = [this.Z(), this.X(), this.Y()]; 1707 1708 if (this.parents.length !== 0) { 1709 p = this.parents; 1710 } 1711 1712 if (this.type === Const.OBJECT_TYPE_GLIDER) { 1713 p = [this.X(), this.Y(), this.slideObject.id]; 1714 } 1715 1716 return p; 1717 } 1718 1719 }); 1720 1721 /** 1722 * Generic method to create point, text or image. 1723 * Determines the type of the construction, i.e. free, or constrained by function, 1724 * transformation or of glider type. 1725 * @param{Object} Callback Object type, e.g. JXG.Point, JXG.Text or JXG.Image 1726 * @param{Object} board Link to the board object 1727 * @param{Array} coords Array with coordinates. This may be: array of numbers, function 1728 * returning an array of numbers, array of functions returning a number, object and transformation. 1729 * If the attribute "slideObject" exists, a glider element is constructed. 1730 * @param{Object} attr Attributes object 1731 * @param{Object} arg1 Optional argument 1: in case of text this is the text content, 1732 * in case of an image this is the url. 1733 * @param{Array} arg2 Optional argument 2: in case of image this is an array containing the size of 1734 * the image. 1735 * @returns{Object} returns the created object or false. 1736 */ 1737 JXG.CoordsElement.create = function (Callback, board, coords, attr, arg1, arg2) { 1738 var el, isConstrained = false, i; 1739 1740 for (i = 0; i < coords.length; i++) { 1741 if (Type.isFunction(coords[i]) || Type.isString(coords[i])) { 1742 isConstrained = true; 1743 } 1744 } 1745 1746 if (!isConstrained) { 1747 if (Type.isNumber(coords[0]) && Type.isNumber(coords[1])) { 1748 el = new Callback(board, coords, attr, arg1, arg2); 1749 1750 if (Type.exists(attr.slideobject)) { 1751 el.makeGlider(attr.slideobject); 1752 } else { 1753 // Free element 1754 el.baseElement = el; 1755 } 1756 el.isDraggable = true; 1757 } else if (Type.isObject(coords[0]) && 1758 (Type.isObject(coords[1]) || // Transformation 1759 (Type.isArray(coords[1]) && coords[1].length > 0 && Type.isObject(coords[1][0])) 1760 )) { // Array of transformations 1761 1762 // Transformation 1763 el = new Callback(board, [0, 0], attr, arg1, arg2); 1764 el.addTransform(coords[0], coords[1]); 1765 el.isDraggable = false; 1766 } else { 1767 return false; 1768 } 1769 } else { 1770 el = new Callback(board, [0, 0], attr, arg1, arg2); 1771 el.addConstraint(coords); 1772 } 1773 1774 el.handleSnapToGrid(); 1775 el.handleSnapToPoints(); 1776 el.handleAttractors(); 1777 1778 el.addParents(coords); 1779 return el; 1780 }; 1781 1782 return JXG.CoordsElement; 1783 1784 }); 1785