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 measure = function () { return p.L(); }; 243 break; 244 case 'slope': 245 measure = function () { return p.Slope(); }; 246 break; 247 case 'area': 248 measure = function () { return p.Area(); }; 249 break; 250 case 'radius': 251 measure = function () { return p.Radius(); }; 252 break; 253 case 'perimeter': 254 measure = function () { return p.Perimeter(); }; 255 break; 256 case 'rad': 257 measure = function () { return p.Value(); }; 258 break; 259 case 'deg': 260 measure = function () { return p.Value() * 180 / Math.PI; }; 261 break; 262 default: 263 measure = function () { return 0.0; }; 264 } 265 266 return function () { 267 var str = '', 268 val, 269 txt = Type.evaluate(user_supplied_text), 270 digits = Type.evaluate(el.visProp.digits), 271 u = Type.evaluate(el.visProp.unit), 272 pre = Type.evaluate(el.visProp.prefix), 273 suf = Type.evaluate(el.visProp.suffix), 274 mj = Type.evaluate(el.visProp.usemathjax) || Type.evaluate(el.visProp.usekatex); 275 276 if (txt === '') { 277 if (el.useLocale()) { 278 val = el.formatNumberLocale(measure(), digits); 279 } else { 280 val = Type.toFixed(measure(), digits); 281 } 282 if (mj) { 283 str = ['\\(', pre, val, '\\,', u, suf, '\\)'].join(''); 284 } else { 285 str = [pre, val, u, suf].join(''); 286 } 287 } else { 288 str = txt; 289 } 290 return str; 291 }; 292 }; 293 294 if (p.elementClass === Const.OBJECT_CLASS_POINT) { 295 el = board.create('text', [ 296 function () { return p.X(); }, 297 function () { return p.Y(); }, 298 '' 299 ], attr); 300 301 txt_fun = function () { 302 var str = '', 303 txt = Type.evaluate(user_supplied_text), 304 digits = Type.evaluate(el.visProp.digits), 305 u = Type.evaluate(el.visProp.unit), 306 pre = Type.evaluate(el.visProp.prefix), 307 suf = Type.evaluate(el.visProp.suffix), 308 dir = Type.evaluate(el.visProp.dir), 309 mj = Type.evaluate(el.visProp.usemathjax) || Type.evaluate(el.visProp.usekatex), 310 x, y; 311 312 if (el.useLocale()) { 313 x = el.formatNumberLocale(p.X(), digits); 314 y = el.formatNumberLocale(p.Y(), digits); 315 } else { 316 x = Type.toFixed(p.X(), digits); 317 y = Type.toFixed(p.Y(), digits); 318 } 319 320 if (txt === '') { 321 if (dir === 'row') { 322 if (mj) { 323 str = ['\\(', pre, x, '\\,', u, ' / ', y, '\\,', u, suf, '\\)'].join(''); 324 } else { 325 str = [pre, x, ' ', u, ' / ', y, ' ', u, suf].join(''); 326 } 327 } else if (dir.indexOf('col') === 0) { // Starts with 'col' 328 if (mj) { 329 str = ['\\(', pre, '\\left(\\array{', x, '\\,', u, '\\\\ ', y, '\\,', u, '}\\right)', suf, '\\)'].join(''); 330 } else { 331 str = [pre, x, ' ', u, '<br/>', y, ' ', u, suf].join(''); 332 } 333 } 334 } else { 335 str = txt; 336 } 337 return str; 338 }; 339 340 } else if (p.elementClass === Const.OBJECT_CLASS_LINE) { 341 342 if (attr.measure === 'length') { 343 el = board.create('text', [ 344 function () { return (p.point1.X() + p.point2.X()) * 0.5; }, 345 function () { return (p.point1.Y() + p.point2.Y()) * 0.5; }, 346 '' 347 ], attr); 348 txt_fun = getTextFun(el, p, 'line', 'length'); 349 350 } else if (attr.measure === 'slope') { 351 el = board.create('text', [ 352 function () { return (p.point1.X() * 0.25 + p.point2.X() * 0.75); }, 353 function () { return (p.point1.Y() * 0.25 + p.point2.Y() * 0.75); }, 354 '' 355 ], attr); 356 txt_fun = getTextFun(el, p, 'line', 'slope'); 357 } 358 359 } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) { 360 if (attr.measure === 'radius') { 361 el = board.create('text', [ 362 function () { return p.center.X() + p.Radius() * 0.5; }, 363 function () { return p.center.Y(); }, 364 '' 365 ], attr); 366 txt_fun = getTextFun(el, p, 'circle', 'radius'); 367 368 } else if (attr.measure === 'area') { 369 el = board.create('text', [ 370 function () { return p.center.X(); }, 371 function () { return p.center.Y() + p.Radius() * 0.5; }, 372 '' 373 ], attr); 374 txt_fun = getTextFun(el, p, 'circle', 'area'); 375 376 } else if (attr.measure === 'circumference' || attr.measure === 'perimeter') { 377 el = board.create('text', [ 378 function () { return p.getLabelAnchor(); }, 379 '' 380 ], attr); 381 txt_fun = getTextFun(el, p, 'circle', 'perimeter'); 382 383 } 384 } else if (p.type === Const.OBJECT_TYPE_POLYGON) { 385 if (attr.measure === 'area') { 386 el = board.create('text', [ 387 function () { return p.getTextAnchor(); }, 388 '' 389 ], attr); 390 txt_fun = getTextFun(el, p, 'polygon', 'area'); 391 392 } else if (attr.measure === 'perimeter') { 393 el = board.create('text', [ 394 function () { 395 var last = p.borders.length - 1; 396 if (last >= 0) { 397 return [ 398 (p.borders[last].point1.X() + p.borders[last].point2.X()) * 0.5, 399 (p.borders[last].point1.Y() + p.borders[last].point2.Y()) * 0.5 400 ]; 401 } else { 402 return p.getTextAnchor(); 403 } 404 }, 405 '' 406 ], attr); 407 txt_fun = getTextFun(el, p, 'polygon', 'perimeter'); 408 } 409 410 } else if (p.type === Const.OBJECT_TYPE_ANGLE) { 411 el = board.create('text', [ 412 function () { 413 return p.getLabelAnchor(); 414 }, 415 '' 416 ], attr); 417 txt_fun = getTextFun(el, p, 'angle', attr.measure); 418 } 419 420 if (Type.exists(el)) { 421 el.setText(txt_fun); 422 p.addChild(el); 423 el.setParents([p]); 424 } 425 426 return el; 427 }; 428 429 JXG.registerElement("smartlabel", JXG.createSmartLabel); 430