1 /*
  2     Copyright 2008,2009
  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: you can redistribute it and/or modify
 13     it under the terms of the GNU Lesser General Public License as published by
 14     the Free Software Foundation, either version 3 of the License, or
 15     (at your option) any later version.
 16 
 17     JSXGraph is distributed in the hope that it will be useful,
 18     but WITHOUT ANY WARRANTY; without even the implied warranty of
 19     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 20     GNU Lesser General Public License for more details.
 21 
 22     You should have received a copy of the GNU Lesser General Public License
 23     along with JSXGraph.  If not, see <http://www.gnu.org/licenses/>.
 24 */
 25 
 26 /**
 27  * @fileoverview In this file the geometry object Ticks is defined. Ticks provides
 28  * methods for creation and management of ticks on an axis.
 29  * @author graphjs
 30  * @version 0.1
 31  */
 32 
 33 /**
 34  * Creates ticks for an axis.
 35  * @class Ticks provides methods for creation and management
 36  * of ticks on an axis.
 37  * @param {JXG.Line} line Reference to the axis the ticks are drawn on.
 38  * @param {Number|Array} ticks Number defining the distance between two major ticks or an array defining static ticks.
 39  * @param {Object} attributes Properties
 40  * @see JXG.Line#addTicks
 41  * @constructor
 42  * @extends JXG.GeometryElement
 43  */
 44 JXG.Ticks = function (line, ticks, attributes) {
 45     this.constructor(line.board, attributes, JXG.OBJECT_TYPE_TICKS, JXG.OBJECT_CLASS_OTHER);
 46 
 47     /**
 48      * The line the ticks belong to.
 49      * @type JXG.Line
 50      */
 51     this.line = line;
 52 
 53     /**
 54      * The board the ticks line is drawn on.
 55      * @type JXG.Board
 56      */
 57     this.board = this.line.board;
 58 
 59     /**
 60      * A function calculating ticks delta depending on the ticks number.
 61      * @type Function
 62      */
 63     this.ticksFunction = null;
 64 
 65     /**
 66      * Array of fixed ticks.
 67      * @type Array
 68      */
 69     this.fixedTicks = null;
 70 
 71     /**
 72      * Equidistant ticks. Distance is defined by ticksFunction
 73      * @type Boolean
 74      */
 75     this.equidistant = false;
 76 
 77     if(JXG.isFunction(ticks)) {
 78         this.ticksFunction = ticks;
 79         throw new Error("Function arguments are no longer supported.");
 80     } else if(JXG.isArray(ticks)) {
 81         this.fixedTicks = ticks;
 82     } else {
 83         if(Math.abs(ticks) < JXG.Math.eps)
 84             ticks = attributes.defaultdistance;
 85         this.ticksFunction = function (i) { return ticks; };
 86         this.equidistant = true;
 87     }
 88 
 89     /**
 90      * Least distance between two ticks, measured in pixels.
 91      * @type int
 92      */
 93     this.minTicksDistance = attributes.minticksdistance;
 94 
 95     /**
 96      * Maximum distance between two ticks, measured in pixels. Is used only when insertTicks
 97      * is set to true.
 98      * @type int
 99      * @see #insertTicks
100      * @deprecated This value will be ignored.
101      */
102     this.maxTicksDistance = attributes.maxticksdistance;
103 
104     /**
105      * Array where the labels are saved. There is an array element for every tick,
106      * even for minor ticks which don't have labels. In this case the array element
107      * contains just <tt>null</tt>.
108      * @type Array
109      */
110     this.labels = [];
111 
112     this.id = this.line.addTicks(this);
113     this.board.setId(this,'Ti');
114 };
115 
116 JXG.Ticks.prototype = new JXG.GeometryElement();
117 
118 JXG.extend(JXG.Ticks.prototype, /** @lends JXG.Ticks.prototype */ {
119     // documented in JXG.GeometryElement
120     hasPoint: function (x, y) {
121        return false;
122     },
123 
124     /**
125      * (Re-)calculates the ticks coordinates.
126      */
127     calculateTicksCoordinates: function() {
128             // Point 1 of the line
129         var p1 = this.line.point1,
130             // Point 2 of the line
131             p2 = this.line.point2,
132             // Distance between the two points from above
133             distP1P2 = p1.Dist(p2),
134             // Distance of X coordinates of two major ticks
135             // Initialized with the distance of Point 1 to a point between Point 1 and Point 2 on the line and with distance 1
136             // this equals always 1 for lines parallel to x = 0 or y = 0. It's only important for lines other than that.
137             deltaX = (p2.coords.usrCoords[1] - p1.coords.usrCoords[1])/distP1P2,
138             // The same thing for Y coordinates
139             deltaY = (p2.coords.usrCoords[2] - p1.coords.usrCoords[2])/distP1P2,
140             // Distance of p1 to the unit point in screen coordinates
141             distScr = p1.coords.distance(JXG.COORDS_BY_SCREEN, new JXG.Coords(JXG.COORDS_BY_USER, [p1.coords.usrCoords[1] + deltaX, p1.coords.usrCoords[2] + deltaY], this.board)),
142             // Distance between two major ticks in user coordinates
143             ticksDelta = (this.equidistant ? this.ticksFunction(1) : 1),
144             // This factor is for enlarging ticksDelta and it switches between 5 and 2
145             // Hence, if two major ticks are too close together they'll be expanded to a distance of 5
146             // if they're still too close together, they'll be expanded to a distance of 10 etc
147             factor = 5,
148             // Coordinates of the current tick
149             tickCoords,
150             // Coordinates of the first drawn tick
151             startTick,
152             // a counter
153             i,
154             // the distance of the tick to p1. Is displayed on the board using a label
155             // for majorTicks
156             tickPosition,
157             // infinite or finite tick length
158             style,
159             // new position
160             nx = 0,
161             ny = 0,
162             ti,
163 			dirs = 2, dir = -1, 
164 			center, d, bb, perp,
165             
166             // the following variables are used to define ticks height and slope
167             eps = JXG.Math.eps, pos, lb, ub,
168             distMaj = this.visProp.majorheight/2,
169             distMin = this.visProp.minorheight/2,
170             dxMaj, dyMaj,
171             dxMin, dyMin;
172         // END OF variable declaration
173 
174            
175         // Grid-like ticks
176         if (this.visProp.minorheight < 0)  {
177             this.minStyle = 'infinite';
178         } else {
179             this.minStyle = 'finite';
180         }
181             
182         if(this.visProp.majorheight < 0) {
183             this.majStyle = 'infinite';
184         } else {
185             this.majStyle = 'finite';
186         }
187 
188         // Set lower and upper bound for the tick distance.
189         // This is necessary for segments.
190         if (this.line.visProp.straightfirst) {
191             lb = Number.NEGATIVE_INFINITY;
192         } else {
193             lb = 0 + eps;
194         }
195 
196         if (this.line.visProp.straightlast) {
197             ub = Number.POSITIVE_INFINITY;
198         } else {
199             ub = distP1P2 - eps;
200         }
201 
202         // this piece of code used to be in AbstractRenderer.updateAxisTicksInnerLoop
203         // and has been moved in here to clean up the renderers code.
204         //
205         // The code above only calculates the position of the ticks. The following code parts
206         // calculate the dx and dy values which make ticks out of this positions, i.e. from the
207         // position (p_x, p_y) calculated above we have to draw a line from
208         // (p_x - dx, py - dy) to (p_x + dx, p_y + dy) to get a tick.
209         dxMaj = this.line.stdform[1];
210         dyMaj = this.line.stdform[2];
211         dxMin = dxMaj;
212         dyMin = dyMaj;
213         d = Math.sqrt(dxMaj*dxMaj + dyMaj*dyMaj);
214         dxMaj *= distMaj/d;
215         dyMaj *= distMaj/d;
216         dxMin *= distMin/d;
217         dyMin *= distMin/d;
218 
219         // Begin cleanup
220         this.removeTickLabels();
221 
222         // initialize storage arrays
223         // ticks stores the ticks coordinates
224         this.ticks = [];
225         
226         // labels stores the text to display beside the ticks
227         this.labels = [];
228         // END cleanup
229         
230         // we have an array of fixed ticks we have to draw
231         if(!this.equidistant) {
232             for (i = 0; i < this.fixedTicks.length; i++) {
233                 nx = p1.coords.usrCoords[1] + this.fixedTicks[i]*deltaX;
234                 ny = p1.coords.usrCoords[2] + this.fixedTicks[i]*deltaY;
235                 tickCoords = new JXG.Coords(JXG.COORDS_BY_USER, [nx, ny], this.board);
236                 
237                 ti = this._tickEndings(tickCoords, dxMaj, dyMaj, dxMin, dyMin, /*major:*/ true);
238 				// Compute the start position and the end position of a tick.
239 				// If both positions are out of the canvas, ti is empty.
240                 if (ti.length==2 && this.fixedTicks[i]>=lb && this.fixedTicks[i]<ub) {
241                     this.ticks.push(ti);
242                 }
243                 this.labels.push(this._makeLabel(this.fixedTicks[i], tickCoords, this.board, this.visProp.drawlabels, this.id, i));
244                 // visibility test missing
245             }
246             return;
247         }
248 
249         // ok, we have equidistant ticks and not special ticks, so we continue here with generating them:
250         // adjust distances
251         if (this.visProp.insertticks && this.minTicksDistance > JXG.Math.eps) {
252             ticksDelta = this._adjustTickDistance(ticksDelta, distScr, factor, p1.coords, deltaX, deltaY);
253             this.ticksDelta = ticksDelta;
254         }
255 
256         if (!this.visProp.insertticks) {
257             ticksDelta /= this.visProp.minorticks+1;
258         }
259 
260 		// We shoot into the middle of the canvas
261 		// to the tick position which is closest to the center
262 		// of the canvas. We do this by an orthogonal projection
263 		// of the canvas center to the line and by rounding of the
264 		// distance of the projected point to point1 of the line.
265 		// This position is saved in
266 		// center and startTick.
267         bb = this.board.getBoundingBox();
268         nx = (bb[0]+bb[2])*0.5;
269         ny = (bb[1]+bb[3])*0.5;
270 		
271 		// Project the center of the canvas to the line.
272 		perp = [nx*this.line.stdform[2]-ny*this.line.stdform[1], 
273 				-this.line.stdform[2], 
274 				 this.line.stdform[1]];
275 		center = JXG.Math.crossProduct(this.line.stdform, perp);
276 		center[1] /= center[0];
277 		center[2] /= center[0];
278 		center[0] = 1;
279 		// Round the distance of center to point1
280         tickCoords = new JXG.Coords(JXG.COORDS_BY_USER, center.slice(1), this.board);
281         d = p1.coords.distance(JXG.COORDS_BY_USER, tickCoords);
282         if ((p2.X()-p1.X())*(center[1]-p1.X())<0 || (p2.Y()-p1.Y())*(center[2]-p1.Y())<0) {
283             d *= -1;
284         }
285         tickPosition = Math.round(d/ticksDelta)*ticksDelta;
286 
287 		// Find the correct direction of center from point1
288         if (Math.abs(tickPosition)>JXG.Math.eps) {
289             dir = Math.abs(tickPosition)/tickPosition;
290         }
291 		// From now on, we jump around center
292         center[1] = p1.coords.usrCoords[1] + deltaX*tickPosition;
293         center[2] = p1.coords.usrCoords[2] + deltaY*tickPosition;
294         startTick = tickPosition;
295         tickPosition = 0;
296 
297         nx = center[1];
298         ny = center[2];
299         i = 0;          // counter for label ids
300 		// Now, we jump around center
301 		// until we are outside of the canvas.
302 		// If this is the case we proceed in the other
303 		// direction until we are out of the canvas in this direction, too.
304 		// Then we are done.
305         do {
306             tickCoords = new JXG.Coords(JXG.COORDS_BY_USER, [nx, ny], this.board);
307 
308 			// Test if tick is a major tick.
309             // This is the case if (dir*tickPosition+startTick)/ticksDelta is
310             // a multiple of the number of minorticks+1
311             if (Math.round((dir*tickPosition+startTick)/ticksDelta) % (this.visProp.minorticks+1) === 0) {
312                 tickCoords.major = true;
313             } else {
314                 tickCoords.major = false;
315             }
316             
317 			// Compute the start position and the end position of a tick.
318 			// If both positions are out of the canvas, ti is empty.
319             ti = this._tickEndings(tickCoords, dxMaj, dyMaj, dxMin, dyMin, tickCoords.major);
320             if (ti.length==2) {  // The tick has an overlap with the board
321                 pos = dir*tickPosition+startTick;
322                 if ( (Math.abs(pos)<=eps && this.visProp.drawzero)
323                      || (pos>lb && pos<ub)
324                     ) {
325                     this.ticks.push(ti);
326                     if (tickCoords.major) {
327                         this.labels.push(this._makeLabel(pos, tickCoords, this.board, this.visProp.drawlabels, this.id, i));
328                     } else {
329                         this.labels.push(null);
330                     }
331                     i++;
332                 }
333                 
334 				// Toggle direction
335                 if (dirs==2) {
336                     dir *= (-1);
337                 } 
338 				// Increase distance from center
339                 if (dir==1 || dirs==1) {
340                     tickPosition += ticksDelta;  
341                 }
342             } else {
343                 dir *= (-1);
344                 dirs--;
345             }
346             
347             nx = center[1] + dir*deltaX*tickPosition;
348             ny = center[2] + dir*deltaY*tickPosition;
349         } while (dirs>0);
350     },
351     
352     _adjustTickDistance: function(ticksDelta, distScr, factor, p1c, deltaX, deltaY) {
353         var nx, ny;
354         
355         while (distScr > 4*this.minTicksDistance) {
356             ticksDelta /= 10;
357             nx = p1c.usrCoords[1] + deltaX*ticksDelta;
358             ny = p1c.usrCoords[2] + deltaY*ticksDelta;
359             distScr = p1c.distance(JXG.COORDS_BY_SCREEN, new JXG.Coords(JXG.COORDS_BY_USER, [nx, ny], this.board));
360         }
361 
362         // If necessary, enlarge ticksDelta
363         while (distScr < this.minTicksDistance) {
364             ticksDelta *= factor;
365             factor = (factor == 5 ? 2 : 5);
366             nx = p1c.usrCoords[1] + deltaX*ticksDelta;
367             ny = p1c.usrCoords[2] + deltaY*ticksDelta;
368             distScr = p1c.distance(JXG.COORDS_BY_SCREEN, new JXG.Coords(JXG.COORDS_BY_USER, [nx, ny], this.board));
369         }
370         return ticksDelta;
371     },
372     
373     _tickEndings: function(coords, dxMaj, dyMaj, dxMin, dyMin, major) {
374         var i, c, 
375             cw = this.board.canvasWidth,
376             ch = this.board.canvasHeight,
377             x = [-1000*cw, -1000*ch],
378             y = [-1000*cw, -1000*ch], 
379             dx, dy, dxs, dys, d,
380             s, style,
381             count = 0,
382             isInsideCanvas = false;
383 
384 		c = coords.scrCoords;
385 		if (major) {
386 			dx = dxMaj;
387 			dy = dyMaj;
388 			style = this.majStyle;
389 		} else {
390 			dx = dxMin;
391 			dy = dyMin;
392 			style = this.minStyle;
393 		}
394 		// This is necessary to compute the correct direction of infinite grid lines
395 		// if unitX!=unitY.
396 		dxs = dx*this.board.unitX;
397 		dys = dy*this.board.unitY;
398 
399 		// For all ticks regardless if of finite or infinite
400 		// tick length the intersection with the canvas border is 
401 		// computed. 
402 		
403 		// vertical tick 
404 		if (Math.abs(dx)<JXG.Math.eps) {
405 			x[0] = c[1];
406 			x[1] = c[1];
407 			y[0] = 0;
408 			y[1] = ch;
409 		// horizontal tick
410 		} else if (Math.abs(dy)<JXG.Math.eps) {
411 			x[0] = 0;
412 			x[1] = cw;
413 			y[0] = c[2];
414 			y[1] = c[2];
415 		// other
416 		} else {
417 			count = 0;
418 			s = JXG.Math.crossProduct([0,0,1], [-dys*c[1]-dxs*c[2], dys, dxs]); // intersect with top
419 			s[1] /= s[0];
420 			if (s[1]>=0 && s[1]<=cw) {  
421 				x[count] = s[1];
422 				y[count] = 0;
423 				count++;
424 			}
425 			s = JXG.Math.crossProduct([0,1,0], [-dys*c[1]-dxs*c[2], dys, dxs]); // intersect with left
426 			s[2] /= s[0];
427 			if (s[2]>=0 && s[2]<=ch) {  
428 				x[count] = 0;
429 				y[count] = s[2];
430 				count++;
431 			}
432 			if (count<2) {
433 				s = JXG.Math.crossProduct([ch*ch,0,-ch], [-dys*c[1]-dxs*c[2], dys, dxs]); // intersect with bottom
434 				s[1] /= s[0];
435 				if (s[1]>=0 && s[1]<=cw) {  
436 					x[count] = s[1];
437 					y[count] = ch;
438 					count++;
439 				}
440 			}
441 			if (count<2) {
442 				s = JXG.Math.crossProduct([cw*cw, -cw, 0], [-dys*c[1]-dxs*c[2], dys, dxs]); // intersect with right
443 				s[2] /= s[0];
444 				if (s[2]>=0 && s[2]<=ch) {  
445 					x[count] = cw;
446 					y[count] = s[2];
447 				}
448 			}
449 		}
450 		if ((x[0]>=0 && x[0]<=cw && y[0]>=0 && y[0]<=ch ) 
451 			|| 
452 			(x[1]>=0 && x[1]<=cw && y[1]>=0 && y[1]<=ch)
453 		   ) {
454 			isInsideCanvas = true;
455 		} else { 
456 			isInsideCanvas = false;
457 		}
458 		// finite tick length
459 		if (style=='finite') {
460 			d = Math.sqrt(this.board.unitX*this.board.unitX+this.board.unitY*this.board.unitY);
461 			x[0] = c[1] + dx*this.board.unitX/d;
462 			y[0] = c[2] - dy*this.board.unitY/d;
463 			x[1] = c[1] - dx*this.board.unitX/d;
464 			y[1] = c[2] + dy*this.board.unitY/d;
465 		}
466         if (isInsideCanvas) {
467             return [x,y];
468         } else { 
469             return [];
470         }
471     },
472 
473     /** 
474      * Create a tick label 
475      * @private
476      **/
477     _makeLabel: function(pos, newTick, board, drawLabels, id, i) {
478                 var labelText, label;
479 
480                 if (!drawLabels) {
481                     return null;
482                 }
483                 
484                 labelText = pos.toString();
485                 if (Math.abs(pos) < JXG.Math.eps) {
486                     labelText = '0';
487                 }
488 
489                 if(labelText.length > 5 || labelText.indexOf('e') != -1) {
490                     labelText = pos.toPrecision(3).toString();
491                 }
492                 if (labelText.indexOf('.') > -1) {
493                     // trim trailing zeros
494                     labelText = labelText.replace(/0+$/, '');
495                     // trim trailing .
496                     labelText = labelText.replace(/\.$/, '');
497                 }
498                 
499                 label = JXG.createText(board, [newTick.usrCoords[1], newTick.usrCoords[2], labelText], {
500                     id: id + i + 'Label',
501                     isLabel: true,
502                     layer: board.options.layer.line,
503                     highlightStrokeColor: board.options.text.strokeColor,
504                     highlightStrokeWidth: board.options.text.strokeWidth,
505                     highlightStrokeOpacity: board.options.text.strokeOpacity
506                 });
507                 label.isDraggable = false;
508                 label.dump = false;
509                 label.distanceX = 4;
510                 label.distanceY = -parseInt(label.visProp.fontsize)+3; //-9;
511                 label.setCoords(newTick.usrCoords[1] + label.distanceX / (board.unitX),
512                                 newTick.usrCoords[2] + label.distanceY / (board.unitY));
513                 
514                 label.visProp.visible = drawLabels;
515                 label.prepareUpdate().update().updateRenderer();
516                 return label;
517      },
518 
519     /**
520      * Removes the HTML divs of the tick labels
521      * before repositioning
522      */
523     removeTickLabels: function () {
524         var j;
525 
526         // remove existing tick labels
527         if(this.ticks != null) {
528             if ((this.board.needsFullUpdate||this.needsRegularUpdate) &&
529                 !(this.board.options.renderer=='canvas'&&this.board.options.text.display=='internal')
530                ) {
531                 for(j=0; j<this.ticks.length; j++) {
532                     if(this.labels[j]!=null && this.labels[j].visProp.visible) {
533 //                        this.board.renderer.remove(this.labels[j].rendNode);
534                         this.board.removeObject(this.labels[j]);
535                     }
536                 }
537             }
538         }
539     },
540 
541     /**
542      * Recalculate the tick positions and the labels.
543      */
544     update: function () {
545         if (this.needsUpdate) {
546             this.calculateTicksCoordinates();
547         }
548         return this;
549     },
550 
551     /**
552      * Uses the boards renderer to update the arc.
553      */
554     updateRenderer: function () {
555         if (this.needsUpdate) {
556             if (this.ticks) {
557                 this.board.renderer.updateTicks(this, this.dxMaj, this.dyMaj, this.dxMin, this.dyMin, this.minStyle, this.majStyle);
558             }
559             this.needsUpdate = false;
560         }
561         return this;
562     },
563 
564     hideElement: function () {
565         var i;
566 
567         this.visProp.visible = false;
568         this.board.renderer.hide(this);
569 
570         for (i=0; i<this.labels.length; i++) {
571             if (JXG.exists(this.labels[i]))
572                 this.labels[i].hideElement();
573         }
574 
575         return this;
576     },
577 
578     showElement: function () {
579         var i;
580 
581         this.visProp.visible = true;
582         this.board.renderer.show(this);
583 
584         for (i=0; i<this.labels.length; i++) {
585             if (JXG.exists(this.labels[i]))
586                 this.labels[i].showElement();
587         }
588 
589         return this;
590     }
591 });
592 
593 /**
594  * @class Ticks are used as distance markers on a line.
595  * @pseudo
596  * @description
597  * @name Ticks
598  * @augments JXG.Ticks
599  * @constructor
600  * @type JXG.Ticks
601  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
602  * @param {JXG.Line,Number} line,_distance The parents consist of the line the ticks are going to be attached to and optional the
603  * distance between two major ticks. If no distance is given the attribute {@link JXG.Ticks#ticksDistance} is used.
604  * @example
605  * // Create an axis providing two coord pairs.
606  *   var p1 = board.create('point', [0, 3]);
607  *   var p2 = board.create('point', [1, 3]);
608  *   var l1 = board.create('line', [p1, p2]);
609  *   var t = board.create('ticks', [l1], {ticksDistance: 2});
610  * </pre><div id="ee7f2d68-75fc-4ec0-9931-c76918427e63" style="width: 300px; height: 300px;"></div>
611  * <script type="text/javascript">
612  * (function () {
613  *   var board = JXG.JSXGraph.initBoard('ee7f2d68-75fc-4ec0-9931-c76918427e63', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false});
614  *   var p1 = board.create('point', [0, 3]);
615  *   var p2 = board.create('point', [1, 3]);
616  *   var l1 = board.create('line', [p1, p2]);
617  *   var t = board.create('ticks', [l1], {ticksDistance: 2});
618  * })();
619  * </script><pre>
620  */
621 JXG.createTicks = function(board, parents, attributes) {
622     var el, dist,
623         attr = JXG.copyAttributes(attributes, board.options, 'ticks');
624 
625     if (parents.length < 2) {
626         dist = attributes.ticksDistance;
627     } else {
628         dist = parents[1];
629     }
630 
631     if ( (parents[0].elementClass == JXG.OBJECT_CLASS_LINE) && (JXG.isFunction(parents[1]) || JXG.isArray(parents[1]) || JXG.isNumber(parents[1]))) {
632         el = new JXG.Ticks(parents[0], dist, attr);
633     } else
634         throw new Error("JSXGraph: Can't create Ticks with parent types '" + (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'.");
635 
636     return el;
637 };
638 
639 JXG.JSXGraph.registerElement('ticks', JXG.createTicks);
640