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