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