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