1 /* 2 Copyright 2008-2026 3 Matthias Ehmann, 4 Carsten Miller, 5 Alfred Wassermann 6 7 This file is part of JSXGraph. 8 9 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 10 11 You can redistribute it and/or modify it under the terms of the 12 13 * GNU Lesser General Public License as published by 14 the Free Software Foundation, either version 3 of the License, or 15 (at your option) any later version 16 OR 17 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 18 19 JSXGraph is distributed in the hope that it will be useful, 20 but WITHOUT ANY WARRANTY; without even the implied warranty of 21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 GNU Lesser General Public License for more details. 23 24 You should have received a copy of the GNU Lesser General Public License and 25 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 26 and <https://opensource.org/licenses/MIT/>. 27 */ 28 29 /*global JXG: true, define: true*/ 30 /*jslint nomen: true, plusplus: true*/ 31 32 /** 33 * @fileoverview Implementation of smart labels.. 34 */ 35 36 import JXG from "../jxg.js"; 37 import Const from "../base/constants.js"; 38 import Type from "../utils/type.js"; 39 40 /** 41 * @class Customized text elements for displaying measurements of JSXGraph elements, 42 * Examples are length of a 43 * segment, perimeter or area of a circle or polygon (including polygonal chain), 44 * slope of a line, value of an angle, and coordinates of a point. 45 * <p> 46 * If additionally a text, or a function is supplied and the content is not the empty string, 47 * that text is displayed instead of the measurement. 48 * <p> 49 * Smartlabels use custom made CSS layouts defined in jsxgraph.css. Therefore, the inclusion of the file jsxgraph.css is mandatory or 50 * the CSS classes have to be replaced by other classes. 51 * <p> 52 * The default attributes for smartlabels are defined for each type of measured element in the following sub-objects. 53 * This is a deviation from the usual JSXGraph attribute usage. 54 * <ul> 55 * <li> <tt>JXG.Options.smartlabelangle</tt> for smartlabels of angle objects 56 * <li> <tt>JXG.Options.smartlabelcircle</tt> for smartlabels of circle objects 57 * <li> <tt>JXG.Options.smartlabelline</tt> for smartlabels of line objects 58 * <li> <tt>JXG.Options.smartlabelpoint</tt> for smartlabels of point objects. 59 * <li> <tt>JXG.Options.smartlabelpolygon</tt> for smartlabels of polygon objects. 60 * </ul> 61 * 62 * 63 * @pseudo 64 * @name Smartlabel 65 * @augments JXG.Text 66 * @constructor 67 * @type JXG.Text 68 * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown. 69 * @param {JXG.GeometryElement} Parent parent object: point, line, circle, polygon, angle. 70 * @param {String|Function} Txt Optional text. In case, this content is not the empty string, 71 * the measurement is overwritten by this text. 72 * 73 * @example 74 * var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false}); 75 * board.create('smartlabel', [p1], {digits: 1, baseUnit: 'm', dir: 'col', useMathJax: false}); 76 * 77 * </pre><div id="JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f" class="jxgbox" style="width: 300px; height: 300px;"></div> 78 * <script type="text/javascript"> 79 * (function() { 80 * var board = JXG.JSXGraph.initBoard('JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f', 81 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 82 * var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false}); 83 * board.create('smartlabel', [p1], {digits: 1, baseUnit: 'cm', dir: 'col', useMathJax: false}); 84 * 85 * })(); 86 * 87 * </script><pre> 88 * 89 * @example 90 * var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}}); 91 * board.create('smartlabel', [s1], {baseUnit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false}); 92 * board.create('smartlabel', [s1], {baseUnit: 'm', measure: 'slope', prefix: 'Δ = ', useMathJax: false}); 93 * 94 * 95 * </pre><div id="JXGfb4423dc-ee3a-4122-a186-82123019a835" class="jxgbox" style="width: 300px; height: 300px;"></div> 96 * <script type="text/javascript"> 97 * (function() { 98 * var board = JXG.JSXGraph.initBoard('JXGfb4423dc-ee3a-4122-a186-82123019a835', 99 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 100 * var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}}); 101 * board.create('smartlabel', [s1], {baseUnit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false}); 102 * board.create('smartlabel', [s1], {baseUnit: 'm', measure: 'slope', prefix: 'Δ = ', useMathJax: false}); 103 * 104 * 105 * })(); 106 * 107 * </script><pre> 108 * 109 * @example 110 * var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}}); 111 * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false}); 112 * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false}); 113 * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false}); 114 * 115 * 116 * </pre><div id="JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93" class="jxgbox" style="width: 300px; height: 300px;"></div> 117 * <script type="text/javascript"> 118 * (function() { 119 * var board = JXG.JSXGraph.initBoard('JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93', 120 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 121 * var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}}); 122 * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false}); 123 * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false}); 124 * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false}); 125 * 126 * 127 * })(); 128 * 129 * </script><pre> 130 * 131 * @example 132 * var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {}); 133 * board.create('smartlabel', [p2], { 134 * baseUnit: 'm', 135 * measure: 'area', 136 * prefix: 'A = ', 137 * cssClass: 'smart-label-pure smart-label-polygon', 138 * highlightCssClass: 'smart-label-pure smart-label-polygon', 139 * useMathJax: false 140 * }); 141 * board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], { 142 * measure: 'perimeter', 143 * cssClass: 'smart-label-outline smart-label-polygon', 144 * highlightCssClass: 'smart-label-outline smart-label-polygon', 145 * useMathJax: false 146 * }); 147 * 148 * </pre><div id="JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8" class="jxgbox" style="width: 300px; height: 300px;"></div> 149 * <script type="text/javascript"> 150 * (function() { 151 * var board = JXG.JSXGraph.initBoard('JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8', 152 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 153 * var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {}); 154 * board.create('smartlabel', [p2], { 155 * baseUnit: 'm', 156 * measure: 'area', 157 * prefix: 'A = ', 158 * cssClass: 'smart-label-pure smart-label-polygon', 159 * highlightCssClass: 'smart-label-pure smart-label-polygon', 160 * useMathJax: false 161 * }); 162 * board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], { 163 * measure: 'perimeter', 164 * cssClass: 'smart-label-outline smart-label-polygon', 165 * highlightCssClass: 'smart-label-outline smart-label-polygon', 166 * useMathJax: false 167 * }); 168 * 169 * })(); 170 * 171 * </script><pre> 172 * 173 * @example 174 * var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false}); 175 * var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', baseUnit: '°', useMathJax: false}); 176 * 177 * </pre><div id="JXG48d6d1ae-e04a-45f4-a743-273976712c0b" class="jxgbox" style="width: 300px; height: 300px;"></div> 178 * <script type="text/javascript"> 179 * (function() { 180 * var board = JXG.JSXGraph.initBoard('JXG48d6d1ae-e04a-45f4-a743-273976712c0b', 181 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 182 * var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false}); 183 * var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', baseUnit: '°', useMathJax: false}); 184 * 185 * })(); 186 * 187 * </script><pre> 188 * 189 */ 190 JXG.createSmartLabel = function (board, parents, attributes) { 191 var el, attr, 192 p, user_supplied_text; 193 194 if (parents.length === 0 || ( 195 [Const.OBJECT_CLASS_POINT, Const.OBJECT_CLASS_LINE, Const.OBJECT_CLASS_CIRCLE].indexOf(parents[0].elementClass) < 0 && 196 [Const.OBJECT_TYPE_POLYGON, Const.OBJECT_TYPE_ANGLE].indexOf(parents[0].type) < 0 197 )) { 198 throw new Error( 199 "JSXGraph: Can't create smartlabel with parent types " + 200 "'" + typeof parents[0] + "', " + 201 "'" + typeof parents[1] + "'." 202 ); 203 } 204 205 p = parents[0]; 206 user_supplied_text = parents[1] || ''; 207 208 attr = Type.copyAttributes(attributes, board.options, 'smartlabel'); 209 210 if (p.elementClass === Const.OBJECT_CLASS_POINT) { 211 attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelpoint')); 212 213 } else if (p.elementClass === Const.OBJECT_CLASS_LINE) { 214 attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelline')); 215 /** 216 * @class 217 * @ignore 218 */ 219 attr.rotate = function (self) { 220 var orientation = self.evalVisProp('orientation'), 221 add; 222 switch (orientation) { 223 case 'none': 224 return 0; 225 case 'orthogonal': 226 add = 270; 227 break; 228 case 'orthogonal-inverted': 229 add = 90; 230 break; 231 case 'parallel-inverted': 232 case 'inverted': 233 add = 0; 234 break; 235 default: 236 add = 360; 237 } 238 return (Math.atan(p.getSlope()) * 180 / Math.PI + add) % 360; 239 }; 240 /** 241 * @class 242 * @ignore 243 */ 244 attr.visible = function (self) { 245 var orientation = self.evalVisProp('orientation'), 246 thres = self.evalVisProp('visibleThreshold'), 247 sizeLabel = self.getSize(), 248 sizeParent, 249 c1, c2, 250 dx, dy; 251 252 c1 = p.point1.coords.scrCoords; 253 c2 = p.point2.coords.scrCoords; 254 dx = c2[1] - c1[1]; 255 dy = c2[2] - c1[2]; 256 sizeParent = Math.floor(Math.sqrt(dx * dx + dy * dy)); 257 258 switch (orientation) { 259 case 'parallel': 260 case 'parallel-inverted': 261 case 'inverted': 262 return sizeLabel[0] < sizeParent * thres; 263 264 case 'orthogonal': 265 case 'orthogonal-inverted': 266 return sizeLabel[1] < sizeParent * thres; 267 268 case 'none': 269 default: 270 return p.L() >= 1.5; 271 } 272 }; 273 274 } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) { 275 attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelcircle')); 276 /** 277 * @class 278 * @ignore 279 */ 280 attr.visible = function (self) { 281 var sizeLabel = self.getSize(), 282 thres = self.evalVisProp('visibleThreshold'), 283 sizeParent, 284 c1, c2, 285 dx, dy; 286 287 c1 = p.center.coords.scrCoords; 288 if (p.point2) { 289 c2 = p.point2.coords.scrCoords; 290 } else { 291 c2 = new JXG.Coords(JXG.COORDS_BY_USER, [p.center.coords.usrCoords[0], p.center.coords.usrCoords[1] + p.Radius()], p.board).scrCoords; 292 } 293 dx = c2[1] - c1[1]; 294 dy = c2[2] - c1[2]; 295 sizeParent = Math.floor(Math.sqrt(dx * dx + dy * dy)) * 2; 296 297 return sizeLabel[0] < sizeParent * thres; 298 }; 299 300 } else if (p.type === Const.OBJECT_TYPE_POLYGON) { 301 attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelpolygon')); 302 } else if (p.type === Const.OBJECT_TYPE_ANGLE) { 303 attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelangle')); 304 /** 305 * @class 306 * @ignore 307 */ 308 attr.rotate = function () { 309 var c1 = p.center.coords.usrCoords, 310 c2 = p.getLabelAnchor().usrCoords, 311 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360; 312 return (v > 90 && v < 270) ? v + 180 : v; 313 }; 314 /** 315 * @class 316 * @ignore 317 */ 318 attr.anchorX = function () { 319 var c1 = p.center.coords.usrCoords, 320 c2 = p.getLabelAnchor().usrCoords, 321 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360; 322 return (v > 90 && v < 270) ? 'right' : 'left'; 323 }; 324 } 325 326 if (p.elementClass === Const.OBJECT_CLASS_POINT) { 327 el = board.create('text', [ 328 function () { return p.X(); }, 329 function () { return p.Y(); }, 330 '' 331 ], attr); 332 333 } else if (p.elementClass === Const.OBJECT_CLASS_LINE) { 334 335 if (attr.measure === 'length') { 336 el = board.create('text', [ 337 function () { return (p.point1.X() + p.point2.X()) * 0.5; }, 338 function () { return (p.point1.Y() + p.point2.Y()) * 0.5; }, 339 '' 340 ], attr); 341 342 } else if (attr.measure === 'slope') { 343 el = board.create('text', [ 344 function () { return (p.point1.X() * 0.25 + p.point2.X() * 0.75); }, 345 function () { return (p.point1.Y() * 0.25 + p.point2.Y() * 0.75); }, 346 '' 347 ], attr); 348 } 349 350 } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) { 351 if (attr.measure === 'radius') { 352 el = board.create('text', [ 353 function () { return p.center.X() + p.Radius() * 0.5; }, 354 function () { return p.center.Y(); }, 355 '' 356 ], attr); 357 358 } else if (attr.measure === 'area') { 359 el = board.create('text', [ 360 function () { return p.center.X(); }, 361 function () { return p.center.Y() + p.Radius() * 0.5; }, 362 '' 363 ], attr); 364 365 } else if (attr.measure === 'circumference' || attr.measure === 'perimeter') { 366 el = board.create('text', [ 367 function () { return p.getLabelAnchor(); }, 368 '' 369 ], attr); 370 371 } 372 } else if (p.type === Const.OBJECT_TYPE_POLYGON) { 373 if (attr.measure === 'area') { 374 el = board.create('text', [ 375 function () { return p.getTextAnchor(); }, 376 '' 377 ], attr); 378 379 } else if (attr.measure === 'perimeter') { 380 el = board.create('text', [ 381 function () { 382 var last = p.borders.length - 1; 383 if (last >= 0) { 384 return [ 385 (p.borders[last].point1.X() + p.borders[last].point2.X()) * 0.5, 386 (p.borders[last].point1.Y() + p.borders[last].point2.Y()) * 0.5 387 ]; 388 } else { 389 return p.getTextAnchor(); 390 } 391 }, 392 '' 393 ], attr); 394 } 395 396 } else if (p.type === Const.OBJECT_TYPE_ANGLE) { 397 el = board.create('text', [ 398 function () { 399 return p.getLabelAnchor(); 400 }, 401 '' 402 ], attr); 403 } 404 405 if (!Type.exists(el)) { 406 return null; 407 } 408 409 el.elType = 'smartlabel'; 410 411 el.parentObject = p; 412 413 el.Value = function () { 414 var mType = this.evalVisProp('measure'); 415 416 switch (mType) { 417 case 'length': 418 return p.L(); 419 420 case 'slope': 421 return p.Slope(); 422 423 case 'area': 424 return p.Area(); 425 426 case 'radius': 427 return p.Radius(); 428 429 case 'perimeter': 430 case 'circumference': 431 return p.Perimeter(); 432 433 case 'rad': 434 return p.Value(); 435 436 case 'deg': 437 return p.Value() * 180 / Math.PI; 438 439 case 'coords': 440 return [p.X(), p.Y()]; 441 442 default: 443 return 0.0; 444 } 445 }; 446 447 el.Dimension = function () { 448 var mType = this.evalVisProp('measure'); 449 450 switch (mType) { 451 case 'area': 452 return 2; 453 454 case 'length': 455 case 'radius': 456 case 'perimeter': 457 case 'circumference': 458 return 1; 459 460 case 'slope': 461 return 0; 462 463 case 'rad': 464 case 'deg': 465 // Angles in various units has dimension 0 466 return 0; 467 468 case 'coords': 469 return 1; 470 471 default: 472 return 0; 473 } 474 }; 475 476 el.Unit = function (dimension) { 477 var unit = '', 478 units = el.evalVisProp('units'), 479 dim = dimension, 480 dims = {}, i; 481 482 if (!Type.exists(dim)) { 483 dim = el.Dimension(); 484 } 485 486 if (Type.isArray(dimension)) { 487 for (i = 0; i < dimension.length; i++) { 488 dims['dim' + dimension[i]] = el.Unit(dimension[i]); 489 } 490 return dims; 491 } 492 493 if (Type.isObject(units) && Type.exists(units[dim]) && units[dim] !== false) { 494 unit = el.eval(units[dim]); 495 } else if (Type.isObject(units) && Type.exists(units['dim' + dim]) && units['dim' + dim] !== false) { 496 // In some cases, object keys must not be numbers. This allows key 'dim1' instead of '1'. 497 unit = el.eval(units['dim' + dim]); 498 } else { 499 unit = el.evalVisProp('baseUnit'); 500 501 if (unit === '' && el.evalVisProp('unit') !== '') { 502 // Backwards compatibility 503 unit = el.evalVisProp('unit'); 504 } 505 506 if (dim === 0) { 507 unit = ''; 508 } else if (dim > 1 && unit !== '') { 509 unit = unit + '^{' + dim + '}'; 510 } 511 } 512 513 return unit; 514 }; 515 516 el.setText(function () { 517 var digits, val, u, 518 txt = Type.evaluate(user_supplied_text), 519 str = '', 520 pre = '', 521 suf = '', 522 dir, 523 mj, 524 i; 525 526 if (txt !== '') { 527 return txt; 528 } 529 530 val = el.Value(); 531 digits = el.evalVisProp('digits'); 532 u = el.Unit(); 533 pre = ''; 534 suf = ''; 535 dir = el.evalVisProp('dir'); 536 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex'); 537 538 if (el.evalVisProp('showPrefix')) { 539 pre = el.evalVisProp('prefix'); 540 } 541 if (el.evalVisProp('showSuffix')) { 542 suf = el.evalVisProp('suffix'); 543 } 544 545 if (el.useLocale()) { 546 if (Type.isArray(val)) { 547 for (i = 0; i < val.length; i++) { 548 val[i] = el.formatNumberLocale(val[i], digits); 549 } 550 } else { 551 val = el.formatNumberLocale(val, digits); 552 } 553 } else { 554 if (Type.isArray(val)) { 555 for (i = 0; i < val.length; i++) { 556 val[i] = Type.toFixed(val[i], digits); 557 } 558 } else { 559 val = Type.toFixed(val, digits); 560 } 561 } 562 563 if (Type.isFunction(el.visProp.formatvalue)) { 564 val = el.visProp.formatvalue(el, val); 565 } 566 567 if (Type.isArray(val)) { 568 if (dir === 'row') { 569 str = []; 570 if (mj) { 571 str.push('\\(', pre); 572 for (i = 0; i < val.length; i++) { 573 str.push(val[i], '\\,', u); 574 if (i < val.length - 1) { 575 str.push(' / '); 576 } 577 } 578 str.push(suf, '\\)'); 579 } else { 580 str.push(pre); 581 for (i = 0; i < val.length; i++) { 582 str.push(val[i], ' ', u); 583 if (i < val.length - 1) { 584 str.push(' / '); 585 } 586 } 587 str.push(suf); 588 } 589 str = str.join(''); 590 } else if (dir.indexOf('col') === 0) { // Starts with 'col' 591 str = []; 592 if (mj) { 593 str.push('\\(', pre, '\\left(\\array{'); 594 for (i = 0; i < val.length; i++) { 595 str.push(val[i], '\\,', u); 596 if (i < val.length - 1) { 597 str.push('\\\\ '); 598 } 599 } 600 str.push('}\\right)', suf, '\\)'); 601 602 } else { 603 str.push(pre); 604 for (i = 0; i < val.length; i++) { 605 str.push(val[i], ' ', u); 606 if (i < val.length - 1) { 607 str.push('<br />'); 608 } 609 } 610 str.push(suf); 611 } 612 str = str.join(''); 613 } 614 } else { 615 if (mj) { 616 str = ['\\(', pre, val, '\\,', u, suf, '\\)'].join(''); 617 } else { 618 str = [pre, val, u, suf].join(''); 619 } 620 } 621 622 return str; 623 }); 624 625 p.addChild(el); 626 el.setParents([p]); 627 628 el.methodMap = Type.deepCopy(el.methodMap, { 629 Value: "Value", 630 V: "Value", 631 Dimension: "Dimension", 632 Unit: "Unit", 633 parent: "parentObject", 634 parentObject: "parentObject" 635 }); 636 637 return el; 638 }; 639 640 JXG.registerElement("smartlabel", JXG.createSmartLabel); 641