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