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