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