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