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