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