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