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