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