      1 /*
      2     Copyright 2008-2015
      3         Matthias Ehmann,
      4         Michael Gerhaeuser,
      5         Carsten Miller,
      6         Bianca Valentin,
      7         Alfred Wassermann,
      8         Peter Wilfahrt
      9
     10     This file is part of JSXGraph.
     11
     12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     13
     14     You can redistribute it and/or modify it under the terms of the
     15
     16       * GNU Lesser General Public License as published by
     17         the Free Software Foundation, either version 3 of the License, or
     18         (at your option) any later version
     19       OR
     20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     21
     22     JSXGraph is distributed in the hope that it will be useful,
     23     but WITHOUT ANY WARRANTY; without even the implied warranty of
     24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     25     GNU Lesser General Public License for more details.
     26
     27     You should have received a copy of the GNU Lesser General Public License and
     28     the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/>
     29     and <http://opensource.org/licenses/MIT/>.
     30  */
     31
     32
     33 /*global JXG: true, define: true*/
     34 /*jslint nomen: true, plusplus: true*/
     35
     36 /* depends:
     37  jxg
     38  math/math
     39  math/geometry
     40  base/constants
     41  base/element
     42  base/coords
     43  utils/type
     44   elements:
     45    text
     46  */
     47
     48 /**
     49  * @fileoverview In this file the geometry object Ticks is defined. Ticks provides
     50  * methods for creation and management of ticks on an axis.
     51  * @author graphjs
     52  * @version 0.1
     53  */
     54
     55 define([
     56     'jxg', 'math/math', 'math/geometry', 'base/constants', 'base/element', 'base/coords', 'utils/type', 'base/text'
     57 ], function (JXG, Mat, Geometry, Const, GeometryElement, Coords, Type, Text) {
     58
     59     "use strict";
     60
     61     /**
     62      * Creates ticks for an axis.
     63      * @class Ticks provides methods for creation and management
     64      * of ticks on an axis.
     65      * @param {JXG.Line} line Reference to the axis the ticks are drawn on.
     66      * @param {Number|Array} ticks Number defining the distance between two major ticks or an array defining static ticks.
     67      * @param {Object} attributes Properties
     68      * @see JXG.Line#addTicks
     69      * @constructor
     70      * @extends JXG.GeometryElement
     71      */
     72     JXG.Ticks = function (line, ticks, attributes) {
     73         this.constructor(line.board, attributes, Const.OBJECT_TYPE_TICKS, Const.OBJECT_CLASS_OTHER);
     74
     75         /**
     76          * The line the ticks belong to.
     77          * @type JXG.Line
     78          */
     79         this.line = line;
     80
     81         /**
     82          * The board the ticks line is drawn on.
     83          * @type JXG.Board
     84          */
     85         this.board = this.line.board;
     86
     87         /**
     88          * A function calculating ticks delta depending on the ticks number.
     89          * @type Function
     90          */
     91         this.ticksFunction = null;
     92
     93         /**
     94          * Array of fixed ticks.
     95          * @type Array
     96          */
     97         this.fixedTicks = null;
     98
     99         /**
    100          * Equidistant ticks. Distance is defined by ticksFunction
    101          * @type Boolean
    102          */
    103         this.equidistant = false;
    104
    105         if (Type.isFunction(ticks)) {
    106             this.ticksFunction = ticks;
    107             throw new Error("Function arguments are no longer supported.");
    108         } else if (Type.isArray(ticks)) {
    109             this.fixedTicks = ticks;
    110         } else {
    111             if (Math.abs(ticks) < Mat.eps || ticks < 0) {
    112                 ticks = attributes.defaultdistance;
    113             }
    114
    115             /*
    116              * Ticks function:
    117              * determines the distance (in user units) of two major ticks
    118              */
    119             this.ticksFunction = function () {
    120                 var delta, b, dist;
    121
    122                 if (this.visProp.insertticks) {
    123                     b = this.getLowerAndUpperBounds(this.getZeroCoordinates(), 'ticksdistance');
    124                     dist = b.upper - b.lower;
    125                     delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10));
    126                     if (dist <= 6 * delta) {
    127                         delta *= 0.5;
    128                     }
    129                     return delta;
    130                 } else {
    131                     // upto 0.99.1
    132                     return ticks;
    133                 }
    134             };
    135
    136             this.equidistant = true;
    137         }
    138
    139         /**
    140          * Least distance between two ticks, measured in pixels.
    141          * @type int
    142          */
    143         this.minTicksDistance = attributes.minticksdistance;
    144
    145         /**
    146          * Stores the ticks coordinates
    147          * @type {Array}
    148          */
    149         this.ticks = [];
    150
    151         /**
    152          * Distance between two major ticks in user coordinates
    153          * @type {Number}
    154          */
    155         this.ticksDelta = 1;
    156
    157         /**
    158          * Array where the labels are saved. There is an array element for every tick,
    159          * even for minor ticks which don't have labels. In this case the array element
    160          * contains just <tt>null</tt>.
    161          * @type Array
    162          */
    163         this.labels = [];
    164
    165         /**
    166          * A list of labels that are currently unused and ready for reassignment.
    167          * @type {Array}
    168          */
    169         this.labelsRepo = [];
    170
    171         /**
    172          * To ensure the uniqueness of label ids this counter is used.
    173          * @type {number}
    174          */
    175         this.labelCounter = 0;
    176
    177         this.id = this.line.addTicks(this);
    178         this.board.setId(this, 'Ti');
    179     };
    180
    181     JXG.Ticks.prototype = new GeometryElement();
    182
    183     JXG.extend(JXG.Ticks.prototype, /** @lends JXG.Ticks.prototype */ {
    184         /**
    185          * Checks whether (x,y) is near the line.
    186          * @param {Number} x Coordinate in x direction, screen coordinates.
    187          * @param {Number} y Coordinate in y direction, screen coordinates.
    188          * @return {Boolean} True if (x,y) is near the line, False otherwise.
    189          */
    190         hasPoint: function (x, y) {
    191             var i, t,
    192                 len = (this.ticks && this.ticks.length) || 0,
    193                 r = this.board.options.precision.hasPoint;
    194
    195             if (!this.line.visProp.scalable) {
    196                 return false;
    197             }
    198
    199             // Ignore non-axes and axes that are not horizontal or vertical
    200             if (this.line.stdform[1] !== 0 && this.line.stdform[2] !== 0 && this.line.type !== Const.OBJECT_TYPE_AXIS) {
    201                 return false;
    202             }
    203
    204             for (i = 0; i < len; i++) {
    205                 t = this.ticks[i];
    206
    207                 // Skip minor ticks
    208                 if (t[2]) {
    209                     // Ignore ticks at zero
    210                     if (!((this.line.stdform[1] === 0 && Math.abs(t[0][0] - this.line.point1.coords.scrCoords[1]) < Mat.eps) ||
    211                             (this.line.stdform[2] === 0 && Math.abs(t[1][0] - this.line.point1.coords.scrCoords[2]) < Mat.eps))) {
    212                         // tick length is not zero, ie. at least one pixel
    213                         if (Math.abs(t[0][0] - t[0][1]) >= 1 || Math.abs(t[1][0] - t[1][1]) >= 1) {
    214                             if (this.line.stdform[1] === 0) {
    215                                 // Allow dragging near axes only.
    216                                 if (Math.abs(y - (t[1][0] + t[1][1]) * 0.5) < 2 * r && t[0][0] - r < x && x < t[0][1] + r) {
    217                                     return true;
    218                                 }
    219                             } else if (this.line.stdform[2] === 0) {
    220                                 if (Math.abs(x - (t[0][0] + t[0][1]) * 0.5) < 2 * r && t[1][0] - r < y && y < t[1][1] + r) {
    221                                     return true;
    222                                 }
    223                             }
    224                         }
    225                     }
    226                 }
    227             }
    228
    229             return false;
    230         },
    231
    232         /**
    233          * Sets x and y coordinate of the tick.
    234          * @param {number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}.
    235          * @param {Array} coords coordinates in screen/user units
    236          * @param {Array} oldcoords previous coordinates in screen/user units
    237          * @returns {JXG.Ticks} this element
    238          */
    239         setPositionDirectly: function (method, coords, oldcoords) {
    240             var dx, dy, i,
    241                 c = new Coords(method, coords, this.board),
    242                 oldc = new Coords(method, oldcoords, this.board),
    243                 bb = this.board.getBoundingBox();
    244
    245             if (!this.line.visProp.scalable) {
    246                 return this;
    247             }
    248
    249             // horizontal line
    250             if (Math.abs(this.line.stdform[1]) < Mat.eps && Math.abs(c.usrCoords[1] * oldc.usrCoords[1]) > Mat.eps) {
    251                 dx = oldc.usrCoords[1] / c.usrCoords[1];
    252                 bb[0] *= dx;
    253                 bb[2] *= dx;
    254                 this.board.setBoundingBox(bb, false);
    255             // vertical line
    256             } else if (Math.abs(this.line.stdform[2]) < Mat.eps && Math.abs(c.usrCoords[2] * oldc.usrCoords[2]) > Mat.eps) {
    257                 dy = oldc.usrCoords[2] / c.usrCoords[2];
    258                 bb[3] *= dy;
    259                 bb[1] *= dy;
    260                 this.board.setBoundingBox(bb, false);
    261             }
    262
    263             return this;
    264         },
    265
    266          /**
    267          * (Re-)calculates the ticks coordinates.
    268          * @private
    269          */
    270         calculateTicksCoordinates: function () {
    271             var coordsZero, bounds, i,
    272                 oldRepoLength = this.labelsRepo.length;
    273
    274             // Calculate Ticks width and height in Screen and User Coordinates
    275             this.setTicksSizeVariables();
    276             // If the parent line is not finite, we can stop here.
    277             if (Math.abs(this.dx) < Mat.eps && Math.abs(this.dy) < Mat.eps) {
    278                 return;
    279             }
    280
    281             // Get Zero
    282             coordsZero = this.getZeroCoordinates();
    283
    284             // Calculate lower bound and upper bound limits based on distance between p1 and centre and p2 and centre
    285             bounds = this.getLowerAndUpperBounds(coordsZero);
    286
    287             // Clean up
    288             this.removeTickLabels();
    289             this.ticks = [];
    290             this.labels = [];
    291
    292             // Create Ticks Coordinates and Labels
    293             if (this.equidistant) {
    294                 this.generateEquidistantTicks(coordsZero, bounds);
    295             } else {
    296                 this.generateFixedTicks(coordsZero, bounds);
    297             }
    298
    299             // Hide unused labels in labelsRepo
    300             for (i = oldRepoLength; i < this.labelsRepo.length; i++) {
    301                 this.labelsRepo[i].setAttribute({visible: false});
    302             }
    303         },
    304
    305         /**
    306          * Sets the variables used to set the height and slope of each tick.
    307          *
    308          * @private
    309          */
    310         setTicksSizeVariables: function () {
    311             var d,
    312                 distMaj = this.visProp.majorheight * 0.5,
    313                 distMin = this.visProp.minorheight * 0.5;
    314
    315             // ticks width and height in screen units
    316             this.dxMaj = this.line.stdform[1];
    317             this.dyMaj = this.line.stdform[2];
    318             this.dxMin = this.dxMaj;
    319             this.dyMin = this.dyMaj;
    320
    321             // ticks width and height in user units
    322             this.dx = this.dxMaj;
    323             this.dy = this.dyMaj;
    324
    325             // After this, the length of the vector (dxMaj, dyMaj) in screen coordinates is equal to distMaj pixel.
    326             d = Math.sqrt(
    327                 this.dxMaj * this.dxMaj * this.board.unitX * this.board.unitX +
    328                     this.dyMaj * this.dyMaj * this.board.unitY * this.board.unitY
    329             );
    330             this.dxMaj *= distMaj / d * this.board.unitX;
    331             this.dyMaj *= distMaj / d * this.board.unitY;
    332             this.dxMin *= distMin / d * this.board.unitX;
    333             this.dyMin *= distMin / d * this.board.unitY;
    334
    335             // Grid-like ticks?
    336             this.minStyle = 'finite';
    337             if (this.visProp.minorheight < 0) {
    338                 this.minStyle = 'infinite';
    339             }
    340
    341             this.majStyle = 'finite';
    342             if (this.visProp.majorheight < 0) {
    343                 this.majStyle = 'infinite';
    344             }
    345         },
    346
    347         /**
    348          * Returns the coordinates of the point zero of the line.
    349          *
    350          * If the line is an {@link Axis}, the coordinates of the projection of the board's zero point is returned
    351          *
    352          * Otherwise, the coordinates of the point that acts as zero are established depending on the value of {@link JXG.Ticks#anchor}
    353          *
    354          * @return {JXG.Coords} Coords object for the Zero point on the line
    355          * @private
    356          */
    357         getZeroCoordinates: function () {
    358             if (this.line.type === Const.OBJECT_TYPE_AXIS) {
    359                 return Geometry.projectPointToLine({
    360                     coords: {
    361                         usrCoords: [1, 0, 0]
    362                     }
    363                 }, this.line, this.board);
    364             }
    365
    366             if (this.visProp.anchor === 'right') {
    367                 return this.line.point2.coords;
    368             }
    369
    370             if (this.visProp.anchor === 'middle') {
    371                 return new Coords(Const.COORDS_BY_USER, [
    372                     (this.line.point1.coords.usrCoords[1] + this.line.point2.coords.usrCoords[1]) / 2,
    373                     (this.line.point1.coords.usrCoords[2] + this.line.point2.coords.usrCoords[2]) / 2
    374                 ], this.board);
    375             }
    376
    377             return this.line.point1.coords;
    378         },
    379
    380         /**
    381          * Calculate the lower and upper bounds for tick rendering
    382          * If {@link JXG.Ticks#includeBoundaries} is false, the boundaries will exclude point1 and point2
    383          *
    384          * @param  {JXG.Coords} coordsZero
    385          * @return {String} type  (Optional) If type=='ticksdistance' the bounds are the intersection of the line with the bounding box of the board.
    386          *              Otherwise it is the projection of the corners of the bounding box to the line. The first case i s needed to automatically
    387          *              generate ticks. The second case is for drawing of the ticks.
    388          * @return {Object}     contains the lower and upper bounds
    389          *
    390          * @private
    391          */
    392         getLowerAndUpperBounds: function (coordsZero, type) {
    393             var lowerBound, upperBound,
    394                 // The line's defining points that will be adjusted to be within the board limits
    395                 point1 = new Coords(Const.COORDS_BY_USER, this.line.point1.coords.usrCoords, this.board),
    396                 point2 = new Coords(Const.COORDS_BY_USER, this.line.point2.coords.usrCoords, this.board),
    397                 // Are the original defining points within the board?
    398                 isPoint1inBoard = (Math.abs(point1.usrCoords[0]) >= Mat.eps &&
    399                     point1.scrCoords[1] >= 0.0 && point1.scrCoords[1] <= this.board.canvasWidth &&
    400                     point1.scrCoords[2] >= 0.0 && point1.scrCoords[2] <= this.board.canvasHeight),
    401                 isPoint2inBoard = (Math.abs(point2.usrCoords[0]) >= Mat.eps &&
    402                     point2.scrCoords[1] >= 0.0 && point2.scrCoords[1] <= this.board.canvasWidth &&
    403                     point2.scrCoords[2] >= 0.0 && point2.scrCoords[2] <= this.board.canvasHeight),
    404                 // We use the distance from zero to P1 and P2 to establish lower and higher points
    405                 dZeroPoint1, dZeroPoint2;
    406
    407             // Adjust line limit points to be within the board
    408             if (JXG.exists(type) || type === 'tickdistance') {
    409                 // The good old calcStraight is needed for determining the distance between major ticks.
    410                 // Here, only the visual area is of importance
    411                 Geometry.calcStraight(this.line, point1, point2);
    412             } else {
    413                 // This function projects the corners of the board to the line.
    414                 // This is important for diagonal lines with infinite tick lines.
    415                 Geometry.calcLineDelimitingPoints(this.line, point1, point2);
    416             }
    417
    418             // Calculate distance from Zero to P1 and to P2
    419             dZeroPoint1 = this.getDistanceFromZero(coordsZero, point1);
    420             dZeroPoint2 = this.getDistanceFromZero(coordsZero, point2);
    421
    422             // We have to establish if the direction is P1->P2 or P2->P1 to set the lower and upper
    423             // boundaries appropriately. As the distances contain also a sign to indicate direction,
    424             // we can compare dZeroPoint1 and dZeroPoint2 to establish the line direction
    425             if (dZeroPoint1 < dZeroPoint2) { // Line goes P1->P2
    426                 lowerBound = dZeroPoint1;
    427                 if (!this.line.visProp.straightfirst && isPoint1inBoard && !this.visProp.includeboundaries) {
    428                     lowerBound += Mat.eps;
    429                 }
    430                 upperBound = dZeroPoint2;
    431                 if (!this.line.visProp.straightlast && isPoint2inBoard && !this.visProp.includeboundaries) {
    432                     upperBound -= Mat.eps;
    433                 }
    434             } else if (dZeroPoint2 < dZeroPoint1) { // Line goes P2->P1
    435                 lowerBound = dZeroPoint2;
    436                 if (!this.line.visProp.straightlast && isPoint2inBoard && !this.visProp.includeboundaries) {
    437                     lowerBound += Mat.eps;
    438                 }
    439                 upperBound = dZeroPoint1;
    440                 if (!this.line.visProp.straightfirst && isPoint1inBoard && !this.visProp.includeboundaries) {
    441                     upperBound -= Mat.eps;
    442                 }
    443             } else { // P1 = P2 = Zero, we can't do a thing
    444                 lowerBound = 0;
    445                 upperBound = 0;
    446             }
    447
    448             return {
    449                 lower: lowerBound,
    450                 upper: upperBound
    451             };
    452         },
    453
    454         /**
    455          * Calculates the distance in user coordinates from zero to a given point including its sign
    456          *
    457          * @param  {JXG.Coords} zero  coordinates of the point considered zero
    458          * @param  {JXG.Coords} point coordinates of the point to find out the distance
    459          * @return {Number}           distance between zero and point, including its sign
    460          * @private
    461          */
    462         getDistanceFromZero: function (zero, point) {
    463             var eps = Mat.eps * Mat.eps,
    464                 distance = zero.distance(Const.COORDS_BY_USER, point);
    465
    466             // Establish sign
    467             if (this.line.type === Const.OBJECT_TYPE_AXIS) {
    468                 if (zero.usrCoords[1] - point.usrCoords[1] > eps ||
    469                         (Math.abs(zero.usrCoords[1] - point.usrCoords[1]) < eps &&
    470                         zero.usrCoords[2] - point.usrCoords[2] > eps)) {
    471                     distance *= -1;
    472                 }
    473             } else if (this.visProp.anchor === 'right') {
    474                 if (Geometry.isSameDirection(zero, this.line.point1.coords, point)) {
    475                     distance *= -1;
    476                 }
    477             } else {
    478                 if (!Geometry.isSameDirection(zero, this.line.point2.coords, point)) {
    479                     distance *= -1;
    480                 }
    481             }
    482             return distance;
    483         },
    484
    485         /**
    486          * Creates ticks coordinates and labels automatically.
    487          * The frequency of ticks is affected by the values of {@link JXG.Ticks#insertTicks} and {@link JXG.Ticks#ticksDistance}
    488          *
    489          * @param  {JXG.Coords} coordsZero coordinates of the point considered zero
    490          * @param  {Object}     bounds     contains the lower and upper boundaries for ticks placement
    491          * @private
    492          */
    493         generateEquidistantTicks: function (coordsZero, bounds) {
    494             var tickPosition,
    495                 // Point 1 of the line
    496                 p1 = this.line.point1,
    497                 // Point 2 of the line
    498                 p2 = this.line.point2,
    499                 // Calculate X and Y distance between two major ticks
    500                 deltas = this.getXandYdeltas(),
    501                 // Distance between two major ticks in user coordinates
    502                 ticksDelta = (this.equidistant ? this.ticksFunction(1) : this.ticksDelta);
    503
    504             // adjust ticks distance
    505             ticksDelta *= this.visProp.scale;
    506             if (this.visProp.insertticks && this.minTicksDistance > Mat.eps) {
    507                 ticksDelta = this.adjustTickDistance(ticksDelta, coordsZero, deltas);
    508                 ticksDelta /= (this.visProp.minorticks + 1);
    509             } else if (!this.visProp.insertticks) {
    510                 ticksDelta /= (this.visProp.minorticks + 1);
    511             }
    512             this.ticksDelta = ticksDelta;
    513
    514             // Position ticks from zero to the positive side while not reaching the upper boundary
    515             tickPosition = 0;
    516             if (!this.visProp.drawzero) {
    517                 tickPosition = ticksDelta;
    518             }
    519             while (tickPosition <= bounds.upper) {
    520                 // Only draw ticks when we are within bounds, ignore case where  tickPosition < lower < upper
    521                 if (tickPosition >= bounds.lower) {
    522                     this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas);
    523                 }
    524                 tickPosition += ticksDelta;
    525             }
    526
    527             // Position ticks from zero (not inclusive) to the negative side while not reaching the lower boundary
    528             tickPosition = -ticksDelta;
    529             while (tickPosition >= bounds.lower) {
    530                 // Only draw ticks when we are within bounds, ignore case where lower < upper < tickPosition
    531                 if (tickPosition <= bounds.upper) {
    532                     this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas);
    533                 }
    534                 tickPosition -= ticksDelta;
    535             }
    536         },
    537
    538         /**
    539          * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to adjust the
    540          * distance between two ticks depending on {@link JXG.Ticks#minTicksDistance} value
    541          *
    542          * @param  {Number}     ticksDelta  distance between two major ticks in user coordinates
    543          * @param  {JXG.Coords} coordsZero  coordinates of the point considered zero
    544          * @param  {Object}     deltas      x and y distance in pixel between two user units
    545          * @param  {Object}     bounds      upper and lower bound of the tick positions in user units.
    546          * @private
    547          */
    548         adjustTickDistance: function (ticksDelta, coordsZero, deltas) {
    549             var nx, ny, bounds,
    550                 distScr, dist,
    551                 sgn = 1;
    552
    553             bounds = this.getLowerAndUpperBounds(coordsZero, 'ticksdistance');
    554             nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta;
    555             ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta;
    556             distScr = coordsZero.distance(Const.COORDS_BY_SCREEN, new Coords(Const.COORDS_BY_USER, [nx, ny], this.board));
    557             dist = bounds.upper - bounds.lower;
    558             while (distScr / (this.visProp.minorticks + 1) < this.minTicksDistance) {
    559                 if (sgn === 1) {
    560                     ticksDelta *= 2;
    561                 } else {
    562                     ticksDelta *= 5;
    563                 }
    564                 sgn *= -1;
    565
    566                 nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta;
    567                 ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta;
    568                 distScr = coordsZero.distance(Const.COORDS_BY_SCREEN, new Coords(Const.COORDS_BY_USER, [nx, ny], this.board));
    569             }
    570             return ticksDelta;
    571         },
    572
    573         /**
    574          * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to create a tick
    575          * in the line at the given tickPosition.
    576          *
    577          * @param  {JXG.Coords} coordsZero    coordinates of the point considered zero
    578          * @param  {Number}     tickPosition  current tick position relative to zero
    579          * @param  {Number}     ticksDelta    distance between two major ticks in user coordinates
    580          * @param  {Object}     deltas      x and y distance between two major ticks
    581          * @private
    582          */
    583         processTickPosition: function (coordsZero, tickPosition, ticksDelta, deltas) {
    584             var x, y, tickCoords, ti, labelText;
    585             // Calculates tick coordinates
    586             x = coordsZero.usrCoords[1] + tickPosition * deltas.x;
    587             y = coordsZero.usrCoords[2] + tickPosition * deltas.y;
    588             tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board);
    589
    590             // Test if tick is a major tick.
    591             // This is the case if tickPosition/ticksDelta is
    592             // a multiple of the number of minorticks+1
    593             tickCoords.major = Math.round(tickPosition / ticksDelta) % (this.visProp.minorticks + 1) === 0;
    594
    595             // Compute the start position and the end position of a tick.
    596             // If both positions are out of the canvas, ti is empty.
    597             ti = this.tickEndings(tickCoords, tickCoords.major);
    598             if (ti.length === 3) {
    599                 this.ticks.push(ti);
    600
    601                 if (tickCoords.major && this.visProp.drawlabels) {
    602                     labelText = this.generateLabelText(tickCoords, coordsZero);
    603                     this.labels.push(this.generateLabel(labelText, tickCoords, this.ticks.length));
    604                 } else {
    605                     this.labels.push(null);
    606                 }
    607             }
    608         },
    609
    610         /**
    611          * Creates ticks coordinates and labels based on {@link JXG.Ticks#fixedTicks} and {@link JXG.Ticks#labels}.
    612          *
    613          * @param  {JXG.Coords} coordsZero Coordinates of the point considered zero
    614          * @param  {Object}     bounds     contains the lower and upper boundaries for ticks placement
    615          * @private
    616          */
    617         generateFixedTicks: function (coordsZero, bounds) {
    618             var tickCoords, labelText, i, ti,
    619                 x, y,
    620                 hasLabelOverrides = Type.isArray(this.visProp.labels),
    621                 // Calculate X and Y distance between two major points in the line
    622                 deltas = this.getXandYdeltas();
    623
    624             for (i = 0; i < this.fixedTicks.length; i++) {
    625                 x = coordsZero.usrCoords[1] + this.fixedTicks[i] * deltas.x;
    626                 y = coordsZero.usrCoords[2] + this.fixedTicks[i] * deltas.y;
    627                 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board);
    628
    629                 // Compute the start position and the end position of a tick.
    630                 // If tick is out of the canvas, ti is empty.
    631                 ti = this.tickEndings(tickCoords, true);
    632                 if (ti.length === 3 && this.fixedTicks[i] >= bounds.lower && this.fixedTicks[i] <= bounds.upper) {
    633                     this.ticks.push(ti);
    634
    635                     if (this.visProp.drawlabels && (hasLabelOverrides || Type.exists(this.visProp.labels[i]))) {
    636                         labelText = hasLabelOverrides ? this.visProp.labels[i] : this.fixedTicks[i];
    637                         this.labels.push(
    638                             this.generateLabel(this.generateLabelText(tickCoords, coordsZero, labelText), tickCoords, i)
    639                         );
    640                     } else {
    641                         this.labels.push(null);
    642                     }
    643                 }
    644             }
    645         },
    646
    647         /**
    648          * Calculates the x and y distance in pixel between two units in user space.
    649          *
    650          * @return {Object}
    651          * @private
    652          */
    653         getXandYdeltas: function () {
    654             var
    655                 // Auxiliary points to store the start and end of the line according to its direction
    656                 point1UsrCoords, point2UsrCoords,
    657                 distP1P2 = this.line.point1.Dist(this.line.point2);
    658
    659             if (this.line.type === Const.OBJECT_TYPE_AXIS) {
    660                 // When line is an Axis, direction depends on Board Coordinates system
    661
    662                 // assume line.point1 and line.point2 are in correct order
    663                 point1UsrCoords = this.line.point1.coords.usrCoords;
    664                 point2UsrCoords = this.line.point2.coords.usrCoords;
    665
    666                 // Check if direction is incorrect, then swap
    667                 if (point1UsrCoords[1] > point2UsrCoords[1] ||
    668                         (Math.abs(point1UsrCoords[1] - point2UsrCoords[1]) < Mat.eps &&
    669                         point1UsrCoords[2] > point2UsrCoords[2])) {
    670                     point1UsrCoords = this.line.point2.coords.usrCoords;
    671                     point2UsrCoords = this.line.point1.coords.usrCoords;
    672                 }
    673             } else {
    674                 // line direction is always from P1 to P2 for non Axis types
    675                 point1UsrCoords = this.line.point1.coords.usrCoords;
    676                 point2UsrCoords = this.line.point2.coords.usrCoords;
    677             }
    678             return {
    679                 x: (point2UsrCoords[1] - point1UsrCoords[1]) / distP1P2,
    680                 y: (point2UsrCoords[2] - point1UsrCoords[2]) / distP1P2
    681             };
    682         },
    683
    684         /**
    685          * @param {JXG.Coords} coords Coordinates of the tick on the line.
    686          * @param {Boolean} major True if tick is major tick.
    687          * @return {Array} Array of length 3 containing start and end coordinates in screen coordinates
    688          *                 of the tick (arrays of length 2). 3rd entry is true if major tick otherwise false.
    689          *                 If the tick is outside of the canvas, the return array is empty.
    690          * @private
    691          */
    692         tickEndings: function (coords, major) {
    693             var i, c, lineStdForm, intersection,
    694                 dxs, dys,
    695                 s, style,
    696                 cw = this.board.canvasWidth,
    697                 ch = this.board.canvasHeight,
    698                 x = [-1000 * cw, -1000 * ch],
    699                 y = [-1000 * cw, -1000 * ch],
    700                 count = 0,
    701                 isInsideCanvas = false;
    702
    703             c = coords.scrCoords;
    704             if (major) {
    705                 dxs = this.dxMaj;
    706                 dys = this.dyMaj;
    707                 style = this.majStyle;
    708             } else {
    709                 dxs = this.dxMin;
    710                 dys = this.dyMin;
    711                 style = this.minStyle;
    712             }
    713             lineStdForm = [-dys * c[1] - dxs * c[2], dys, dxs];
    714
    715             // For all ticks regardless if of finite or infinite
    716             // tick length the intersection with the canvas border is
    717             // computed.
    718
    719             if (style === 'infinite') {
    720                 intersection = Geometry.meetLineBoard(lineStdForm, this.board);
    721                 x[0] = intersection[0].scrCoords[1];
    722                 x[1] = intersection[1].scrCoords[1];
    723                 y[0] = intersection[0].scrCoords[2];
    724                 y[1] = intersection[1].scrCoords[2];
    725             } else {
    726                 x[0] = c[1] + dxs * this.visProp.tickendings[0];
    727                 y[0] = c[2] - dys * this.visProp.tickendings[0];
    728                 x[1] = c[1] - dxs * this.visProp.tickendings[1];
    729                 y[1] = c[2] + dys * this.visProp.tickendings[1];
    730             }
    731
    732             // check if (parts of) the tick is inside the canvas.
    733             isInsideCanvas = (x[0] >= 0 && x[0] <= cw && y[0] >= 0 && y[0] <= ch) ||
    734                 (x[1] >= 0 && x[1] <= cw && y[1] >= 0 && y[1] <= ch);
    735
    736             if (isInsideCanvas) {
    737                 return [x, y, major];
    738             }
    739
    740             return [];
    741         },
    742
    743         /**
    744          * Creates the label text for a given tick. A value for the text can be provided as a number or string
    745          *
    746          * @param  {JXG.Coords}    tick  The Coords-object of the tick to create a label for
    747          * @param  {JXG.Coords}    zero  The Coords-object of line's zero
    748          * @param  {Number|String} value A predefined value for this tick
    749          * @return {String}
    750          * @private
    751          */
    752         generateLabelText: function (tick, zero, value) {
    753             var labelText,
    754                 distance = this.getDistanceFromZero(zero, tick);
    755
    756             if (Math.abs(distance) < Mat.eps) { // Point is zero
    757                 labelText = '0';
    758             } else {
    759                 // No value provided, equidistant, so assign distance as value
    760                 if (!Type.exists(value)) { // could be null or undefined
    761                     value = distance / this.visProp.scale;
    762                 }
    763
    764                 labelText = value.toString();
    765
    766                 // if value is Number
    767                 if (Type.isNumber(value)) {
    768                     if (labelText.length > this.visProp.maxlabellength || labelText.indexOf('e') !== -1) {
    769                         labelText = value.toPrecision(this.visProp.precision).toString();
    770                     }
    771                     if (labelText.indexOf('.') > -1 && labelText.indexOf('e') === -1) {
    772                         // trim trailing zeros
    773                         labelText = labelText.replace(/0+$/, '');
    774                         // trim trailing .
    775                         labelText = labelText.replace(/\.$/, '');
    776                     }
    777                 }
    778
    779                 if (this.visProp.scalesymbol.length > 0) {
    780                     if (labelText === '1') {
    781                         labelText = this.visProp.scalesymbol;
    782                     } else if (labelText === '-1') {
    783                         labelText = '-' + this.visProp.scalesymbol;
    784                     } else if (labelText !== '0') {
    785                         labelText = labelText + this.visProp.scalesymbol;
    786                     }
    787                 }
    788             }
    789
    790             return labelText;
    791         },
    792
    793         /**
    794          * Create a tick label
    795          * @param  {String}     labelText
    796          * @param  {JXG.Coords} tick
    797          * @param  {Number}     tickNumber
    798          * @return {JXG.Text}
    799          * @private
    800          */
    801         generateLabel: function (labelText, tick, tickNumber) {
    802             var label,
    803                 attr = {
    804                     isLabel: true,
    805                     layer: this.board.options.layer.line,
    806                     highlightStrokeColor: this.board.options.text.strokeColor,
    807                     highlightStrokeWidth: this.board.options.text.strokeWidth,
    808                     highlightStrokeOpacity: this.board.options.text.strokeOpacity,
    809                     visible: this.visProp.visible,
    810                     priv: this.visProp.priv
    811                 };
    812
    813             attr = Type.deepCopy(attr, this.visProp.label);
    814
    815             if (this.labelsRepo.length > 0) {
    816                 label = this.labelsRepo.pop();
    817                 label.setText(labelText);
    818                 label.setAttribute(attr);
    819             } else {
    820                 this.labelCounter += 1;
    821                 attr.id = this.id + tickNumber + 'Label' + this.labelCounter;
    822                 label = Text.createText(this.board, [tick.usrCoords[1], tick.usrCoords[2], labelText], attr);
    823             }
    824
    825             label.isDraggable = false;
    826             label.dump = false;
    827
    828             label.distanceX = this.visProp.label.offset[0];
    829             label.distanceY = this.visProp.label.offset[1];
    830             label.setCoords(
    831                 tick.usrCoords[1] + label.distanceX / (this.board.unitX),
    832                 tick.usrCoords[2] + label.distanceY / (this.board.unitY)
    833             );
    834
    835             return label;
    836         },
    837
    838         /**
    839          * Removes the HTML divs of the tick labels
    840          * before repositioning
    841          * @private
    842          */
    843         removeTickLabels: function () {
    844             var j;
    845
    846             // remove existing tick labels
    847             if (Type.exists(this.labels)) {
    848                 if ((this.board.needsFullUpdate || this.needsRegularUpdate || this.needsUpdate) &&
    849                         !(this.board.renderer.type === 'canvas' && this.board.options.text.display === 'internal')) {
    850                     for (j = 0; j < this.labels.length; j++) {
    851                         if (Type.exists(this.labels[j])) {
    852                             this.labelsRepo.push(this.labels[j]);
    853                         }
    854                     }
    855                 }
    856             }
    857         },
    858
    859         /**
    860          * Recalculate the tick positions and the labels.
    861          * @returns {JXG.Ticks}
    862          */
    863         update: function () {
    864             if (this.needsUpdate) {
    865                 // A canvas with no width or height will create an endless loop, so ignore it
    866                 if (this.board.canvasWidth !== 0 && this.board.canvasHeight !== 0) {
    867                     this.calculateTicksCoordinates();
    868                 }
    869             }
    870
    871             return this;
    872         },
    873
    874         /**
    875          * Uses the boards renderer to update the arc.
    876          * @returns {JXG.Ticks}
    877          */
    878         updateRenderer: function () {
    879             if (this.needsUpdate) {
    880                 this.board.renderer.updateTicks(this);
    881                 this.needsUpdate = false;
    882             }
    883
    884             return this;
    885         },
    886
    887         hideElement: function () {
    888             var i;
    889
    890             this.visProp.visible = false;
    891             this.board.renderer.hide(this);
    892
    893             for (i = 0; i < this.labels.length; i++) {
    894                 if (Type.exists(this.labels[i])) {
    895                     this.labels[i].hideElement();
    896                 }
    897             }
    898
    899             return this;
    900         },
    901
    902         showElement: function () {
    903             var i;
    904
    905             this.visProp.visible = true;
    906             this.board.renderer.show(this);
    907
    908             for (i = 0; i < this.labels.length; i++) {
    909                 if (Type.exists(this.labels[i])) {
    910                     this.labels[i].showElement();
    911                 }
    912             }
    913
    914             return this;
    915         }
    916     });
    917
    918     /**
    919      * @class Ticks are used as distance markers on a line.
    920      * @pseudo
    921      * @description
    922      * @name Ticks
    923      * @augments JXG.Ticks
    924      * @constructor
    925      * @type JXG.Ticks
    926      * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    927      * @param {JXG.Line,Number,Function} line,_distance,_generateLabelFunc The parents consist of the line the ticks are going to be attached to and the
    928      * distance between two major ticks.
    929      * The third parameter (optional) is a function which determines the tick label. It has as parameter a coords object containing the coordinates of the new tick.
    930      * @example
    931      * // Create an axis providing two coord pairs.
    932      *   var p1 = board.create('point', [0, 3]);
    933      *   var p2 = board.create('point', [1, 3]);
    934      *   var l1 = board.create('line', [p1, p2]);
    935      *   var t = board.create('ticks', [l1], {ticksDistance: 2});
    936      * </pre><div id="ee7f2d68-75fc-4ec0-9931-c76918427e63" style="width: 300px; height: 300px;"></div>
    937      * <script type="text/javascript">
    938      * (function () {
    939      *   var board = JXG.JSXGraph.initBoard('ee7f2d68-75fc-4ec0-9931-c76918427e63', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false});
    940      *   var p1 = board.create('point', [0, 3]);
    941      *   var p2 = board.create('point', [1, 3]);
    942      *   var l1 = board.create('line', [p1, p2]);
    943      *   var t = board.create('ticks', [l1, 2], {ticksDistance: 2});
    944      * })();
    945      * </script><pre>
    946      */
    947     JXG.createTicks = function (board, parents, attributes) {
    948         var el, dist,
    949             attr = Type.copyAttributes(attributes, board.options, 'ticks');
    950
    951         if (parents.length < 2) {
    952             dist = attr.ticksdistance;
    953         } else {
    954             dist = parents[1];
    955         }
    956
    957         if (parents[0].elementClass === Const.OBJECT_CLASS_LINE) {
    958             el = new JXG.Ticks(parents[0], dist, attr);
    959         } else {
    960             throw new Error("JSXGraph: Can't create Ticks with parent types '" + (typeof parents[0]) + "'.");
    961         }
    962
    963         // deprecated
    964         if (typeof attr.generatelabelvalue === 'function') {
    965             el.generateLabelText = attr.generatelabelvalue;
    966         }
    967         if (typeof attr.generatelabeltext === 'function') {
    968             el.generateLabelText = attr.generatelabeltext;
    969         }
    970
    971         el.isDraggable = true;
    972
    973         return el;
    974     };
    975
    976     /**
    977      * @class Hashes can be used to mark congruent lines.
    978      * @pseudo
    979      * @description
    980      * @name Hatch
    981      * @augments JXG.Ticks
    982      * @constructor
    983      * @type JXG.Ticks
    984      * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    985      * @param {JXG.Line,Number} line,numberofhashes The parents consist of the line the hatch marks are going to be attached to and the
    986      * number of dashes.
    987      * @example
    988      * // Create an axis providing two coord pairs.
    989      *   var p1 = board.create('point', [0, 3]);
    990      *   var p2 = board.create('point', [1, 3]);
    991      *   var l1 = board.create('line', [p1, p2]);
    992      *   var t = board.create('hatch', [l1, 3]);
    993      * </pre><div id="4a20af06-4395-451c-b7d1-002757cf01be" style="width: 300px; height: 300px;"></div>
    994      * <script type="text/javascript">
    995      * (function () {
    996      *   var board = JXG.JSXGraph.initBoard('4a20af06-4395-451c-b7d1-002757cf01be', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false});
    997      *   var p1 = board.create('point', [0, 3]);
    998      *   var p2 = board.create('point', [1, 3]);
    999      *   var l1 = board.create('line', [p1, p2]);
    1000      *   var t = board.create('hatch', [l1, 3]);
    1001      * })();
    1002      * </script><pre>
    1003      */
    1004     JXG.createHatchmark = function (board, parents, attributes) {
    1005         var num, i, base, width, totalwidth, el,
    1006             pos = [],
    1007             attr = Type.copyAttributes(attributes, board.options, 'hatch');
    1008
    1009         if (parents[0].elementClass !== Const.OBJECT_CLASS_LINE || typeof parents[1] !== 'number') {
    1010             throw new Error("JSXGraph: Can't create Hatch mark with parent types '" + (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'.");
    1011         }
    1012
    1013         num = parents[1];
    1014         width = attr.ticksdistance;
    1015         totalwidth = (num - 1) * width;
    1016         base = -totalwidth / 2;
    1017
    1018         for (i = 0; i < num; i++) {
    1019             pos[i] = base + i * width;
    1020         }
    1021
    1022         el = board.create('ticks', [parents[0], pos], attr);
    1023         el.elType = 'hatch';
    1024     };
    1025
    1026     JXG.registerElement('ticks', JXG.createTicks);
    1027     JXG.registerElement('hash', JXG.createHatchmark);
    1028     JXG.registerElement('hatch', JXG.createHatchmark);
    1029
    1030     return {
    1031         Ticks: JXG.Ticks,
    1032         createTicks: JXG.createTicks,
    1033         createHashmark: JXG.createHatchmark,
    1034         createHatchmark: JXG.createHatchmark
    1035     };
    1036 });
    1037