1 /* 2 Copyright 2008-2017 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 which have to be displayed in updateRenderer. 154 * @type {Array} 155 */ 156 this.labelData = []; 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.inherits.push(this.labels); 167 this.board.setId(this, 'Ti'); 168 }; 169 170 JXG.Ticks.prototype = new GeometryElement(); 171 172 JXG.extend(JXG.Ticks.prototype, /** @lends JXG.Ticks.prototype */ { 173 174 /** 175 * Ticks function: 176 * determines the distance (in user units) of two major ticks. 177 * See above in constructor and in @see JXG.GeometryElement#setAttribute 178 * 179 * @private 180 * @param {Number} ticks Distance between two major ticks 181 * @returns {Function} returns method ticksFunction 182 */ 183 makeTicksFunction: function (ticks) { 184 return function () { 185 var delta, b, dist; 186 187 if (Type.evaluate(this.visProp.insertticks)) { 188 b = this.getLowerAndUpperBounds(this.getZeroCoordinates(), 'ticksdistance'); 189 dist = b.upper - b.lower; 190 delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10)); 191 if (dist <= 6 * delta) { 192 delta *= 0.5; 193 } 194 return delta; 195 } 196 197 // upto 0.99.1: 198 return ticks; 199 }; 200 }, 201 202 /** 203 * Checks whether (x,y) is near the line. 204 * @param {Number} x Coordinate in x direction, screen coordinates. 205 * @param {Number} y Coordinate in y direction, screen coordinates. 206 * @returns {Boolean} True if (x,y) is near the line, False otherwise. 207 */ 208 hasPoint: function (x, y) { 209 var i, t, 210 len = (this.ticks && this.ticks.length) || 0, 211 r = this.board.options.precision.hasPoint + 212 Type.evaluate(this.visProp.strokewidth) * 0.5; 213 214 if (!Type.evaluate(this.line.visProp.scalable)) { 215 return false; 216 } 217 218 // Ignore non-axes and axes that are not horizontal or vertical 219 if (this.line.stdform[1] !== 0 && this.line.stdform[2] !== 0 && this.line.type !== Const.OBJECT_TYPE_AXIS) { 220 return false; 221 } 222 223 for (i = 0; i < len; i++) { 224 t = this.ticks[i]; 225 226 // Skip minor ticks 227 if (t[2]) { 228 // Ignore ticks at zero 229 if (!((this.line.stdform[1] === 0 && Math.abs(t[0][0] - this.line.point1.coords.scrCoords[1]) < Mat.eps) || 230 (this.line.stdform[2] === 0 && Math.abs(t[1][0] - this.line.point1.coords.scrCoords[2]) < Mat.eps))) { 231 // tick length is not zero, ie. at least one pixel 232 if (Math.abs(t[0][0] - t[0][1]) >= 1 || Math.abs(t[1][0] - t[1][1]) >= 1) { 233 if (this.line.stdform[1] === 0) { 234 // Allow dragging near axes only. 235 if (Math.abs(y - (t[1][0] + t[1][1]) * 0.5) < 2 * r && t[0][0] - r < x && x < t[0][1] + r) { 236 return true; 237 } 238 } else if (this.line.stdform[2] === 0) { 239 if (Math.abs(x - (t[0][0] + t[0][1]) * 0.5) < 2 * r && t[1][0] - r < y && y < t[1][1] + r) { 240 return true; 241 } 242 } 243 } 244 } 245 } 246 } 247 248 return false; 249 }, 250 251 /** 252 * Sets x and y coordinate of the tick. 253 * @param {number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 254 * @param {Array} coords coordinates in screen/user units 255 * @param {Array} oldcoords previous coordinates in screen/user units 256 * @returns {JXG.Ticks} this element 257 */ 258 setPositionDirectly: function (method, coords, oldcoords) { 259 var dx, dy, 260 c = new Coords(method, coords, this.board), 261 oldc = new Coords(method, oldcoords, this.board), 262 bb = this.board.getBoundingBox(); 263 264 if (!Type.evaluate(this.line.visProp.scalable)) { 265 return this; 266 } 267 268 // horizontal line 269 if (Math.abs(this.line.stdform[1]) < Mat.eps && Math.abs(c.usrCoords[1] * oldc.usrCoords[1]) > Mat.eps) { 270 dx = oldc.usrCoords[1] / c.usrCoords[1]; 271 bb[0] *= dx; 272 bb[2] *= dx; 273 this.board.setBoundingBox(bb, false); 274 // vertical line 275 } else if (Math.abs(this.line.stdform[2]) < Mat.eps && Math.abs(c.usrCoords[2] * oldc.usrCoords[2]) > Mat.eps) { 276 dy = oldc.usrCoords[2] / c.usrCoords[2]; 277 bb[3] *= dy; 278 bb[1] *= dy; 279 this.board.setBoundingBox(bb, false); 280 } 281 282 return this; 283 }, 284 285 /** 286 * (Re-)calculates the ticks coordinates. 287 * @private 288 */ 289 calculateTicksCoordinates: function () { 290 var coordsZero, bounds; 291 292 // Calculate Ticks width and height in Screen and User Coordinates 293 this.setTicksSizeVariables(); 294 // If the parent line is not finite, we can stop here. 295 if (Math.abs(this.dx) < Mat.eps && 296 Math.abs(this.dy) < Mat.eps) { 297 return; 298 } 299 300 // Get Zero 301 coordsZero = this.getZeroCoordinates(); 302 303 // Calculate lower bound and upper bound limits based on distance between p1 and centre and p2 and centre 304 bounds = this.getLowerAndUpperBounds(coordsZero); 305 306 // Clean up 307 this.ticks = []; 308 this.labelsData = []; 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 return this; 317 }, 318 319 /** 320 * Sets the variables used to set the height and slope of each tick. 321 * 322 * @private 323 */ 324 setTicksSizeVariables: function () { 325 var d, 326 distMaj = Type.evaluate(this.visProp.majorheight) * 0.5, 327 distMin = Type.evaluate(this.visProp.minorheight) * 0.5; 328 329 // ticks width and height in screen units 330 this.dxMaj = this.line.stdform[1]; 331 this.dyMaj = this.line.stdform[2]; 332 this.dxMin = this.dxMaj; 333 this.dyMin = this.dyMaj; 334 335 // ticks width and height in user units 336 this.dx = this.dxMaj; 337 this.dy = this.dyMaj; 338 339 // After this, the length of the vector (dxMaj, dyMaj) in screen coordinates is equal to distMaj pixel. 340 d = Math.sqrt( 341 this.dxMaj * this.dxMaj * this.board.unitX * this.board.unitX + 342 this.dyMaj * this.dyMaj * this.board.unitY * this.board.unitY 343 ); 344 this.dxMaj *= distMaj / d * this.board.unitX; 345 this.dyMaj *= distMaj / d * this.board.unitY; 346 this.dxMin *= distMin / d * this.board.unitX; 347 this.dyMin *= distMin / d * this.board.unitY; 348 349 // Grid-like ticks? 350 this.minStyle= (Type.evaluate(this.visProp.minorheight) < 0) ? 'infinite' : 'finite'; 351 this.majStyle= (Type.evaluate(this.visProp.majorheight) < 0) ? 'infinite' : 'finite'; 352 }, 353 354 /** 355 * Returns the coordinates of the point zero of the line. 356 * 357 * If the line is an {@link Axis}, the coordinates of the projection of the board's zero point is returned 358 * 359 * Otherwise, the coordinates of the point that acts as zero are established depending on the value of {@link JXG.Ticks#anchor} 360 * 361 * @returns {JXG.Coords} Coords object for the Zero point on the line 362 * @private 363 */ 364 getZeroCoordinates: function () { 365 var c1x, c1y, c1z, c2x, c2y, c2z, 366 ev_a = Type.evaluate(this.visProp.anchor); 367 368 if (this.line.type === Const.OBJECT_TYPE_AXIS) { 369 return Geometry.projectPointToLine({ 370 coords: { 371 usrCoords: [1, 0, 0] 372 } 373 }, this.line, this.board); 374 } 375 376 c1z = this.line.point1.coords.usrCoords[0]; 377 c1x = this.line.point1.coords.usrCoords[1]; 378 c1y = this.line.point1.coords.usrCoords[2]; 379 c2z = this.line.point2.coords.usrCoords[0]; 380 c2x = this.line.point2.coords.usrCoords[1]; 381 c2y = this.line.point2.coords.usrCoords[2]; 382 383 if (ev_a === 'right') { 384 return this.line.point2.coords; 385 } else if (ev_a === 'middle') { 386 return new Coords(Const.COORDS_BY_USER, [ 387 (c1z + c2z) * 0.5, 388 (c1x + c2x) * 0.5, 389 (c1y + c2y) * 0.5 390 ], this.board); 391 } else if (Type.isNumber(ev_a)) { 392 return new Coords(Const.COORDS_BY_USER, [ 393 c1z + (c2z - c1z) * ev_a, 394 c1x + (c2x - c1x) * ev_a, 395 c1y + (c2y - c1y) * ev_a 396 ], this.board); 397 } 398 399 return this.line.point1.coords; 400 }, 401 402 /** 403 * Calculate the lower and upper bounds for tick rendering 404 * If {@link JXG.Ticks#includeBoundaries} is false, the boundaries will exclude point1 and point2 405 * 406 * @param {JXG.Coords} coordsZero 407 * @returns {String} type (Optional) If type=='ticksdistance' the bounds are 408 * the intersection of the line with the bounding box of the board. 409 * Otherwise it is the projection of the corners of the bounding box 410 * to the line. The first case i s needed to automatically 411 * generate ticks. The second case is for drawing of the ticks. 412 * @returns {Object} contains the lower and upper bounds 413 * 414 * @private 415 */ 416 getLowerAndUpperBounds: function (coordsZero, type) { 417 var lowerBound, upperBound, 418 // The line's defining points that will be adjusted to be within the board limits 419 point1 = new Coords(Const.COORDS_BY_USER, this.line.point1.coords.usrCoords, this.board), 420 point2 = new Coords(Const.COORDS_BY_USER, this.line.point2.coords.usrCoords, this.board), 421 // Are the original defining points within the board? 422 isPoint1inBoard = (Math.abs(point1.usrCoords[0]) >= Mat.eps && 423 point1.scrCoords[1] >= 0.0 && point1.scrCoords[1] <= this.board.canvasWidth && 424 point1.scrCoords[2] >= 0.0 && point1.scrCoords[2] <= this.board.canvasHeight), 425 isPoint2inBoard = (Math.abs(point2.usrCoords[0]) >= Mat.eps && 426 point2.scrCoords[1] >= 0.0 && point2.scrCoords[1] <= this.board.canvasWidth && 427 point2.scrCoords[2] >= 0.0 && point2.scrCoords[2] <= this.board.canvasHeight), 428 // We use the distance from zero to P1 and P2 to establish lower and higher points 429 dZeroPoint1, dZeroPoint2, 430 ev_sf = Type.evaluate(this.line.visProp.straightfirst), 431 ev_sl = Type.evaluate(this.line.visProp.straightlast), 432 ev_i = Type.evaluate(this.visProp.includeboundaries), 433 obj; 434 435 // Adjust line limit points to be within the board 436 if (Type.exists(type) || type === 'tickdistance') { 437 // The good old calcStraight is needed for determining the distance between major ticks. 438 // Here, only the visual area is of importance 439 Geometry.calcStraight(this.line, point1, point2, Type.evaluate(this.line.visProp.margin)); 440 } else { 441 // This function projects the corners of the board to the line. 442 // This is important for diagonal lines with infinite tick lines. 443 Geometry.calcLineDelimitingPoints(this.line, point1, point2); 444 } 445 // Shorten ticks bounds such that ticks are not through arrow heads 446 obj = this.board.renderer.getPositionArrowHead(this.line, point1, point2, 447 Type.evaluate(this.line.visProp.strokewidth)); 448 point1.setCoordinates(Const.COORDS_BY_SCREEN, [ 449 point1.scrCoords[1] - obj.d1x, 450 point1.scrCoords[2] - obj.d1y, 451 ]); 452 point2.setCoordinates(Const.COORDS_BY_SCREEN, [ 453 point2.scrCoords[1] - obj.d2x, 454 point2.scrCoords[2] - obj.d2y, 455 ]); 456 457 // Calculate distance from Zero to P1 and to P2 458 dZeroPoint1 = this.getDistanceFromZero(coordsZero, point1); 459 dZeroPoint2 = this.getDistanceFromZero(coordsZero, point2); 460 461 // We have to establish if the direction is P1->P2 or P2->P1 to set the lower and upper 462 // boundaries appropriately. As the distances contain also a sign to indicate direction, 463 // we can compare dZeroPoint1 and dZeroPoint2 to establish the line direction 464 if (dZeroPoint1 < dZeroPoint2) { // Line goes P1->P2 465 lowerBound = dZeroPoint1; 466 if (!ev_sf && isPoint1inBoard && !ev_i) { 467 lowerBound += Mat.eps; 468 } 469 upperBound = dZeroPoint2; 470 if (!ev_sl && isPoint2inBoard && !ev_i) { 471 upperBound -= Mat.eps; 472 } 473 } else if (dZeroPoint2 < dZeroPoint1) { // Line goes P2->P1 474 lowerBound = dZeroPoint2; 475 if (!ev_sl && isPoint2inBoard && !ev_i) { 476 lowerBound += Mat.eps; 477 } 478 upperBound = dZeroPoint1; 479 if (!ev_sf && isPoint1inBoard && !ev_i) { 480 upperBound -= Mat.eps; 481 } 482 } else { // P1 = P2 = Zero, we can't do a thing 483 lowerBound = 0; 484 upperBound = 0; 485 } 486 487 return { 488 lower: lowerBound, 489 upper: upperBound 490 }; 491 }, 492 493 /** 494 * Calculates the distance in user coordinates from zero to a given point including its sign 495 * 496 * @param {JXG.Coords} zero coordinates of the point considered zero 497 * @param {JXG.Coords} point coordinates of the point to find out the distance 498 * @returns {Number} distance between zero and point, including its sign 499 * @private 500 */ 501 getDistanceFromZero: function (zero, point) { 502 var eps = Mat.eps, 503 distance = zero.distance(Const.COORDS_BY_USER, point); 504 505 // Establish sign 506 if (this.line.type === Const.OBJECT_TYPE_AXIS) { 507 if ((Mat.relDif(zero.usrCoords[1], point.usrCoords[1]) > eps && 508 zero.usrCoords[1] - point.usrCoords[1] > eps) || 509 (Mat.relDif(zero.usrCoords[2], point.usrCoords[2]) > eps && 510 zero.usrCoords[2] - point.usrCoords[2] > eps)) { 511 512 distance *= -1; 513 } 514 } else if (Type.evaluate(this.visProp.anchor) === 'right') { 515 if (Geometry.isSameDirection(zero, this.line.point1.coords, point)) { 516 distance *= -1; 517 } 518 } else { 519 if (!Geometry.isSameDirection(zero, this.line.point2.coords, point)) { 520 distance *= -1; 521 } 522 } 523 return distance; 524 }, 525 526 /** 527 * Creates ticks coordinates and labels automatically. 528 * The frequency of ticks is affected by the values of {@link JXG.Ticks#insertTicks} and {@link JXG.Ticks#ticksDistance} 529 * 530 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 531 * @param {Object} bounds contains the lower and upper boundaries for ticks placement 532 * @private 533 */ 534 generateEquidistantTicks: function (coordsZero, bounds) { 535 var tickPosition, 536 // Calculate X and Y distance between two major ticks 537 deltas = this.getXandYdeltas(), 538 // Distance between two major ticks in user coordinates 539 ticksDelta = (this.equidistant ? this.ticksFunction(1) : this.ticksDelta), 540 ev_it = Type.evaluate(this.visProp.insertticks), 541 ev_mt = Type.evaluate(this.visProp.minorticks); 542 543 // adjust ticks distance 544 ticksDelta *= Type.evaluate(this.visProp.scale); 545 if (ev_it && this.minTicksDistance > Mat.eps) { 546 ticksDelta = this.adjustTickDistance(ticksDelta, coordsZero, deltas); 547 ticksDelta /= (ev_mt + 1); 548 } else if (!ev_it) { 549 ticksDelta /= (ev_mt + 1); 550 } 551 this.ticksDelta = ticksDelta; 552 553 if (ticksDelta < Mat.eps) { 554 return; 555 } 556 557 // Position ticks from zero to the positive side while not reaching the upper boundary 558 tickPosition = 0; 559 if (!Type.evaluate(this.visProp.drawzero)) { 560 tickPosition = ticksDelta; 561 } 562 while (tickPosition <= bounds.upper) { 563 // Only draw ticks when we are within bounds, ignore case where tickPosition < lower < upper 564 if (tickPosition >= bounds.lower) { 565 this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas); 566 } 567 tickPosition += ticksDelta; 568 } 569 570 // Position ticks from zero (not inclusive) to the negative side while not reaching the lower boundary 571 tickPosition = -ticksDelta; 572 while (tickPosition >= bounds.lower) { 573 // Only draw ticks when we are within bounds, ignore case where lower < upper < tickPosition 574 if (tickPosition <= bounds.upper) { 575 this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas); 576 } 577 tickPosition -= ticksDelta; 578 } 579 }, 580 581 /** 582 * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to adjust the 583 * distance between two ticks depending on {@link JXG.Ticks#minTicksDistance} value 584 * 585 * @param {Number} ticksDelta distance between two major ticks in user coordinates 586 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 587 * @param {Object} deltas x and y distance in pixel between two user units 588 * @param {Object} bounds upper and lower bound of the tick positions in user units. 589 * @private 590 */ 591 adjustTickDistance: function (ticksDelta, coordsZero, deltas) { 592 var nx, ny, bounds, 593 distScr, 594 sgn = 1; 595 596 bounds = this.getLowerAndUpperBounds(coordsZero, 'ticksdistance'); 597 nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 598 ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 599 distScr = coordsZero.distance(Const.COORDS_BY_SCREEN, new Coords(Const.COORDS_BY_USER, [nx, ny], this.board)); 600 while (distScr / (Type.evaluate(this.visProp.minorticks) + 1) < this.minTicksDistance) { 601 if (sgn === 1) { 602 ticksDelta *= 2; 603 } else { 604 ticksDelta *= 5; 605 } 606 sgn *= -1; 607 608 nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 609 ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 610 distScr = coordsZero.distance(Const.COORDS_BY_SCREEN, new Coords(Const.COORDS_BY_USER, [nx, ny], this.board)); 611 } 612 return ticksDelta; 613 }, 614 615 /** 616 * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to create a tick 617 * in the line at the given tickPosition. 618 * 619 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 620 * @param {Number} tickPosition current tick position relative to zero 621 * @param {Number} ticksDelta distance between two major ticks in user coordinates 622 * @param {Object} deltas x and y distance between two major ticks 623 * @private 624 */ 625 processTickPosition: function (coordsZero, tickPosition, ticksDelta, deltas) { 626 var x, y, tickCoords, ti; 627 628 // Calculates tick coordinates 629 x = coordsZero.usrCoords[1] + tickPosition * deltas.x; 630 y = coordsZero.usrCoords[2] + tickPosition * deltas.y; 631 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 632 633 // Test if tick is a major tick. 634 // This is the case if tickPosition/ticksDelta is 635 // a multiple of the number of minorticks+1 636 tickCoords.major = Math.round(tickPosition / ticksDelta) % (Type.evaluate(this.visProp.minorticks) + 1) === 0; 637 638 // Compute the start position and the end position of a tick. 639 // If both positions are out of the canvas, ti is empty. 640 ti = this.tickEndings(tickCoords, tickCoords.major); 641 if (ti.length === 3) { 642 this.ticks.push(ti); 643 if (tickCoords.major && Type.evaluate(this.visProp.drawlabels)) { 644 this.labelsData.push( 645 this.generateLabelData( 646 this.generateLabelText(tickCoords, coordsZero), 647 tickCoords, 648 this.ticks.length 649 ) 650 ); 651 } else { 652 this.labelsData.push(null); 653 } 654 } 655 }, 656 657 /** 658 * Creates ticks coordinates and labels based on {@link JXG.Ticks#fixedTicks} and {@link JXG.Ticks#labels}. 659 * 660 * @param {JXG.Coords} coordsZero Coordinates of the point considered zero 661 * @param {Object} bounds contains the lower and upper boundaries for ticks placement 662 * @private 663 */ 664 generateFixedTicks: function (coordsZero, bounds) { 665 var tickCoords, labelText, i, ti, 666 x, y, 667 hasLabelOverrides = Type.isArray(this.visProp.labels), 668 // Calculate X and Y distance between two major points in the line 669 deltas = this.getXandYdeltas(), 670 ev_dl = Type.evaluate(this.visProp.drawlabels); 671 672 for (i = 0; i < this.fixedTicks.length; i++) { 673 x = coordsZero.usrCoords[1] + this.fixedTicks[i] * deltas.x; 674 y = coordsZero.usrCoords[2] + this.fixedTicks[i] * deltas.y; 675 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 676 677 // Compute the start position and the end position of a tick. 678 // If tick is out of the canvas, ti is empty. 679 ti = this.tickEndings(tickCoords, true); 680 if (ti.length === 3 && this.fixedTicks[i] >= bounds.lower && 681 this.fixedTicks[i] <= bounds.upper) { 682 this.ticks.push(ti); 683 684 if (ev_dl && 685 (hasLabelOverrides || Type.exists(this.visProp.labels[i]))) { 686 labelText = hasLabelOverrides ? 687 Type.evaluate(this.visProp.labels[i]) : this.fixedTicks[i]; 688 this.labelsData.push( 689 this.generateLabelData( 690 this.generateLabelText(tickCoords, coordsZero, labelText), 691 tickCoords, 692 i 693 ) 694 ); 695 } else { 696 this.labelsData.push(null); 697 } 698 } 699 } 700 }, 701 702 /** 703 * Calculates the x and y distance in pixel between two units in user space. 704 * 705 * @returns {Object} 706 * @private 707 */ 708 getXandYdeltas: function () { 709 var 710 // Auxiliary points to store the start and end of the line according to its direction 711 point1UsrCoords, point2UsrCoords, 712 distP1P2 = this.line.point1.Dist(this.line.point2); 713 714 if (this.line.type === Const.OBJECT_TYPE_AXIS) { 715 // When line is an Axis, direction depends on Board Coordinates system 716 717 // assume line.point1 and line.point2 are in correct order 718 point1UsrCoords = this.line.point1.coords.usrCoords; 719 point2UsrCoords = this.line.point2.coords.usrCoords; 720 721 // Check if direction is incorrect, then swap 722 if (point1UsrCoords[1] > point2UsrCoords[1] || 723 (Math.abs(point1UsrCoords[1] - point2UsrCoords[1]) < Mat.eps && 724 point1UsrCoords[2] > point2UsrCoords[2])) { 725 point1UsrCoords = this.line.point2.coords.usrCoords; 726 point2UsrCoords = this.line.point1.coords.usrCoords; 727 } 728 } else { 729 // line direction is always from P1 to P2 for non Axis types 730 point1UsrCoords = this.line.point1.coords.usrCoords; 731 point2UsrCoords = this.line.point2.coords.usrCoords; 732 } 733 return { 734 x: (point2UsrCoords[1] - point1UsrCoords[1]) / distP1P2, 735 y: (point2UsrCoords[2] - point1UsrCoords[2]) / distP1P2 736 }; 737 }, 738 739 /** 740 * Check if (parts of) the tick is inside the canvas. The tick intersects the boundary 741 * at two positions: [x[0], y[0]] and [x[1], y[1]] in screen coordinates. 742 * @param {Array} x Array of length two 743 * @param {Array} y Array of length two 744 * @return {Boolean} true if parts of the tick are inside of the canvas or on the boundary. 745 */ 746 _isInsideCanvas: function(x, y, m) { 747 var cw = this.board.canvasWidth, 748 ch = this.board.canvasHeight; 749 750 if (m === undefined) { 751 m = 0; 752 } 753 return (x[0] >= m && x[0] <= cw - m && y[0] >= m && y[0] <= ch - m) || 754 (x[1] >= m && x[1] <= cw - m && y[1] >= m && y[1] <= ch - m); 755 }, 756 757 /** 758 * @param {JXG.Coords} coords Coordinates of the tick on the line. 759 * @param {Boolean} major True if tick is major tick. 760 * @returns {Array} Array of length 3 containing start and end coordinates in screen coordinates 761 * of the tick (arrays of length 2). 3rd entry is true if major tick otherwise false. 762 * If the tick is outside of the canvas, the return array is empty. 763 * @private 764 */ 765 tickEndings: function (coords, major) { 766 var c, lineStdForm, intersection, 767 dxs, dys, 768 style, 769 x = [-2000000, -2000000], 770 y = [-2000000, -2000000]; 771 772 c = coords.scrCoords; 773 if (major) { 774 dxs = this.dxMaj; 775 dys = this.dyMaj; 776 style = this.majStyle; 777 } else { 778 dxs = this.dxMin; 779 dys = this.dyMin; 780 style = this.minStyle; 781 } 782 lineStdForm = [-dys * c[1] - dxs * c[2], dys, dxs]; 783 784 // For all ticks regardless if of finite or infinite 785 // tick length the intersection with the canvas border is 786 // computed. 787 if (style === 'infinite') { 788 intersection = Geometry.meetLineBoard(lineStdForm, this.board); 789 x[0] = intersection[0].scrCoords[1]; 790 x[1] = intersection[1].scrCoords[1]; 791 y[0] = intersection[0].scrCoords[2]; 792 y[1] = intersection[1].scrCoords[2]; 793 } else { 794 x[0] = c[1] + dxs * Type.evaluate(this.visProp.tickendings[0]); 795 y[0] = c[2] - dys * Type.evaluate(this.visProp.tickendings[0]); 796 x[1] = c[1] - dxs * Type.evaluate(this.visProp.tickendings[1]); 797 y[1] = c[2] + dys * Type.evaluate(this.visProp.tickendings[1]); 798 } 799 800 // Check if (parts of) the tick is inside the canvas. 801 if (this._isInsideCanvas(x, y)) { 802 return [x, y, major]; 803 } 804 805 return []; 806 }, 807 808 /** 809 * Format label texts. Show the desired number of digits 810 * and use utf-8 minus sign. 811 * @param {Number} value Number to be displayed 812 * @return {String} The value converted into a string. 813 * @private 814 */ 815 formatLabelText: function(value) { 816 var labelText = value.toString(), 817 ev_s = Type.evaluate(this.visProp.scalesymbol); 818 819 // if value is Number 820 if (Type.isNumber(value)) { 821 if (labelText.length > Type.evaluate(this.visProp.maxlabellength) || 822 labelText.indexOf('e') !== -1) { 823 labelText = value.toPrecision(Type.evaluate(this.visProp.precision)).toString(); 824 } 825 if (labelText.indexOf('.') > -1 && labelText.indexOf('e') === -1) { 826 // trim trailing zeros 827 labelText = labelText.replace(/0+$/, ''); 828 // trim trailing . 829 labelText = labelText.replace(/\.$/, ''); 830 } 831 } 832 833 if (ev_s.length > 0) { 834 if (labelText === '1') { 835 labelText = ev_s; 836 } else if (labelText === '-1') { 837 labelText = '-' + ev_s; 838 } else if (labelText !== '0') { 839 labelText = labelText + ev_s; 840 } 841 } 842 843 if (Type.evaluate(this.visProp.useunicodeminus)) { 844 labelText = labelText.replace(/-/g, '\u2212'); 845 } 846 return labelText; 847 }, 848 849 /** 850 * Creates the label text for a given tick. A value for the text can be provided as a number or string 851 * 852 * @param {JXG.Coords} tick The Coords-object of the tick to create a label for 853 * @param {JXG.Coords} zero The Coords-object of line's zero 854 * @param {Number|String} value A predefined value for this tick 855 * @returns {String} 856 * @private 857 */ 858 generateLabelText: function (tick, zero, value) { 859 var labelText, 860 distance = this.getDistanceFromZero(zero, tick); 861 862 if (Math.abs(distance) < Mat.eps) { // Point is zero 863 labelText = '0'; 864 } else { 865 // No value provided, equidistant, so assign distance as value 866 if (!Type.exists(value)) { // could be null or undefined 867 value = distance / Type.evaluate(this.visProp.scale); 868 } 869 870 labelText = this.formatLabelText(value); 871 } 872 873 return labelText; 874 }, 875 876 /** 877 * Create a tick label data, i.e. text and coordinates 878 * @param {String} labelText 879 * @param {JXG.Coords} tick 880 * @param {Number} tickNumber 881 * @returns {Object} with properties 'x', 'y', 't' (text), 'i' (tick number) or null in case of o label 882 * @private 883 */ 884 generateLabelData: function (labelText, tick, tickNumber) { 885 var xa, ya, m, fs; 886 887 // Test if large portions of the label are inside of the canvas 888 // This is the last chance to abandon the creation of the label if it is mostly 889 // outside of the canvas. 890 fs = Type.evaluate(this.visProp.label.fontsize); 891 xa = [tick.scrCoords[1], tick.scrCoords[1]]; 892 ya = [tick.scrCoords[2], tick.scrCoords[2]]; 893 m = (fs === undefined) ? 12 : fs; 894 m *= 1.5; 895 if (!this._isInsideCanvas(xa, ya, m)) { 896 return null; 897 } 898 899 xa = Type.evaluate(this.visProp.label.offset[0]); 900 ya = Type.evaluate(this.visProp.label.offset[1]); 901 902 return { 903 x: tick.usrCoords[1] + xa / (this.board.unitX), 904 y: tick.usrCoords[2] + ya / (this.board.unitY), 905 t: labelText, 906 i: tickNumber 907 }; 908 }, 909 910 /** 911 * Recalculate the tick positions and the labels. 912 * @returns {JXG.Ticks} 913 */ 914 update: function () { 915 if (this.needsUpdate) { 916 //this.visPropCalc.visible = Type.evaluate(this.visProp.visible); 917 // A canvas with no width or height will create an endless loop, so ignore it 918 if (this.board.canvasWidth !== 0 && this.board.canvasHeight !== 0) { 919 this.calculateTicksCoordinates(); 920 } 921 // this.updateVisibility(this.line.visPropCalc.visible); 922 // 923 // for (var i = 0; i < this.labels.length; i++) { 924 // if (this.labels[i] !== null) { 925 // this.labels[i].prepareUpdate() 926 // .updateVisibility(this.line.visPropCalc.visible) 927 // .updateRenderer(); 928 // } 929 // } 930 } 931 932 return this; 933 }, 934 935 /** 936 * Uses the boards renderer to update the arc. 937 * @returns {JXG.Ticks} Reference to the object. 938 */ 939 updateRenderer: function () { 940 if (!this.needsUpdate) { 941 return this; 942 } 943 944 if (this.visPropCalc.visible) { 945 this.board.renderer.updateTicks(this); 946 } 947 this.updateRendererLabels(); 948 949 this.setDisplayRendNode(); 950 // if (this.visPropCalc.visible != this.visPropOld.visible) { 951 // this.board.renderer.display(this, this.visPropCalc.visible); 952 // this.visPropOld.visible = this.visPropCalc.visible; 953 // } 954 955 this.needsUpdate = false; 956 return this; 957 }, 958 959 /** 960 * Updates the label elements of the major ticks. 961 * 962 * @private 963 * @returns {JXG.Ticks} Reference to the object. 964 */ 965 updateRendererLabels: function() { 966 var i, j, 967 lenData, lenLabels, 968 attr, 969 label, ld, 970 visible; 971 972 // The number of labels needed 973 lenData = this.labelsData.length; 974 // The number of labels which already exist 975 lenLabels = this.labels.length; 976 977 for (i = 0, j = 0; i < lenData; i++) { 978 if (this.labelsData[i] === null) { 979 continue; 980 } 981 982 ld = this.labelsData[i]; 983 if (j < lenLabels) { 984 // Take an already existing text element 985 label = this.labels[j]; 986 label.setText(ld.t); 987 label.setCoords(ld.x, ld.y); 988 j++; 989 } else { 990 // A new text element is needed 991 this.labelCounter += 1; 992 993 attr = { 994 isLabel: true, 995 layer: this.board.options.layer.line, 996 highlightStrokeColor: this.board.options.text.strokeColor, 997 highlightStrokeWidth: this.board.options.text.strokeWidth, 998 highlightStrokeOpacity: this.board.options.text.strokeOpacity, 999 priv: this.visProp.priv 1000 }; 1001 attr = Type.deepCopy(attr, this.visProp.label); 1002 attr.id = this.id + ld.i + 'Label' + this.labelCounter; 1003 1004 label = Text.createText(this.board, [ld.x, ld.y, ld.t], attr); 1005 label.isDraggable = false; 1006 label.dump = false; 1007 this.labels.push(label); 1008 } 1009 1010 visible = Type.evaluate(this.visProp.label.visible); 1011 if (visible === 'inherit') { 1012 visible = this.visPropCalc.visible; 1013 } 1014 label.prepareUpdate() 1015 .updateVisibility(visible) 1016 .updateRenderer(); 1017 //this.board.renderer.display(label, visible); 1018 1019 label.distanceX = Type.evaluate(this.visProp.label.offset[0]); 1020 label.distanceY = Type.evaluate(this.visProp.label.offset[1]); 1021 } 1022 1023 // Hide unused labels 1024 lenData = j; 1025 for (j = lenData; j < lenLabels; j++) { 1026 this.board.renderer.display(this.labels[j], false); 1027 // Tick labels have the attribute "visible: 'inherit'" 1028 // This must explicitely set to false, otherwise 1029 // this labels would be set to visible in the upcoming 1030 // update of the labels. 1031 this.labels[j].visProp.visible = false; 1032 } 1033 1034 return this; 1035 }, 1036 1037 hideElement: function () { 1038 var i; 1039 1040 JXG.deprecated('Element.hideElement()', 'Element.setDisplayRendNode()'); 1041 1042 this.visPropCalc.visible = false; 1043 this.board.renderer.display(this, false); 1044 for (i = 0; i < this.labels.length; i++) { 1045 if (Type.exists(this.labels[i])) { 1046 this.labels[i].hideElement(); 1047 } 1048 } 1049 1050 return this; 1051 }, 1052 1053 showElement: function () { 1054 var i; 1055 1056 JXG.deprecated('Element.showElement()', 'Element.setDisplayRendNode()'); 1057 1058 this.visPropCalc.visible = true; 1059 this.board.renderer.display(this, false); 1060 1061 for (i = 0; i < this.labels.length; i++) { 1062 if (Type.exists(this.labels[i])) { 1063 this.labels[i].showElement(); 1064 } 1065 } 1066 1067 return this; 1068 } 1069 }); 1070 1071 /** 1072 * @class Ticks are used as distance markers on a line. 1073 * @pseudo 1074 * @description 1075 * @name Ticks 1076 * @augments JXG.Ticks 1077 * @constructor 1078 * @type JXG.Ticks 1079 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1080 * @param {JXG.Line} line The parents consist of the line the ticks are going to be attached to. 1081 * @param {Number} distance Number defining the distance between two major ticks or an 1082 * array defining static ticks. Alternatively, the distance can be specified with the attribute 1083 * "ticksDistance". For arbitrary lines (and not axes) a "zero coordinate" is determined 1084 * which defines where the first tick is positioned. This zero coordinate 1085 * can be altered with the attribute "anchor". Possible values are "left", "middle", "right" or a number. 1086 * The default value is "middle". 1087 * 1088 * @example 1089 * // Create an axis providing two coord pairs. 1090 * var p1 = board.create('point', [0, 3]); 1091 * var p2 = board.create('point', [1, 3]); 1092 * var l1 = board.create('line', [p1, p2]); 1093 * var t = board.create('ticks', [l1], {ticksDistance: 2}); 1094 * </pre><div class="jxgbox" id="ee7f2d68-75fc-4ec0-9931-c76918427e63" style="width: 300px; height: 300px;"></div> 1095 * <script type="text/javascript"> 1096 * (function () { 1097 * var board = JXG.JSXGraph.initBoard('ee7f2d68-75fc-4ec0-9931-c76918427e63', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false}); 1098 * var p1 = board.create('point', [0, 3]); 1099 * var p2 = board.create('point', [1, 3]); 1100 * var l1 = board.create('line', [p1, p2]); 1101 * var t = board.create('ticks', [l1, 2], {ticksDistance: 2}); 1102 * })(); 1103 * </script><pre> 1104 */ 1105 JXG.createTicks = function (board, parents, attributes) { 1106 var el, dist, 1107 attr = Type.copyAttributes(attributes, board.options, 'ticks'); 1108 1109 if (parents.length < 2) { 1110 dist = attr.ticksdistance; 1111 } else { 1112 dist = parents[1]; 1113 } 1114 1115 if (parents[0].elementClass === Const.OBJECT_CLASS_LINE) { 1116 el = new JXG.Ticks(parents[0], dist, attr); 1117 } else { 1118 throw new Error("JSXGraph: Can't create Ticks with parent types '" + (typeof parents[0]) + "'."); 1119 } 1120 1121 // deprecated 1122 if (Type.isFunction(attr.generatelabelvalue)) { 1123 el.generateLabelText = attr.generatelabelvalue; 1124 } 1125 if (Type.isFunction(attr.generatelabeltext)) { 1126 el.generateLabelText = attr.generatelabeltext; 1127 } 1128 1129 el.setParents(parents[0]); 1130 el.isDraggable = true; 1131 el.fullUpdate(parents[0].visPropCalc.visible); 1132 1133 return el; 1134 }; 1135 1136 /** 1137 * @class Hashes can be used to mark congruent lines. 1138 * @pseudo 1139 * @description 1140 * @name Hatch 1141 * @augments JXG.Ticks 1142 * @constructor 1143 * @type JXG.Ticks 1144 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1145 * @param {JXG.Line} line The line the hatch marks are going to be attached to. 1146 * @param {Number} numberofhashes Number of dashes. 1147 * @example 1148 * // Create an axis providing two coord pairs. 1149 * var p1 = board.create('point', [0, 3]); 1150 * var p2 = board.create('point', [1, 3]); 1151 * var l1 = board.create('line', [p1, p2]); 1152 * var t = board.create('hatch', [l1, 3]); 1153 * </pre><div class="jxgbox" id="4a20af06-4395-451c-b7d1-002757cf01be" style="width: 300px; height: 300px;"></div> 1154 * <script type="text/javascript"> 1155 * (function () { 1156 * var board = JXG.JSXGraph.initBoard('4a20af06-4395-451c-b7d1-002757cf01be', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false}); 1157 * var p1 = board.create('point', [0, 3]); 1158 * var p2 = board.create('point', [1, 3]); 1159 * var l1 = board.create('line', [p1, p2]); 1160 * var t = board.create('hatch', [l1, 3]); 1161 * })(); 1162 * </script><pre> 1163 * 1164 * @example 1165 * // Alter the position of the hatch 1166 * var board = JXG.JSXGraph.initBoard('jxgbox', {boundingbox: [-10, 10, 10, -5], keepaspectratio:true}); 1167 * 1168 * var p = board.create('point', [-5, 0]); 1169 * var q = board.create('point', [5, 0]); 1170 * var li = board.create('line', [p, q]); 1171 * var h = board.create('hatch', [li, 2], {anchor: 0.2}); 1172 * 1173 * </pre><div id="05d720ee-99c9-11e6-a9c7-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div> 1174 * <script type="text/javascript"> 1175 * (function() { 1176 * var board = JXG.JSXGraph.initBoard('05d720ee-99c9-11e6-a9c7-901b0e1b8723', 1177 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1178 * var board = JXG.JSXGraph.initBoard('jxgbox', {boundingbox: [-10, 10, 10, -5], keepaspectratio:true}); 1179 * 1180 * var p = board.create('point', [-5, 0]); 1181 * var q = board.create('point', [5, 0]); 1182 * var li = board.create('line', [p, q]); 1183 * var h = board.create('hatch', [li, 2], {anchor: 0.2}); 1184 * 1185 * })(); 1186 * 1187 * </script><pre> 1188 * 1189 */ 1190 JXG.createHatchmark = function (board, parents, attributes) { 1191 var num, i, base, width, totalwidth, el, 1192 pos = [], 1193 attr = Type.copyAttributes(attributes, board.options, 'hatch'); 1194 1195 if (parents[0].elementClass !== Const.OBJECT_CLASS_LINE || typeof parents[1] !== 'number') { 1196 throw new Error("JSXGraph: Can't create Hatch mark with parent types '" + (typeof parents[0]) + "' and '" + (typeof parents[1]) + " and ''" + (typeof parents[2]) + "'."); 1197 } 1198 1199 num = parents[1]; 1200 width = attr.ticksdistance; 1201 totalwidth = (num - 1) * width; 1202 base = -totalwidth * 0.5; 1203 1204 for (i = 0; i < num; i++) { 1205 pos[i] = base + i * width; 1206 } 1207 1208 el = board.create('ticks', [parents[0], pos], attr); 1209 el.elType = 'hatch'; 1210 1211 return el; 1212 }; 1213 1214 JXG.registerElement('ticks', JXG.createTicks); 1215 JXG.registerElement('hash', JXG.createHatchmark); 1216 JXG.registerElement('hatch', JXG.createHatchmark); 1217 1218 return { 1219 Ticks: JXG.Ticks, 1220 createTicks: JXG.createTicks, 1221 createHashmark: JXG.createHatchmark, 1222 createHatchmark: JXG.createHatchmark 1223 }; 1224 }); 1225