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,Function} ticks Number, array or function defining the ticks.
 39  * @param {int} major Every major-th tick is drawn with heightmajorHeight, the other ones are drawn with height minorHeight.
 40  * @param {int} majorHeight The height used to draw major ticks.
 41  * @param {int} minorHeight The height used to draw minor ticks.
 42  * @param {String} id Unique identifier for this object.  If null or an empty string is given,
 43  * an unique id will be generated by Board.
 44  * @param {String} name Not necessarily unique name, won't be visible or used by this object.
 45  * @see JXG.Board#addTicks
 46  * @constructor
 47  * @extends JXG.GeometryElement
 48  */
 49 JXG.Ticks = function (line, ticks, minor, majorHeight, minorHeight, id, name, layer) {
 50     /* Call the constructor of GeometryElement */
 51     this.constructor();
 52 
 53     /**
 54      * Type of GeometryElement, value is OBJECT_TYPE_ARC.
 55      * @final
 56      * @type int
 57      */
 58     this.type = JXG.OBJECT_TYPE_TICKS;
 59 
 60     /**
 61      * Class of the element, value is OBJECT_CLASS_CIRCLE.
 62      * @final
 63      * @type int
 64      */
 65     this.elementClass = JXG.OBJECT_CLASS_OTHER;
 66 
 67     /**
 68      * Set the display layer.
 69      */
 70     //if (layer == null) layer = board.options.layer['line']; // no board available
 71     //this.layer = layer;
 72 
 73     /**
 74      * The line the ticks belong to.
 75      * @type JXG.Line
 76      */
 77     this.line = line;
 78 
 79     /**
 80      * The board the ticks line is drawn on.
 81      * @type JXG.Board
 82      */
 83     this.board = this.line.board;
 84 
 85     /**
 86      * A function calculating ticks delta depending on the ticks number.
 87      * @type Function
 88      */
 89     this.ticksFunction = null;
 90 
 91     /**
 92      * Array of fixed ticks.
 93      * @type Array
 94      */
 95     this.fixedTicks = null;
 96 
 97     /**
 98      * Equidistant ticks. Distance is defined by ticksFunction
 99      * @type bool
100      */
101     this.equidistant = false;
102 
103     if(JXG.isFunction(ticks)) {
104         this.ticksFunction = ticks;
105         throw new Error("Function arguments are no longer supported.");
106     } else if(JXG.isArray(ticks))
107         this.fixedTicks = ticks;
108     else {
109         if(Math.abs(ticks) < JXG.Math.eps)
110             ticks = this.board.options.line.ticks.defaultDistance;
111         this.ticksFunction = function (i) { return ticks; };
112         this.equidistant = true;
113     }
114 
115     /**
116      * minorTicks is the number of minor ticks between two major ticks.
117      * @type int
118      */
119     this.minorTicks = ( (minor == null)? this.board.options.line.ticks.minorTicks : minor);
120     if(this.minorTicks < 0)
121         this.minorTicks = -this.minorTicks;
122 
123     /**
124      * Total height of a major tick.
125      * @type int
126      */
127     this.majorHeight = ( (majorHeight == null) || (majorHeight == 0) ? this.board.options.line.ticks.majorHeight : majorHeight);
128     if(this.majorHeight < 0)
129         this.majorHeight = -this.majorHeight;
130 
131     /**
132      * Total height of a minor tick.
133      * @type int
134      */
135     this.minorHeight = ( (minorHeight == null) || (minorHeight == 0) ? this.board.options.line.ticks.minorHeight : minorHeight);
136     if(this.minorHeight < 0)
137         this.minorHeight = -this.minorHeight;
138 
139     /**
140      * Least distance between two ticks, measured in pixels.
141      * @type int
142      */
143     this.minTicksDistance = this.board.options.line.ticks.minTicksDistance;
144 
145     /**
146      * Maximum distance between two ticks, measured in pixels. Is used only when insertTicks
147      * is set to true.
148      * @type int
149      * @see #insertTicks
150      */
151     this.maxTicksDistance = this.board.options.line.ticks.maxTicksDistance;
152 
153     /**
154      * If the distance between two ticks is too big we could insert new ticks. If insertTicks
155      * is <tt>true</tt>, we'll do so, otherwise we leave the distance as is.
156      * This option is ignored if equidistant is false.
157      * @type bool
158      * @see #equidistant
159      * @see #maxTicksDistance
160      */
161     this.insertTicks = this.board.options.line.ticks.insertTicks;
162 
163     /**
164      * Draw the zero tick, that lies at line.point1?
165      * @type bool
166      */
167     this.drawZero = this.board.options.line.ticks.drawZero;
168 
169     /**
170      * Draw labels yes/no
171      * @type bool
172      */
173     this.drawLabels = this.board.options.line.ticks.drawLabels;
174 
175     /**
176      * Array where the labels are saved. There is an array element for every tick,
177      * even for minor ticks which don't have labels. In this case the array element
178      * contains just <tt>null</tt>.
179      * @type array
180      */
181     this.labels = [];
182 
183     /* Call init defined in GeometryElement to set board, id and name property */
184     this.init(this.board, id, name);
185 
186     this.visProp['visible'] = true;
187 
188     this.visProp['fillColor'] = this.line.visProp['fillColor'];
189     this.visProp['highlightFillColor'] = this.line.visProp['highlightFillColor'];
190     this.visProp['strokeColor'] = this.line.visProp['strokeColor'];
191     this.visProp['highlightStrokeColor'] = this.line.visProp['highlightStrokeColor'];
192     this.visProp['strokeWidth'] = this.line.visProp['strokeWidth'];
193 
194     /* Register ticks at line*/
195     this.id = this.line.addTicks(this);
196     /* Register ticks at board*/
197     this.board.setId(this,'Ti');
198 };
199 
200 JXG.Ticks.prototype = new JXG.GeometryElement;
201 
202 /**
203  * Always returns false.
204  * @param {int} x Coordinate in x direction, screen coordinates.
205  * @param {int} y Coordinate in y direction, screen coordinates.
206  * @return {bool} Always returns false.
207  */
208 JXG.Ticks.prototype.hasPoint = function (x, y) {
209    return false;
210 };
211 
212 /**
213  * (Re-)calculates the ticks coordinates.
214  */
215 JXG.Ticks.prototype.calculateTicksCoordinates = function() {
216     
217     /*
218      * 
219      * It's all new in here but works pretty well.
220      * Known bugs:
221      *   * Special ticks behave oddly. See example ticked_lines.html and drag P2 around P1.
222      * 
223      */
224         // Point 1 of the line
225     var p1 = this.line.point1,
226         // Point 2 of the line
227         p2 = this.line.point2,
228         // Distance between the two points from above
229         distP1P2 = p1.coords.distance(JXG.COORDS_BY_USER, p2.coords),
230         // Distance of X coordinates of two major ticks
231         // Initialized with the distance of Point 1 to a point between Point 1 and Point 2 on the line and with distance 1
232         deltaX = (p2.coords.usrCoords[1] - p1.coords.usrCoords[1])/distP1P2,
233         // The same thing for Y coordinates
234         deltaY = (p2.coords.usrCoords[2] - p1.coords.usrCoords[2])/distP1P2,
235         // Distance of p1 to the unit point in screen coordinates
236         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)),
237         // Distance between two major ticks in user coordinates
238         ticksDelta = (this.equidistant ? this.ticksFunction(1) : 1),
239         // This factor is for enlarging ticksDelta and it switches between 5 and 2
240         // Hence, if two major ticks are too close together they'll be expanded to a distance of 5
241         // if they're still too close together, they'll be expanded to a distance of 10 etc
242         factor = 5,
243         // Edge points: This is where the display of the line starts and ends, e.g. the intersection points
244         // of the line with the edges of the viewing area if the line is a straight.
245         e1, e2,
246         // Which direction do we go? Plus or Minus
247         dir = 1,
248         // what's the first/last tick to draw?
249         begin, end,
250         // Coordinates of the current tick
251         tickCoords,
252         // Coordinates of the first drawn tick
253         startTick,
254         // a counter
255         i,
256         // the distance of the tick to p1. Is displayed on the board using a label
257         // for majorTicks
258         tickPosition,
259         // creates a label
260         makeLabel = function(pos, newTick, board, drawLabels, id) {
261             var labelText, label;
262             
263             labelText = pos.toString();
264             if(labelText.length > 5)
265                 labelText = pos.toPrecision(3).toString();
266             label = new JXG.Text(board, labelText, null, [newTick.usrCoords[1], newTick.usrCoords[2]], id+i+"Label", '', null, true, board.options.text.defaultDisplay);
267             label.distanceX = 0;
268             label.distanceY = -10;
269             label.setCoords(newTick.usrCoords[1]*1+label.distanceX/(board.stretchX), 
270                             newTick.usrCoords[2]*1+label.distanceY/(board.stretchY));
271             
272             label.visProp['visible'] = drawLabels;
273             return label;
274         },
275         
276         respDelta = function(val) {
277             return Math.floor(val) - (Math.floor(val) % ticksDelta);
278         },
279         
280         // the following variables are used to define ticks height and slope
281         eps = JXG.Math.eps,
282         slope = -this.line.getSlope(),
283         distMaj = this.majorHeight/2,
284         distMin = this.minorHeight/2,
285         dxMaj = 0, dyMaj = 0,
286         dxMin = 0, dyMin = 0;
287         
288     // END OF variable declaration
289         
290 
291     // this piece of code used to be in AbstractRenderer.updateAxisTicksInnerLoop
292     // and has been moved in here to clean up the renderers code.
293     //
294     // The code above only calculates the position of the ticks. The following code parts
295     // calculate the dx and dy values which make ticks out of this positions, i.e. from the
296     // position (p_x, p_y) calculated above we have to draw a line from
297     // (p_x - dx, py - dy) to (p_x + dx, p_y + dy) to get a tick.
298 
299     if(Math.abs(slope) < eps) {
300         // if the slope of the line is (almost) 0, we can set dx and dy directly
301         dxMaj = 0;
302         dyMaj = distMaj;
303         dxMin = 0;
304         dyMin = distMin;
305     } else if((Math.abs(slope) > 1/eps) || (isNaN(slope))) {
306         // if the slope of the line is (theoretically) infinite, we can set dx and dy directly
307         dxMaj = distMaj;
308         dyMaj = 0;
309         dxMin = distMin;
310         dyMin = 0;
311     } else {
312         // here we have to calculate dx and dy depending on the slope and the length of the tick (dist)
313         // if slope is the line's slope, the tick's slope is given by
314         //
315         //            1          dy
316         //     -   -------  =   ----                 (I)
317         //          slope        dx
318         //
319         // when dist is the length of the tick, using the pythagorean theorem we get
320         //
321         //     dx*dx + dy*dy = dist*dist             (II)
322         //
323         // dissolving (I) by dy and applying that to equation (II) we get the following formulas for dx and dy
324         dxMaj = -distMaj/Math.sqrt(1/(slope*slope) + 1);
325         dyMaj = dxMaj/slope;
326         dxMin = -distMin/Math.sqrt(1/(slope*slope) + 1);
327         dyMin = dxMin/slope;
328     }
329 
330     // Begin cleanup
331     this.removeTickLabels();
332 
333     // initialize storage arrays
334     // ticks stores the ticks coordinates
335     this.ticks = new Array();
336     
337     // labels stores the text to display beside the ticks
338     this.labels = new Array();
339     // END cleanup
340     
341     // calculate start (e1) and end (e2) points
342     // for that first copy existing lines point coordinates...
343     e1 = new JXG.Coords(JXG.COORDS_BY_USER, [p1.coords.usrCoords[1], p1.coords.usrCoords[2]], this.board);
344     e2 = new JXG.Coords(JXG.COORDS_BY_USER, [p2.coords.usrCoords[1], p2.coords.usrCoords[2]], this.board);
345         
346     // ... and calculate the drawn start and end point
347     this.board.renderer.calcStraight(this.line, e1, e2);
348         
349     if(!this.equidistant) {
350         // we have an array of fixed ticks we have to draw
351         var dx_minus = p1.coords.usrCoords[1]-e1.usrCoords[1];
352         var dy_minus = p1.coords.usrCoords[2]-e1.usrCoords[2];
353         var length_minus = Math.sqrt(dx_minus*dx_minus + dy_minus*dy_minus);
354 
355         var dx_plus = p1.coords.usrCoords[1]-e2.usrCoords[1];
356         var dy_plus = p1.coords.usrCoords[2]-e2.usrCoords[2];
357         var length_plus = Math.sqrt(dx_plus*dx_plus + dy_plus*dy_plus);
358 
359         // new ticks coordinates
360         var nx = 0;
361         var ny = 0;
362 
363         for(var i=0; i<this.fixedTicks.length; i++) {
364             // is this tick visible?
365             if((-length_minus <= this.fixedTicks[i]) && (this.fixedTicks[i] <= length_plus)) {
366                 if(this.fixedTicks[i] < 0) {
367                     nx = Math.abs(dx_minus) * this.fixedTicks[i]/length_minus;
368                     ny = Math.abs(dy_minus) * this.fixedTicks[i]/length_minus;
369                 } else {
370                     nx = Math.abs(dx_plus) * this.fixedTicks[i]/length_plus;
371                     ny = Math.abs(dy_plus) * this.fixedTicks[i]/length_plus;
372                 }
373 
374                 tickCoords = new JXG.Coords(JXG.COORDS_BY_USER, [p1.coords.usrCoords[1] + nx, p1.coords.usrCoords[2] + ny], this.board);
375                 this.ticks.push(tickCoords);
376                 this.ticks[this.ticks.length-1].major = true;
377                 
378                 this.labels.push(makeLabel(this.fixedTicks[i], tickCoords, this.board, this.drawLabels, this.id));
379             }
380         }
381         this.dxMaj = dxMaj;
382         this.dyMaj = dyMaj;
383         this.dxMin = dxMin;
384         this.dyMin = dyMin;
385         //this.board.renderer.updateTicks(this, dxMaj, dyMaj, dxMin, dyMin);
386         return;
387     } // ok, we have equidistant ticks and not special ticks, so we continue here with generating them:
388     
389     // adjust distances
390     while(distScr > 4*this.minTicksDistance) {
391         ticksDelta /= 10;
392         deltaX /= 10;
393         deltaY /= 10;
394 
395         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));
396     }
397 
398     // If necessary, enlarge ticksDelta
399     while(distScr < this.minTicksDistance) {
400         ticksDelta *= factor;
401         deltaX *= factor;
402         deltaY *= factor;
403 
404         factor = (factor == 5 ? 2 : 5);
405         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));
406     }
407 
408     /*
409      * In the following code comments are sometimes talking about "respect ticksDelta". this could be done
410      * by calculating the modulus of the distance wrt to ticksDelta and add resp. subtract a ticksDelta from that.
411      */
412 
413     // p1 is outside the visible area or the line is a segment
414     if(this.board.renderer.isSameDirection(p1.coords, e1, e2)) {
415         // calculate start and end points
416         begin = respDelta(p1.coords.distance(JXG.COORDS_BY_USER, e1));
417         end = p1.coords.distance(JXG.COORDS_BY_USER, e2);
418         
419         if(this.board.renderer.isSameDirection(p1.coords, p2.coords, e1)) {
420             if(this.line.visProp.straightFirst)
421                 begin -=  2*ticksDelta;
422         } else {
423             end = -1*end;
424             begin = -1*begin;
425             if(this.line.visProp.straightFirst)
426                 begin -= 2*ticksDelta
427         }
428         
429         // TODO: We should check here if the line is visible at all. If it's not visible but
430         // close to the viewport there may be drawn some ticks without a line visible.
431         
432     } else {
433         // p1 is inside the visible area and direction is PLUS
434 
435         // now we have to calculate the index of the first tick
436         if(!this.line.visProp.straightFirst) {
437             begin = 0; 
438         } else {
439             begin = -respDelta(p1.coords.distance(JXG.COORDS_BY_USER, e1)) - 2*ticksDelta;
440         }
441         
442         if(!this.line.visProp.straightLast) {
443             end = distP1P2;
444         } else {
445             end = p1.coords.distance(JXG.COORDS_BY_USER, e2);
446         }
447     }
448 
449     startTick = new JXG.Coords(JXG.COORDS_BY_USER, [p1.coords.usrCoords[1] + begin*deltaX/ticksDelta, p1.coords.usrCoords[2] + begin*deltaY/ticksDelta], this.board);
450     tickCoords = new JXG.Coords(JXG.COORDS_BY_USER, [p1.coords.usrCoords[1] + begin*deltaX/ticksDelta, p1.coords.usrCoords[2] + begin*deltaY/ticksDelta], this.board);
451     
452     deltaX /= this.minorTicks+1;
453     deltaY /= this.minorTicks+1;
454     
455 //    JXG.debug('begin: ' + begin + '; e1: ' + e1.usrCoords[1] + ', ' + e1.usrCoords[2]);
456 //    JXG.debug('end: ' + end + '; e2: ' + e2.usrCoords[1] + ', ' + e2.usrCoords[2]);
457     
458     
459     // After all the precalculations from above here finally comes the tick-production:
460     i = 0;
461     tickPosition = begin;
462     while(startTick.distance(JXG.COORDS_BY_USER, tickCoords) < Math.abs(end - begin) + JXG.Math.eps) {
463         if(i % (this.minorTicks+1) == 0) {
464             tickCoords.major = true;
465             this.labels.push(makeLabel(tickPosition, tickCoords, this.board, this.drawLabels, this.id));
466             tickPosition += ticksDelta;
467         } else {
468             tickCoords.major = false;
469             this.labels.push(null);
470         }
471         i++;
472 
473         this.ticks.push(tickCoords);
474         tickCoords = new JXG.Coords(JXG.COORDS_BY_USER, [tickCoords.usrCoords[1] + deltaX, tickCoords.usrCoords[2] + deltaY], this.board);
475         if(!this.drawZero && tickCoords.distance(JXG.COORDS_BY_USER, p1.coords) <= JXG.Math.eps) {
476             // zero point is always a major tick. hence, we have to set i = 0;
477             i++;
478             tickPosition += ticksDelta;
479             tickCoords = new JXG.Coords(JXG.COORDS_BY_USER, [tickCoords.usrCoords[1] + deltaX, tickCoords.usrCoords[2] + deltaY], this.board);
480         }
481     }
482 
483     this.dxMaj = dxMaj;
484     this.dyMaj = dyMaj;
485     this.dxMin = dxMin;
486     this.dyMin = dyMin;
487 };
488 
489 /**
490  * Removes the HTML divs of the tick labels
491  * before repositioning
492  */
493 JXG.Ticks.prototype.removeTickLabels = function () {
494     var j;
495     // BEGIN: clean up the mess we left from our last run through this function
496     // remove existing tick labels
497     if(this.ticks != null) {
498         if ((this.board.needsFullUpdate||this.needsRegularUpdate) && 
499             !(this.board.options.renderer=='canvas'&&this.board.options.text.defaultDisplay=='internal')
500            ) {
501             for(j=0; j<this.ticks.length; j++) {
502                 if(this.labels[j]!=null && this.labels[j].visProp['visible']) { 
503                     this.board.renderer.remove(this.labels[j].rendNode); 
504                 }
505             }
506         }
507     }
508 }; 
509 
510 /**
511  * Recalculate the tick positions and the labels.
512  */
513 JXG.Ticks.prototype.update = function () {
514     if (this.needsUpdate) {
515         this.calculateTicksCoordinates();
516     }
517     return this;
518 };
519 
520 /**
521  * Uses the boards renderer to update the arc.
522  */
523 JXG.Ticks.prototype.updateRenderer = function () {
524     if (this.needsUpdate) {
525         if (this.ticks) {
526             this.board.renderer.updateTicks(this, this.dxMaj, this.dyMaj, this.dxMin, this.dyMin);
527         }
528         this.needsUpdate = false;
529     }
530     return this;
531 };
532 
533 /**
534  * Creates new ticks.
535  * @param {JXG.Board} board The board the ticks are put on.
536  * @param {Array} parents Array containing a line and an array of positions, where ticks should be put on that line or
537  *   a function that calculates the distance based on the ticks number that is given as a parameter. E.g.:<br />
538  *   <tt>var ticksFunc = function(i) {</tt><br />
539  *   <tt>    return 2;</tt><br />
540  *   <tt>}</tt><br />
541  *   for ticks with distance 2 between each tick.
542  * @param {Object} attributs Object containing properties for the element such as stroke-color and visibility. See @see JXG.GeometryElement#setProperty
543  * @type JXG.Ticks
544  * @return Reference to the created ticks object.
545  */
546 JXG.createTicks = function(board, parents, attributes) {
547     var el;
548     attributes = JXG.checkAttributes(attributes,{layer:null});
549     if ( (parents[0].elementClass == JXG.OBJECT_CLASS_LINE) && (JXG.isFunction(parents[1]) || JXG.isArray(parents[1]) || JXG.isNumber(parents[1]))) {
550         el = new JXG.Ticks(parents[0], parents[1], attributes['minorTicks'], attributes['majHeight'], attributes['minHeight'], attributes['id'], attributes['name'], attributes['layer']);
551     } else
552         throw new Error("JSXGraph: Can't create Ticks with parent types '" + (typeof parents[0]) + "' and '" + (typeof parents[1]) + "' and '" + (typeof parents[2]) + "'.");
553 
554     return el;
555 };
556 
557 JXG.JSXGraph.registerElement('ticks', JXG.createTicks);
558