1 /* 2 Copyright 2008-2023 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Andreas Walter, 8 Alfred Wassermann, 9 Peter Wilfahrt 10 11 This file is part of JSXGraph. 12 13 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 14 15 You can redistribute it and/or modify it under the terms of the 16 17 * GNU Lesser General Public License as published by 18 the Free Software Foundation, either version 3 of the License, or 19 (at your option) any later version 20 OR 21 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 22 23 JSXGraph is distributed in the hope that it will be useful, 24 but WITHOUT ANY WARRANTY; without even the implied warranty of 25 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 GNU Lesser General Public License for more details. 27 28 You should have received a copy of the GNU Lesser General Public License and 29 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 30 and <https://opensource.org/licenses/MIT/>. 31 */ 32 33 /*global JXG: true, define: true, html_sanitize: true*/ 34 /*jslint nomen: true, plusplus: true*/ 35 36 /** 37 * @fileoverview type.js contains several functions to help deal with javascript's weak types. 38 * This file mainly consists of detector functions which verify if a variable is or is not of 39 * a specific type and converter functions that convert variables to another type or normalize 40 * the type of a variable. 41 */ 42 43 import JXG from "../jxg"; 44 import Const from "../base/constants"; 45 46 JXG.extend( 47 JXG, 48 /** @lends JXG */ { 49 /** 50 * Checks if the given string is an id within the given board. 51 * @param {JXG.Board} board 52 * @param {String} s 53 * @returns {Boolean} 54 */ 55 isId: function (board, s) { 56 return typeof s === "string" && !!board.objects[s]; 57 }, 58 59 /** 60 * Checks if the given string is a name within the given board. 61 * @param {JXG.Board} board 62 * @param {String} s 63 * @returns {Boolean} 64 */ 65 isName: function (board, s) { 66 return typeof s === "string" && !!board.elementsByName[s]; 67 }, 68 69 /** 70 * Checks if the given string is a group id within the given board. 71 * @param {JXG.Board} board 72 * @param {String} s 73 * @returns {Boolean} 74 */ 75 isGroup: function (board, s) { 76 return typeof s === "string" && !!board.groups[s]; 77 }, 78 79 /** 80 * Checks if the value of a given variable is of type string. 81 * @param v A variable of any type. 82 * @returns {Boolean} True, if v is of type string. 83 */ 84 isString: function (v) { 85 return typeof v === "string"; 86 }, 87 88 /** 89 * Checks if the value of a given variable is of type number. 90 * @param v A variable of any type. 91 * @returns {Boolean} True, if v is of type number. 92 */ 93 isNumber: function (v) { 94 return ( 95 typeof v === "number" || Object.prototype.toString.call(v) === "[Object Number]" 96 ); 97 }, 98 99 /** 100 * Checks if a given variable references a function. 101 * @param v A variable of any type. 102 * @returns {Boolean} True, if v is a function. 103 */ 104 isFunction: function (v) { 105 return typeof v === "function"; 106 }, 107 108 /** 109 * Checks if a given variable references an array. 110 * @param v A variable of any type. 111 * @returns {Boolean} True, if v is of type array. 112 */ 113 isArray: function (v) { 114 var r; 115 116 // use the ES5 isArray() method and if that doesn't exist use a fallback. 117 if (Array.isArray) { 118 r = Array.isArray(v); 119 } else { 120 r = 121 v !== null && 122 typeof v === "object" && 123 typeof v.splice === "function" && 124 typeof v.join === "function"; 125 } 126 127 return r; 128 }, 129 130 /** 131 * Tests if the input variable is an Object 132 * @param v 133 */ 134 isObject: function (v) { 135 return typeof v === "object" && !this.isArray(v); 136 }, 137 138 /** 139 * Tests if the input variable is a DOM Document or DocumentFragment node 140 * @param v A variable of any type 141 */ 142 isDocumentOrFragment: function (v) { 143 return this.isObject(v) && ( 144 v.nodeType === 9 || // Node.DOCUMENT_NODE 145 v.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE 146 ); 147 }, 148 149 /** 150 * Checks if a given variable is a reference of a JSXGraph Point element. 151 * @param v A variable of any type. 152 * @returns {Boolean} True, if v is of type JXG.Point. 153 */ 154 isPoint: function (v) { 155 if (v !== null && typeof v === "object" && this.exists(v.elementClass)) { 156 return v.elementClass === Const.OBJECT_CLASS_POINT; 157 } 158 159 return false; 160 }, 161 162 isPoint3D: function (v) { 163 if (v !== null && typeof v === "object" && this.exists(v.elType)) { 164 return v.elType === "point3d"; 165 } 166 167 return false; 168 }, 169 170 /** 171 * Checks if a given variable is a reference of a JSXGraph Point element or an array of length at least two or 172 * a function returning an array of length two or three. 173 * @param {JXG.Board} board 174 * @param v A variable of any type. 175 * @returns {Boolean} True, if v is of type JXG.Point. 176 */ 177 isPointType: function (board, v) { 178 var val, p; 179 180 if (this.isArray(v)) { 181 return true; 182 } 183 if (this.isFunction(v)) { 184 val = v(); 185 if (this.isArray(val) && val.length > 1) { 186 return true; 187 } 188 } 189 p = board.select(v); 190 return this.isPoint(p); 191 }, 192 193 /** 194 * Checks if a given variable is a reference of a JSXGraph transformation element or an array 195 * of JSXGraph transformation elements. 196 * @param v A variable of any type. 197 * @returns {Boolean} True, if v is of type JXG.Transformation. 198 */ 199 isTransformationOrArray: function (v) { 200 if (v !== null) { 201 if (this.isArray(v) && v.length > 0) { 202 return this.isTransformationOrArray(v[0]); 203 } 204 if (typeof v === "object") { 205 return v.type === Const.OBJECT_TYPE_TRANSFORMATION; 206 } 207 } 208 return false; 209 }, 210 211 /** 212 * Checks if a given variable is neither undefined nor null. You should not use this together with global 213 * variables! 214 * @param v A variable of any type. 215 * @param {Boolean} [checkEmptyString=false] If set to true, it is also checked whether v is not equal to ''. 216 * @returns {Boolean} True, if v is neither undefined nor null. 217 */ 218 exists: function (v, checkEmptyString) { 219 /* eslint-disable eqeqeq */ 220 var result = !(v == undefined || v === null); 221 /* eslint-enable eqeqeq */ 222 checkEmptyString = checkEmptyString || false; 223 224 if (checkEmptyString) { 225 return result && v !== ""; 226 } 227 return result; 228 }, 229 // exists: (function (undef) { 230 // return function (v, checkEmptyString) { 231 // var result = !(v === undef || v === null); 232 233 // checkEmptyString = checkEmptyString || false; 234 235 // if (checkEmptyString) { 236 // return result && v !== ''; 237 // } 238 // return result; 239 // }; 240 // }()), 241 242 /** 243 * Checks if v is an empty object or empty. 244 * @param v {Object|Array} 245 * @returns {boolean} True, if v is an empty object or array. 246 */ 247 isEmpty: function (v) { 248 return Object.keys(v).length === 0; 249 }, 250 251 /** 252 * Handle default parameters. 253 * @param v Given value 254 * @param d Default value 255 * @returns <tt>d</tt>, if <tt>v</tt> is undefined or null. 256 */ 257 def: function (v, d) { 258 if (this.exists(v)) { 259 return v; 260 } 261 262 return d; 263 }, 264 265 /** 266 * Converts a string containing either <strong>true</strong> or <strong>false</strong> into a boolean value. 267 * @param {String} s String containing either <strong>true</strong> or <strong>false</strong>. 268 * @returns {Boolean} String typed boolean value converted to boolean. 269 */ 270 str2Bool: function (s) { 271 if (!this.exists(s)) { 272 return true; 273 } 274 275 if (typeof s === "boolean") { 276 return s; 277 } 278 279 if (this.isString(s)) { 280 return s.toLowerCase() === "true"; 281 } 282 283 return false; 284 }, 285 286 /** 287 * Convert a String, a number or a function into a function. This method is used in Transformation.js 288 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 289 * by a GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 290 * values is of type string. 291 * @param {Array} param An array containing strings, numbers, or functions. 292 * @param {Number} n Length of <tt>param</tt>. 293 * @returns {Function} A function taking one parameter k which specifies the index of the param element 294 * to evaluate. 295 */ 296 createEvalFunction: function (board, param, n) { 297 var f = [], func, i, e, 298 deps = {}; 299 300 for (i = 0; i < n; i++) { 301 f[i] = JXG.createFunction(param[i], board, "", true); 302 for (e in f[i].deps) { 303 deps[e] = f[i].deps; 304 } 305 } 306 307 func = function (k) { 308 return f[k](); 309 }; 310 func.deps = deps; 311 312 return func; 313 }, 314 315 /** 316 * Convert a String, number or function into a function. 317 * @param {String|Number|Function} term A variable of type string, function or number. 318 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 319 * by a GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 320 * values is of type string. 321 * @param {String} variableName Only required if evalGeonext is set to true. Describes the variable name 322 * of the variable in a GEONE<sub>X</sub>T string given as term. 323 * @param {Boolean} [evalGeonext=true] Set this true, if term should be treated as a GEONE<sub>X</sub>T string. 324 * @returns {Function} A function evaluation the value given by term or null if term is not of type string, 325 * function or number. 326 */ 327 createFunction: function (term, board, variableName, evalGeonext) { 328 var f = null; 329 330 if ((!this.exists(evalGeonext) || evalGeonext) && this.isString(term)) { 331 // Convert GEONExT syntax into JavaScript syntax 332 //newTerm = JXG.GeonextParser.geonext2JS(term, board); 333 //return new Function(variableName,'return ' + newTerm + ';'); 334 335 //term = JXG.GeonextParser.replaceNameById(term, board); 336 //term = JXG.GeonextParser.geonext2JS(term, board); 337 f = board.jc.snippet(term, true, variableName, true); 338 } else if (this.isFunction(term)) { 339 f = term; 340 f.deps = {}; 341 } else if (this.isNumber(term)) { 342 /** @ignore */ 343 f = function () { 344 return term; 345 }; 346 f.deps = {}; 347 } else if (this.isString(term)) { 348 // In case of string function like fontsize 349 /** @ignore */ 350 f = function () { 351 return term; 352 }; 353 f.deps = {}; 354 } 355 356 if (f !== null) { 357 f.origin = term; 358 } 359 360 return f; 361 }, 362 363 /** 364 * Test if the parents array contains existing points. If instead parents contains coordinate arrays or 365 * function returning coordinate arrays 366 * free points with these coordinates are created. 367 * 368 * @param {JXG.Board} board Board object 369 * @param {Array} parents Array containing parent elements for a new object. This array may contain 370 * <ul> 371 * <li> {@link JXG.Point} objects 372 * <li> {@link JXG.GeometryElement#name} of {@link JXG.Point} objects 373 * <li> {@link JXG.GeometryElement#id} of {@link JXG.Point} objects 374 * <li> Coordinates of points given as array of numbers of length two or three, e.g. [2, 3]. 375 * <li> Coordinates of points given as array of functions of length two or three. Each function returns one coordinate, e.g. 376 * [function(){ return 2; }, function(){ return 3; }] 377 * <li> Function returning coordinates, e.g. function() { return [2, 3]; } 378 * </ul> 379 * In the last three cases a new point will be created. 380 * @param {String} attrClass Main attribute class of newly created points, see {@link JXG#copyAttributes} 381 * @param {Array} attrArray List of subtype attributes for the newly created points. The list of subtypes is mapped to the list of new points. 382 * @returns {Array} List of newly created {@link JXG.Point} elements or false if not all returned elements are points. 383 */ 384 providePoints: function (board, parents, attributes, attrClass, attrArray) { 385 var i, 386 j, 387 len, 388 lenAttr = 0, 389 points = [], 390 attr, 391 val; 392 393 if (!this.isArray(parents)) { 394 parents = [parents]; 395 } 396 len = parents.length; 397 if (this.exists(attrArray)) { 398 lenAttr = attrArray.length; 399 } 400 if (lenAttr === 0) { 401 attr = this.copyAttributes(attributes, board.options, attrClass); 402 } 403 404 for (i = 0; i < len; ++i) { 405 if (lenAttr > 0) { 406 j = Math.min(i, lenAttr - 1); 407 attr = this.copyAttributes( 408 attributes, 409 board.options, 410 attrClass, 411 attrArray[j] 412 ); 413 } 414 if (this.isArray(parents[i]) && parents[i].length > 1) { 415 points.push(board.create("point", parents[i], attr)); 416 points[points.length - 1]._is_new = true; 417 } else if (this.isFunction(parents[i])) { 418 val = parents[i](); 419 if (this.isArray(val) && val.length > 1) { 420 points.push(board.create("point", [parents[i]], attr)); 421 points[points.length - 1]._is_new = true; 422 } 423 } else { 424 points.push(board.select(parents[i])); 425 } 426 427 if (!this.isPoint(points[i])) { 428 return false; 429 } 430 } 431 432 return points; 433 }, 434 435 /** 436 * Test if the parents array contains existing points. If instead parents contains coordinate arrays or 437 * function returning coordinate arrays 438 * free points with these coordinates are created. 439 * 440 * @param {JXG.View3D} view View3D object 441 * @param {Array} parents Array containing parent elements for a new object. This array may contain 442 * <ul> 443 * <li> {@link JXG.Point3D} objects 444 * <li> {@link JXG.GeometryElement#name} of {@link JXG.Point3D} objects 445 * <li> {@link JXG.GeometryElement#id} of {@link JXG.Point3D} objects 446 * <li> Coordinates of 3D points given as array of numbers of length three, e.g. [2, 3, 1]. 447 * <li> Coordinates of 3D points given as array of functions of length three. Each function returns one coordinate, e.g. 448 * [function(){ return 2; }, function(){ return 3; }, function(){ return 1; }] 449 * <li> Function returning coordinates, e.g. function() { return [2, 3, 1]; } 450 * </ul> 451 * In the last three cases a new 3D point will be created. 452 * @param {String} attrClass Main attribute class of newly created 3D points, see {@link JXG#copyAttributes} 453 * @param {Array} attrArray List of subtype attributes for the newly created 3D points. The list of subtypes is mapped to the list of new 3D points. 454 * @returns {Array} List of newly created {@link JXG.Point3D} elements or false if not all returned elements are 3D points. 455 */ 456 providePoints3D: function (view, parents, attributes, attrClass, attrArray) { 457 var i, 458 j, 459 len, 460 lenAttr = 0, 461 points = [], 462 attr, 463 val; 464 465 if (!this.isArray(parents)) { 466 parents = [parents]; 467 } 468 len = parents.length; 469 if (this.exists(attrArray)) { 470 lenAttr = attrArray.length; 471 } 472 if (lenAttr === 0) { 473 attr = this.copyAttributes(attributes, view.board.options, attrClass); 474 } 475 476 for (i = 0; i < len; ++i) { 477 if (lenAttr > 0) { 478 j = Math.min(i, lenAttr - 1); 479 attr = this.copyAttributes( 480 attributes, 481 view.board.options, 482 attrClass, 483 attrArray[j] 484 ); 485 } 486 487 if (this.isArray(parents[i]) && parents[i].length > 1) { 488 points.push(view.create("point3d", parents[i], attr)); 489 points[points.length - 1]._is_new = true; 490 } else if (this.isFunction(parents[i])) { 491 val = parents[i](); 492 if (this.isArray(val) && val.length > 1) { 493 points.push(view.create("point3d", [parents[i]], attr)); 494 points[points.length - 1]._is_new = true; 495 } 496 } else { 497 points.push(view.select(parents[i])); 498 } 499 500 if (!this.isPoint3D(points[i])) { 501 return false; 502 } 503 } 504 505 return points; 506 }, 507 508 /** 509 * Generates a function which calls the function fn in the scope of owner. 510 * @param {Function} fn Function to call. 511 * @param {Object} owner Scope in which fn is executed. 512 * @returns {Function} A function with the same signature as fn. 513 */ 514 bind: function (fn, owner) { 515 return function () { 516 return fn.apply(owner, arguments); 517 }; 518 }, 519 520 /** 521 * If <tt>val</tt> is a function, it will be evaluated without giving any parameters, else the input value 522 * is just returned. 523 * @param val Could be anything. Preferably a number or a function. 524 * @returns If <tt>val</tt> is a function, it is evaluated and the result is returned. Otherwise <tt>val</tt> is returned. 525 */ 526 evaluate: function (val) { 527 if (this.isFunction(val)) { 528 return val(); 529 } 530 531 return val; 532 }, 533 534 /** 535 * Search an array for a given value. 536 * @param {Array} array 537 * @param value 538 * @param {String} [sub] Use this property if the elements of the array are objects. 539 * @returns {Number} The index of the first appearance of the given value, or 540 * <tt>-1</tt> if the value was not found. 541 */ 542 indexOf: function (array, value, sub) { 543 var i, 544 s = this.exists(sub); 545 546 if (Array.indexOf && !s) { 547 return array.indexOf(value); 548 } 549 550 for (i = 0; i < array.length; i++) { 551 if ((s && array[i][sub] === value) || (!s && array[i] === value)) { 552 return i; 553 } 554 } 555 556 return -1; 557 }, 558 559 /** 560 * Eliminates duplicate entries in an array consisting of numbers and strings. 561 * @param {Array} a An array of numbers and/or strings. 562 * @returns {Array} The array with duplicate entries eliminated. 563 */ 564 eliminateDuplicates: function (a) { 565 var i, 566 len = a.length, 567 result = [], 568 obj = {}; 569 570 for (i = 0; i < len; i++) { 571 obj[a[i]] = 0; 572 } 573 574 for (i in obj) { 575 if (obj.hasOwnProperty(i)) { 576 result.push(i); 577 } 578 } 579 580 return result; 581 }, 582 583 /** 584 * Swaps to array elements. 585 * @param {Array} arr 586 * @param {Number} i 587 * @param {Number} j 588 * @returns {Array} Reference to the given array. 589 */ 590 swap: function (arr, i, j) { 591 var tmp; 592 593 tmp = arr[i]; 594 arr[i] = arr[j]; 595 arr[j] = tmp; 596 597 return arr; 598 }, 599 600 /** 601 * Generates a copy of an array and removes the duplicate entries. The original 602 * Array will be altered. 603 * @param {Array} arr 604 * @returns {Array} 605 */ 606 uniqueArray: function (arr) { 607 var i, 608 j, 609 isArray, 610 ret = []; 611 612 if (arr.length === 0) { 613 return []; 614 } 615 616 for (i = 0; i < arr.length; i++) { 617 isArray = this.isArray(arr[i]); 618 619 if (!this.exists(arr[i])) { 620 arr[i] = ""; 621 continue; 622 } 623 for (j = i + 1; j < arr.length; j++) { 624 if (isArray && JXG.cmpArrays(arr[i], arr[j])) { 625 arr[i] = []; 626 } else if (!isArray && arr[i] === arr[j]) { 627 arr[i] = ""; 628 } 629 } 630 } 631 632 j = 0; 633 634 for (i = 0; i < arr.length; i++) { 635 isArray = this.isArray(arr[i]); 636 637 if (!isArray && arr[i] !== "") { 638 ret[j] = arr[i]; 639 j++; 640 } else if (isArray && arr[i].length !== 0) { 641 ret[j] = arr[i].slice(0); 642 j++; 643 } 644 } 645 646 arr = ret; 647 return ret; 648 }, 649 650 /** 651 * Checks if an array contains an element equal to <tt>val</tt> but does not check the type! 652 * @param {Array} arr 653 * @param val 654 * @returns {Boolean} 655 */ 656 isInArray: function (arr, val) { 657 return JXG.indexOf(arr, val) > -1; 658 }, 659 660 /** 661 * Converts an array of {@link JXG.Coords} objects into a coordinate matrix. 662 * @param {Array} coords 663 * @param {Boolean} split 664 * @returns {Array} 665 */ 666 coordsArrayToMatrix: function (coords, split) { 667 var i, 668 x = [], 669 m = []; 670 671 for (i = 0; i < coords.length; i++) { 672 if (split) { 673 x.push(coords[i].usrCoords[1]); 674 m.push(coords[i].usrCoords[2]); 675 } else { 676 m.push([coords[i].usrCoords[1], coords[i].usrCoords[2]]); 677 } 678 } 679 680 if (split) { 681 m = [x, m]; 682 } 683 684 return m; 685 }, 686 687 /** 688 * Compare two arrays. 689 * @param {Array} a1 690 * @param {Array} a2 691 * @returns {Boolean} <tt>true</tt>, if the arrays coefficients are of same type and value. 692 */ 693 cmpArrays: function (a1, a2) { 694 var i; 695 696 // trivial cases 697 if (a1 === a2) { 698 return true; 699 } 700 701 if (a1.length !== a2.length) { 702 return false; 703 } 704 705 for (i = 0; i < a1.length; i++) { 706 if (this.isArray(a1[i]) && this.isArray(a2[i])) { 707 if (!this.cmpArrays(a1[i], a2[i])) { 708 return false; 709 } 710 } else if (a1[i] !== a2[i]) { 711 return false; 712 } 713 } 714 715 return true; 716 }, 717 718 /** 719 * Removes an element from the given array 720 * @param {Array} ar 721 * @param el 722 * @returns {Array} 723 */ 724 removeElementFromArray: function (ar, el) { 725 var i; 726 727 for (i = 0; i < ar.length; i++) { 728 if (ar[i] === el) { 729 ar.splice(i, 1); 730 return ar; 731 } 732 } 733 734 return ar; 735 }, 736 737 /** 738 * Truncate a number <tt>n</tt> after <tt>p</tt> decimals. 739 * @param {Number} n 740 * @param {Number} p 741 * @returns {Number} 742 */ 743 trunc: function (n, p) { 744 p = JXG.def(p, 0); 745 746 return this.toFixed(n, p); 747 }, 748 749 /** 750 * Decimal adjustment of a number. 751 * From https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Math/round 752 * 753 * @param {String} type The type of adjustment. 754 * @param {Number} value The number. 755 * @param {Number} exp The exponent (the 10 logarithm of the adjustment base). 756 * @returns {Number} The adjusted value. 757 * 758 * @private 759 */ 760 _decimalAdjust: function (type, value, exp) { 761 // If the exp is undefined or zero... 762 if (exp === undefined || +exp === 0) { 763 return Math[type](value); 764 } 765 766 value = +value; 767 exp = +exp; 768 // If the value is not a number or the exp is not an integer... 769 if (isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) { 770 return NaN; 771 } 772 773 // Shift 774 value = value.toString().split("e"); 775 value = Math[type](+(value[0] + "e" + (value[1] ? +value[1] - exp : -exp))); 776 777 // Shift back 778 value = value.toString().split("e"); 779 return +(value[0] + "e" + (value[1] ? +value[1] + exp : exp)); 780 }, 781 782 /** 783 * Round a number to given number of decimal digits. 784 * 785 * Example: JXG._toFixed(3.14159, -2) gives 3.14 786 * @param {Number} value Number to be rounded 787 * @param {Number} exp Number of decimal digits given as negative exponent 788 * @return {Number} Rounded number. 789 * 790 * @private 791 */ 792 _round10: function (value, exp) { 793 return this._decimalAdjust("round", value, exp); 794 }, 795 796 /** 797 * "Floor" a number to given number of decimal digits. 798 * 799 * Example: JXG._toFixed(3.14159, -2) gives 3.14 800 * @param {Number} value Number to be floored 801 * @param {Number} exp Number of decimal digits given as negative exponent 802 * @return {Number} "Floored" number. 803 * 804 * @private 805 */ 806 _floor10: function (value, exp) { 807 return this._decimalAdjust("floor", value, exp); 808 }, 809 810 /** 811 * "Ceil" a number to given number of decimal digits. 812 * 813 * Example: JXG._toFixed(3.14159, -2) gives 3.15 814 * @param {Number} value Number to be ceiled 815 * @param {Number} exp Number of decimal digits given as negative exponent 816 * @return {Number} "Ceiled" number. 817 * 818 * @private 819 */ 820 _ceil10: function (value, exp) { 821 return this._decimalAdjust("ceil", value, exp); 822 }, 823 824 /** 825 * Replacement of the default toFixed() method. 826 * It does a correct rounding (independent of the browser) and 827 * returns "0.00" for toFixed(-0.000001, 2) instead of "-0.00" which 828 * is returned by JavaScript's toFixed() 829 * 830 * @memberOf JXG 831 * @param {Number} num Number tp be rounded 832 * @param {Number} digits Decimal digits 833 * @return {String} Rounded number is returned as string 834 */ 835 toFixed: function (num, digits) { 836 return this._round10(num, -digits).toFixed(digits); 837 }, 838 839 /** 840 * Truncate a number <tt>val</tt> automatically. 841 * @memberOf JXG 842 * @param val 843 * @returns {Number} 844 */ 845 autoDigits: function (val) { 846 var x = Math.abs(val), 847 str; 848 849 if (x >= 0.1) { 850 str = this.toFixed(val, 2); 851 } else if (x >= 0.01) { 852 str = this.toFixed(val, 4); 853 } else if (x >= 0.0001) { 854 str = this.toFixed(val, 6); 855 } else { 856 str = val; 857 } 858 return str; 859 }, 860 861 /** 862 * Extracts the keys of a given object. 863 * @param object The object the keys are to be extracted 864 * @param onlyOwn If true, hasOwnProperty() is used to verify that only keys are collected 865 * the object owns itself and not some other object in the prototype chain. 866 * @returns {Array} All keys of the given object. 867 */ 868 keys: function (object, onlyOwn) { 869 var keys = [], 870 property; 871 872 // the caller decides if we use hasOwnProperty 873 /*jslint forin:true*/ 874 for (property in object) { 875 if (onlyOwn) { 876 if (object.hasOwnProperty(property)) { 877 keys.push(property); 878 } 879 } else { 880 keys.push(property); 881 } 882 } 883 /*jslint forin:false*/ 884 885 return keys; 886 }, 887 888 /** 889 * This outputs an object with a base class reference to the given object. This is useful if 890 * you need a copy of an e.g. attributes object and want to overwrite some of the attributes 891 * without changing the original object. 892 * @param {Object} obj Object to be embedded. 893 * @returns {Object} An object with a base class reference to <tt>obj</tt>. 894 */ 895 clone: function (obj) { 896 var cObj = {}; 897 898 cObj.prototype = obj; 899 900 return cObj; 901 }, 902 903 /** 904 * Embeds an existing object into another one just like {@link #clone} and copies the contents of the second object 905 * to the new one. Warning: The copied properties of obj2 are just flat copies. 906 * @param {Object} obj Object to be copied. 907 * @param {Object} obj2 Object with data that is to be copied to the new one as well. 908 * @returns {Object} Copy of given object including some new/overwritten data from obj2. 909 */ 910 cloneAndCopy: function (obj, obj2) { 911 var r, 912 cObj = function () { 913 return undefined; 914 }; 915 916 cObj.prototype = obj; 917 918 // no hasOwnProperty on purpose 919 /*jslint forin:true*/ 920 /*jshint forin:true*/ 921 922 for (r in obj2) { 923 cObj[r] = obj2[r]; 924 } 925 926 /*jslint forin:false*/ 927 /*jshint forin:false*/ 928 929 return cObj; 930 }, 931 932 /** 933 * Recursively merges obj2 into obj1. Contrary to {@link JXG#deepCopy} this won't create a new object 934 * but instead will overwrite obj1. 935 * @param {Object} obj1 936 * @param {Object} obj2 937 * @returns {Object} 938 */ 939 merge: function (obj1, obj2) { 940 var i, j; 941 942 for (i in obj2) { 943 if (obj2.hasOwnProperty(i)) { 944 if (this.isArray(obj2[i])) { 945 if (!obj1[i]) { 946 obj1[i] = []; 947 } 948 949 for (j = 0; j < obj2[i].length; j++) { 950 if (typeof obj2[i][j] === "object") { 951 obj1[i][j] = this.merge(obj1[i][j], obj2[i][j]); 952 } else { 953 obj1[i][j] = obj2[i][j]; 954 } 955 } 956 } else if (typeof obj2[i] === "object") { 957 if (!obj1[i]) { 958 obj1[i] = {}; 959 } 960 961 obj1[i] = this.merge(obj1[i], obj2[i]); 962 } else { 963 obj1[i] = obj2[i]; 964 } 965 } 966 } 967 968 return obj1; 969 }, 970 971 /** 972 * Creates a deep copy of an existing object, i.e. arrays or sub-objects are copied component resp. 973 * element-wise instead of just copying the reference. If a second object is supplied, the two objects 974 * are merged into one object. The properties of the second object have priority. 975 * @param {Object} obj This object will be copied. 976 * @param {Object} obj2 This object will merged into the newly created object 977 * @param {Boolean} [toLower=false] If true the keys are convert to lower case. This is needed for visProp, see JXG#copyAttributes 978 * @returns {Object} copy of obj or merge of obj and obj2. 979 */ 980 deepCopy: function (obj, obj2, toLower) { 981 var c, i, prop, i2; 982 983 toLower = toLower || false; 984 if (typeof obj !== 'object' || obj === null) { 985 return obj; 986 } 987 988 // missing hasOwnProperty is on purpose in this function 989 if (this.isArray(obj)) { 990 c = []; 991 for (i = 0; i < obj.length; i++) { 992 prop = obj[i]; 993 if (typeof prop === "object") { 994 // We certainly do not want to recurse into a JSXGraph object. 995 // This would for sure result in an infinite recursion. 996 // As alternative we copy the id of the object. 997 if (this.exists(prop.board)) { 998 c[i] = prop.id; 999 } else { 1000 c[i] = this.deepCopy(prop); 1001 } 1002 } else { 1003 c[i] = prop; 1004 } 1005 } 1006 } else { 1007 c = {}; 1008 for (i in obj) { 1009 if (obj.hasOwnProperty(i)) { 1010 i2 = toLower ? i.toLowerCase() : i; 1011 prop = obj[i]; 1012 if (prop !== null && typeof prop === "object") { 1013 if (this.exists(prop.board)) { 1014 c[i2] = prop.id; 1015 } else { 1016 c[i2] = this.deepCopy(prop); 1017 } 1018 } else { 1019 c[i2] = prop; 1020 } 1021 } 1022 } 1023 1024 for (i in obj2) { 1025 if (obj2.hasOwnProperty(i)) { 1026 i2 = toLower ? i.toLowerCase() : i; 1027 1028 prop = obj2[i]; 1029 if (typeof prop === "object") { 1030 if (this.isArray(prop) || !this.exists(c[i2])) { 1031 c[i2] = this.deepCopy(prop); 1032 } else { 1033 c[i2] = this.deepCopy(c[i2], prop, toLower); 1034 } 1035 } else { 1036 c[i2] = prop; 1037 } 1038 } 1039 } 1040 } 1041 1042 return c; 1043 }, 1044 1045 /** 1046 * In-place (deep) merging of attributes. Allows attributes like `{shadow: {enabled: true...}}` 1047 * 1048 * @param {Object} attr Object with attributes - usually containing default options 1049 * @param {Object} special Special option values which overwrite (recursively) the default options 1050 * @param {Boolean} [toLower=true] If true the keys are convert to lower case. 1051 * 1052 * @private 1053 */ 1054 mergeAttr: function (attr, special, toLower) { 1055 var e, e2, o; 1056 1057 toLower = toLower || true; 1058 1059 for (e in special) { 1060 if (special.hasOwnProperty(e)) { 1061 e2 = (toLower) ? e.toLowerCase(): e; 1062 1063 o = special[e]; 1064 if (this.isObject(o) && o !== null && 1065 // Do not recurse into a document object or a JSXGraph object 1066 !this.isDocumentOrFragment(o) && !this.exists(o.board) && 1067 // Do not recurse if a string is provided as "new String(...)" 1068 typeof o.valueOf() !== 'string') { 1069 if (attr[e2] === undefined || attr[e2] === null || !this.isObject(attr[e2])) { 1070 // The last test handles the case: 1071 // attr.draft = false; 1072 // special.draft = { strokewidth: 4} 1073 attr[e2] = {}; 1074 } 1075 this.mergeAttr(attr[e2], o, toLower); 1076 } else { 1077 // Flat copy 1078 // This is also used in the cases 1079 // attr.shadow = { enabled: true ...} 1080 // special.shadow = false; 1081 // and 1082 // special.anchor is a JSXGraph element 1083 attr[e2] = o; 1084 } 1085 } 1086 } 1087 }, 1088 1089 /** 1090 * Generates an attributes object that is filled with default values from the Options object 1091 * and overwritten by the user specified attributes. 1092 * @param {Object} attributes user specified attributes 1093 * @param {Object} options defaults options 1094 * @param {String} s variable number of strings, e.g. 'slider', subtype 'point1'. 1095 * @returns {Object} The resulting attributes object 1096 */ 1097 copyAttributes: function (attributes, options, s) { 1098 var a, 1099 i, 1100 len, 1101 o, 1102 isAvail, 1103 primitives = { 1104 circle: 1, 1105 curve: 1, 1106 image: 1, 1107 line: 1, 1108 point: 1, 1109 polygon: 1, 1110 text: 1, 1111 ticks: 1, 1112 integral: 1 1113 }; 1114 1115 len = arguments.length; 1116 if (len < 3 || primitives[s]) { 1117 // Default options from Options.elements 1118 a = JXG.deepCopy(options.elements, null, true); 1119 } else { 1120 a = {}; 1121 } 1122 1123 // Only the layer of the main element is set. 1124 if (len < 4 && this.exists(s) && this.exists(options.layer[s])) { 1125 a.layer = options.layer[s]; 1126 } 1127 1128 // Default options from the specific element like 'line' in 1129 // copyAttribute(attributes, board.options, 'line') 1130 o = options; 1131 isAvail = true; 1132 for (i = 2; i < len; i++) { 1133 if (this.exists(o[arguments[i]])) { 1134 o = o[arguments[i]]; 1135 } else { 1136 isAvail = false; 1137 break; 1138 } 1139 } 1140 if (isAvail) { 1141 a = JXG.deepCopy(a, o, true); 1142 } 1143 1144 // Merge the specific options given in the parameter 'attributes' 1145 // into the default options. 1146 // Additionally, we step into a subelement of attribute like line.point1 in case it is supplied as in 1147 // copyAttribute(attributes, board.options, 'line', 'point1') 1148 // In this case we would merge attributes.point1 into the global line.point1 attributes. 1149 o = (typeof attributes === 'object') ? attributes : {}; 1150 isAvail = true; 1151 for (i = 3; i < len; i++) { 1152 if (this.exists(o[arguments[i]])) { 1153 o = o[arguments[i]]; 1154 } else { 1155 isAvail = false; 1156 break; 1157 } 1158 } 1159 if (isAvail) { 1160 this.mergeAttr(a, o, true); 1161 } 1162 1163 if (arguments[2] === "board") { 1164 // For board attributes we are done now. 1165 return a; 1166 } 1167 1168 // Special treatment of labels 1169 o = options; 1170 isAvail = true; 1171 for (i = 2; i < len; i++) { 1172 if (this.exists(o[arguments[i]])) { 1173 o = o[arguments[i]]; 1174 } else { 1175 isAvail = false; 1176 break; 1177 } 1178 } 1179 if (isAvail && this.exists(o.label)) { 1180 a.label = JXG.deepCopy(o.label, a.label); 1181 } 1182 a.label = JXG.deepCopy(options.label, a.label); 1183 1184 return a; 1185 }, 1186 1187 /** 1188 * Copy all prototype methods from object "superObject" to object 1189 * "subObject". The constructor of superObject will be available 1190 * in subObject as subObject.constructor[constructorName]. 1191 * @param {Object} subObj A JavaScript object which receives new methods. 1192 * @param {Object} superObj A JavaScript object which lends its prototype methods to subObject 1193 * @returns {String} constructorName Under this name the constructor of superObj will be available 1194 * in subObject. 1195 * @private 1196 */ 1197 copyPrototypeMethods: function (subObject, superObject, constructorName) { 1198 var key; 1199 1200 subObject.prototype[constructorName] = superObject.prototype.constructor; 1201 for (key in superObject.prototype) { 1202 if (superObject.prototype.hasOwnProperty(key)) { 1203 subObject.prototype[key] = superObject.prototype[key]; 1204 } 1205 } 1206 }, 1207 1208 /** 1209 * Converts a JavaScript object into a JSON string. 1210 * @param {Object} obj A JavaScript object, functions will be ignored. 1211 * @param {Boolean} [noquote=false] No quotes around the name of a property. 1212 * @returns {String} The given object stored in a JSON string. 1213 */ 1214 toJSON: function (obj, noquote) { 1215 var list, prop, i, s, val; 1216 1217 noquote = JXG.def(noquote, false); 1218 1219 // check for native JSON support: 1220 if (typeof JSON && JSON.stringify && !noquote) { 1221 try { 1222 s = JSON.stringify(obj); 1223 return s; 1224 } catch (e) { 1225 // if something goes wrong, e.g. if obj contains functions we won't return 1226 // and use our own implementation as a fallback 1227 } 1228 } 1229 1230 switch (typeof obj) { 1231 case "object": 1232 if (obj) { 1233 list = []; 1234 1235 if (this.isArray(obj)) { 1236 for (i = 0; i < obj.length; i++) { 1237 list.push(JXG.toJSON(obj[i], noquote)); 1238 } 1239 1240 return "[" + list.join(",") + "]"; 1241 } 1242 1243 for (prop in obj) { 1244 if (obj.hasOwnProperty(prop)) { 1245 try { 1246 val = JXG.toJSON(obj[prop], noquote); 1247 } catch (e2) { 1248 val = ""; 1249 } 1250 1251 if (noquote) { 1252 list.push(prop + ":" + val); 1253 } else { 1254 list.push('"' + prop + '":' + val); 1255 } 1256 } 1257 } 1258 1259 return "{" + list.join(",") + "} "; 1260 } 1261 return "null"; 1262 case "string": 1263 return "'" + obj.replace(/(["'])/g, "\\$1") + "'"; 1264 case "number": 1265 case "boolean": 1266 return obj.toString(); 1267 } 1268 1269 return "0"; 1270 }, 1271 1272 /** 1273 * Resets visPropOld. 1274 * @param {JXG.GeometryElement} el 1275 * @returns {GeometryElement} 1276 */ 1277 clearVisPropOld: function (el) { 1278 el.visPropOld = { 1279 cssclass: "", 1280 cssdefaultstyle: "", 1281 cssstyle: "", 1282 fillcolor: "", 1283 fillopacity: "", 1284 firstarrow: false, 1285 fontsize: -1, 1286 lastarrow: false, 1287 left: -100000, 1288 linecap: "", 1289 shadow: false, 1290 strokecolor: "", 1291 strokeopacity: "", 1292 strokewidth: "", 1293 tabindex: -100000, 1294 transitionduration: 0, 1295 top: -100000, 1296 visible: null 1297 }; 1298 1299 return el; 1300 }, 1301 1302 /** 1303 * Checks if an object contains a key, whose value equals to val. 1304 * @param {Object} obj 1305 * @param val 1306 * @returns {Boolean} 1307 */ 1308 isInObject: function (obj, val) { 1309 var el; 1310 1311 for (el in obj) { 1312 if (obj.hasOwnProperty(el)) { 1313 if (obj[el] === val) { 1314 return true; 1315 } 1316 } 1317 } 1318 1319 return false; 1320 }, 1321 1322 /** 1323 * Replaces all occurences of & by &, > by >, and < by <. 1324 * @param {String} str 1325 * @returns {String} 1326 */ 1327 escapeHTML: function (str) { 1328 return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 1329 }, 1330 1331 /** 1332 * Eliminates all substrings enclosed by < and > and replaces all occurences of 1333 * & by &, > by >, and < by <. 1334 * @param {String} str 1335 * @returns {String} 1336 */ 1337 unescapeHTML: function (str) { 1338 // This regex is NOT insecure. We are replacing everything found with '' 1339 /*jslint regexp:true*/ 1340 return str 1341 .replace(/<\/?[^>]+>/gi, "") 1342 .replace(/&/g, "&") 1343 .replace(/</g, "<") 1344 .replace(/>/g, ">"); 1345 }, 1346 1347 /** 1348 * Makes a string lower case except for the first character which will be upper case. 1349 * @param {String} str Arbitrary string 1350 * @returns {String} The capitalized string. 1351 */ 1352 capitalize: function (str) { 1353 return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); 1354 }, 1355 1356 /** 1357 * Make numbers given as strings nicer by removing all unnecessary leading and trailing zeroes. 1358 * @param {String} str 1359 * @returns {String} 1360 */ 1361 trimNumber: function (str) { 1362 str = str.replace(/^0+/, ""); 1363 str = str.replace(/0+$/, ""); 1364 1365 if (str[str.length - 1] === "." || str[str.length - 1] === ",") { 1366 str = str.slice(0, -1); 1367 } 1368 1369 if (str[0] === "." || str[0] === ",") { 1370 str = "0" + str; 1371 } 1372 1373 return str; 1374 }, 1375 1376 /** 1377 * Filter an array of elements. 1378 * @param {Array} list 1379 * @param {Object|function} filter 1380 * @returns {Array} 1381 */ 1382 filterElements: function (list, filter) { 1383 var i, 1384 f, 1385 item, 1386 flower, 1387 value, 1388 visPropValue, 1389 pass, 1390 l = list.length, 1391 result = []; 1392 1393 if (typeof filter !== "function" && typeof filter !== "object") { 1394 return result; 1395 } 1396 1397 for (i = 0; i < l; i++) { 1398 pass = true; 1399 item = list[i]; 1400 1401 if (typeof filter === "object") { 1402 for (f in filter) { 1403 if (filter.hasOwnProperty(f)) { 1404 flower = f.toLowerCase(); 1405 1406 if (typeof item[f] === "function") { 1407 value = item[f](); 1408 } else { 1409 value = item[f]; 1410 } 1411 1412 if (item.visProp && typeof item.visProp[flower] === "function") { 1413 visPropValue = item.visProp[flower](); 1414 } else { 1415 visPropValue = item.visProp && item.visProp[flower]; 1416 } 1417 1418 if (typeof filter[f] === "function") { 1419 pass = filter[f](value) || filter[f](visPropValue); 1420 } else { 1421 pass = value === filter[f] || visPropValue === filter[f]; 1422 } 1423 1424 if (!pass) { 1425 break; 1426 } 1427 } 1428 } 1429 } else if (typeof filter === "function") { 1430 pass = filter(item); 1431 } 1432 1433 if (pass) { 1434 result.push(item); 1435 } 1436 } 1437 1438 return result; 1439 }, 1440 1441 /** 1442 * Remove all leading and trailing whitespaces from a given string. 1443 * @param {String} str 1444 * @returns {String} 1445 */ 1446 trim: function (str) { 1447 // str = str.replace(/^\s+/, ''); 1448 // str = str.replace(/\s+$/, ''); 1449 // 1450 // return str; 1451 return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ""); 1452 }, 1453 1454 /** 1455 * Convert HTML tags to entities or use html_sanitize if the google caja html sanitizer is available. 1456 * @param {String} str 1457 * @param {Boolean} caja 1458 * @returns {String} Sanitized string 1459 */ 1460 sanitizeHTML: function (str, caja) { 1461 if (typeof html_sanitize === "function" && caja) { 1462 return html_sanitize( 1463 str, 1464 function () { 1465 return undefined; 1466 }, 1467 function (id) { 1468 return id; 1469 } 1470 ); 1471 } 1472 1473 if (str && typeof str === "string") { 1474 str = str.replace(/</g, "<").replace(/>/g, ">"); 1475 } 1476 1477 return str; 1478 }, 1479 1480 /** 1481 * If <tt>s</tt> is a slider, it returns the sliders value, otherwise it just returns the given value. 1482 * @param {*} s 1483 * @returns {*} s.Value() if s is an element of type slider, s otherwise 1484 */ 1485 evalSlider: function (s) { 1486 if (s && s.type === Const.OBJECT_TYPE_GLIDER && typeof s.Value === "function") { 1487 return s.Value(); 1488 } 1489 1490 return s; 1491 } 1492 } 1493 ); 1494 1495 export default JXG; 1496