1 /* 2 Copyright 2008-2023 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 <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true*/ 33 /*jslint nomen: true, plusplus: true*/ 34 35 /** 36 * @fileoverview In this file the geometry object Ticks is defined. Ticks provides 37 * methods for creation and management of ticks on an axis. 38 * @author graphjs 39 * @version 0.1 40 */ 41 42 import JXG from "../jxg"; 43 import Mat from "../math/math"; 44 import Geometry from "../math/geometry"; 45 import Numerics from "../math/numerics"; 46 import Const from "./constants"; 47 import GeometryElement from "./element"; 48 import Coords from "./coords"; 49 import Type from "../utils/type"; 50 51 /** 52 * Creates ticks for an axis. 53 * @class Ticks provides methods for creation and management 54 * of ticks on an axis. 55 * @param {JXG.Line} line Reference to the axis the ticks are drawn on. 56 * @param {Number|Array} ticks Number defining the distance between two major ticks or an array defining static ticks. 57 * @param {Object} attributes Properties 58 * @see JXG.Line#addTicks 59 * @constructor 60 * @augments JXG.GeometryElement 61 */ 62 JXG.Ticks = function (line, ticks, attributes) { 63 this.constructor(line.board, attributes, Const.OBJECT_TYPE_TICKS, Const.OBJECT_CLASS_OTHER); 64 65 /** 66 * The line the ticks belong to. 67 * @type JXG.Line 68 * @private 69 */ 70 this.line = line; 71 72 /** 73 * The board the ticks line is drawn on. 74 * @type JXG.Board 75 * @private 76 */ 77 this.board = this.line.board; 78 79 // /** 80 // * A function calculating ticks delta depending on the ticks number. 81 // * @type Function 82 // */ 83 // // this.ticksFunction = null; 84 85 /** 86 * Array of fixed ticks. 87 * @type Array 88 * @private 89 */ 90 this.fixedTicks = null; 91 92 /** 93 * Flag if the ticks are equidistant. If true, their distance is defined by ticksFunction. 94 * @type Boolean 95 * @private 96 */ 97 this.equidistant = false; 98 99 /** 100 * A list of labels which have to be displayed in updateRenderer. 101 * @type Array 102 * @private 103 */ 104 this.labelsData = []; 105 106 if (Type.isFunction(ticks)) { 107 this.ticksFunction = ticks; 108 throw new Error("Function arguments are no longer supported."); 109 } 110 111 if (Type.isArray(ticks)) { 112 this.fixedTicks = ticks; 113 } else { 114 // Obsolete: 115 // if (Math.abs(ticks) < Mat.eps || ticks < 0) { 116 // ticks = attributes.defaultdistance; 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 * @private 131 */ 132 this.ticks = []; 133 134 // /** 135 // * Distance between two major ticks in user coordinates 136 // * @type Number 137 // */ 138 // this.ticksDelta = 1; 139 140 /** 141 * Array where the labels are saved. There is an array element for every tick, 142 * even for minor ticks which don't have labels. In this case the array element 143 * contains just <tt>null</tt>. 144 * @type Array 145 * @private 146 */ 147 this.labels = []; 148 149 /** 150 * Used to ensure the uniqueness of label ids this counter is used. 151 * @type number 152 * @private 153 */ 154 this.labelCounter = 0; 155 156 this.id = this.line.addTicks(this); 157 this.elType = "ticks"; 158 this.inherits.push(this.labels); 159 this.board.setId(this, "Ti"); 160 }; 161 162 JXG.Ticks.prototype = new GeometryElement(); 163 164 JXG.extend( 165 JXG.Ticks.prototype, 166 /** @lends JXG.Ticks.prototype */ { 167 // /** 168 // * Ticks function: 169 // * determines the distance (in user units) of two major ticks. 170 // * See above in constructor and in @see JXG.GeometryElement#setAttribute 171 // * 172 // * @private 173 // * @param {Number} ticks Distance between two major ticks 174 // * @returns {Function} returns method ticksFunction 175 // */ 176 // // makeTicksFunction: function (ticks) { 177 // // return function () { 178 // ticksFunction: function () { 179 // var delta, b, dist, 180 // number_major_tick_intervals = 5; 181 182 // if (Type.evaluate(this.visProp.insertticks)) { 183 // b = this.getLowerAndUpperBounds(this.getZeroCoordinates(), 'ticksdistance'); 184 // dist = b.upper - b.lower; 185 186 // // delta: Proposed distance in user units between two major ticks 187 // delta = Math.pow(10, Math.floor(Math.log(dist / number_major_tick_intervals) / Math.LN10)); 188 // console.log("delta", delta, b.upper, b.lower, dist, dist / number_major_tick_intervals * 1.1) 189 // if (5 * delta < dist / number_major_tick_intervals * 1.1) { 190 // return 5 * delta; 191 // } 192 // if (2 * delta < dist / number_major_tick_intervals * 1.1) { 193 // return 2 * delta; 194 // } 195 196 // // < v1.6.0: 197 // // delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10)); 198 // if (false && dist <= 6 * delta) { 199 // delta *= 0.5; 200 // } 201 // return delta; 202 // } 203 204 // // In case of insertTicks==false 205 // return Type.evaluate(this.visProp.ticksdistance); 206 // // return ticks; 207 // // }; 208 // }, 209 210 /** 211 * Checks whether (x,y) is near the line. 212 * Only available for line elements, not for ticks on curves. 213 * @param {Number} x Coordinate in x direction, screen coordinates. 214 * @param {Number} y Coordinate in y direction, screen coordinates. 215 * @returns {Boolean} True if (x,y) is near the line, False otherwise. 216 */ 217 hasPoint: function (x, y) { 218 var i, t, r, type, 219 len = (this.ticks && this.ticks.length) || 0; 220 221 if (Type.isObject(Type.evaluate(this.visProp.precision))) { 222 type = this.board._inputDevice; 223 r = Type.evaluate(this.visProp.precision[type]); 224 } else { 225 // 'inherit' 226 r = this.board.options.precision.hasPoint; 227 } 228 r += Type.evaluate(this.visProp.strokewidth) * 0.5; 229 if ( 230 !Type.evaluate(this.line.visProp.scalable) || 231 this.line.elementClass === Const.OBJECT_CLASS_CURVE 232 ) { 233 return false; 234 } 235 236 // Ignore non-axes and axes that are not horizontal or vertical 237 if ( 238 this.line.stdform[1] !== 0 && 239 this.line.stdform[2] !== 0 && 240 this.line.type !== Const.OBJECT_TYPE_AXIS 241 ) { 242 return false; 243 } 244 245 for (i = 0; i < len; i++) { 246 t = this.ticks[i]; 247 248 // Skip minor ticks 249 if (t[2]) { 250 // Ignore ticks at zero 251 if ( 252 !( 253 (this.line.stdform[1] === 0 && 254 Math.abs(t[0][0] - this.line.point1.coords.scrCoords[1]) < 255 Mat.eps) || 256 (this.line.stdform[2] === 0 && 257 Math.abs(t[1][0] - this.line.point1.coords.scrCoords[2]) < 258 Mat.eps) 259 ) 260 ) { 261 // tick length is not zero, ie. at least one pixel 262 if ( 263 Math.abs(t[0][0] - t[0][1]) >= 1 || 264 Math.abs(t[1][0] - t[1][1]) >= 1 265 ) { 266 if (this.line.stdform[1] === 0) { 267 // Allow dragging near axes only. 268 if ( 269 Math.abs(y - this.line.point1.coords.scrCoords[2]) < 2 * r && 270 t[0][0] - r < x && x < t[0][1] + r 271 ) { 272 return true; 273 } 274 } else if (this.line.stdform[2] === 0) { 275 if ( 276 Math.abs(x - this.line.point1.coords.scrCoords[1]) < 2 * r && 277 t[1][0] - r < y && y < t[1][1] + r 278 ) { 279 return true; 280 } 281 } 282 } 283 } 284 } 285 } 286 287 return false; 288 }, 289 290 /** 291 * Sets x and y coordinate of the tick. 292 * @param {number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}. 293 * @param {Array} coords coordinates in screen/user units 294 * @param {Array} oldcoords previous coordinates in screen/user units 295 * @returns {JXG.Ticks} this element 296 */ 297 setPositionDirectly: function (method, coords, oldcoords) { 298 var dx, dy, 299 c = new Coords(method, coords, this.board), 300 oldc = new Coords(method, oldcoords, this.board), 301 bb = this.board.getBoundingBox(); 302 303 if ( 304 this.line.type !== Const.OBJECT_TYPE_AXIS || 305 !Type.evaluate(this.line.visProp.scalable) 306 ) { 307 return this; 308 } 309 310 if ( 311 Math.abs(this.line.stdform[1]) < Mat.eps && 312 Math.abs(c.usrCoords[1] * oldc.usrCoords[1]) > Mat.eps 313 ) { 314 // Horizontal line 315 dx = oldc.usrCoords[1] / c.usrCoords[1]; 316 bb[0] *= dx; 317 bb[2] *= dx; 318 this.board.setBoundingBox(bb, this.board.keepaspectratio, "update"); 319 } else if ( 320 Math.abs(this.line.stdform[2]) < Mat.eps && 321 Math.abs(c.usrCoords[2] * oldc.usrCoords[2]) > Mat.eps 322 ) { 323 // Vertical line 324 dy = oldc.usrCoords[2] / c.usrCoords[2]; 325 bb[3] *= dy; 326 bb[1] *= dy; 327 this.board.setBoundingBox(bb, this.board.keepaspectratio, "update"); 328 } 329 330 return this; 331 }, 332 333 /** 334 * (Re-)calculates the ticks coordinates. 335 * @private 336 */ 337 calculateTicksCoordinates: function () { 338 var coordsZero, b, r_max, bb; 339 340 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 341 // Calculate Ticks width and height in Screen and User Coordinates 342 this.setTicksSizeVariables(); 343 344 // If the parent line is not finite, we can stop here. 345 if (Math.abs(this.dx) < Mat.eps && Math.abs(this.dy) < Mat.eps) { 346 return; 347 } 348 } 349 350 // Get Zero (coords element for lines, number for curves) 351 coordsZero = this.getZeroCoordinates(); 352 353 // Calculate lower bound and upper bound limits based on distance 354 // between p1 and center and p2 and center 355 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 356 b = this.getLowerAndUpperBounds(coordsZero, 'ticksdistance'); 357 } else { 358 b = { 359 lower: this.line.minX(), 360 upper: this.line.maxX(), 361 a1: 0, 362 a2: 0, 363 m1: 0, 364 m2: 0 365 }; 366 } 367 368 if (Type.evaluate(this.visProp.type) === "polar") { 369 bb = this.board.getBoundingBox(); 370 r_max = Math.max( 371 Mat.hypot(bb[0], bb[1]), 372 Mat.hypot(bb[2], bb[3]) 373 ); 374 b.upper = r_max; 375 } 376 377 // Clean up 378 this.ticks = []; 379 this.labelsData = []; 380 // Create Ticks Coordinates and Labels 381 if (this.equidistant) { 382 this.generateEquidistantTicks(coordsZero, b); 383 } else { 384 this.generateFixedTicks(coordsZero, b); 385 } 386 387 return this; 388 }, 389 390 /** 391 * Sets the variables used to set the height and slope of each tick. 392 * 393 * @private 394 */ 395 setTicksSizeVariables: function (pos) { 396 var d, 397 mi, 398 ma, 399 len, 400 distMaj = Type.evaluate(this.visProp.majorheight) * 0.5, 401 distMin = Type.evaluate(this.visProp.minorheight) * 0.5; 402 403 // For curves: 404 if (Type.exists(pos)) { 405 mi = this.line.minX(); 406 ma = this.line.maxX(); 407 len = this.line.points.length; 408 if (len < 2) { 409 this.dxMaj = 0; 410 this.dyMaj = 0; 411 } else if (Mat.relDif(pos, mi) < Mat.eps) { 412 this.dxMaj = 413 this.line.points[0].usrCoords[2] - this.line.points[1].usrCoords[2]; 414 this.dyMaj = 415 this.line.points[1].usrCoords[1] - this.line.points[0].usrCoords[1]; 416 } else if (Mat.relDif(pos, ma) < Mat.eps) { 417 this.dxMaj = 418 this.line.points[len - 2].usrCoords[2] - 419 this.line.points[len - 1].usrCoords[2]; 420 this.dyMaj = 421 this.line.points[len - 1].usrCoords[1] - 422 this.line.points[len - 2].usrCoords[1]; 423 } else { 424 this.dxMaj = -Numerics.D(this.line.Y)(pos); 425 this.dyMaj = Numerics.D(this.line.X)(pos); 426 } 427 } else { 428 // ticks width and height in screen units 429 this.dxMaj = this.line.stdform[1]; 430 this.dyMaj = this.line.stdform[2]; 431 } 432 this.dxMin = this.dxMaj; 433 this.dyMin = this.dyMaj; 434 435 // ticks width and height in user units 436 this.dx = this.dxMaj; 437 this.dy = this.dyMaj; 438 439 // After this, the length of the vector (dxMaj, dyMaj) in screen coordinates is equal to distMaj pixel. 440 d = Mat.hypot(this.dxMaj * this.board.unitX, this.dyMaj * this.board.unitY); 441 this.dxMaj *= (distMaj / d) * this.board.unitX; 442 this.dyMaj *= (distMaj / d) * this.board.unitY; 443 this.dxMin *= (distMin / d) * this.board.unitX; 444 this.dyMin *= (distMin / d) * this.board.unitY; 445 446 // Grid-like ticks? 447 this.minStyle = Type.evaluate(this.visProp.minorheight) < 0 ? "infinite" : "finite"; 448 this.majStyle = Type.evaluate(this.visProp.majorheight) < 0 ? "infinite" : "finite"; 449 }, 450 451 /** 452 * Returns the coordinates of the point zero of the line. 453 * 454 * If the line is an {@link Axis}, the coordinates of the projection of the board's zero point is returned 455 * 456 * Otherwise, the coordinates of the point that acts as zero are 457 * established depending on the value of {@link JXG.Ticks#anchor} 458 * 459 * @returns {JXG.Coords} Coords object for the zero point on the line 460 * @private 461 */ 462 getZeroCoordinates: function () { 463 var c1x, c1y, c1z, c2x, c2y, c2z, 464 t, mi, ma, 465 ev_a = Type.evaluate(this.visProp.anchor); 466 467 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 468 if (this.line.type === Const.OBJECT_TYPE_AXIS) { 469 return Geometry.projectPointToLine( 470 { 471 coords: { 472 usrCoords: [1, 0, 0] 473 } 474 }, 475 this.line, 476 this.board 477 ); 478 } 479 c1z = this.line.point1.coords.usrCoords[0]; 480 c1x = this.line.point1.coords.usrCoords[1]; 481 c1y = this.line.point1.coords.usrCoords[2]; 482 c2z = this.line.point2.coords.usrCoords[0]; 483 c2x = this.line.point2.coords.usrCoords[1]; 484 c2y = this.line.point2.coords.usrCoords[2]; 485 486 if (ev_a === "right") { 487 return this.line.point2.coords; 488 } 489 if (ev_a === "middle") { 490 return new Coords( 491 Const.COORDS_BY_USER, 492 [(c1z + c2z) * 0.5, (c1x + c2x) * 0.5, (c1y + c2y) * 0.5], 493 this.board 494 ); 495 } 496 if (Type.isNumber(ev_a)) { 497 return new Coords( 498 Const.COORDS_BY_USER, 499 [ 500 c1z + (c2z - c1z) * ev_a, 501 c1x + (c2x - c1x) * ev_a, 502 c1y + (c2y - c1y) * ev_a 503 ], 504 this.board 505 ); 506 } 507 return this.line.point1.coords; 508 } 509 mi = this.line.minX(); 510 ma = this.line.maxX(); 511 if (ev_a === "right") { 512 t = ma; 513 } else if (ev_a === "middle") { 514 t = (mi + ma) * 0.5; 515 } else if (Type.isNumber(ev_a)) { 516 t = mi * (1 - ev_a) + ma * ev_a; 517 // t = ev_a; 518 } else { 519 t = mi; 520 } 521 return t; 522 }, 523 524 /** 525 * Calculate the lower and upper bounds for tick rendering. 526 * If {@link JXG.Ticks#includeBoundaries} is false, the boundaries will exclude point1 and point2. 527 * 528 * @param {JXG.Coords} coordsZero 529 * @returns {String} [type] If type=='ticksdistance', the bounds are 530 * the intersection of the line with the bounding box of the board, respecting 531 * the value of the line attribute 'margin' and the width of arrow heads. 532 * Otherwise, it is the projection of the corners of the bounding box 533 * to the line - without the attribute 'margin' and width of arrow heads. 534 * <br> 535 * The first case is needed to determine which ticks are displayed, i.e. where to stop. 536 * The second case is to determine the distance between ticks in case of 'insertTicks:true'. 537 * @returns {Object} {lower: Number, upper: Number } containing the lower and upper bounds in user units. 538 * 539 * @private 540 */ 541 getLowerAndUpperBounds: function (coordsZero, type) { 542 var lowerBound, upperBound, 543 fA, lA, 544 point1, point2, 545 isPoint1inBoard, isPoint2inBoard, 546 // We use the distance from zero to P1 and P2 to establish lower and higher points 547 dZeroPoint1, dZeroPoint2, 548 arrowData, angle, 549 a1, a2, m1, m2, 550 eps = Mat.eps * 10, 551 ev_sf = Type.evaluate(this.line.visProp.straightfirst), 552 ev_sl = Type.evaluate(this.line.visProp.straightlast), 553 ev_i = Type.evaluate(this.visProp.includeboundaries); 554 555 // The line's defining points that will be adjusted to be within the board limits 556 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 557 return { 558 lower: this.line.minX(), 559 upper: this.line.maxX() 560 }; 561 } 562 563 point1 = new Coords(Const.COORDS_BY_USER, this.line.point1.coords.usrCoords, this.board); 564 point2 = new Coords(Const.COORDS_BY_USER, this.line.point2.coords.usrCoords, this.board); 565 566 // Are the original defining points within the board? 567 isPoint1inBoard = 568 Math.abs(point1.usrCoords[0]) >= Mat.eps && 569 point1.scrCoords[1] >= 0.0 && 570 point1.scrCoords[1] <= this.board.canvasWidth && 571 point1.scrCoords[2] >= 0.0 && 572 point1.scrCoords[2] <= this.board.canvasHeight; 573 isPoint2inBoard = 574 Math.abs(point2.usrCoords[0]) >= Mat.eps && 575 point2.scrCoords[1] >= 0.0 && 576 point2.scrCoords[1] <= this.board.canvasWidth && 577 point2.scrCoords[2] >= 0.0 && 578 point2.scrCoords[2] <= this.board.canvasHeight; 579 580 // Adjust line limit points to be within the board 581 if (Type.exists(type) && type === 'ticksdistance') { 582 // The good old calcStraight is needed for determining the distance between major ticks. 583 // Here, only the visual area is of importance 584 Geometry.calcStraight(this.line, point1, point2, 0); 585 m1 = this.getDistanceFromZero(coordsZero, point1); 586 m2 = this.getDistanceFromZero(coordsZero, point2); 587 Geometry.calcStraight(this.line, point1, point2, Type.evaluate(this.line.visProp.margin)); 588 m1 = this.getDistanceFromZero(coordsZero, point1) - m1; 589 m2 = this.getDistanceFromZero(coordsZero, point2).m2; 590 } else { 591 // This function projects the corners of the board to the line. 592 // This is important for diagonal lines with infinite tick lines. 593 Geometry.calcLineDelimitingPoints(this.line, point1, point2); 594 } 595 596 // If the hosting line points backwards, 597 // the respective coordinates have to be multiplied by -1. 598 // Otherwise the ticks are created in the wrong direction. 599 angle = Math.atan2(this.line.point2.Y() - this.line.point1.Y(), this.line.point2.X() - this.line.point1.X()); 600 angle = (angle + 2 * Math.PI) % (2 * Math.PI); 601 602 if (angle > Math.PI * 0.5 && angle < 3 * Math.PI * 0.5) { 603 point1.usrCoords[1] *= -1; 604 point2.usrCoords[1] *= -1; 605 point1.usr2screen(); 606 point2.usr2screen(); 607 } 608 if (angle > Math.PI && angle < 2 * Math.PI) { 609 point1.usrCoords[2] *= -1; 610 point2.usrCoords[2] *= -1; 611 point1.usr2screen(); 612 point2.usr2screen(); 613 } 614 615 // Shorten ticks bounds such that ticks are not through arrow heads 616 fA = Type.evaluate(this.line.visProp.firstarrow); 617 lA = Type.evaluate(this.line.visProp.lastarrow); 618 619 a1 = this.getDistanceFromZero(coordsZero, point1); 620 a2 = this.getDistanceFromZero(coordsZero, point2); 621 if (fA || lA) { 622 // Do not display ticks at through arrow heads. 623 // In arrowData we ignore the highlighting status. 624 // Ticks would appear to be too nervous. 625 arrowData = this.board.renderer.getArrowHeadData( 626 this.line, 627 Type.evaluate(this.line.visProp.strokewidth), 628 '' 629 ); 630 631 this.board.renderer.getPositionArrowHead( 632 this.line, 633 point1, 634 point2, 635 arrowData 636 ); 637 } 638 // Calculate (signed) distance from Zero to P1 and to P2 639 dZeroPoint1 = this.getDistanceFromZero(coordsZero, point1); 640 dZeroPoint2 = this.getDistanceFromZero(coordsZero, point2); 641 642 // Recompute lengths of arrow heads 643 a1 = dZeroPoint1 - a1; 644 a2 = dZeroPoint1 - a2; 645 646 // We have to establish if the direction is P1->P2 or P2->P1 to set the lower and upper 647 // bounds appropriately. As the distances contain also a sign to indicate direction, 648 // we can compare dZeroPoint1 and dZeroPoint2 to establish the line direction 649 if (dZeroPoint1 < dZeroPoint2) { 650 // Line goes P1->P2 651 lowerBound = dZeroPoint1; 652 upperBound = dZeroPoint2; 653 654 if (!ev_sf && isPoint1inBoard && !ev_i) { 655 lowerBound += eps; 656 } 657 if (!ev_sl && isPoint2inBoard && !ev_i) { 658 upperBound -= eps; 659 } 660 } else if (dZeroPoint2 < dZeroPoint1) { 661 // Line goes P2->P1 662 lowerBound = dZeroPoint2; 663 upperBound = dZeroPoint1; 664 665 if (!ev_sl && isPoint2inBoard && !ev_i) { 666 lowerBound += eps; 667 } 668 if (!ev_sf && isPoint1inBoard && !ev_i) { 669 upperBound -= eps; 670 } 671 } else { 672 // P1 = P2 = Zero, we can't do a thing 673 lowerBound = 0; 674 upperBound = 0; 675 } 676 677 return { 678 lower: lowerBound, 679 upper: upperBound, 680 a1: a1, 681 a2: a2, 682 m1: m1, 683 m2: m2 684 }; 685 }, 686 687 /** 688 * Calculates the distance in user coordinates from zero to a given point including its sign. 689 * Sign is positive, if the direction from zero to point is the same as the direction 690 * zero to point2 of the line. 691 * 692 * @param {JXG.Coords} zero coordinates of the point considered zero 693 * @param {JXG.Coords} point coordinates of the point to find out the distance 694 * @returns {Number} distance between zero and point, including its sign 695 * @private 696 */ 697 getDistanceFromZero: function (zero, point) { 698 var p1, p2, dirLine, dirPoint, distance; 699 700 p1 = this.line.point1.coords; 701 p2 = this.line.point2.coords; 702 distance = zero.distance(Const.COORDS_BY_USER, point); 703 704 // Establish sign 705 dirLine = [ 706 p2.usrCoords[0] - p1.usrCoords[0], 707 p2.usrCoords[1] - p1.usrCoords[1], 708 p2.usrCoords[2] - p1.usrCoords[2] 709 ]; 710 dirPoint = [ 711 point.usrCoords[0] - zero.usrCoords[0], 712 point.usrCoords[1] - zero.usrCoords[1], 713 point.usrCoords[2] - zero.usrCoords[2] 714 ]; 715 if (Mat.innerProduct(dirLine, dirPoint, 3) < 0) { 716 distance *= -1; 717 } 718 719 return distance; 720 }, 721 722 /** 723 * Creates ticks coordinates and labels automatically. 724 * The frequency of ticks is affected by the values of {@link JXG.Ticks#insertTicks}, {@link JXG.Ticks#minTicksDistance}, 725 * and {@link JXG.Ticks#ticksDistance} 726 * 727 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 728 * @param {Object} bounds contains the lower and upper bounds for ticks placement 729 * @private 730 */ 731 generateEquidistantTicks: function (coordsZero, bounds) { 732 var tickPosition, 733 eps = Mat.eps, 734 deltas, ticksDelta, 735 // ev_mia = Type.evaluate(this.visProp.minorticksinarrow), 736 // ev_maa = Type.evaluate(this.visProp.minorticksinarrow), 737 // ev_mla = Type.evaluate(this.visProp.minorticksinarrow), 738 ev_mt = Type.evaluate(this.visProp.minorticks); 739 740 // Determine a proposed distance between major ticks in user units 741 ticksDelta = this.getDistanceMajorTicks(); 742 743 // Obsolete, since this.equidistant is always true at this point 744 // ticksDelta = this.equidistant ? this.ticksFunction(1) : this.ticksDelta; 745 746 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 747 // Calculate x and y distances between two points on the line which are 1 unit apart 748 // In essence, these are cosine and sine. 749 deltas = this.getXandYdeltas(); 750 } 751 752 ticksDelta *= Type.evaluate(this.visProp.scale); 753 754 // In case of insertTicks, adjust ticks distance to satisfy the minTicksDistance restriction. 755 // if (ev_it) { // } && this.minTicksDistance > Mat.eps) { 756 // ticksDelta = this.adjustTickDistance(ticksDelta, coordsZero, deltas); 757 // } 758 759 // Convert ticksdelta to the distance between two minor ticks 760 ticksDelta /= (ev_mt + 1); 761 this.ticksDelta = ticksDelta; 762 763 if (ticksDelta < Mat.eps) { 764 return; 765 } 766 767 // Position ticks from zero to the positive side while not reaching the upper boundary 768 tickPosition = 0; 769 if (!Type.evaluate(this.visProp.drawzero)) { 770 tickPosition = ticksDelta; 771 } 772 while (tickPosition <= bounds.upper + eps) { 773 // Only draw ticks when we are within bounds, ignore case where tickPosition < lower < upper 774 if (tickPosition >= bounds.lower - eps) { 775 this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas); 776 } 777 tickPosition += ticksDelta; 778 779 // Emergency out 780 if (bounds.upper - tickPosition > ticksDelta * 10000) { 781 break; 782 } 783 } 784 785 // Position ticks from zero (not inclusive) to the negative side while not reaching the lower boundary 786 tickPosition = -ticksDelta; 787 while (tickPosition >= bounds.lower - eps) { 788 // Only draw ticks when we are within bounds, ignore case where lower < upper < tickPosition 789 if (tickPosition <= bounds.upper + eps) { 790 this.processTickPosition(coordsZero, tickPosition, ticksDelta, deltas); 791 } 792 tickPosition -= ticksDelta; 793 794 // Emergency out 795 if (tickPosition - bounds.lower > ticksDelta * 10000) { 796 break; 797 } 798 } 799 }, 800 801 /** 802 * Calculates the distance between two major ticks in user units. 803 * <ul> 804 * <li> If the attribute "insertTicks" is false, the value of the attribute 805 * "ticksDistance" is returned. The attribute "minTicksDistance" is ignored in this case. 806 * <li> If the attribute "insertTicks" is true, the attribute "ticksDistance" is ignored. 807 * The distance between two major ticks is computed 808 * as <i>a 10<sup>i</sup></i>, where <i>a</i> is one of <i>{1, 2, 5}</i> and 809 * the number <i>a 10<sup>i</sup></i> is maximized such that there are approximately 810 * 6 major ticks and there are at least "minTicksDistance" pixel between minor ticks. 811 * The latter restriction has priority over the number of major ticks. 812 * </ul> 813 * @returns Number 814 * @private 815 */ 816 getDistanceMajorTicks: function () { 817 var delta, delta2, 818 b, d, dist, 819 scale, 820 numberMajorTicks = 5, 821 maxDist, minDist, ev_minti; 822 823 if (Type.evaluate(this.visProp.insertticks)) { 824 // Case of insertTicks==true: 825 // Here, we ignore the attribute 'margin' 826 b = this.getLowerAndUpperBounds(this.getZeroCoordinates(), ''); 827 828 dist = (b.upper - b.lower); 829 scale = Type.evaluate(this.visProp.scale); 830 831 maxDist = dist / (numberMajorTicks + 1) / scale; 832 minDist = Type.evaluate(this.visProp.minticksdistance) / scale; 833 ev_minti = Type.evaluate(this.visProp.minorticks); 834 835 d = this.getXandYdeltas(); 836 d.x *= this.board.unitX; 837 d.y *= this.board.unitY; 838 minDist /= Mat.hypot(d.x, d.y); 839 minDist *= (ev_minti + 1); 840 841 // Determine minimal delta to fulfill the minTicksDistance constraint 842 delta = Math.pow(10, Math.floor(Math.log(minDist) / Math.LN10)); 843 if (2 * delta >= minDist) { 844 delta *= 2; 845 } else if (5 * delta >= minDist) { 846 delta *= 5; 847 } 848 849 // Determine maximal delta to fulfill the constraint to have approx. "numberMajorTicks" majorTicks 850 delta2 = Math.pow(10, Math.floor(Math.log(maxDist) / Math.LN10)); 851 if (5 * delta2 < maxDist) { 852 delta2 *= 5; 853 } else if (2 * delta2 < maxDist) { 854 delta2 *= 2; 855 } 856 // Take the larger value of the two delta's, that is 857 // minTicksDistance has priority over numberMajorTicks 858 delta = Math.max(delta, delta2); 859 860 // < v1.6.0: 861 // delta = Math.pow(10, Math.floor(Math.log(0.6 * dist) / Math.LN10)); 862 // if (false && dist <= 6 * delta) { 863 // delta *= 0.5; 864 // } 865 return delta; 866 } 867 868 // Case of insertTicks==false 869 return Type.evaluate(this.visProp.ticksdistance); 870 }, 871 872 // /** 873 // * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to adjust the 874 // * distance between two ticks depending on {@link JXG.Ticks#minTicksDistance} value 875 // * 876 // * @param {Number} ticksDelta distance between two major ticks in user coordinates 877 // * @param {JXG.Coords} coordsZero coordinates of the point considered zero 878 // * @param {Object} deltas x and y distance in pixel between two user units 879 // * @param {Object} bounds upper and lower bound of the tick positions in user units. 880 // * @private 881 // */ 882 // adjustTickDistance: function (ticksDelta, coordsZero, deltas) { 883 // var nx, 884 // ny, 885 // // bounds, 886 // distScr, 887 // sgn = 1, 888 // ev_mintd = Type.evaluate(this.visProp.minticksdistance), 889 // ev_minti = Type.evaluate(this.visProp.minorticks); 890 891 // if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 892 // return ticksDelta; 893 // } 894 // // Seems to be ignored: 895 // // bounds = this.getLowerAndUpperBounds(coordsZero, "ticksdistance"); 896 897 // // distScr is the distance between two major Ticks in pixel 898 // nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 899 // ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 900 // distScr = coordsZero.distance( 901 // Const.COORDS_BY_SCREEN, 902 // new Coords(Const.COORDS_BY_USER, [nx, ny], this.board) 903 // ); 904 // // console.log(deltas, distScr, this.board.unitX, this.board.unitY, "ticksDelta:", ticksDelta); 905 906 // if (ticksDelta === 0.0) { 907 // return 0.0; 908 // } 909 910 // // console.log(":", distScr, ev_minti + 1, distScr / (ev_minti + 1), ev_mintd) 911 // while (false && distScr / (ev_minti + 1) < ev_mintd) { 912 // if (sgn === 1) { 913 // ticksDelta *= 2; 914 // } else { 915 // ticksDelta *= 5; 916 // } 917 // sgn *= -1; 918 919 // nx = coordsZero.usrCoords[1] + deltas.x * ticksDelta; 920 // ny = coordsZero.usrCoords[2] + deltas.y * ticksDelta; 921 // distScr = coordsZero.distance( 922 // Const.COORDS_BY_SCREEN, 923 // new Coords(Const.COORDS_BY_USER, [nx, ny], this.board) 924 // ); 925 // } 926 927 // return ticksDelta; 928 // }, 929 930 /** 931 * Auxiliary method used by {@link JXG.Ticks#generateEquidistantTicks} to create a tick 932 * in the line at the given tickPosition. 933 * 934 * @param {JXG.Coords} coordsZero coordinates of the point considered zero 935 * @param {Number} tickPosition current tick position relative to zero 936 * @param {Number} ticksDelta distance between two major ticks in user coordinates 937 * @param {Object} deltas x and y distance between two major ticks 938 * @private 939 */ 940 processTickPosition: function (coordsZero, tickPosition, ticksDelta, deltas) { 941 var x, 942 y, 943 tickCoords, 944 ti, 945 isLabelPosition, 946 ticksPerLabel = Type.evaluate(this.visProp.ticksperlabel), 947 labelVal = null; 948 949 // Calculates tick coordinates 950 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 951 x = coordsZero.usrCoords[1] + tickPosition * deltas.x; 952 y = coordsZero.usrCoords[2] + tickPosition * deltas.y; 953 } else { 954 x = this.line.X(coordsZero + tickPosition); 955 y = this.line.Y(coordsZero + tickPosition); 956 } 957 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 958 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 959 labelVal = coordsZero + tickPosition; 960 this.setTicksSizeVariables(labelVal); 961 } 962 963 // Test if tick is a major tick. 964 // This is the case if tickPosition/ticksDelta is 965 // a multiple of the number of minorticks+1 966 tickCoords.major = 967 Math.round(tickPosition / ticksDelta) % 968 (Type.evaluate(this.visProp.minorticks) + 1) === 969 0; 970 971 if (!ticksPerLabel) { 972 // In case of null, 0 or false, majorTicks are labelled 973 ticksPerLabel = Type.evaluate(this.visProp.minorticks) + 1; 974 } 975 isLabelPosition = Math.round(tickPosition / ticksDelta) % ticksPerLabel === 0; 976 977 // Compute the start position and the end position of a tick. 978 // If both positions are out of the canvas, ti is empty. 979 ti = this.createTickPath(tickCoords, tickCoords.major); 980 if (ti.length === 3) { 981 this.ticks.push(ti); 982 if (isLabelPosition && Type.evaluate(this.visProp.drawlabels)) { 983 // Create a label at this position 984 this.labelsData.push( 985 this.generateLabelData( 986 this.generateLabelText(tickCoords, coordsZero, labelVal), 987 tickCoords, 988 this.ticks.length 989 ) 990 ); 991 } else { 992 // minor ticks have no labels 993 this.labelsData.push(null); 994 } 995 } 996 }, 997 998 /** 999 * Creates ticks coordinates and labels based on {@link JXG.Ticks#fixedTicks} and {@link JXG.Ticks#labels}. 1000 * 1001 * @param {JXG.Coords} coordsZero Coordinates of the point considered zero 1002 * @param {Object} bounds contains the lower and upper bounds for ticks placement 1003 * @private 1004 */ 1005 generateFixedTicks: function (coordsZero, bounds) { 1006 var tickCoords, 1007 labelText, 1008 i, 1009 ti, 1010 x, 1011 y, 1012 eps2 = Mat.eps, 1013 fixedTick, 1014 hasLabelOverrides = Type.isArray(this.visProp.labels), 1015 deltas, 1016 ev_dl = Type.evaluate(this.visProp.drawlabels); 1017 1018 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 1019 // Calculate x and y distances between two points on the line which are 1 unit apart 1020 // In essence, these are cosine and sine. 1021 deltas = this.getXandYdeltas(); 1022 } 1023 for (i = 0; i < this.fixedTicks.length; i++) { 1024 if (this.line.elementClass === Const.OBJECT_CLASS_LINE) { 1025 fixedTick = this.fixedTicks[i]; 1026 x = coordsZero.usrCoords[1] + fixedTick * deltas.x; 1027 y = coordsZero.usrCoords[2] + fixedTick * deltas.y; 1028 } else { 1029 fixedTick = coordsZero + this.fixedTicks[i]; 1030 x = this.line.X(fixedTick); 1031 y = this.line.Y(fixedTick); 1032 } 1033 tickCoords = new Coords(Const.COORDS_BY_USER, [x, y], this.board); 1034 1035 if (this.line.elementClass === Const.OBJECT_CLASS_CURVE) { 1036 this.setTicksSizeVariables(fixedTick); 1037 } 1038 1039 // Compute the start position and the end position of a tick. 1040 // If tick is out of the canvas, ti is empty. 1041 ti = this.createTickPath(tickCoords, true); 1042 if ( 1043 ti.length === 3 && 1044 fixedTick >= bounds.lower - eps2 && 1045 fixedTick <= bounds.upper + eps2 1046 ) { 1047 this.ticks.push(ti); 1048 1049 if (ev_dl && (hasLabelOverrides || Type.exists(this.visProp.labels[i]))) { 1050 labelText = hasLabelOverrides 1051 ? Type.evaluate(this.visProp.labels[i]) 1052 : fixedTick; 1053 this.labelsData.push( 1054 this.generateLabelData( 1055 this.generateLabelText(tickCoords, coordsZero, labelText), 1056 tickCoords, 1057 i 1058 ) 1059 ); 1060 } else { 1061 this.labelsData.push(null); 1062 } 1063 } 1064 } 1065 }, 1066 1067 /** 1068 * Calculates the x and y distances in user coordinates between two units in user space. 1069 * In essence, these are cosine and sine. The only work to be done is to determine 1070 * the direction of the line. 1071 * 1072 * @returns {Object} 1073 * @private 1074 */ 1075 getXandYdeltas: function () { 1076 var // Auxiliary points to store the start and end of the line according to its direction 1077 point1UsrCoords, 1078 point2UsrCoords, 1079 distP1P2 = this.line.point1.Dist(this.line.point2); 1080 1081 if (this.line.type === Const.OBJECT_TYPE_AXIS) { 1082 // When line is an Axis, direction depends on board coordinates system 1083 // Assume line.point1 and line.point2 are in correct order 1084 point1UsrCoords = this.line.point1.coords.usrCoords; 1085 point2UsrCoords = this.line.point2.coords.usrCoords; 1086 // Check if direction is incorrect, then swap 1087 if ( 1088 point1UsrCoords[1] > point2UsrCoords[1] || 1089 (Math.abs(point1UsrCoords[1] - point2UsrCoords[1]) < Mat.eps && 1090 point1UsrCoords[2] > point2UsrCoords[2]) 1091 ) { 1092 point1UsrCoords = this.line.point2.coords.usrCoords; 1093 point2UsrCoords = this.line.point1.coords.usrCoords; 1094 } 1095 } /* if (this.line.elementClass === Const.OBJECT_CLASS_LINE)*/ else { 1096 // Line direction is always from P1 to P2 for non axis types 1097 point1UsrCoords = this.line.point1.coords.usrCoords; 1098 point2UsrCoords = this.line.point2.coords.usrCoords; 1099 } 1100 return { 1101 x: (point2UsrCoords[1] - point1UsrCoords[1]) / distP1P2, 1102 y: (point2UsrCoords[2] - point1UsrCoords[2]) / distP1P2 1103 }; 1104 }, 1105 1106 /** 1107 * Check if (parts of) the tick is inside the canvas. The tick intersects the boundary 1108 * at two positions: [x[0], y[0]] and [x[1], y[1]] in screen coordinates. 1109 * @param {Array} x Array of length two 1110 * @param {Array} y Array of length two 1111 * @return {Boolean} true if parts of the tick are inside of the canvas or on the boundary. 1112 */ 1113 _isInsideCanvas: function (x, y, m) { 1114 var cw = this.board.canvasWidth, 1115 ch = this.board.canvasHeight; 1116 1117 if (m === undefined) { 1118 m = 0; 1119 } 1120 return ( 1121 (x[0] >= m && x[0] <= cw - m && y[0] >= m && y[0] <= ch - m) || 1122 (x[1] >= m && x[1] <= cw - m && y[1] >= m && y[1] <= ch - m) 1123 ); 1124 }, 1125 1126 /** 1127 * @param {JXG.Coords} coords Coordinates of the tick on the line. 1128 * @param {Boolean} major True if tick is major tick. 1129 * @returns {Array} Array of length 3 containing path coordinates in screen coordinates 1130 * of the tick (arrays of length 2). 3rd entry is true if major tick otherwise false. 1131 * If the tick is outside of the canvas, the return array is empty. 1132 * @private 1133 */ 1134 createTickPath: function (coords, major) { 1135 var c, 1136 lineStdForm, 1137 intersection, 1138 dxs, 1139 dys, 1140 dxr, 1141 dyr, 1142 alpha, 1143 style, 1144 x = [-2000000, -2000000], 1145 y = [-2000000, -2000000], 1146 i, r, r_max, bb, full, delta, 1147 // Used for infinite ticks 1148 te0, te1, // Tick ending visProps 1149 dists; // 'signed' distances of intersections to the parent line 1150 1151 c = coords.scrCoords; 1152 if (major) { 1153 dxs = this.dxMaj; 1154 dys = this.dyMaj; 1155 style = this.majStyle; 1156 te0 = Type.evaluate(this.visProp.majortickendings[0]) > 0; 1157 te1 = Type.evaluate(this.visProp.majortickendings[1]) > 0; 1158 } else { 1159 dxs = this.dxMin; 1160 dys = this.dyMin; 1161 style = this.minStyle; 1162 te0 = Type.evaluate(this.visProp.tickendings[0]) > 0; 1163 te1 = Type.evaluate(this.visProp.tickendings[1]) > 0; 1164 } 1165 lineStdForm = [-dys * c[1] - dxs * c[2], dys, dxs]; 1166 1167 // For all ticks regardless if of finite or infinite 1168 // tick length the intersection with the canvas border is 1169 // computed. 1170 if (major && Type.evaluate(this.visProp.type) === "polar") { 1171 // polar style 1172 bb = this.board.getBoundingBox(); 1173 full = 2.0 * Math.PI; 1174 delta = full / 180; 1175 //ratio = this.board.unitY / this.board.X; 1176 1177 // usrCoords: Test if 'circle' is inside of the canvas 1178 c = coords.usrCoords; 1179 r = Mat.hypot(c[1], c[2]); 1180 r_max = Math.max( 1181 Mat.hypot(bb[0], bb[1]), 1182 Mat.hypot(bb[2], bb[3]) 1183 ); 1184 1185 if (r < r_max) { 1186 // Now, switch to screen coords 1187 x = []; 1188 y = []; 1189 for (i = 0; i <= full; i += delta) { 1190 x.push( 1191 this.board.origin.scrCoords[1] + r * Math.cos(i) * this.board.unitX 1192 ); 1193 y.push( 1194 this.board.origin.scrCoords[2] + r * Math.sin(i) * this.board.unitY 1195 ); 1196 } 1197 return [x, y, major]; 1198 } 1199 } else { 1200 // line style 1201 if (style === 'infinite') { 1202 // Problematic are infinite ticks which have set tickendings:[0,1]. 1203 // For example, this is the default setting for minor ticks 1204 if (Type.evaluate(this.visProp.ignoreinfinitetickendings)) { 1205 te0 = te1 = true; 1206 } 1207 intersection = Geometry.meetLineBoard(lineStdForm, this.board); 1208 1209 if (te0 && te1) { 1210 x[0] = intersection[0].scrCoords[1]; 1211 x[1] = intersection[1].scrCoords[1]; 1212 y[0] = intersection[0].scrCoords[2]; 1213 y[1] = intersection[1].scrCoords[2]; 1214 } else { 1215 // Assuming the usrCoords of both intersections are normalized, a 'signed distance' 1216 // with respect to the parent line is computed for the intersections. The sign is 1217 // used to conclude whether the point is either at the left or right side of the 1218 // line. The magnitude can be used to compare the points and determine which point 1219 // is closest to the line. 1220 dists = [ 1221 Mat.innerProduct( 1222 intersection[0].usrCoords.slice(1, 3), 1223 this.line.stdform.slice(1, 3) 1224 ) + this.line.stdform[0], 1225 Mat.innerProduct( 1226 intersection[1].usrCoords.slice(1, 3), 1227 this.line.stdform.slice(1, 3) 1228 ) + this.line.stdform[0] 1229 ]; 1230 1231 // Reverse intersection array order if first intersection is not the leftmost one. 1232 if (dists[0] < dists[1]) { 1233 intersection.reverse(); 1234 dists.reverse(); 1235 } 1236 1237 if (te0) { // Left-infinite tick 1238 if (dists[0] < 0) { // intersections at the wrong side of line 1239 return []; 1240 } else if (dists[1] < 0) { // 'default' case, tick drawn from line to board bounds 1241 x[0] = intersection[0].scrCoords[1]; 1242 y[0] = intersection[0].scrCoords[2]; 1243 x[1] = c[1]; 1244 y[1] = c[2]; 1245 } else { // tick visible, but coords of tick on line are outside the visible area 1246 x[0] = intersection[0].scrCoords[1]; 1247 y[0] = intersection[0].scrCoords[2]; 1248 x[1] = intersection[1].scrCoords[1]; 1249 y[1] = intersection[1].scrCoords[2]; 1250 } 1251 } else if (te1) { // Right-infinite tick 1252 if (dists[1] > 0) { // intersections at the wrong side of line 1253 return []; 1254 } else if (dists[0] > 0) { // 'default' case, tick drawn from line to board bounds 1255 x[0] = c[1]; 1256 y[0] = c[2]; 1257 x[1] = intersection[1].scrCoords[1]; 1258 y[1] = intersection[1].scrCoords[2]; 1259 } else { // tick visible, but coords of tick on line are outside the visible area 1260 x[0] = intersection[0].scrCoords[1]; 1261 y[0] = intersection[0].scrCoords[2]; 1262 x[1] = intersection[1].scrCoords[1]; 1263 y[1] = intersection[1].scrCoords[2]; 1264 } 1265 } 1266 } 1267 } else { 1268 if (Type.evaluate(this.visProp.face) === ">") { 1269 alpha = Math.PI / 4; 1270 } else if (Type.evaluate(this.visProp.face) === "<") { 1271 alpha = -Math.PI / 4; 1272 } else { 1273 alpha = 0; 1274 } 1275 dxr = Math.cos(alpha) * dxs - Math.sin(alpha) * dys; 1276 dyr = Math.sin(alpha) * dxs + Math.cos(alpha) * dys; 1277 1278 x[0] = c[1] + dxr * te0; // Type.evaluate(this.visProp.tickendings[0]); 1279 y[0] = c[2] - dyr * te0; // Type.evaluate(this.visProp.tickendings[0]); 1280 x[1] = c[1]; 1281 y[1] = c[2]; 1282 1283 alpha = -alpha; 1284 dxr = Math.cos(alpha) * dxs - Math.sin(alpha) * dys; 1285 dyr = Math.sin(alpha) * dxs + Math.cos(alpha) * dys; 1286 1287 x[2] = c[1] - dxr * te1; // Type.evaluate(this.visProp.tickendings[1]); 1288 y[2] = c[2] + dyr * te1; // Type.evaluate(this.visProp.tickendings[1]); 1289 } 1290 1291 // Check if (parts of) the tick is inside the canvas. 1292 if (this._isInsideCanvas(x, y)) { 1293 return [x, y, major]; 1294 } 1295 } 1296 1297 return []; 1298 }, 1299 1300 /** 1301 * Format label texts. Show the desired number of digits 1302 * and use utf-8 minus sign. 1303 * @param {Number} value Number to be displayed 1304 * @return {String} The value converted into a string. 1305 * @private 1306 */ 1307 formatLabelText: function (value) { 1308 var labelText, 1309 digits, 1310 ev_s = Type.evaluate(this.visProp.scalesymbol); 1311 1312 if (Type.isNumber(value)) { 1313 digits = Type.evaluate(this.visProp.digits); 1314 1315 if (this.useLocale()) { 1316 labelText = this.formatNumberLocale(value, digits); 1317 } else { 1318 labelText = (Math.round(value * 1e11) / 1e11).toString(); 1319 1320 if ( 1321 labelText.length > Type.evaluate(this.visProp.maxlabellength) || 1322 labelText.indexOf("e") !== -1 1323 ) { 1324 if (Type.evaluate(this.visProp.precision) !== 3 && digits === 3) { 1325 // Use the deprecated attribute "precision" 1326 digits = Type.evaluate(this.visProp.precision); 1327 } 1328 1329 //labelText = value.toPrecision(digits).toString(); 1330 labelText = value.toExponential(digits).toString(); 1331 } 1332 } 1333 1334 if (Type.evaluate(this.visProp.beautifulscientificticklabels)) { 1335 labelText = this.beautifyScientificNotationLabel(labelText); 1336 } 1337 1338 if (labelText.indexOf(".") > -1 && labelText.indexOf("e") === -1) { 1339 // trim trailing zeros 1340 labelText = labelText.replace(/0+$/, ""); 1341 // trim trailing . 1342 labelText = labelText.replace(/\.$/, ""); 1343 } 1344 } else { 1345 labelText = value.toString(); 1346 } 1347 1348 if (ev_s.length > 0) { 1349 if (labelText === "1") { 1350 labelText = ev_s; 1351 } else if (labelText === "-1") { 1352 labelText = "-" + ev_s; 1353 } else if (labelText !== "0") { 1354 labelText = labelText + ev_s; 1355 } 1356 } 1357 1358 if (Type.evaluate(this.visProp.useunicodeminus)) { 1359 labelText = labelText.replace(/-/g, "\u2212"); 1360 } 1361 return labelText; 1362 }, 1363 1364 /** 1365 * Formats label texts to make labels displayed in scientific notation look beautiful. 1366 * For example, label 5.00e+6 will become 5•10⁶, label -1.00e-7 will become into -1•10⁻⁷ 1367 * @param {String} labelText - The label that we want to convert 1368 * @returns {String} If labelText was not in scientific notation, return labelText without modifications. 1369 * Otherwise returns beautified labelText with proper superscript notation. 1370 */ 1371 beautifyScientificNotationLabel: function (labelText) { 1372 var returnString; 1373 1374 if (labelText.indexOf("e") === -1) { 1375 return labelText; 1376 } 1377 1378 // Clean up trailing 0's, so numbers like 5.00e+6.0 for example become into 5e+6 1379 returnString = 1380 parseFloat(labelText.substring(0, labelText.indexOf("e"))) + 1381 labelText.substring(labelText.indexOf("e")); 1382 1383 // Replace symbols like -,0,1,2,3,4,5,6,7,8,9 with their superscript version. 1384 // Gets rid of + symbol since there is no need for it anymore. 1385 returnString = returnString.replace(/e(.*)$/g, function (match, $1) { 1386 var temp = "\u2022" + "10"; 1387 // Note: Since board ticks do not support HTTP elements like <sub>, we need to replace 1388 // all the numbers with superscript Unicode characters. 1389 temp += $1 1390 .replace(/-/g, "\u207B") 1391 .replace(/\+/g, "") 1392 .replace(/0/g, "\u2070") 1393 .replace(/1/g, "\u00B9") 1394 .replace(/2/g, "\u00B2") 1395 .replace(/3/g, "\u00B3") 1396 .replace(/4/g, "\u2074") 1397 .replace(/5/g, "\u2075") 1398 .replace(/6/g, "\u2076") 1399 .replace(/7/g, "\u2077") 1400 .replace(/8/g, "\u2078") 1401 .replace(/9/g, "\u2079"); 1402 1403 return temp; 1404 }); 1405 1406 return returnString; 1407 }, 1408 1409 /** 1410 * Creates the label text for a given tick. A value for the text can be provided as a number or string 1411 * 1412 * @param {JXG.Coords} tick The Coords-object of the tick to create a label for 1413 * @param {JXG.Coords} zero The Coords-object of line's zero 1414 * @param {Number|String} value A predefined value for this tick 1415 * @returns {String} 1416 * @private 1417 */ 1418 generateLabelText: function (tick, zero, value) { 1419 var labelText, distance; 1420 1421 // No value provided, equidistant, so assign distance as value 1422 if (!Type.exists(value)) { 1423 // could be null or undefined 1424 distance = this.getDistanceFromZero(zero, tick); 1425 if (Math.abs(distance) < Mat.eps) { 1426 // Point is zero 1427 return "0"; 1428 } 1429 value = distance / Type.evaluate(this.visProp.scale); 1430 } 1431 labelText = this.formatLabelText(value); 1432 1433 return labelText; 1434 }, 1435 1436 /** 1437 * Create a tick label data, i.e. text and coordinates 1438 * @param {String} labelText 1439 * @param {JXG.Coords} tick 1440 * @param {Number} tickNumber 1441 * @returns {Object} with properties 'x', 'y', 't' (text), 'i' (tick number) or null in case of o label 1442 * @private 1443 */ 1444 generateLabelData: function (labelText, tick, tickNumber) { 1445 var xa, ya, m, fs; 1446 1447 // Test if large portions of the label are inside of the canvas 1448 // This is the last chance to abandon the creation of the label if it is mostly 1449 // outside of the canvas. 1450 fs = Type.evaluate(this.visProp.label.fontsize); 1451 xa = [tick.scrCoords[1], tick.scrCoords[1]]; 1452 ya = [tick.scrCoords[2], tick.scrCoords[2]]; 1453 m = fs === undefined ? 12 : fs; 1454 m *= 0.5; 1455 if (!this._isInsideCanvas(xa, ya, m)) { 1456 return null; 1457 } 1458 1459 xa = Type.evaluate(this.visProp.label.offset[0]); 1460 ya = Type.evaluate(this.visProp.label.offset[1]); 1461 1462 return { 1463 x: tick.usrCoords[1] + xa / this.board.unitX, 1464 y: tick.usrCoords[2] + ya / this.board.unitY, 1465 t: labelText, 1466 i: tickNumber 1467 }; 1468 }, 1469 1470 /** 1471 * Recalculate the tick positions and the labels. 1472 * @returns {JXG.Ticks} 1473 */ 1474 update: function () { 1475 if (this.needsUpdate) { 1476 //this.visPropCalc.visible = Type.evaluate(this.visProp.visible); 1477 // A canvas with no width or height will create an endless loop, so ignore it 1478 if (this.board.canvasWidth !== 0 && this.board.canvasHeight !== 0) { 1479 this.calculateTicksCoordinates(); 1480 } 1481 // this.updateVisibility(this.line.visPropCalc.visible); 1482 // 1483 // for (var i = 0; i < this.labels.length; i++) { 1484 // if (this.labels[i] !== null) { 1485 // this.labels[i].prepareUpdate() 1486 // .updateVisibility(this.line.visPropCalc.visible) 1487 // .updateRenderer(); 1488 // } 1489 // } 1490 } 1491 1492 return this; 1493 }, 1494 1495 /** 1496 * Uses the boards renderer to update the arc. 1497 * @returns {JXG.Ticks} Reference to the object. 1498 */ 1499 updateRenderer: function () { 1500 if (!this.needsUpdate) { 1501 return this; 1502 } 1503 1504 if (this.visPropCalc.visible) { 1505 this.board.renderer.updateTicks(this); 1506 } 1507 this.updateRendererLabels(); 1508 1509 this.setDisplayRendNode(); 1510 // if (this.visPropCalc.visible != this.visPropOld.visible) { 1511 // this.board.renderer.display(this, this.visPropCalc.visible); 1512 // this.visPropOld.visible = this.visPropCalc.visible; 1513 // } 1514 1515 this.needsUpdate = false; 1516 return this; 1517 }, 1518 1519 /** 1520 * Updates the label elements of the major ticks. 1521 * 1522 * @private 1523 * @returns {JXG.Ticks} Reference to the object. 1524 */ 1525 updateRendererLabels: function () { 1526 var i, j, lenData, lenLabels, attr, label, ld, visible; 1527 1528 // The number of labels needed 1529 lenData = this.labelsData.length; 1530 // The number of labels which already exist 1531 // The existing labels are stored in this.labels[] 1532 // The new label positions and label values are stored in this.labelsData[] 1533 lenLabels = this.labels.length; 1534 1535 for (i = 0, j = 0; i < lenData; i++) { 1536 if (this.labelsData[i] === null) { 1537 // This is a tick without label 1538 continue; 1539 } 1540 1541 ld = this.labelsData[i]; 1542 if (j < lenLabels) { 1543 // Take an already existing text element 1544 label = this.labels[j]; 1545 label.setText(ld.t); 1546 label.setCoords(ld.x, ld.y); 1547 j++; 1548 } else { 1549 // A new text element is needed 1550 this.labelCounter += 1; 1551 1552 attr = { 1553 isLabel: true, 1554 layer: this.board.options.layer.line, 1555 highlightStrokeColor: this.board.options.text.strokeColor, 1556 highlightStrokeWidth: this.board.options.text.strokeWidth, 1557 highlightStrokeOpacity: this.board.options.text.strokeOpacity, 1558 priv: this.visProp.priv 1559 }; 1560 attr = Type.deepCopy(attr, this.visProp.label); 1561 attr.id = this.id + ld.i + "Label" + this.labelCounter; 1562 1563 label = JXG.createText(this.board, [ld.x, ld.y, ld.t], attr); 1564 this.addChild(label); 1565 label.setParents(this); 1566 label.isDraggable = false; 1567 label.dump = false; 1568 this.labels.push(label); 1569 } 1570 1571 // Look-ahead if the label inherits visibility. 1572 // If yes, update label. 1573 visible = Type.evaluate(this.visProp.label.visible); 1574 if (visible === 'inherit') { 1575 visible = this.visPropCalc.visible; 1576 } 1577 1578 label.prepareUpdate().updateVisibility(visible).updateRenderer(); 1579 1580 label.distanceX = Type.evaluate(this.visProp.label.offset[0]); 1581 label.distanceY = Type.evaluate(this.visProp.label.offset[1]); 1582 } 1583 1584 // Hide unused labels 1585 lenData = j; 1586 for (j = lenData; j < lenLabels; j++) { 1587 this.board.renderer.display(this.labels[j], false); 1588 // Tick labels have the attribute "visible: 'inherit'" 1589 // This must explicitly set to false, otherwise 1590 // this labels would be set to visible in the upcoming 1591 // update of the labels. 1592 this.labels[j].visProp.visible = this.labels[j].visPropCalc.visible = false; 1593 } 1594 1595 return this; 1596 }, 1597 1598 hideElement: function () { 1599 var i; 1600 1601 JXG.deprecated("Element.hideElement()", "Element.setDisplayRendNode()"); 1602 1603 this.visPropCalc.visible = false; 1604 this.board.renderer.display(this, false); 1605 for (i = 0; i < this.labels.length; i++) { 1606 if (Type.exists(this.labels[i])) { 1607 this.labels[i].hideElement(); 1608 } 1609 } 1610 1611 return this; 1612 }, 1613 1614 showElement: function () { 1615 var i; 1616 1617 JXG.deprecated("Element.showElement()", "Element.setDisplayRendNode()"); 1618 1619 this.visPropCalc.visible = true; 1620 this.board.renderer.display(this, false); 1621 1622 for (i = 0; i < this.labels.length; i++) { 1623 if (Type.exists(this.labels[i])) { 1624 this.labels[i].showElement(); 1625 } 1626 } 1627 1628 return this; 1629 } 1630 } 1631 ); 1632 1633 /** 1634 * @class Ticks are used as distance markers on a line or curve. 1635 * They are mainly used for axis elements and slider elements. Ticks may stretch infinitely 1636 * or finitely, which can be set with {@link Ticks#majorHeight} and {@link Ticks#minorHeight}. 1637 * <p> 1638 * There are the following ways to position the tick lines: 1639 * <ol> 1640 * <li> If an array is given as optional second parameter for the constructor 1641 * like e.g. <tt>board.create('ticks', [line, [1, 4, 5]])</tt>, then there will be (fixed) ticks at position 1642 * 1, 4 and 5 of the line. 1643 * <li> If there is only one parameter given, like e.g. <tt>board.create('ticks', [line])</tt>, the ticks will be set 1644 * equidistant across the line element. There are two variants: 1645 * <ol type="i"> 1646 * <li> Setting the attribute <tt>insertTicks:false</tt>: in this case the distance between two major ticks 1647 * is determined by the attribute <tt>ticksDistance</tt>. This distance is given in user units. 1648 * <li> Setting the attribute <tt>insertTicks:true</tt>: in this case the distance between two major ticks 1649 * is set automatically, depending on 1650 * <ul> 1651 * <li> the size of the board, 1652 * <li> the attribute <tt>minTicksDistance</tt>, which is the minimum distance between two consecutive minor ticks (in pixel). 1653 * </ul> 1654 * The distance between two major ticks is a value of the form 1655 * <i>a 10<sup>i</sup></i>, where <i>a</i> is one of <i>{1, 2, 5}</i> and 1656 * the number <i>a 10<sup>i</sup></i> is maximized such that there are approximately 1657 * 6 major ticks and there are at least "minTicksDistance" pixel between minor ticks. 1658 * </ol> 1659 * <p> 1660 * For arbitrary lines (and not axes) a "zero coordinate" is determined 1661 * which defines where the first tick is positioned. This zero coordinate 1662 * can be altered with the attribute <tt>anchor</tt>. Possible values are "left", "middle", "right" or a number. 1663 * The default value is "left". 1664 * 1665 * @pseudo 1666 * @name Ticks 1667 * @augments JXG.Ticks 1668 * @constructor 1669 * @type JXG.Ticks 1670 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1671 * @param {JXG.Line|JXG.Curve} line The parents consist of the line or curve the ticks are going to be attached to. 1672 * @param {Array} [ticks] Optional array of numbers. If given, a fixed number of static ticks is created 1673 * at these user-supplied positions. 1674 * <p> 1675 * Deprecated: Alternatively, a number defining the distance between two major ticks 1676 * can be specified. However, this is meanwhile ignored. Use attribute <tt>ticksDistance</tt> instead. 1677 * 1678 * @example 1679 * // Add ticks to line 'l1' through 'p1' and 'p2'. The major ticks are 1680 * // two units apart and 40 px long. 1681 * var p1 = board.create('point', [0, 3]); 1682 * var p2 = board.create('point', [1, 3]); 1683 * var l1 = board.create('line', [p1, p2]); 1684 * var t = board.create('ticks', [l1], { 1685 * ticksDistance: 2, 1686 * majorHeight: 40 1687 * }); 1688 * </pre><div class="jxgbox" id="JXGee7f2d68-75fc-4ec0-9931-c76918427e63" style="width: 300px; height: 300px;"></div> 1689 * <script type="text/javascript"> 1690 * (function () { 1691 * var board = JXG.JSXGraph.initBoard('JXGee7f2d68-75fc-4ec0-9931-c76918427e63', { 1692 * boundingbox: [-1, 7, 7, -1], axis: true, showcopyright: false, shownavigation: true}); 1693 * var p1 = board.create('point', [0, 3]); 1694 * var p2 = board.create('point', [1, 3]); 1695 * var l1 = board.create('line', [p1, p2]); 1696 * var t = board.create('ticks', [l1, 2], {ticksDistance: 2, majorHeight: 40}); 1697 * })(); 1698 * </script><pre> 1699 */ 1700 JXG.createTicks = function (board, parents, attributes) { 1701 var el, 1702 dist, 1703 attr = Type.copyAttributes(attributes, board.options, "ticks"); 1704 1705 if (parents.length < 2) { 1706 dist = attr.ticksdistance; // Will be ignored anyhow and attr.ticksDistance will be used instead 1707 } else { 1708 dist = parents[1]; 1709 } 1710 1711 if ( 1712 parents[0].elementClass === Const.OBJECT_CLASS_LINE || 1713 parents[0].elementClass === Const.OBJECT_CLASS_CURVE 1714 ) { 1715 el = new JXG.Ticks(parents[0], dist, attr); 1716 } else { 1717 throw new Error( 1718 "JSXGraph: Can't create Ticks with parent types '" + typeof parents[0] + "'." 1719 ); 1720 } 1721 1722 // deprecated 1723 if (Type.isFunction(attr.generatelabelvalue)) { 1724 el.generateLabelText = attr.generatelabelvalue; 1725 } 1726 if (Type.isFunction(attr.generatelabeltext)) { 1727 el.generateLabelText = attr.generatelabeltext; 1728 } 1729 1730 el.setParents(parents[0]); 1731 el.isDraggable = true; 1732 el.fullUpdate(parents[0].visPropCalc.visible); 1733 1734 return el; 1735 }; 1736 1737 /** 1738 * @class Hatches can be used to mark congruent lines or curves. 1739 * @pseudo 1740 * @name Hatch 1741 * @augments JXG.Ticks 1742 * @constructor 1743 * @type JXG.Ticks 1744 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1745 * @param {JXG.Line|JXG.curve} line The line or curve the hatch marks are going to be attached to. 1746 * @param {Number} numberofhashes Number of dashes. The distance of the hashes can be controlled with the attribute ticksDistance. 1747 * @example 1748 * // Create an axis providing two coords pairs. 1749 * var p1 = board.create('point', [0, 3]); 1750 * var p2 = board.create('point', [1, 3]); 1751 * var l1 = board.create('line', [p1, p2]); 1752 * var t = board.create('hatch', [l1, 3]); 1753 * </pre><div class="jxgbox" id="JXG4a20af06-4395-451c-b7d1-002757cf01be" style="width: 300px; height: 300px;"></div> 1754 * <script type="text/javascript"> 1755 * (function () { 1756 * var board = JXG.JSXGraph.initBoard('JXG4a20af06-4395-451c-b7d1-002757cf01be', {boundingbox: [-1, 7, 7, -1], showcopyright: false, shownavigation: false}); 1757 * var p1 = board.create('point', [0, 3]); 1758 * var p2 = board.create('point', [1, 3]); 1759 * var l1 = board.create('line', [p1, p2]); 1760 * var t = board.create('hatch', [l1, 3]); 1761 * })(); 1762 * </script><pre> 1763 * 1764 * @example 1765 * // Alter the position of the hatch 1766 * 1767 * var p = board.create('point', [-5, 0]); 1768 * var q = board.create('point', [5, 0]); 1769 * var li = board.create('line', [p, q]); 1770 * var h = board.create('hatch', [li, 2], {anchor: 0.2, ticksDistance:0.4}); 1771 * 1772 * </pre><div id="JXG05d720ee-99c9-11e6-a9c7-901b0e1b8723" class="jxgbox" style="width: 300px; height: 300px;"></div> 1773 * <script type="text/javascript"> 1774 * (function() { 1775 * var board = JXG.JSXGraph.initBoard('JXG05d720ee-99c9-11e6-a9c7-901b0e1b8723', 1776 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1777 * 1778 * var p = board.create('point', [-5, 0]); 1779 * var q = board.create('point', [5, 0]); 1780 * var li = board.create('line', [p, q]); 1781 * var h = board.create('hatch', [li, 2], {anchor: 0.2, ticksDistance:0.4}); 1782 * 1783 * })(); 1784 * 1785 * </script><pre> 1786 * 1787 * @example 1788 * // Alternative hatch faces 1789 * 1790 * var li = board.create('line', [[-6,0], [6,3]]); 1791 * var h1 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'|'}); 1792 * var h2 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'>', anchor: 0.3}); 1793 * var h3 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'<', anchor: 0.7}); 1794 * 1795 * </pre><div id="JXG974f7e89-eac8-4187-9aa3-fb8068e8384b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1796 * <script type="text/javascript"> 1797 * (function() { 1798 * var board = JXG.JSXGraph.initBoard('JXG974f7e89-eac8-4187-9aa3-fb8068e8384b', 1799 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1800 * // Alternative hatch faces 1801 * 1802 * var li = board.create('line', [[-6,0], [6,3]]); 1803 * var h1 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'|'}); 1804 * var h2 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'>', anchor: 0.3}); 1805 * var h3 = board.create('hatch', [li, 2], {tickEndings: [1,1], face:'<', anchor: 0.7}); 1806 * 1807 * })(); 1808 * 1809 * </script><pre> 1810 * 1811 */ 1812 JXG.createHatchmark = function (board, parents, attributes) { 1813 var num, i, base, width, totalwidth, el, 1814 pos = [], 1815 attr = Type.copyAttributes(attributes, board.options, 'hatch'); 1816 1817 if ( 1818 (parents[0].elementClass !== Const.OBJECT_CLASS_LINE && 1819 parents[0].elementClass !== Const.OBJECT_CLASS_CURVE) || 1820 typeof parents[1] !== "number" 1821 ) { 1822 throw new Error( 1823 "JSXGraph: Can't create Hatch mark with parent types '" + 1824 typeof parents[0] + 1825 "' and '" + 1826 typeof parents[1] + 1827 " and ''" + 1828 typeof parents[2] + 1829 "'." 1830 ); 1831 } 1832 1833 num = parents[1]; 1834 width = attr.ticksdistance; 1835 totalwidth = (num - 1) * width; 1836 base = -totalwidth * 0.5; 1837 1838 for (i = 0; i < num; i++) { 1839 pos[i] = base + i * width; 1840 } 1841 1842 el = board.create('ticks', [parents[0], pos], attr); 1843 el.elType = 'hatch'; 1844 parents[0].inherits.push(el); 1845 1846 return el; 1847 }; 1848 1849 JXG.registerElement("ticks", JXG.createTicks); 1850 JXG.registerElement("hash", JXG.createHatchmark); 1851 JXG.registerElement("hatch", JXG.createHatchmark); 1852 1853 export default JXG.Ticks; 1854 // export default { 1855 // Ticks: JXG.Ticks, 1856 // createTicks: JXG.createTicks, 1857 // createHashmark: JXG.createHatchmark, 1858 // createHatchmark: JXG.createHatchmark 1859 // }; 1860