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