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