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