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