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, unit: '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, unit: '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], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false}); 92 * board.create('smartlabel', [s1], {unit: '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], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false}); 102 * board.create('smartlabel', [s1], {unit: '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], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false}); 112 * board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false}); 113 * board.create('smartlabel', [c1], {unit: '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], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false}); 123 * board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false}); 124 * board.create('smartlabel', [c1], {unit: '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 * unit: '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 * unit: '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 + '=', unit: '°', 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 + '=', unit: '°', 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 getTextFun, txt_fun; 194 195 if (parents.length === 0 || ( 196 [Const.OBJECT_CLASS_POINT, Const.OBJECT_CLASS_LINE,Const.OBJECT_CLASS_CIRCLE].indexOf(parents[0].elementClass) < 0 && 197 [Const.OBJECT_TYPE_POLYGON, Const.OBJECT_TYPE_ANGLE].indexOf(parents[0].type) < 0 198 ) 199 ) { 200 throw new Error( 201 "JSXGraph: Can't create smartlabel with parent types " + 202 "'" + typeof parents[0] + "', " + 203 "'" + typeof parents[1] + "'." 204 ); 205 } 206 207 p = parents[0]; 208 user_supplied_text = parents[1] || ''; 209 210 if (p.elementClass === Const.OBJECT_CLASS_POINT) { 211 attr = Type.copyAttributes(attributes, board.options, 'smartlabelpoint'); 212 213 } else if (p.elementClass === Const.OBJECT_CLASS_LINE) { 214 attr = Type.copyAttributes(attributes, board.options, 'smartlabelline'); 215 /** 216 * @class 217 * @ignore 218 */ 219 attr.rotate = function () { 220 return (Math.atan(p.getSlope()) * 180 / Math.PI + 360) % 360; 221 }; 222 /** 223 * @class 224 * @ignore 225 */ 226 attr.visible = function () { return (p.L() < 1.5) ? false : true; }; 227 228 } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) { 229 attr = Type.copyAttributes(attributes, board.options, 'smartlabelcircle'); 230 /** 231 * @class 232 * @ignore 233 */ 234 attr.visible = function () { return (p.Radius() < 1.5) ? false : true; }; 235 236 } else if (p.type === Const.OBJECT_TYPE_POLYGON) { 237 attr = Type.copyAttributes(attributes, board.options, 'smartlabelpolygon'); 238 } else if (p.type === Const.OBJECT_TYPE_ANGLE) { 239 attr = Type.copyAttributes(attributes, board.options, 'smartlabelangle'); 240 /** 241 * @class 242 * @ignore 243 */ 244 attr.rotate = function () { 245 var c1 = p.center.coords.usrCoords, 246 c2 = p.getLabelAnchor().usrCoords, 247 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360; 248 return (v > 90 && v < 270) ? v + 180 : v; 249 }; 250 /** 251 * @class 252 * @ignore 253 */ 254 attr.anchorX = function () { 255 var c1 = p.center.coords.usrCoords, 256 c2 = p.getLabelAnchor().usrCoords, 257 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360; 258 return (v > 90 && v < 270) ? 'right' : 'left'; 259 }; 260 } 261 262 getTextFun = function (el, p, elType, mType) { 263 var measure; 264 switch (mType) { 265 case 'length': 266 /** 267 * @ignore 268 */ 269 measure = function () { return p.L(); }; 270 break; 271 case 'slope': 272 /** 273 * @ignore 274 */ 275 measure = function () { return p.Slope(); }; 276 break; 277 case 'area': 278 /** 279 * @ignore 280 */ 281 measure = function () { return p.Area(); }; 282 break; 283 case 'radius': 284 /** 285 * @ignore 286 */ 287 measure = function () { return p.Radius(); }; 288 break; 289 case 'perimeter': 290 /** 291 * @ignore 292 */ 293 measure = function () { return p.Perimeter(); }; 294 break; 295 case 'rad': 296 /** 297 * @ignore 298 */ 299 measure = function () { return p.Value(); }; 300 break; 301 case 'deg': 302 /** 303 * @ignore 304 */ 305 measure = function () { return p.Value() * 180 / Math.PI; }; 306 break; 307 default: 308 /** 309 * @ignore 310 */ 311 measure = function () { return 0.0; }; 312 } 313 314 return function () { 315 var str = '', 316 val, 317 txt = Type.evaluate(user_supplied_text), 318 digits = el.evalVisProp('digits'), 319 u = el.evalVisProp('unit'), 320 pre = el.evalVisProp('prefix'), 321 suf = el.evalVisProp('suffix'), 322 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex'); 323 324 if (txt === '') { 325 if (el.useLocale()) { 326 val = el.formatNumberLocale(measure(), digits); 327 } else { 328 val = Type.toFixed(measure(), digits); 329 } 330 if (mj) { 331 str = ['\\(', pre, val, '\\,', u, suf, '\\)'].join(''); 332 } else { 333 str = [pre, val, u, suf].join(''); 334 } 335 } else { 336 str = txt; 337 } 338 return str; 339 }; 340 }; 341 342 if (p.elementClass === Const.OBJECT_CLASS_POINT) { 343 el = board.create('text', [ 344 function () { return p.X(); }, 345 function () { return p.Y(); }, 346 '' 347 ], attr); 348 349 txt_fun = function () { 350 var str = '', 351 txt = Type.evaluate(user_supplied_text), 352 digits = el.evalVisProp('digits'), 353 u = el.evalVisProp('unit'), 354 pre = el.evalVisProp('prefix'), 355 suf = el.evalVisProp('suffix'), 356 dir = el.evalVisProp('dir'), 357 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex'), 358 x, y; 359 360 if (el.useLocale()) { 361 x = el.formatNumberLocale(p.X(), digits); 362 y = el.formatNumberLocale(p.Y(), digits); 363 } else { 364 x = Type.toFixed(p.X(), digits); 365 y = Type.toFixed(p.Y(), digits); 366 } 367 368 if (txt === '') { 369 if (dir === 'row') { 370 if (mj) { 371 str = ['\\(', pre, x, '\\,', u, ' / ', y, '\\,', u, suf, '\\)'].join(''); 372 } else { 373 str = [pre, x, ' ', u, ' / ', y, ' ', u, suf].join(''); 374 } 375 } else if (dir.indexOf('col') === 0) { // Starts with 'col' 376 if (mj) { 377 str = ['\\(', pre, '\\left(\\array{', x, '\\,', u, '\\\\ ', y, '\\,', u, '}\\right)', suf, '\\)'].join(''); 378 } else { 379 str = [pre, x, ' ', u, '<br />', y, ' ', u, suf].join(''); 380 } 381 } 382 } else { 383 str = txt; 384 } 385 return str; 386 }; 387 388 } else if (p.elementClass === Const.OBJECT_CLASS_LINE) { 389 390 if (attr.measure === 'length') { 391 el = board.create('text', [ 392 function () { return (p.point1.X() + p.point2.X()) * 0.5; }, 393 function () { return (p.point1.Y() + p.point2.Y()) * 0.5; }, 394 '' 395 ], attr); 396 txt_fun = getTextFun(el, p, 'line', 'length'); 397 398 } else if (attr.measure === 'slope') { 399 el = board.create('text', [ 400 function () { return (p.point1.X() * 0.25 + p.point2.X() * 0.75); }, 401 function () { return (p.point1.Y() * 0.25 + p.point2.Y() * 0.75); }, 402 '' 403 ], attr); 404 txt_fun = getTextFun(el, p, 'line', 'slope'); 405 } 406 407 } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) { 408 if (attr.measure === 'radius') { 409 el = board.create('text', [ 410 function () { return p.center.X() + p.Radius() * 0.5; }, 411 function () { return p.center.Y(); }, 412 '' 413 ], attr); 414 txt_fun = getTextFun(el, p, 'circle', 'radius'); 415 416 } else if (attr.measure === 'area') { 417 el = board.create('text', [ 418 function () { return p.center.X(); }, 419 function () { return p.center.Y() + p.Radius() * 0.5; }, 420 '' 421 ], attr); 422 txt_fun = getTextFun(el, p, 'circle', 'area'); 423 424 } else if (attr.measure === 'circumference' || attr.measure === 'perimeter') { 425 el = board.create('text', [ 426 function () { return p.getLabelAnchor(); }, 427 '' 428 ], attr); 429 txt_fun = getTextFun(el, p, 'circle', 'perimeter'); 430 431 } 432 } else if (p.type === Const.OBJECT_TYPE_POLYGON) { 433 if (attr.measure === 'area') { 434 el = board.create('text', [ 435 function () { return p.getTextAnchor(); }, 436 '' 437 ], attr); 438 txt_fun = getTextFun(el, p, 'polygon', 'area'); 439 440 } else if (attr.measure === 'perimeter') { 441 el = board.create('text', [ 442 function () { 443 var last = p.borders.length - 1; 444 if (last >= 0) { 445 return [ 446 (p.borders[last].point1.X() + p.borders[last].point2.X()) * 0.5, 447 (p.borders[last].point1.Y() + p.borders[last].point2.Y()) * 0.5 448 ]; 449 } else { 450 return p.getTextAnchor(); 451 } 452 }, 453 '' 454 ], attr); 455 txt_fun = getTextFun(el, p, 'polygon', 'perimeter'); 456 } 457 458 } else if (p.type === Const.OBJECT_TYPE_ANGLE) { 459 el = board.create('text', [ 460 function () { 461 return p.getLabelAnchor(); 462 }, 463 '' 464 ], attr); 465 txt_fun = getTextFun(el, p, 'angle', attr.measure); 466 } 467 468 if (Type.exists(el)) { 469 el.setText(txt_fun); 470 p.addChild(el); 471 el.setParents([p]); 472 } 473 474 return el; 475 }; 476 477 JXG.registerElement("smartlabel", JXG.createSmartLabel); 478