1 /* 2 Copyright 2008-2024 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 Smart label. These are customized text elements for displaying measurements of JSXGraph elements, like length of a 42 * segment, perimeter or area of a circle or polygon (including polygonal chain), slope of a line, value of an angle, and coordinates of a point. 43 * <p> 44 * If additionally a text, or a function is supplied and the content is not the empty string, 45 * that text is displayed instead of the measurement. 46 * <p> 47 * Smartlabels use custom made CSS layouts defined in jsxgraph.css. Therefore, the inclusion of the file jsxgraph.css is mandatory or 48 * the CSS classes have to be replaced by other classes. 49 * <p> 50 * The default attributes for smartlabels are defined for each type of measured element in the following sub-objects. 51 * This is a deviation from the usual JSXGraph attribute usage. 52 * <ul> 53 * <li> <tt>JXG.Options.smartlabelangle</tt> for smartlabels of angle objects 54 * <li> <tt>JXG.Options.smartlabelcircle</tt> for smartlabels of circle objects 55 * <li> <tt>JXG.Options.smartlabelline</tt> for smartlabels of line objects 56 * <li> <tt>JXG.Options.smartlabelpoint</tt> for smartlabels of point objects. 57 * <li> <tt>JXG.Options.smartlabelpolygon</tt> for smartlabels of polygon objects. 58 * </ul> 59 * 60 * 61 * @pseudo 62 * @name Smartlabel 63 * @augments JXG.Text 64 * @constructor 65 * @type JXG.Text 66 * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown. 67 * @param {JXG.GeometryElement} Parent parent object: point, line, circle, polygon, angle. 68 * @param {String|Function} Txt Optional text. In case, this content is not the empty string, 69 * the measurement is overwritten by this text. 70 * 71 * @example 72 * var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false}); 73 * board.create('smartlabel', [p1], {digits: 1, unit: 'm', dir: 'col', useMathJax: false}); 74 * 75 * </pre><div id="JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f" class="jxgbox" style="width: 300px; height: 300px;"></div> 76 * <script type="text/javascript"> 77 * (function() { 78 * var board = JXG.JSXGraph.initBoard('JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f', 79 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 80 * var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false}); 81 * board.create('smartlabel', [p1], {digits: 1, unit: 'cm', dir: 'col', useMathJax: false}); 82 * 83 * })(); 84 * 85 * </script><pre> 86 * 87 * @example 88 * var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}}); 89 * board.create('smartlabel', [s1], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false}); 90 * board.create('smartlabel', [s1], {unit: 'm', measure: 'slope', prefix: 'Δ = ', useMathJax: false}); 91 * 92 * 93 * </pre><div id="JXGfb4423dc-ee3a-4122-a186-82123019a835" class="jxgbox" style="width: 300px; height: 300px;"></div> 94 * <script type="text/javascript"> 95 * (function() { 96 * var board = JXG.JSXGraph.initBoard('JXGfb4423dc-ee3a-4122-a186-82123019a835', 97 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 98 * var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}}); 99 * board.create('smartlabel', [s1], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false}); 100 * board.create('smartlabel', [s1], {unit: 'm', measure: 'slope', prefix: 'Δ = ', useMathJax: false}); 101 * 102 * 103 * })(); 104 * 105 * </script><pre> 106 * 107 * @example 108 * var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}}); 109 * board.create('smartlabel', [c1], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false}); 110 * board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false}); 111 * board.create('smartlabel', [c1], {unit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false}); 112 * 113 * 114 * </pre><div id="JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93" class="jxgbox" style="width: 300px; height: 300px;"></div> 115 * <script type="text/javascript"> 116 * (function() { 117 * var board = JXG.JSXGraph.initBoard('JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93', 118 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 119 * var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}}); 120 * board.create('smartlabel', [c1], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false}); 121 * board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false}); 122 * board.create('smartlabel', [c1], {unit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false}); 123 * 124 * 125 * })(); 126 * 127 * </script><pre> 128 * 129 * @example 130 * var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {}); 131 * board.create('smartlabel', [p2], { 132 * unit: 'm', 133 * measure: 'area', 134 * prefix: 'A = ', 135 * cssClass: 'smart-label-pure smart-label-polygon', 136 * highlightCssClass: 'smart-label-pure smart-label-polygon', 137 * useMathJax: false 138 * }); 139 * board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], { 140 * measure: 'perimeter', 141 * cssClass: 'smart-label-outline smart-label-polygon', 142 * highlightCssClass: 'smart-label-outline smart-label-polygon', 143 * useMathJax: false 144 * }); 145 * 146 * </pre><div id="JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8" class="jxgbox" style="width: 300px; height: 300px;"></div> 147 * <script type="text/javascript"> 148 * (function() { 149 * var board = JXG.JSXGraph.initBoard('JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8', 150 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 151 * var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {}); 152 * board.create('smartlabel', [p2], { 153 * unit: 'm', 154 * measure: 'area', 155 * prefix: 'A = ', 156 * cssClass: 'smart-label-pure smart-label-polygon', 157 * highlightCssClass: 'smart-label-pure smart-label-polygon', 158 * useMathJax: false 159 * }); 160 * board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], { 161 * measure: 'perimeter', 162 * cssClass: 'smart-label-outline smart-label-polygon', 163 * highlightCssClass: 'smart-label-outline smart-label-polygon', 164 * useMathJax: false 165 * }); 166 * 167 * })(); 168 * 169 * </script><pre> 170 * 171 * @example 172 * var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false}); 173 * var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', unit: '°', useMathJax: false}); 174 * 175 * </pre><div id="JXG48d6d1ae-e04a-45f4-a743-273976712c0b" class="jxgbox" style="width: 300px; height: 300px;"></div> 176 * <script type="text/javascript"> 177 * (function() { 178 * var board = JXG.JSXGraph.initBoard('JXG48d6d1ae-e04a-45f4-a743-273976712c0b', 179 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 180 * var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false}); 181 * var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', unit: '°', useMathJax: false}); 182 * 183 * })(); 184 * 185 * </script><pre> 186 * 187 */ 188 JXG.createSmartLabel = function (board, parents, attributes) { 189 var el, attr, 190 p, user_supplied_text, 191 getTextFun, txt_fun; 192 193 if (parents.length === 0 || ( 194 [Const.OBJECT_CLASS_POINT, Const.OBJECT_CLASS_LINE,Const.OBJECT_CLASS_CIRCLE].indexOf(parents[0].elementClass) < 0 && 195 [Const.OBJECT_TYPE_POLYGON, Const.OBJECT_TYPE_ANGLE].indexOf(parents[0].type) < 0 196 ) 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 if (p.elementClass === Const.OBJECT_CLASS_POINT) { 209 attr = Type.copyAttributes(attributes, board.options, 'smartlabelpoint'); 210 211 } else if (p.elementClass === Const.OBJECT_CLASS_LINE) { 212 attr = Type.copyAttributes(attributes, board.options, 'smartlabelline'); 213 /** 214 * @class 215 * @ignore 216 */ 217 attr.rotate = function () { return Math.atan(p.getSlope()) * 180 / Math.PI; }; 218 /** 219 * @class 220 * @ignore 221 */ 222 attr.visible = function () { return (p.L() < 1.5) ? false : true; }; 223 224 } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) { 225 attr = Type.copyAttributes(attributes, board.options, 'smartlabelcircle'); 226 /** 227 * @class 228 * @ignore 229 */ 230 attr.visible = function () { return (p.Radius() < 1.5) ? false : true; }; 231 232 } else if (p.type === Const.OBJECT_TYPE_POLYGON) { 233 attr = Type.copyAttributes(attributes, board.options, 'smartlabelpolygon'); 234 } else if (p.type === Const.OBJECT_TYPE_ANGLE) { 235 attr = Type.copyAttributes(attributes, board.options, 'smartlabelangle'); 236 /** 237 * @class 238 * @ignore 239 */ 240 attr.rotate = function () { 241 var c1 = p.center.coords.usrCoords, 242 c2 = p.getLabelAnchor().usrCoords, 243 v = Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI; 244 return (v > 90 && v < 270) ? v + 180 : v; 245 }; 246 /** 247 * @class 248 * @ignore 249 */ 250 attr.anchorX = function () { 251 var c1 = p.center.coords.usrCoords, 252 c2 = p.getLabelAnchor().usrCoords, 253 v = Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI; 254 return (v > 90 && v < 270) ? 'right' : 'left'; 255 }; 256 } 257 258 getTextFun = function (el, p, elType, mType) { 259 var measure; 260 switch (mType) { 261 case 'length': 262 /** 263 * @ignore 264 */ 265 measure = function () { return p.L(); }; 266 break; 267 case 'slope': 268 /** 269 * @ignore 270 */ 271 measure = function () { return p.Slope(); }; 272 break; 273 case 'area': 274 /** 275 * @ignore 276 */ 277 measure = function () { return p.Area(); }; 278 break; 279 case 'radius': 280 /** 281 * @ignore 282 */ 283 measure = function () { return p.Radius(); }; 284 break; 285 case 'perimeter': 286 /** 287 * @ignore 288 */ 289 measure = function () { return p.Perimeter(); }; 290 break; 291 case 'rad': 292 /** 293 * @ignore 294 */ 295 measure = function () { return p.Value(); }; 296 break; 297 case 'deg': 298 /** 299 * @ignore 300 */ 301 measure = function () { return p.Value() * 180 / Math.PI; }; 302 break; 303 default: 304 /** 305 * @ignore 306 */ 307 measure = function () { return 0.0; }; 308 } 309 310 return function () { 311 var str = '', 312 val, 313 txt = Type.evaluate(user_supplied_text), 314 digits = el.evalVisProp('digits'), 315 u = el.evalVisProp('unit'), 316 pre = el.evalVisProp('prefix'), 317 suf = el.evalVisProp('suffix'), 318 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex'); 319 320 if (txt === '') { 321 if (el.useLocale()) { 322 val = el.formatNumberLocale(measure(), digits); 323 } else { 324 val = Type.toFixed(measure(), digits); 325 } 326 if (mj) { 327 str = ['\\(', pre, val, '\\,', u, suf, '\\)'].join(''); 328 } else { 329 str = [pre, val, u, suf].join(''); 330 } 331 } else { 332 str = txt; 333 } 334 return str; 335 }; 336 }; 337 338 if (p.elementClass === Const.OBJECT_CLASS_POINT) { 339 el = board.create('text', [ 340 function () { return p.X(); }, 341 function () { return p.Y(); }, 342 '' 343 ], attr); 344 345 txt_fun = function () { 346 var str = '', 347 txt = Type.evaluate(user_supplied_text), 348 digits = el.evalVisProp('digits'), 349 u = el.evalVisProp('unit'), 350 pre = el.evalVisProp('prefix'), 351 suf = el.evalVisProp('suffix'), 352 dir = el.evalVisProp('dir'), 353 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex'), 354 x, y; 355 356 if (el.useLocale()) { 357 x = el.formatNumberLocale(p.X(), digits); 358 y = el.formatNumberLocale(p.Y(), digits); 359 } else { 360 x = Type.toFixed(p.X(), digits); 361 y = Type.toFixed(p.Y(), digits); 362 } 363 364 if (txt === '') { 365 if (dir === 'row') { 366 if (mj) { 367 str = ['\\(', pre, x, '\\,', u, ' / ', y, '\\,', u, suf, '\\)'].join(''); 368 } else { 369 str = [pre, x, ' ', u, ' / ', y, ' ', u, suf].join(''); 370 } 371 } else if (dir.indexOf('col') === 0) { // Starts with 'col' 372 if (mj) { 373 str = ['\\(', pre, '\\left(\\array{', x, '\\,', u, '\\\\ ', y, '\\,', u, '}\\right)', suf, '\\)'].join(''); 374 } else { 375 str = [pre, x, ' ', u, '<br/>', y, ' ', u, suf].join(''); 376 } 377 } 378 } else { 379 str = txt; 380 } 381 return str; 382 }; 383 384 } else if (p.elementClass === Const.OBJECT_CLASS_LINE) { 385 386 if (attr.measure === 'length') { 387 el = board.create('text', [ 388 function () { return (p.point1.X() + p.point2.X()) * 0.5; }, 389 function () { return (p.point1.Y() + p.point2.Y()) * 0.5; }, 390 '' 391 ], attr); 392 txt_fun = getTextFun(el, p, 'line', 'length'); 393 394 } else if (attr.measure === 'slope') { 395 el = board.create('text', [ 396 function () { return (p.point1.X() * 0.25 + p.point2.X() * 0.75); }, 397 function () { return (p.point1.Y() * 0.25 + p.point2.Y() * 0.75); }, 398 '' 399 ], attr); 400 txt_fun = getTextFun(el, p, 'line', 'slope'); 401 } 402 403 } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) { 404 if (attr.measure === 'radius') { 405 el = board.create('text', [ 406 function () { return p.center.X() + p.Radius() * 0.5; }, 407 function () { return p.center.Y(); }, 408 '' 409 ], attr); 410 txt_fun = getTextFun(el, p, 'circle', 'radius'); 411 412 } else if (attr.measure === 'area') { 413 el = board.create('text', [ 414 function () { return p.center.X(); }, 415 function () { return p.center.Y() + p.Radius() * 0.5; }, 416 '' 417 ], attr); 418 txt_fun = getTextFun(el, p, 'circle', 'area'); 419 420 } else if (attr.measure === 'circumference' || attr.measure === 'perimeter') { 421 el = board.create('text', [ 422 function () { return p.getLabelAnchor(); }, 423 '' 424 ], attr); 425 txt_fun = getTextFun(el, p, 'circle', 'perimeter'); 426 427 } 428 } else if (p.type === Const.OBJECT_TYPE_POLYGON) { 429 if (attr.measure === 'area') { 430 el = board.create('text', [ 431 function () { return p.getTextAnchor(); }, 432 '' 433 ], attr); 434 txt_fun = getTextFun(el, p, 'polygon', 'area'); 435 436 } else if (attr.measure === 'perimeter') { 437 el = board.create('text', [ 438 function () { 439 var last = p.borders.length - 1; 440 if (last >= 0) { 441 return [ 442 (p.borders[last].point1.X() + p.borders[last].point2.X()) * 0.5, 443 (p.borders[last].point1.Y() + p.borders[last].point2.Y()) * 0.5 444 ]; 445 } else { 446 return p.getTextAnchor(); 447 } 448 }, 449 '' 450 ], attr); 451 txt_fun = getTextFun(el, p, 'polygon', 'perimeter'); 452 } 453 454 } else if (p.type === Const.OBJECT_TYPE_ANGLE) { 455 el = board.create('text', [ 456 function () { 457 return p.getLabelAnchor(); 458 }, 459 '' 460 ], attr); 461 txt_fun = getTextFun(el, p, 'angle', attr.measure); 462 } 463 464 if (Type.exists(el)) { 465 el.setText(txt_fun); 466 p.addChild(el); 467 el.setParents([p]); 468 } 469 470 return el; 471 }; 472 473 JXG.registerElement("smartlabel", JXG.createSmartLabel); 474