1 /*
  2     Copyright 2008-2017
  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         }
109 
110         if (Type.isArray(ticks)) {
111             this.fixedTicks = ticks;
112         } else {
113             if (Math.abs(ticks) < Mat.eps || ticks < 0) {
114                 ticks = attributes.defaultdistance;
115             }
116 
117             /*
118              * Ticks function:
119              * determines the distance (in user units) of two major ticks
120              */
121             this.ticksFunction = this.makeTicksFunction(ticks);
122 
123             this.equidistant = true;
124         }
125 
126         /**
127          * Least distance between two ticks, measured in pixels.
128          * @type int
129          */
130         this.minTicksDistance = attributes.minticksdistance;
131 
132         /**
133          * Stores the ticks coordinates
134          * @type {Array}
135          */
136         this.ticks = [];
137 
138         /**
139          * Distance between two major ticks in user coordinates
140          * @type {Number}
141          */
142         this.ticksDelta = 1;
143 
144         /**
145          * Array where the labels are saved. There is an array element for every tick,
146          * even for minor ticks which don't have labels. In this case the array element
147          * contains just <tt>null</tt>.
148          * @type Array
149          */
150         this.labels = [];
151 
152         /**
153          * A list of labels which have to be displayed in updateRenderer.
154          * @type {Array}
155          */
156         this.labelData = [];
157 
158         /**
159          * To ensure the uniqueness of label ids this counter is used.
160          * @type {number}
161          */
162         this.labelCounter = 0;
163 
164         this.id = this.line.addTicks(this);
165         this.elType = 'ticks';
166         this.inherits.push(this.labels);
167         this.board.setId(this, 'Ti');
168     };
169 
170     JXG.Ticks.prototype = new GeometryElement();
171 
172     JXG.extend(JXG.Ticks.prototype, /** @lends JXG.Ticks.prototype */ {
173 
174         /**
175          * Ticks function:
176          * determines the distance (in user units) of two major ticks.
177          * See above in constructor and in @see JXG.GeometryElement#setAttribute
178          *
179          * @private
180          * @param {Number} ticks Distance between two major ticks
181          * @returns {Function} returns method ticksFunction
182          */
183         makeTicksFunction: function (ticks) {
184             return function () {
185                 var delta, b, dist;
186 
187                 if (Type.evaluate(this.visProp.insertticks)) {
188                     b = this.getLowerAndUpperBounds(this.getZeroCoordinates(), 'ticksdistance');
189                     dist = b.upper - b.lower;
190                     delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10));
191                     if (dist <= 6 * delta) {
192                         delta *= 0.5;
193                     }
194                     return delta;
195                 }
196 
197                 // upto 0.99.1:
198                 return ticks;
199             };
200         },
201 
202         /**
203          * Checks whether (x,y) is near the line.
204          * @param {Number} x Coordinate in x direction, screen coordinates.
205          * @param {Number} y Coordinate in y direction, screen coordinates.
206          * @returns {Boolean} True if (x,y) is near the line, False otherwise.
207          */
208         hasPoint: function (x, y) {
209             var i, t,
210                 len = (this.ticks && this.ticks.length) || 0,
211                 r = this.board.options.precision.hasPoint +
212                         Type.evaluate(this.visProp.strokewidth) * 0.5;
213 
214             if (!Type.evaluate(this.line.visProp.scalable)) {
215                 return false;
216             }
217 
218             // Ignore non-axes and axes that are not horizontal or vertical
219             if (this.line.stdform[1] !== 0 && this.line.stdform[2] !== 0 && this.line.type !== Const.OBJECT_TYPE_AXIS) {
220                 return false;
221             }
222 
223             for (i = 0; i < len; i++) {
224                 t = this.ticks[i];
225 
226                 // Skip minor ticks
227                 if (t[2]) {
228                     // Ignore ticks at zero
229                     if (!((this.line.stdform[1] === 0 && Math.abs(t[0][0] - this.line.point1.coords.scrCoords[1]) < Mat.eps) ||
230                             (this.line.stdform[2] === 0 && Math.abs(t[1][0] - this.line.point1.coords.scrCoords[2]) < Mat.eps))) {
231                         // tick length is not zero, ie. at least one pixel
232                         if (Math.abs(t[0][0] - t[0][1]) >= 1 || Math.abs(t[1][0] - t[1][1]) >= 1) {
233                             if (this.line.stdform[1] === 0) {
234                                 // Allow dragging near axes only.
235                                 if (Math.abs(y - (t[1][0] + t[1][1]) * 0.5) < 2 * r && t[0][0] - r < x && x < t[0][1] + r) {
236                                     return true;
237                                 }
238                             } else if (this.line.stdform[2] === 0) {
239                                 if (Math.abs(x - (t[0][0] + t[0][1]) * 0.5) < 2 * r && t[1][0] - r < y && y < t[1][1] + r) {
240                                     return true;
241                                 }
242                             }
243                         }
244                     }
245                 }
246             }
247 
248             return false;
249         },
250 
251         /**
252          * Sets x and y coordinate of the tick.
253          * @param {number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}.
254          * @param {Array} coords coordinates in screen/user units
255          * @param {Array} oldcoords previous coordinates in screen/user units
256          * @returns {JXG.Ticks} this element
257          */
258         setPositionDirectly: function (method, coords, oldcoords) {
259             var dx, dy,
260                 c = new Coords(method, coords, this.board),
261                 oldc = new Coords(method, oldcoords, this.board),
262                 bb = this.board.getBoundingBox();
263 
264             if (!Type.evaluate(this.line.visProp.scalable)) {
265                 return this;
266             }
267 
268             // horizontal line
269             if (Math.abs(this.line.stdform[1]) < Mat.eps && Math.abs(c.usrCoords[1] * oldc.usrCoords[1]) > Mat.eps) {
270                 dx = oldc.usrCoords[1] / c.usrCoords[1];
271                 bb[0] *= dx;
272                 bb[2] *= dx;
273                 this.board.setBoundingBox(bb, false);
274             // vertical line
275             } else if (Math.abs(this.line.stdform[2]) < Mat.eps && Math.abs(c.usrCoords[2] * oldc.usrCoords[2]) > Mat.eps) {
276                 dy = oldc.usrCoords[2] / c.usrCoords[2];
277                 bb[3] *= dy;
278                 bb[1] *= dy;
279                 this.board.setBoundingBox(bb, false);
280             }
281 
282             return this;
283         },
284 
285         /**
286          * (Re-)calculates the ticks coordinates.
287          * @private
288          */
289         calculateTicksCoordinates: function () {
290             var coordsZero, bounds;
291 
292             // Calculate Ticks width and height in Screen and User Coordinates
293             this.setTicksSizeVariables();
294             // If the parent line is not finite, we can stop here.
295             if (Math.abs(this.dx) < Mat.eps &&
296                 Math.abs(this.dy) < Mat.eps) {
297                 return;
298             }
299 
300             // Get Zero
301             coordsZero = this.getZeroCoordinates();
302 
303             // Calculate lower bound and upper bound limits based on distance between p1 and centre and p2 and centre
304             bounds = this.getLowerAndUpperBounds(coordsZero);
305 
306             // Clean up
307             this.ticks = [];
308             this.labelsData = [];
309             // Create Ticks Coordinates and Labels
310             if (this.equidistant) {
311                 this.generateEquidistantTicks(coordsZero, bounds);
312             } else {
313                 this.generateFixedTicks(coordsZero, bounds);
314             }
315 
316             return this;
317         },
318 
319         /**
320          * Sets the variables used to set the height and slope of each tick.
321          *
322          * @private
323          */
324         setTicksSizeVariables: function () {
325             var d,
326                 distMaj = Type.evaluate(this.visProp.majorheight) * 0.5,
327                 distMin = Type.evaluate(this.visProp.minorheight) * 0.5;
328 
329             // ticks width and height in screen units
330             this.dxMaj = this.line.stdform[1];
331             this.dyMaj = this.line.stdform[2];
332             this.dxMin = this.dxMaj;
333             this.dyMin = this.dyMaj;
334 
335             // ticks width and height in user units
336             this.dx = this.dxMaj;
337             this.dy = this.dyMaj;
338 
339             // After this, the length of the vector (dxMaj, dyMaj) in screen coordinates is equal to distMaj pixel.
340             d = Math.sqrt(
341                 this.dxMaj * this.dxMaj * this.board.unitX * this.board.unitX +
342                     this.dyMaj * this.dyMaj * this.board.unitY * this.board.unitY
343             );
344             this.dxMaj *= distMaj / d * this.board.unitX;
345             this.dyMaj *= distMaj / d * this.board.unitY;
346             this.dxMin *= distMin / d * this.board.unitX;
347             this.dyMin *= distMin / d * this.board.unitY;
348 
349             // Grid-like ticks?
350             this.minStyle= (Type.evaluate(this.visProp.minorheight) < 0) ? 'infinite' : 'finite';
351             this.majStyle= (Type.evaluate(this.visProp.majorheight) < 0) ? 'infinite' : 'finite';
352         },
353 
354         /**
355          * Returns the coordinates of the point zero of the line.
356          *
357          * If the line is an {@link Axis}, the coordinates of the projection of the board's zero point is returned
358          *
359          * Otherwise, the coordinates of the point that acts as zero are established depending on the value of {@link JXG.Ticks#anchor}
360          *
361          * @returns {JXG.Coords} Coords object for the Zero point on the line
362          * @private
363          */
364         getZeroCoordinates: function () {
365             var c1x, c1y, c1z, c2x, c2y, c2z,
366                 ev_a = Type.evaluate(this.visProp.anchor);
367 
368             if (this.line.type === Const.OBJECT_TYPE_AXIS) {
369                 return Geometry.projectPointToLine({
370                     coords: {
371                         usrCoords: [1, 0, 0]
372                     }
373                 }, this.line, this.board);
374             }
375 
376             c1z = this.line.point1.coords.usrCoords[0];
377             c1x = this.line.point1.coords.usrCoords[1];
378             c1y = this.line.point1.coords.usrCoords[2];
379             c2z = this.line.point2.coords.usrCoords[0];
380             c2x = this.line.point2.coords.usrCoords[1];
381             c2y = this.line.point2.coords.usrCoords[2];
382 
383             if (ev_a === 'right') {
384                 return this.line.point2.coords;
385             } else if (ev_a === 'middle') {
386                 return new Coords(Const.COORDS_BY_USER, [
387                     (c1z + c2z) * 0.5,
388                     (c1x + c2x) * 0.5,
389                     (c1y + c2y) * 0.5
390                 ], this.board);
391             } else if (Type.isNumber(ev_a)) {
392                 return new Coords(Const.COORDS_BY_USER, [
393                     c1z + (c2z - c1z) * ev_a,
394                     c1x + (c2x - c1x) * ev_a,
395                     c1y + (c2y - c1y) * ev_a
396                 ], this.board);
397             }
398 
399             return this.line.point1.coords;
400         },
401 
402         /**
403          * Calculate the lower and upper bounds for tick rendering
404          * If {@link JXG.Ticks#includeBoundaries} is false, the boundaries will exclude point1 and point2
405          *
406          * @param  {JXG.Coords} coordsZero
407          * @returns {String} type  (Optional) If type=='ticksdistance' the bounds are
408          *                         the intersection of the line with the bounding box of the board.
409          *                         Otherwise it is the projection of the corners of the bounding box
410          *                         to the line. The first case i s needed to automatically
411          *                         generate ticks. The second case is for drawing of the ticks.
412          * @returns {Object}     contains the lower and upper bounds
413          *
414          * @private
415          */
416         getLowerAndUpperBounds: function (coordsZero, type) {
417             var lowerBound, upperBound,
418                 // The line's defining points that will be adjusted to be within the board limits
419                 point1 = new Coords(Const.COORDS_BY_USER, this.line.point1.coords.usrCoords, this.board),
420                 point2 = new Coords(Const.COORDS_BY_USER, this.line.point2.coords.usrCoords, this.board),
421                 // Are the original defining points within the board?
422                 isPoint1inBoard = (Math.abs(point1.usrCoords[0]) >= Mat.eps &&
423                     point1.scrCoords[1] >= 0.0 && point1.scrCoords[1] <= this.board.canvasWidth &&
424                     point1.scrCoords[2] >= 0.0 && point1.scrCoords[2] <= this.board.canvasHeight),
425                 isPoint2inBoard = (Math.abs(point2.usrCoords[0]) >= Mat.eps &&
426                     point2.scrCoords[1] >= 0.0 && point2.scrCoords[1] <= this.board.canvasWidth &&
427                     point2.scrCoords[2] >= 0.0 && point2.scrCoords[2] <= this.board.canvasHeight),
428                 // We use the distance from zero to P1 and P2 to establish lower and higher points
429                 dZeroPoint1, dZeroPoint2,
430                 ev_sf = Type.evaluate(this.line.visProp.straightfirst),
431                 ev_sl = Type.evaluate(this.line.visProp.straightlast),
432                 ev_i = Type.evaluate(this.visProp.includeboundaries),
433                 obj;
434 
435             // Adjust line limit points to be within the board
436             if (Type.exists(type) || type === 'tickdistance') {
437                 // The good old calcStraight is needed for determining the distance between major ticks.
438                 // Here, only the visual area is of importance
439                 Geometry.calcStraight(this.line, point1, point2, Type.evaluate(this.line.visProp.margin));
440             } else {
441                 // This function projects the corners of the board to the line.
442                 // This is important for diagonal lines with infinite tick lines.
443                 Geometry.calcLineDelimitingPoints(this.line, point1, point2);
444             }
445             // Shorten ticks bounds such that ticks are not through arrow heads
446             obj = this.board.renderer.getPositionArrowHead(this.line, point1, point2,
447                         Type.evaluate(this.line.visProp.strokewidth));
448             point1.setCoordinates(Const.COORDS_BY_SCREEN, [
449                     point1.scrCoords[1] - obj.d1x,
450                     point1.scrCoords[2] - obj.d1y,
451                 ]);
452             point2.setCoordinates(Const.COORDS_BY_SCREEN, [
453                     point2.scrCoords[1] - obj.d2x,
454                     point2.scrCoords[2] - obj.d2y,
455                 ]);
456 
457             // Calculate distance from Zero to P1 and to P2
458             dZeroPoint1 = this.getDistanceFromZero(coordsZero, point1);
459             dZeroPoint2 = this.getDistanceFromZero(coordsZero, point2);
460 
461             // We have to establish if the direction is P1->P2 or P2->P1 to set the lower and upper
462             // boundaries appropriately. As the distances contain also a sign to indicate direction,
463             // we can compare dZeroPoint1 and dZeroPoint2 to establish the line direction
464             if (dZeroPoint1 < dZeroPoint2) { // Line goes P1->P2
465                 lowerBound = dZeroPoint1;
466                 if (!ev_sf && isPoint1inBoard && !ev_i) {
467                     lowerBound += Mat.eps;
468                 }
469                 upperBound = dZeroPoint2;
470                 if (!ev_sl && isPoint2inBoard && !ev_i) {
471                     upperBound -= Mat.eps;
472                 }
473             } else if (dZeroPoint2 < dZeroPoint1) { // Line goes P2->P1
474                 lowerBound = dZeroPoint2;
475                 if (!ev_sl && isPoint2inBoard && !ev_i) {
476                     lowerBound += Mat.eps;
477                 }
478                 upperBound = dZeroPoint1;
479                 if (!ev_sf && isPoint1inBoard && !ev_i) {
480                     upperBound -= Mat.eps;
481                 }
482             } else { // P1 = P2 = Zero, we can't do a thing
483                 lowerBound = 0;
484                 upperBound = 0;
485             }
486 
487             return {
488                 lower: lowerBound,
489                 upper: upperBound
490             };
491         },
492 
493         /**
494          * Calculates the distance in user coordinates from zero to a given point including its sign
495          *
496          * @param  {JXG.Coords} zero  coordinates of the point considered zero
497          * @param  {JXG.Coords} point coordinates of the point to find out the distance
498          * @returns {Number}           distance between zero and point, including its sign
499          * @private
500          */
501         getDistanceFromZero: function (zero, point) {
502             var eps = Mat.eps,
503                 distance = zero.distance(Const.COORDS_BY_USER, point);
504 
505             // Establish sign
506             if (this.line.type === Const.OBJECT_TYPE_AXIS) {
507                 if ((Mat.relDif(zero.usrCoords[1], point.usrCoords[1]) > eps &&
508                         zero.usrCoords[1] - point.usrCoords[1] > eps) ||
509                     (Mat.relDif(zero.usrCoords[2], point.usrCoords[2]) > eps &&
510                         zero.usrCoords[2] - point.usrCoords[2] > eps)) {
511 
512                     distance *= -1;
513                 }
514             } else if (Type.evaluate(this.visProp.anchor) === 'right') {
515                 if (Geometry.isSameDirection(zero, this.line.point1.coords, point)) {
516                     distance *= -1;
517                 }
518             } else {
519                 if (!Geometry.isSameDirection(zero, this.line.point2.coords, point)) {
520                     distance *= -1;
521                 }
522             }
523             return distance;
524         },
525 
526         /**
527          * Creates ticks coordinates and labels automatically.
528          * The frequency of ticks is affected by the values of {@link JXG.Ticks#insertTicks} and {@link JXG.Ticks#ticksDistance}
529          *
530          * @param  {JXG.Coords} coordsZero coordinates of the point considered zero
531          * @param  {Object}     bounds     contains the lower and upper boundaries for ticks placement
532          * @private
533          */
534         generateEquidistantTicks: function (coordsZero, bounds) {
535             var tickPosition,
536                 // Calculate X and Y distance between two major ticks
537                 deltas = this.getXandYdeltas(),
538                 // Distance between two major ticks in user coordinates
539                 ticksDelta = (this.equidistant ? this.ticksFunction(1) : this.ticksDelta),
540                 ev_it = Type.evaluate(this.visProp.insertticks),
541                 ev_mt = Type.evaluate(this.visProp.minorticks);
542 
543             // adjust ticks distance
544             ticksDelta *= Type.evaluate(this.visProp.scale);
545             if (ev_it && this.minTicksDistance > Mat.eps) {
546                 ticksDelta = this.adjustTickDistance(ticksDelta, coordsZero, deltas);
547                 ticksDelta /= (ev_mt + 1);
548             } else if (!ev_it) {
549                 ticksDelta /= (ev_mt + 1);
550             }
551             this.ticksDelta = ticksDelta;
552 
553             if (ticksDelta < Mat.eps) {
554                 return;
555             }
556 
557             // Position ticks from zero to the positive side while not reaching the upper boundary
558             tickPosition = 0;
559             if (!Type.evaluate(this.visProp.drawzero)) {
560                 tickPosition = ticksDelta;
561             }
562             while (tickPosition <= bounds.upper) {
563                 // Only draw ticks when we are within bounds, ignore case where  tickPosition < lower < upper
564                 if (tickPosition >= bounds.lower) {
565                     this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas);
566                 }
567                 tickPosition += ticksDelta;
568             }
569 
570             // Position ticks from zero (not inclusive) to the negative side while not reaching the lower boundary
571             tickPosition = -ticksDelta;
572             while (tickPosition >= bounds.lower) {
573                 // Only draw ticks when we are within bounds, ignore case where lower < upper < tickPosition
574                 if (tickPosition <= bounds.upper) {
575                     this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas);
576                 }
577                 tickPosition -= ticksDelta;
578             }
579         },
580 
581         /**
582          * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to adjust the
583          * distance between two ticks depending on {@link JXG.Ticks#minTicksDistance} value
584          *
585          * @param  {Number}     ticksDelta  distance between two major ticks in user coordinates
586          * @param  {JXG.Coords} coordsZero  coordinates of the point considered zero
587          * @param  {Object}     deltas      x and y distance in pixel between two user units
588          * @param  {Object}     bounds      upper and lower bound of the tick positions in user units.
589          * @private
590          */
591         adjustTickDistance: function (ticksDelta, coordsZero, deltas) {
592             var nx, ny, bounds,
593                 distScr,
594                 sgn = 1;
595 
596             bounds = this.getLowerAndUpperBounds(coordsZero, 'ticksdistance');
597             nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta;
598             ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta;
599             distScr = coordsZero.distance(Const.COORDS_BY_SCREEN, new Coords(Const.COORDS_BY_USER, [nx, ny], this.board));
600             while (distScr / (Type.evaluate(this.visProp.minorticks) + 1) < this.minTicksDistance) {
601                 if (sgn === 1) {
602                     ticksDelta *= 2;
603                 } else {
604                     ticksDelta *= 5;
605                 }
606                 sgn *= -1;
607 
608                 nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta;
609                 ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta;
610                 distScr = coordsZero.distance(Const.COORDS_BY_SCREEN, new Coords(Const.COORDS_BY_USER, [nx, ny], this.board));
611             }
612             return ticksDelta;
613         },
614 
615         /**
616          * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to create a tick
617          * in the line at the given tickPosition.
618          *
619          * @param  {JXG.Coords} coordsZero    coordinates of the point considered zero
620          * @param  {Number}     tickPosition  current tick position relative to zero
621          * @param  {Number}     ticksDelta    distance between two major ticks in user coordinates
622          * @param  {Object}     deltas      x and y distance between two major ticks
623          * @private
624          */
625         processTickPosition: function (coordsZero, tickPosition, ticksDelta, deltas) {
626             var x, y, tickCoords, ti;
627 
628             // Calculates tick coordinates
629             x = coordsZero.usrCoords[1] + tickPosition * deltas.x;
630             y = coordsZero.usrCoords[2] + tickPosition * deltas.y;
631             tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board);
632 
633             // Test if tick is a major tick.
634             // This is the case if tickPosition/ticksDelta is
635             // a multiple of the number of minorticks+1
636             tickCoords.major = Math.round(tickPosition / ticksDelta) % (Type.evaluate(this.visProp.minorticks) + 1) === 0;
637 
638             // Compute the start position and the end position of a tick.
639             // If both positions are out of the canvas, ti is empty.
640             ti = this.tickEndings(tickCoords, tickCoords.major);
641             if (ti.length === 3) {
642                 this.ticks.push(ti);
643                 if (tickCoords.major && Type.evaluate(this.visProp.drawlabels)) {
644                     this.labelsData.push(
645                         this.generateLabelData(
646                             this.generateLabelText(tickCoords, coordsZero),
647                             tickCoords,
648                             this.ticks.length
649                         )
650                     );
651                 } else {
652                     this.labelsData.push(null);
653                 }
654             }
655         },
656 
657         /**
658          * Creates ticks coordinates and labels based on {@link JXG.Ticks#fixedTicks} and {@link JXG.Ticks#labels}.
659          *
660          * @param  {JXG.Coords} coordsZero Coordinates of the point considered zero
661          * @param  {Object}     bounds     contains the lower and upper boundaries for ticks placement
662          * @private
663          */
664         generateFixedTicks: function (coordsZero, bounds) {
665             var tickCoords, labelText, i, ti,
666                 x, y,
667                 hasLabelOverrides = Type.isArray(this.visProp.labels),
668                 // Calculate X and Y distance between two major points in the line
669                 deltas = this.getXandYdeltas(),
670                 ev_dl = Type.evaluate(this.visProp.drawlabels);
671 
672             for (i = 0; i < this.fixedTicks.length; i++) {
673                 x = coordsZero.usrCoords[1] + this.fixedTicks[i] * deltas.x;
674                 y = coordsZero.usrCoords[2] + this.fixedTicks[i] * deltas.y;
675                 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board);
676 
677                 // Compute the start position and the end position of a tick.
678                 // If tick is out of the canvas, ti is empty.
679                 ti = this.tickEndings(tickCoords, true);
680                 if (ti.length === 3 && this.fixedTicks[i] >= bounds.lower &&
681                     this.fixedTicks[i] <= bounds.upper) {
682                     this.ticks.push(ti);
683 
684                     if (ev_dl &&
685                             (hasLabelOverrides || Type.exists(this.visProp.labels[i]))) {
686                         labelText = hasLabelOverrides ?
687                                         Type.evaluate(this.visProp.labels[i]) : this.fixedTicks[i];
688                         this.labelsData.push(
689                             this.generateLabelData(
690                                 this.generateLabelText(tickCoords, coordsZero, labelText),
691                                 tickCoords,
692                                 i
693                             )
694                         );
695                     } else {
696                         this.labelsData.push(null);
697                     }
698                 }
699             }
700         },
701 
702         /**
703          * Calculates the x and y distance in pixel between two units in user space.
704          *
705          * @returns {Object}
706          * @private
707          */
708         getXandYdeltas: function () {
709             var
710                 // Auxiliary points to store the start and end of the line according to its direction
711                 point1UsrCoords, point2UsrCoords,
712                 distP1P2 = this.line.point1.Dist(this.line.point2);
713 
714             if (this.line.type === Const.OBJECT_TYPE_AXIS) {
715                 // When line is an Axis, direction depends on Board Coordinates system
716 
717                 // assume line.point1 and line.point2 are in correct order
718                 point1UsrCoords = this.line.point1.coords.usrCoords;
719                 point2UsrCoords = this.line.point2.coords.usrCoords;
720 
721                 // Check if direction is incorrect, then swap
722                 if (point1UsrCoords[1] > point2UsrCoords[1] ||
723                         (Math.abs(point1UsrCoords[1] - point2UsrCoords[1]) < Mat.eps &&
724                         point1UsrCoords[2] > point2UsrCoords[2])) {
725                     point1UsrCoords = this.line.point2.coords.usrCoords;
726                     point2UsrCoords = this.line.point1.coords.usrCoords;
727                 }
728             } else {
729                 // line direction is always from P1 to P2 for non Axis types
730                 point1UsrCoords = this.line.point1.coords.usrCoords;
731                 point2UsrCoords = this.line.point2.coords.usrCoords;
732             }
733             return {
734                 x: (point2UsrCoords[1] - point1UsrCoords[1]) / distP1P2,
735                 y: (point2UsrCoords[2] - point1UsrCoords[2]) / distP1P2
736             };
737         },
738 
739         /**
740          * Check if (parts of) the tick is inside the canvas. The tick intersects the boundary
741          * at two positions: [x[0], y[0]] and [x[1], y[1]] in screen coordinates.
742          * @param  {Array}  x Array of length two
743          * @param  {Array}  y Array of length two
744          * @return {Boolean}   true if parts of the tick are inside of the canvas or on the boundary.
745          */
746         _isInsideCanvas: function(x, y, m) {
747             var cw = this.board.canvasWidth,
748                 ch = this.board.canvasHeight;
749 
750             if (m === undefined) {
751                 m = 0;
752             }
753             return (x[0] >= m && x[0] <= cw - m && y[0] >= m && y[0] <= ch - m) ||
754                     (x[1] >= m && x[1] <= cw - m && y[1] >= m && y[1] <= ch - m);
755         },
756 
757         /**
758          * @param {JXG.Coords} coords Coordinates of the tick on the line.
759          * @param {Boolean} major True if tick is major tick.
760          * @returns {Array} Array of length 3 containing start and end coordinates in screen coordinates
761          *                 of the tick (arrays of length 2). 3rd entry is true if major tick otherwise false.
762          *                 If the tick is outside of the canvas, the return array is empty.
763          * @private
764          */
765         tickEndings: function (coords, major) {
766             var c, lineStdForm, intersection,
767                 dxs, dys,
768                 style,
769                 x = [-2000000, -2000000],
770                 y = [-2000000, -2000000];
771 
772             c = coords.scrCoords;
773             if (major) {
774                 dxs = this.dxMaj;
775                 dys = this.dyMaj;
776                 style = this.majStyle;
777             } else {
778                 dxs = this.dxMin;
779                 dys = this.dyMin;
780                 style = this.minStyle;
781             }
782             lineStdForm = [-dys * c[1] - dxs * c[2], dys, dxs];
783 
784             // For all ticks regardless if of finite or infinite
785             // tick length the intersection with the canvas border is
786             // computed.
787             if (style === 'infinite') {
788                 intersection = Geometry.meetLineBoard(lineStdForm, this.board);
789                 x[0] = intersection[0].scrCoords[1];
790                 x[1] = intersection[1].scrCoords[1];
791                 y[0] = intersection[0].scrCoords[2];
792                 y[1] = intersection[1].scrCoords[2];
793             } else {
794                 x[0] = c[1] + dxs * Type.evaluate(this.visProp.tickendings[0]);
795                 y[0] = c[2] - dys * Type.evaluate(this.visProp.tickendings[0]);
796                 x[1] = c[1] - dxs * Type.evaluate(this.visProp.tickendings[1]);
797                 y[1] = c[2] + dys * Type.evaluate(this.visProp.tickendings[1]);
798             }
799 
800             // Check if (parts of) the tick is inside the canvas.
801             if (this._isInsideCanvas(x, y)) {
802                 return [x, y, major];
803             }
804 
805             return [];
806         },
807 
808         /**
809          * Format label texts. Show the desired number of digits
810          * and use utf-8 minus sign.
811          * @param  {Number} value Number to be displayed
812          * @return {String}       The value converted into a string.
813          * @private
814          */
815         formatLabelText: function(value) {
816             var labelText = value.toString(),
817                 ev_s = Type.evaluate(this.visProp.scalesymbol);
818 
819             // if value is Number
820             if (Type.isNumber(value)) {
821                 if (labelText.length > Type.evaluate(this.visProp.maxlabellength) ||
822                         labelText.indexOf('e') !== -1) {
823                     labelText = value.toPrecision(Type.evaluate(this.visProp.precision)).toString();
824                 }
825                 if (labelText.indexOf('.') > -1 && labelText.indexOf('e') === -1) {
826                     // trim trailing zeros
827                     labelText = labelText.replace(/0+$/, '');
828                     // trim trailing .
829                     labelText = labelText.replace(/\.$/, '');
830                 }
831             }
832 
833             if (ev_s.length > 0) {
834                 if (labelText === '1') {
835                     labelText = ev_s;
836                 } else if (labelText === '-1') {
837                     labelText = '-' + ev_s;
838                 } else if (labelText !== '0') {
839                     labelText = labelText + ev_s;
840                 }
841             }
842 
843             if (Type.evaluate(this.visProp.useunicodeminus)) {
844                 labelText = labelText.replace(/-/g, '\u2212');
845             }
846             return labelText;
847         },
848 
849         /**
850          * Creates the label text for a given tick. A value for the text can be provided as a number or string
851          *
852          * @param  {JXG.Coords}    tick  The Coords-object of the tick to create a label for
853          * @param  {JXG.Coords}    zero  The Coords-object of line's zero
854          * @param  {Number|String} value A predefined value for this tick
855          * @returns {String}
856          * @private
857          */
858         generateLabelText: function (tick, zero, value) {
859             var labelText,
860                 distance = this.getDistanceFromZero(zero, tick);
861 
862             if (Math.abs(distance) < Mat.eps) { // Point is zero
863                 labelText = '0';
864             } else {
865                 // No value provided, equidistant, so assign distance as value
866                 if (!Type.exists(value)) { // could be null or undefined
867                     value = distance / Type.evaluate(this.visProp.scale);
868                 }
869 
870                 labelText = this.formatLabelText(value);
871             }
872 
873             return labelText;
874         },
875 
876         /**
877          * Create a tick label data, i.e. text and coordinates
878          * @param  {String}     labelText
879          * @param  {JXG.Coords} tick
880          * @param  {Number}     tickNumber
881          * @returns {Object} with properties 'x', 'y', 't' (text), 'i' (tick number) or null in case of o label
882          * @private
883          */
884          generateLabelData: function (labelText, tick, tickNumber) {
885              var xa, ya, m, fs;
886 
887              // Test if large portions of the label are inside of the canvas
888              // This is the last chance to abandon the creation of the label if it is mostly
889              // outside of the canvas.
890              fs = Type.evaluate(this.visProp.label.fontsize);
891              xa = [tick.scrCoords[1], tick.scrCoords[1]];
892              ya = [tick.scrCoords[2], tick.scrCoords[2]];
893              m = (fs === undefined) ? 12 : fs;
894              m *= 1.5;
895              if (!this._isInsideCanvas(xa, ya, m)) {
896                  return null;
897              }
898 
899              xa = Type.evaluate(this.visProp.label.offset[0]);
900              ya = Type.evaluate(this.visProp.label.offset[1]);
901 
902              return {
903                  x: tick.usrCoords[1] + xa / (this.board.unitX),
904                  y: tick.usrCoords[2] + ya / (this.board.unitY),
905                  t: labelText,
906                  i: tickNumber
907              };
908          },
909 
910         /**
911          * Recalculate the tick positions and the labels.
912          * @returns {JXG.Ticks}
913          */
914         update: function () {
915             if (this.needsUpdate) {
916                 //this.visPropCalc.visible = Type.evaluate(this.visProp.visible);
917                 // A canvas with no width or height will create an endless loop, so ignore it
918                 if (this.board.canvasWidth !== 0 && this.board.canvasHeight !== 0) {
919                     this.calculateTicksCoordinates();
920                 }
921                 // this.updateVisibility(this.line.visPropCalc.visible);
922                 //
923                 // for (var i = 0; i < this.labels.length; i++) {
924                 //     if (this.labels[i] !== null) {
925                 //         this.labels[i].prepareUpdate()
926                 //             .updateVisibility(this.line.visPropCalc.visible)
927                 //             .updateRenderer();
928                 //     }
929                 // }
930             }
931 
932             return this;
933         },
934 
935         /**
936          * Uses the boards renderer to update the arc.
937          * @returns {JXG.Ticks} Reference to the object.
938          */
939         updateRenderer: function () {
940             if (!this.needsUpdate) {
941                 return this;
942             }
943 
944             if (this.visPropCalc.visible) {
945                 this.board.renderer.updateTicks(this);
946             }
947             this.updateRendererLabels();
948 
949             this.setDisplayRendNode();
950             // if (this.visPropCalc.visible != this.visPropOld.visible) {
951             //     this.board.renderer.display(this, this.visPropCalc.visible);
952             //     this.visPropOld.visible = this.visPropCalc.visible;
953             // }
954 
955             this.needsUpdate = false;
956             return this;
957         },
958 
959         /**
960          * Updates the label elements of the major ticks.
961          *
962          * @private
963          * @returns {JXG.Ticks} Reference to the object.
964          */
965         updateRendererLabels: function() {
966             var i, j,
967                 lenData, lenLabels,
968                 attr,
969                 label, ld,
970                 visible;
971 
972             // The number of labels needed
973             lenData = this.labelsData.length;
974             // The number of labels which already exist
975             lenLabels = this.labels.length;
976 
977             for (i = 0, j = 0; i < lenData; i++) {
978                 if (this.labelsData[i] === null) {
979                     continue;
980                 }
981 
982                 ld = this.labelsData[i];
983                 if (j < lenLabels) {
984                     // Take an already existing text element
985                     label = this.labels[j];
986                     label.setText(ld.t);
987                     label.setCoords(ld.x, ld.y);
988                     j++;
989                 } else {
990                     // A new text element is needed
991                     this.labelCounter += 1;
992 
993                     attr = {
994                         isLabel: true,
995                         layer: this.board.options.layer.line,
996                         highlightStrokeColor: this.board.options.text.strokeColor,
997                         highlightStrokeWidth: this.board.options.text.strokeWidth,
998                         highlightStrokeOpacity: this.board.options.text.strokeOpacity,
999                         priv: this.visProp.priv
1000                     };
1001                     attr = Type.deepCopy(attr, this.visProp.label);
1002                     attr.id = this.id + ld.i + 'Label' + this.labelCounter;
1003 
1004                     label = Text.createText(this.board, [ld.x, ld.y, ld.t], attr);
1005                     label.isDraggable = false;
1006                     label.dump = false;
1007                     this.labels.push(label);
1008                 }
1009 
1010                 visible = Type.evaluate(this.visProp.label.visible);
1011                 if (visible === 'inherit') {
1012                     visible = this.visPropCalc.visible;
1013                 }
1014                 label.prepareUpdate()
1015                     .updateVisibility(visible)
1016                     .updateRenderer();
1017                 //this.board.renderer.display(label, visible);
1018 
1019                 label.distanceX = Type.evaluate(this.visProp.label.offset[0]);
1020                 label.distanceY = Type.evaluate(this.visProp.label.offset[1]);
1021             }
1022 
1023             // Hide unused labels
1024             lenData = j;
1025             for (j = lenData; j < lenLabels; j++) {
1026                 this.board.renderer.display(this.labels[j], false);
1027                 // Tick labels have the attribute "visible: 'inherit'"
1028                 // This must explicitely set to false, otherwise
1029                 // this labels would be set to visible in the upcoming
1030                 // update of the labels.
1031                 this.labels[j].visProp.visible = false;
1032             }
1033 
1034             return this;
1035         },
1036 
1037         hideElement: function () {
1038             var i;
1039 
1040             JXG.deprecated('Element.hideElement()', 'Element.setDisplayRendNode()');
1041 
1042             this.visPropCalc.visible = false;
1043             this.board.renderer.display(this, false);
1044             for (i = 0; i < this.labels.length; i++) {
1045                 if (Type.exists(this.labels[i])) {
1046                     this.labels[i].hideElement();
1047                 }
1048             }
1049 
1050             return this;
1051         },
1052 
1053         showElement: function () {
1054             var i;
1055 
1056             JXG.deprecated('Element.showElement()', 'Element.setDisplayRendNode()');
1057 
1058             this.visPropCalc.visible = true;
1059             this.board.renderer.display(this, false);
1060 
1061             for (i = 0; i < this.labels.length; i++) {
1062                 if (Type.exists(this.labels[i])) {
1063                     this.labels[i].showElement();
1064                 }
1065             }
1066 
1067             return this;
1068         }
1069     });
1070 
1071     /**
1072      * @class Ticks are used as distance markers on a line.
1073      * @pseudo
1074      * @description
1075      * @name Ticks
1076      * @augments JXG.Ticks
1077      * @constructor
1078      * @type JXG.Ticks
1079      * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
1080      * @param {JXG.Line} line The parents consist of the line the ticks are going to be attached to.
1081      * @param {Number} distance Number defining the distance between two major ticks or an
1082      * array defining static ticks. Alternatively, the distance can be specified with the attribute
1083      * "ticksDistance". For arbitrary lines (and not axes) a "zero coordinate" is determined
1084      * which defines where the first tick is positioned. This zero coordinate
1085      * can be altered with the attribute "anchor". Possible values are "left", "middle", "right" or a number.
1086      * The default value is "middle".
1087      *
1088      * @example
1089      * // Create an axis providing two coord pairs.
1090      *   var p1 = board.create('point', [0, 3]);
1091      *   var p2 = board.create('point', [1, 3]);
1092      *   var l1 = board.create('line', [p1, p2]);
1093      *   var t = board.create('ticks', [l1], {ticksDistance: 2});
1094      * </pre><div class="jxgbox" id="ee7f2d68-75fc-4ec0-9931-c76918427e63" style="width: 300px; height: 300px;"></div>
1095      * <script type="text/javascript">
1096      * (function () {
1097      *   var board = JXG.JSXGraph.initBoard('ee7f2d68-75fc-4ec0-9931-c76918427e63', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false});
1098      *   var p1 = board.create('point', [0, 3]);
1099      *   var p2 = board.create('point', [1, 3]);
1100      *   var l1 = board.create('line', [p1, p2]);
1101      *   var t = board.create('ticks', [l1, 2], {ticksDistance: 2});
1102      * })();
1103      * </script><pre>
1104      */
1105     JXG.createTicks = function (board, parents, attributes) {
1106         var el, dist,
1107             attr = Type.copyAttributes(attributes, board.options, 'ticks');
1108 
1109         if (parents.length < 2) {
1110             dist = attr.ticksdistance;
1111         } else {
1112             dist = parents[1];
1113         }
1114 
1115         if (parents[0].elementClass === Const.OBJECT_CLASS_LINE) {
1116             el = new JXG.Ticks(parents[0], dist, attr);
1117         } else {
1118             throw new Error("JSXGraph: Can't create Ticks with parent types '" + (typeof parents[0]) + "'.");
1119         }
1120 
1121         // deprecated
1122         if (Type.isFunction(attr.generatelabelvalue)) {
1123             el.generateLabelText = attr.generatelabelvalue;
1124         }
1125         if (Type.isFunction(attr.generatelabeltext)) {
1126             el.generateLabelText = attr.generatelabeltext;
1127         }
1128 
1129         el.setParents(parents[0]);
1130         el.isDraggable = true;
1131         el.fullUpdate(parents[0].visPropCalc.visible);
1132 
1133         return el;
1134     };
1135 
1136     /**
1137      * @class Hashes can be used to mark congruent lines.
1138      * @pseudo
1139      * @description
1140      * @name Hatch
1141      * @augments JXG.Ticks
1142      * @constructor
1143      * @type JXG.Ticks
1144      * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
1145      * @param {JXG.Line} line The line the hatch marks are going to be attached to.
1146      * @param {Number} numberofhashes Number of dashes.
1147      * @example
1148      * // Create an axis providing two coord pairs.
1149      *   var p1 = board.create('point', [0, 3]);
1150      *   var p2 = board.create('point', [1, 3]);
1151      *   var l1 = board.create('line', [p1, p2]);
1152      *   var t = board.create('hatch', [l1, 3]);
1153      * </pre><div class="jxgbox" id="4a20af06-4395-451c-b7d1-002757cf01be" style="width: 300px; height: 300px;"></div>
1154      * <script type="text/javascript">
1155      * (function () {
1156      *   var board = JXG.JSXGraph.initBoard('4a20af06-4395-451c-b7d1-002757cf01be', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false});
1157      *   var p1 = board.create('point', [0, 3]);
1158      *   var p2 = board.create('point', [1, 3]);
1159      *   var l1 = board.create('line', [p1, p2]);
1160      *   var t = board.create('hatch', [l1, 3]);
1161      * })();
1162      * </script><pre>
1163      *
1164      * @example
1165      * // Alter the position of the hatch
1166      * var board = JXG.JSXGraph.initBoard('jxgbox', {boundingbox: [-10, 10, 10, -5], keepaspectratio:true});
1167      *
1168      * var p = board.create('point', [-5, 0]);
1169      * var q = board.create('point', [5, 0]);
1170      * var li = board.create('line', [p, q]);
1171      * var h = board.create('hatch', [li, 2], {anchor: 0.2});
1172      *
1173      * </pre><div id="05d720ee-99c9-11e6-a9c7-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div>
1174      * <script type="text/javascript">
1175      *     (function() {
1176      *         var board = JXG.JSXGraph.initBoard('05d720ee-99c9-11e6-a9c7-901b0e1b8723',
1177      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
1178      *     var board = JXG.JSXGraph.initBoard('jxgbox', {boundingbox: [-10, 10, 10, -5], keepaspectratio:true});
1179      *
1180      *     var p = board.create('point', [-5, 0]);
1181      *     var q = board.create('point', [5, 0]);
1182      *     var li = board.create('line', [p, q]);
1183      *     var h = board.create('hatch', [li, 2], {anchor: 0.2});
1184      *
1185      *     })();
1186      *
1187      * </script><pre>
1188      *
1189      */
1190     JXG.createHatchmark = function (board, parents, attributes) {
1191         var num, i, base, width, totalwidth, el,
1192             pos = [],
1193             attr = Type.copyAttributes(attributes, board.options, 'hatch');
1194 
1195         if (parents[0].elementClass !== Const.OBJECT_CLASS_LINE || typeof parents[1] !== 'number') {
1196             throw new Error("JSXGraph: Can't create Hatch mark with parent types '" + (typeof parents[0]) + "' and '" + (typeof parents[1]) + " and ''" + (typeof parents[2]) + "'.");
1197         }
1198 
1199         num = parents[1];
1200         width = attr.ticksdistance;
1201         totalwidth = (num - 1) * width;
1202         base = -totalwidth * 0.5;
1203 
1204         for (i = 0; i < num; i++) {
1205             pos[i] = base + i * width;
1206         }
1207 
1208         el = board.create('ticks', [parents[0], pos], attr);
1209         el.elType = 'hatch';
1210 
1211         return el;
1212     };
1213 
1214     JXG.registerElement('ticks', JXG.createTicks);
1215     JXG.registerElement('hash', JXG.createHatchmark);
1216     JXG.registerElement('hatch', JXG.createHatchmark);
1217 
1218     return {
1219         Ticks: JXG.Ticks,
1220         createTicks: JXG.createTicks,
1221         createHashmark: JXG.createHatchmark,
1222         createHatchmark: JXG.createHatchmark
1223     };
1224 });
1225