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