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