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