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