1 /* 2 Copyright 2008-2026 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Andreas Walter, 8 Alfred Wassermann, 9 Peter Wilfahrt 10 11 This file is part of JSXGraph. 12 13 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 14 15 You can redistribute it and/or modify it under the terms of the 16 17 * GNU Lesser General Public License as published by 18 the Free Software Foundation, either version 3 of the License, or 19 (at your option) any later version 20 OR 21 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 22 23 JSXGraph is distributed in the hope that it will be useful, 24 but WITHOUT ANY WARRANTY; without even the implied warranty of 25 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 GNU Lesser General Public License for more details. 27 28 You should have received a copy of the GNU Lesser General Public License and 29 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 30 and <https://opensource.org/licenses/MIT/>. 31 */ 32 33 /*global JXG: true, define: true, html_sanitize: true*/ 34 /*jslint nomen: true, plusplus: true*/ 35 36 /** 37 * @fileoverview type.js contains several functions to help deal with javascript's weak types. 38 * This file mainly consists of detector functions which verify if a variable is or is not of 39 * a specific type and converter functions that convert variables to another type or normalize 40 * the type of a variable. 41 */ 42 43 import JXG from "../jxg.js"; 44 import Const from "../base/constants.js"; 45 import Mat from "../math/math.js"; 46 47 JXG.extend( 48 JXG, 49 /** @lends JXG */ { 50 /** 51 * Checks if the given object is an JSXGraph board. 52 * @param {Object} v 53 * @returns {Boolean} 54 */ 55 isBoard: function (v) { 56 return v !== null && 57 typeof v === "object" && 58 this.isNumber(v.BOARD_MODE_NONE) && 59 this.isObject(v.objects) && 60 this.isObject(v.jc) && 61 this.isFunction(v.update) && 62 !!v.containerObj && 63 this.isString(v.id); 64 }, 65 66 /** 67 * Checks if the given string is an id within the given board. 68 * @param {JXG.Board} board 69 * @param {String} s 70 * @returns {Boolean} 71 */ 72 isId: function (board, s) { 73 return typeof s === "string" && !!board.objects[s]; 74 }, 75 76 /** 77 * Checks if the given string is a name within the given board. 78 * @param {JXG.Board} board 79 * @param {String} s 80 * @returns {Boolean} 81 */ 82 isName: function (board, s) { 83 return typeof s === "string" && !!board.elementsByName[s]; 84 }, 85 86 /** 87 * Checks if the given string is a group id within the given board. 88 * @param {JXG.Board} board 89 * @param {String} s 90 * @returns {Boolean} 91 */ 92 isGroup: function (board, s) { 93 return typeof s === "string" && !!board.groups[s]; 94 }, 95 96 /** 97 * Checks if the value of a given variable is of type string. 98 * @param v A variable of any type. 99 * @returns {Boolean} True, if v is of type string. 100 */ 101 isString: function (v) { 102 return typeof v === 'string'; 103 }, 104 105 /** 106 * Checks if the value of a given variable is of type number. 107 * @param v A variable of any type. 108 * @param {Boolean} [acceptStringNumber=false] If set to true, the function returns true for e.g. v='3.1415'. 109 * @param {Boolean} [acceptNaN=true] If set to false, the function returns false for v=NaN. 110 * @returns {Boolean} True, if v is of type number. 111 */ 112 isNumber: function (v, acceptStringNumber, acceptNaN) { 113 var result = ( 114 typeof v === 'number' || Object.prototype.toString.call(v) === '[Object Number]' 115 ); 116 acceptStringNumber = acceptStringNumber || false; 117 acceptNaN = acceptNaN === undefined ? true : acceptNaN; 118 119 if (acceptStringNumber) { 120 result = result || ('' + parseFloat(v)) === v; 121 } 122 if (!acceptNaN) { 123 result = result && !isNaN(v); 124 } 125 return result; 126 }, 127 128 /** 129 * Checks if a given variable references a function. 130 * @param v A variable of any type. 131 * @returns {Boolean} True, if v is a function. 132 */ 133 isFunction: function (v) { 134 return typeof v === 'function'; 135 }, 136 137 /** 138 * Checks if a given variable references an array. 139 * @param v A variable of any type. 140 * @returns {Boolean} True, if v is of type array. 141 */ 142 isArray: function (v) { 143 var r; 144 145 // use the ES5 isArray() method and if that doesn't exist use a fallback. 146 if (Array.isArray) { 147 r = Array.isArray(v); 148 } else { 149 r = 150 v !== null && 151 typeof v === "object" && 152 typeof v.splice === "function" && 153 typeof v.join === 'function'; 154 } 155 156 return r; 157 }, 158 159 /** 160 * Tests if the input variable is an Object 161 * @param v 162 */ 163 isObject: function (v) { 164 return typeof v === "object" && !this.isArray(v); 165 }, 166 167 /** 168 * Tests if the input variable is a DOM Document or DocumentFragment node 169 * @param v A variable of any type 170 */ 171 isDocumentOrFragment: function (v) { 172 return this.isObject(v) && ( 173 v.nodeType === 9 || // Node.DOCUMENT_NODE 174 v.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE 175 ); 176 }, 177 178 /** 179 * Checks if a given variable is a reference of a JSXGraph Point element. 180 * @param v A variable of any type. 181 * @returns {Boolean} True, if v is of type JXG.Point. 182 */ 183 isPoint: function (v) { 184 if (v !== null && typeof v === "object" && this.exists(v.elementClass)) { 185 return v.elementClass === Const.OBJECT_CLASS_POINT; 186 } 187 188 return false; 189 }, 190 191 /** 192 * Checks if a given variable is a reference of a JSXGraph Point3D element. 193 * @param v A variable of any type. 194 * @returns {Boolean} True, if v is of type JXG.Point3D. 195 */ 196 isPoint3D: function (v) { 197 if (v !== null && typeof v === "object" && this.exists(v.type)) { 198 return v.type === Const.OBJECT_TYPE_POINT3D; 199 } 200 201 return false; 202 }, 203 204 /** 205 * Checks if a given variable is a reference of a JSXGraph Point element or an array of length at least two or 206 * a function returning an array of length two or three. 207 * @param {JXG.Board} board 208 * @param v A variable of any type. 209 * @returns {Boolean} True, if v is of type JXG.Point. 210 */ 211 isPointType: function (board, v) { 212 var val, p; 213 214 if (this.isArray(v)) { 215 return true; 216 } 217 if (this.isFunction(v)) { 218 val = v(); 219 if (this.isArray(val) && val.length > 1) { 220 return true; 221 } 222 } 223 p = board.select(v); 224 return this.isPoint(p); 225 }, 226 227 /** 228 * Checks if a given variable is a reference of a JSXGraph Point3D element or an array of length three 229 * or a function returning an array of length three. 230 * @param {JXG.Board} board 231 * @param v A variable of any type. 232 * @returns {Boolean} True, if v is of type JXG.Point3D or an array of length at least 3, or a function returning 233 * such an array. 234 */ 235 isPointType3D: function (board, v) { 236 var val, p; 237 238 if (this.isArray(v) && v.length >= 3) { 239 return true; 240 } 241 if (this.isFunction(v)) { 242 val = v(); 243 if (this.isArray(val) && val.length >= 3) { 244 return true; 245 } 246 } 247 p = board.select(v); 248 return this.isPoint3D(p); 249 }, 250 251 /** 252 * Checks if a given variable is a reference of a JSXGraph transformation element or an array 253 * of JSXGraph transformation elements. 254 * @param v A variable of any type. 255 * @returns {Boolean} True, if v is of type JXG.Transformation. 256 */ 257 isTransformationOrArray: function (v) { 258 if (v !== null) { 259 if (this.isArray(v) && v.length > 0) { 260 return this.isTransformationOrArray(v[0]); 261 } 262 if (typeof v === 'object') { 263 return v.type === Const.OBJECT_TYPE_TRANSFORMATION; 264 } 265 } 266 return false; 267 }, 268 269 /** 270 * Checks if v is an empty object or empty. 271 * @param v {Object|Array} 272 * @returns {boolean} True, if v is an empty object or array. 273 */ 274 isEmpty: function (v) { 275 return Object.keys(v).length === 0; 276 }, 277 278 /** 279 * Checks if a given variable is neither undefined nor null. You should not use this together with global 280 * variables! 281 * @param v A variable of any type. 282 * @param {Boolean} [checkEmptyString=false] If set to true, it is also checked whether v is not equal to ''. 283 * @returns {Boolean} True, if v is neither undefined nor null. 284 */ 285 exists: function (v, checkEmptyString) { 286 /* eslint-disable eqeqeq */ 287 var result = !(v == undefined || v === null); 288 /* eslint-enable eqeqeq */ 289 checkEmptyString = checkEmptyString || false; 290 291 if (checkEmptyString) { 292 return result && v !== ""; 293 } 294 return result; 295 }, 296 // exists: (function (undef) { 297 // return function (v, checkEmptyString) { 298 // var result = !(v === undef || v === null); 299 300 // checkEmptyString = checkEmptyString || false; 301 302 // if (checkEmptyString) { 303 // return result && v !== ''; 304 // } 305 // return result; 306 // }; 307 // }()), 308 309 /** 310 * Handle default parameters. 311 * @param v Given value 312 * @param d Default value 313 * @returns <tt>d</tt>, if <tt>v</tt> is undefined or null. 314 */ 315 def: function (v, d) { 316 if (this.exists(v)) { 317 return v; 318 } 319 320 return d; 321 }, 322 323 /** 324 * Converts a string containing either <strong>true</strong> or <strong>false</strong> into a boolean value. 325 * @param {String} s String containing either <strong>true</strong> or <strong>false</strong>. 326 * @returns {Boolean} String typed boolean value converted to boolean. 327 */ 328 str2Bool: function (s) { 329 if (!this.exists(s)) { 330 return true; 331 } 332 333 if (typeof s === 'boolean') { 334 return s; 335 } 336 337 if (this.isString(s)) { 338 return s.toLowerCase() === 'true'; 339 } 340 341 return false; 342 }, 343 344 /** 345 * Converts a given CSS style string into a JavaScript object. 346 * @param {String} styles String containing CSS styles. 347 * @returns {Object} Object containing CSS styles. 348 */ 349 cssParse: function (styles) { 350 var str = styles; 351 if (!this.isString(str)) return {}; 352 353 str = str.replace(/\s*;\s*$/g, ''); 354 str = str.replace(/\s*;\s*/g, '","'); 355 str = str.replace(/\s*:\s*/g, '":"'); 356 str = str.trim(); 357 str = '{"' + str + '"}'; 358 359 return JSON.parse(str); 360 }, 361 362 /** 363 * Converts a given object into a CSS style string. 364 * @param {Object} styles Object containing CSS styles. 365 * @returns {String} String containing CSS styles. 366 */ 367 cssStringify: function (styles) { 368 var str = '', 369 attr, val; 370 if (!this.isObject(styles)) return ''; 371 372 for (attr in styles) { 373 if (!styles.hasOwnProperty(attr)) continue; 374 val = styles[attr]; 375 if (!this.isString(val) && !this.isNumber(val)) continue; 376 377 str += attr + ':' + val + '; '; 378 } 379 str = str.trim(); 380 381 return str; 382 }, 383 384 /** 385 * Convert a String, a number or a function into a function. This method is used in Transformation.js 386 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 387 * by a JessieCode string, thus it must be a valid reference only in case one of the param 388 * values is of type string. 389 * @param {Array} param An array containing strings, numbers, or functions. 390 * @param {Number} n Length of <tt>param</tt>. 391 * @returns {Function} A function taking one parameter k which specifies the index of the param element 392 * to evaluate. 393 */ 394 createEvalFunction: function (board, param, n) { 395 var f = [], func, i, e, 396 deps = {}; 397 398 for (i = 0; i < n; i++) { 399 f[i] = this.createFunction(param[i], board); 400 for (e in f[i].deps) { 401 deps[e] = f[i].deps; 402 } 403 } 404 405 func = function (k) { 406 return f[k](); 407 }; 408 func.deps = deps; 409 410 return func; 411 }, 412 413 /** 414 * Convert a String, number or function into a function. 415 * @param {String|Number|Function} term A variable of type string, function or number. 416 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 417 * by a JessieCode/GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 418 * values is of type string. 419 * @param {String} variableName Only required if function is supplied as JessieCode string or evalGeonext is set to true. 420 * Describes the variable name of the variable in a JessieCode/GEONE<sub>X</sub>T string given as term. 421 * @param {Boolean} [evalGeonext=false] Obsolete and ignored! Set this true 422 * if term should be treated as a GEONE<sub>X</sub>T string. 423 * @returns {Function} A function evaluating the value given by term or null if term is not of type string, 424 * function or number. 425 */ 426 createFunction: function (term, board, variableName, evalGeonext) { 427 var f = null; 428 429 // if ((!this.exists(evalGeonext) || evalGeonext) && this.isString(term)) { 430 if (this.isString(term)) { 431 // Convert GEONExT syntax into JavaScript syntax 432 //newTerm = JXG.GeonextParser.geonext2JS(term, board); 433 //return new Function(variableName,'return ' + newTerm + ';'); 434 //term = JXG.GeonextParser.replaceNameById(term, board); 435 //term = JXG.GeonextParser.geonext2JS(term, board); 436 437 f = board.jc.snippet(term, true, variableName, false); 438 } else if (this.isFunction(term)) { 439 f = term; 440 f.deps = (this.isObject(term.deps)) ? term.deps : {}; 441 } else if (this.isNumber(term) || this.isArray(term)) { 442 /** @ignore */ 443 f = function () { return term; }; 444 f.deps = {}; 445 // } else if (this.isString(term)) { 446 // // In case of string function like fontsize 447 // /** @ignore */ 448 // f = function () { return term; }; 449 // f.deps = {}; 450 } 451 452 if (f !== null) { 453 f.origin = term; 454 } 455 456 return f; 457 }, 458 459 /** 460 * Test if the parents array contains existing points. If instead parents contains coordinate arrays or 461 * function returning coordinate arrays 462 * free points with these coordinates are created. 463 * 464 * @param {JXG.Board} board Board object 465 * @param {Array} parents Array containing parent elements for a new object. This array may contain 466 * <ul> 467 * <li> {@link JXG.Point} objects 468 * <li> {@link JXG.GeometryElement#name} of {@link JXG.Point} objects 469 * <li> {@link JXG.GeometryElement#id} of {@link JXG.Point} objects 470 * <li> Coordinates of points given as array of numbers of length two or three, e.g. [2, 3]. 471 * <li> Coordinates of points given as array of functions of length two or three. Each function returns one coordinate, e.g. 472 * [function(){ return 2; }, function(){ return 3; }] 473 * <li> Function returning coordinates, e.g. function() { return [2, 3]; } 474 * </ul> 475 * In the last three cases a new point will be created. 476 * @param {String} attrClass Main attribute class of newly created points, see {@link JXG#copyAttributes} 477 * @param {Array} attrArray List of subtype attributes for the newly created points. The list of subtypes is mapped to the list of new points. 478 * @returns {Array} List of newly created {@link JXG.Point} elements or false if not all returned elements are points. 479 */ 480 providePoints: function (board, parents, attributes, attrClass, attrArray) { 481 var i, 482 j, 483 len, 484 lenAttr = 0, 485 points = [], 486 attr, 487 val; 488 489 if (!this.isArray(parents)) { 490 parents = [parents]; 491 } 492 len = parents.length; 493 if (this.exists(attrArray)) { 494 lenAttr = attrArray.length; 495 } 496 if (lenAttr === 0) { 497 attr = this.copyAttributes(attributes, board.options, attrClass); 498 } 499 500 for (i = 0; i < len; ++i) { 501 if (lenAttr > 0) { 502 j = Math.min(i, lenAttr - 1); 503 attr = this.copyAttributes( 504 attributes, 505 board.options, 506 attrClass, 507 attrArray[j].toLowerCase() 508 ); 509 } 510 if (this.isArray(parents[i]) && parents[i].length > 1) { 511 points.push(board.create("point", parents[i], attr)); 512 points[points.length - 1]._is_new = true; 513 } else if (this.isFunction(parents[i])) { 514 val = parents[i](); 515 if (this.isArray(val) && val.length > 1) { 516 points.push(board.create("point", [parents[i]], attr)); 517 points[points.length - 1]._is_new = true; 518 } 519 } else { 520 points.push(board.select(parents[i])); 521 } 522 523 if (!this.isPoint(points[i])) { 524 return false; 525 } 526 } 527 528 return points; 529 }, 530 531 /** 532 * Test if the parents array contains existing points. If instead parents contains coordinate arrays or 533 * function returning coordinate arrays 534 * free points with these coordinates are created. 535 * 536 * @param {JXG.View3D} view View3D object 537 * @param {Array} parents Array containing parent elements for a new object. This array may contain 538 * <ul> 539 * <li> {@link JXG.Point3D} objects 540 * <li> {@link JXG.GeometryElement#name} of {@link JXG.Point3D} objects 541 * <li> {@link JXG.GeometryElement#id} of {@link JXG.Point3D} objects 542 * <li> Coordinates of 3D points given as array of numbers of length three, e.g. [2, 3, 1]. 543 * <li> Coordinates of 3D points given as array of functions of length three. Each function returns one coordinate, e.g. 544 * [function(){ return 2; }, function(){ return 3; }, function(){ return 1; }] 545 * <li> Function returning coordinates, e.g. function() { return [2, 3, 1]; } 546 * </ul> 547 * In the last three cases a new 3D point will be created. 548 * @param {String} attrClass Main attribute class of newly created 3D points, see {@link JXG#copyAttributes} 549 * @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. 550 * @returns {Array} List of newly created {@link JXG.Point3D} elements or false if not all returned elements are 3D points. 551 */ 552 providePoints3D: function (view, parents, attributes, attrClass, attrArray) { 553 var i, 554 j, 555 len, 556 lenAttr = 0, 557 points = [], 558 attr, 559 val; 560 561 if (!this.isArray(parents)) { 562 parents = [parents]; 563 } 564 len = parents.length; 565 if (this.exists(attrArray)) { 566 lenAttr = attrArray.length; 567 } 568 if (lenAttr === 0) { 569 attr = this.copyAttributes(attributes, view.board.options, attrClass); 570 } 571 572 for (i = 0; i < len; ++i) { 573 if (lenAttr > 0) { 574 j = Math.min(i, lenAttr - 1); 575 attr = this.copyAttributes( 576 attributes, 577 view.board.options, 578 attrClass, 579 attrArray[j] 580 ); 581 } 582 583 if (this.isArray(parents[i]) && parents[i].length > 0 && parents[i].every((x)=>this.isArray(x) && this.isNumber(x[0]))) { 584 // Testing for array-of-arrays-of-numbers, like [[1,2,3],[2,3,4]] 585 for (j = 0; j < parents[i].length; j++) { 586 points.push(view.create("point3d", parents[i][j], attr));; 587 points[points.length - 1]._is_new = true; 588 } 589 } else if (this.isArray(parents[i]) && parents[i].every((x)=> this.isNumber(x) || this.isFunction(x))) { 590 // Single array [1,2,3] 591 points.push(view.create("point3d", parents[i], attr)); 592 points[points.length - 1]._is_new = true; 593 594 } else if (this.isPoint3D(parents[i])) { 595 points.push(parents[i]); 596 } else if (this.isFunction(parents[i])) { 597 val = parents[i](); 598 if (this.isArray(val) && val.length > 1) { 599 points.push(view.create("point3d", [parents[i]], attr)); 600 points[points.length - 1]._is_new = true; 601 } 602 } else { 603 points.push(view.select(parents[i])); 604 } 605 606 if (!this.isPoint3D(points[i])) { 607 return false; 608 } 609 } 610 611 return points; 612 }, 613 614 /** 615 * Generates a function which calls the function fn in the scope of owner. 616 * @param {Function} fn Function to call. 617 * @param {Object} owner Scope in which fn is executed. 618 * @returns {Function} A function with the same signature as fn. 619 */ 620 bind: function (fn, owner) { 621 return function () { 622 return fn.apply(owner, arguments); 623 }; 624 }, 625 626 /** 627 * If <tt>val</tt> is a function, it will be evaluated without giving any parameters, else the input value 628 * is just returned. 629 * @param val Could be anything. Preferably a number or a function. 630 * @returns If <tt>val</tt> is a function, it is evaluated and the result is returned. Otherwise <tt>val</tt> is returned. 631 */ 632 evaluate: function (val) { 633 if (this.isFunction(val)) { 634 return val(); 635 } 636 637 return val; 638 }, 639 640 /** 641 * Search an array for a given value. 642 * @param {Array} array 643 * @param value 644 * @param {String} [sub] Use this property if the elements of the array are objects. 645 * @returns {Number} The index of the first appearance of the given value, or 646 * <tt>-1</tt> if the value was not found. 647 */ 648 indexOf: function (array, value, sub) { 649 var i, 650 s = this.exists(sub); 651 652 if (Array.indexOf && !s) { 653 return array.indexOf(value); 654 } 655 656 for (i = 0; i < array.length; i++) { 657 if ((s && array[i][sub] === value) || (!s && array[i] === value)) { 658 return i; 659 } 660 } 661 662 return -1; 663 }, 664 665 /** 666 * Eliminates duplicate entries in an array consisting of numbers and strings. 667 * @param {Array} a An array of numbers and/or strings. 668 * @returns {Array} The array with duplicate entries eliminated. 669 */ 670 eliminateDuplicates: function (a) { 671 var i, 672 len = a.length, 673 result = [], 674 obj = {}; 675 676 for (i = 0; i < len; i++) { 677 obj[a[i]] = 0; 678 } 679 680 for (i in obj) { 681 if (obj.hasOwnProperty(i)) { 682 result.push(i); 683 } 684 } 685 686 return result; 687 }, 688 689 /** 690 * Swaps to array elements. 691 * @param {Array} arr 692 * @param {Number} i 693 * @param {Number} j 694 * @returns {Array} Reference to the given array. 695 */ 696 swap: function (arr, i, j) { 697 var tmp; 698 699 tmp = arr[i]; 700 arr[i] = arr[j]; 701 arr[j] = tmp; 702 703 return arr; 704 }, 705 706 /** 707 * Generates a copy of an array and removes the duplicate entries. 708 * The original array will be altered. 709 * @param {Array} arr 710 * @returns {Array} 711 * 712 * @see JXG.toUniqueArrayFloat 713 */ 714 uniqueArray: function (arr) { 715 var i, 716 j, 717 isArray, 718 ret = []; 719 720 if (arr.length === 0) { 721 return []; 722 } 723 724 for (i = 0; i < arr.length; i++) { 725 isArray = this.isArray(arr[i]); 726 727 if (!this.exists(arr[i])) { 728 arr[i] = ""; 729 continue; 730 } 731 for (j = i + 1; j < arr.length; j++) { 732 if (isArray && JXG.cmpArrays(arr[i], arr[j])) { 733 arr[i] = []; 734 } else if (!isArray && arr[i] === arr[j]) { 735 arr[i] = ""; 736 } 737 } 738 } 739 740 j = 0; 741 742 for (i = 0; i < arr.length; i++) { 743 isArray = this.isArray(arr[i]); 744 745 if (!isArray && arr[i] !== "") { 746 ret[j] = arr[i]; 747 j++; 748 } else if (isArray && arr[i].length !== 0) { 749 ret[j] = arr[i].slice(0); 750 j++; 751 } 752 } 753 754 arr = ret; 755 return ret; 756 }, 757 758 /** 759 * Generates a sorted copy of an array containing numbers and removes the duplicate entries up to a supplied precision eps. 760 * An array element arr[i] will be removed if abs(arr[i] - arr[i-1]) is less than eps. 761 * 762 * The original array will stay unaltered. 763 * @param {Array} arr 764 * @returns {Array} 765 * 766 * @param {Array} arr Array of numbers 767 * @param {Number} eps Precision 768 * @returns {Array} 769 * 770 * @example 771 * var arr = [2.3, 4, Math.PI, 2.300001, Math.PI+0.000000001]; 772 * console.log(JXG.toUniqueArrayFloat(arr, 0.00001)); 773 * // Output: Array(3) [ 2.3, 3.141592653589793, 4 ] 774 * 775 * @see JXG.uniqueArray 776 */ 777 toUniqueArrayFloat: function (arr, eps) { 778 var a, 779 i, le; 780 781 // if (false && Type.exists(arr.toSorted)) { 782 // a = arr.toSorted(function(a, b) { return a - b; }); 783 // } else { 784 // } 785 // Backwards compatibility to avoid toSorted 786 a = arr.slice(); 787 a.sort(function (a, b) { return a - b; }); 788 le = a.length; 789 for (i = le - 1; i > 0; i--) { 790 if (Math.abs(a[i] - a[i - 1]) < eps) { 791 a.splice(i, 1); 792 } 793 } 794 return a; 795 }, 796 797 /** 798 * Checks if an array contains an element equal to <tt>val</tt> but does not check the type! 799 * @param {Array} arr 800 * @param val 801 * @returns {Boolean} 802 */ 803 isInArray: function (arr, val) { 804 return JXG.indexOf(arr, val) > -1; 805 }, 806 807 /** 808 * Converts an array of {@link JXG.Coords} objects into a coordinate matrix. 809 * @param {Array} coords 810 * @param {Boolean} split 811 * @returns {Array} 812 */ 813 coordsArrayToMatrix: function (coords, split) { 814 var i, 815 x = [], 816 m = []; 817 818 for (i = 0; i < coords.length; i++) { 819 if (split) { 820 x.push(coords[i].usrCoords[1]); 821 m.push(coords[i].usrCoords[2]); 822 } else { 823 m.push([coords[i].usrCoords[1], coords[i].usrCoords[2]]); 824 } 825 } 826 827 if (split) { 828 m = [x, m]; 829 } 830 831 return m; 832 }, 833 834 /** 835 * Compare two arrays. 836 * @param {Array} a1 837 * @param {Array} a2 838 * @returns {Boolean} <tt>true</tt>, if the arrays coefficients are of same type and value. 839 */ 840 cmpArrays: function (a1, a2) { 841 var i; 842 843 // trivial cases 844 if (a1 === a2) { 845 return true; 846 } 847 848 if (a1.length !== a2.length) { 849 return false; 850 } 851 852 for (i = 0; i < a1.length; i++) { 853 if (this.isArray(a1[i]) && this.isArray(a2[i])) { 854 if (!this.cmpArrays(a1[i], a2[i])) { 855 return false; 856 } 857 } else if (a1[i] !== a2[i]) { 858 return false; 859 } 860 } 861 862 return true; 863 }, 864 865 /** 866 * Removes an element from the given array 867 * @param {Array} ar 868 * @param el 869 * @returns {Array} 870 */ 871 removeElementFromArray: function (ar, el) { 872 var i; 873 874 for (i = 0; i < ar.length; i++) { 875 if (ar[i] === el) { 876 ar.splice(i, 1); 877 return ar; 878 } 879 } 880 881 return ar; 882 }, 883 884 /** 885 * Truncate a number <tt>n</tt> after <tt>p</tt> decimals. 886 * @param {Number} n 887 * @param {Number} p 888 * @returns {Number} 889 */ 890 trunc: function (n, p) { 891 p = JXG.def(p, 0); 892 893 return this.toFixed(n, p); 894 }, 895 896 /** 897 * Decimal adjustment of a number. 898 * From https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Math/round 899 * 900 * @param {String} type The type of adjustment. 901 * @param {Number} value The number. 902 * @param {Number} exp The exponent (the 10 logarithm of the adjustment base). 903 * @returns {Number} The adjusted value. 904 * 905 * @private 906 */ 907 _decimalAdjust: function (type, value, exp) { 908 // If the exp is undefined or zero... 909 if (exp === undefined || +exp === 0) { 910 return Math[type](value); 911 } 912 913 value = +value; 914 exp = +exp; 915 // If the value is not a number or the exp is not an integer... 916 if (isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) { 917 return NaN; 918 } 919 920 // Shift 921 value = value.toString().split('e'); 922 value = Math[type](+(value[0] + "e" + (value[1] ? +value[1] - exp : -exp))); 923 924 // Shift back 925 value = value.toString().split('e'); 926 return +(value[0] + "e" + (value[1] ? +value[1] + exp : exp)); 927 }, 928 929 /** 930 * Round a number to given number of decimal digits. 931 * 932 * Example: JXG._toFixed(3.14159, -2) gives 3.14 933 * @param {Number} value Number to be rounded 934 * @param {Number} exp Number of decimal digits given as negative exponent 935 * @return {Number} Rounded number. 936 * 937 * @private 938 */ 939 _round10: function (value, exp) { 940 return this._decimalAdjust("round", value, exp); 941 }, 942 943 /** 944 * "Floor" a number to given number of decimal digits. 945 * 946 * Example: JXG._toFixed(3.14159, -2) gives 3.14 947 * @param {Number} value Number to be floored 948 * @param {Number} exp Number of decimal digits given as negative exponent 949 * @return {Number} "Floored" number. 950 * 951 * @private 952 */ 953 _floor10: function (value, exp) { 954 return this._decimalAdjust("floor", value, exp); 955 }, 956 957 /** 958 * "Ceil" a number to given number of decimal digits. 959 * 960 * Example: JXG._toFixed(3.14159, -2) gives 3.15 961 * @param {Number} value Number to be ceiled 962 * @param {Number} exp Number of decimal digits given as negative exponent 963 * @return {Number} "Ceiled" number. 964 * 965 * @private 966 */ 967 _ceil10: function (value, exp) { 968 return this._decimalAdjust("ceil", value, exp); 969 }, 970 971 /** 972 * Replacement of the default toFixed() method. 973 * It does a correct rounding (independent of the browser) and 974 * returns "0.00" for toFixed(-0.000001, 2) instead of "-0.00" which 975 * is returned by JavaScript's toFixed() 976 * 977 * @memberOf JXG 978 * @param {Number} num Number tp be rounded 979 * @param {Number} digits Decimal digits 980 * @return {String} Rounded number is returned as string 981 */ 982 toFixed: function (num, digits) { 983 return this._round10(num, -digits).toFixed(digits); 984 }, 985 986 /** 987 * Truncate a number <tt>val</tt> automatically. 988 * @memberOf JXG 989 * @param val 990 * @returns {Number} 991 */ 992 autoDigits: function (val) { 993 var x = Math.abs(val), 994 str; 995 996 if (x >= 0.1) { 997 str = this.toFixed(val, 2); 998 } else if (x >= 0.01) { 999 str = this.toFixed(val, 4); 1000 } else if (x >= 0.0001) { 1001 str = this.toFixed(val, 6); 1002 } else { 1003 str = val; 1004 } 1005 return str; 1006 }, 1007 1008 /** 1009 * Convert value v. If v has the form 1010 * <ul> 1011 * <li> 'x%': return floating point number x * percentOfWhat * 0.01 1012 * <li> 'xfr': return floating point number x * percentOfWhat 1013 * <li> 'xpx': return x * convertPx or convertPx(x) or x 1014 * <li> x or 'x': return floating point number x 1015 * </ul> 1016 * @param {String|Number} v 1017 * @param {Number} percentOfWhat 1018 * @param {Function|Number|*} convertPx 1019 * @returns {String|Number} 1020 */ 1021 parseNumber: function(v, percentOfWhat, convertPx) { 1022 var str; 1023 1024 if (this.isString(v) && v.indexOf('%') > -1) { 1025 str = v.replace(/\s+%\s+/, ''); 1026 return parseFloat(str) * percentOfWhat * 0.01; 1027 } 1028 if (this.isString(v) && v.indexOf('fr') > -1) { 1029 str = v.replace(/\s+fr\s+/, ''); 1030 return parseFloat(str) * percentOfWhat; 1031 } 1032 if (this.isString(v) && v.indexOf('px') > -1) { 1033 str = v.replace(/\s+px\s+/, ''); 1034 str = parseFloat(str); 1035 if(this.isFunction(convertPx)) { 1036 return convertPx(str); 1037 } else if(this.isNumber(convertPx)) { 1038 return str * convertPx; 1039 } else { 1040 return str; 1041 } 1042 } 1043 // Number or String containing no unit 1044 return parseFloat(v); 1045 }, 1046 1047 /** 1048 * Parse a string for label positioning of the form 'left pos' or 'pos right' 1049 * and return e.g. 1050 * <tt>{ side: 'left', pos: 'pos' }</tt>. 1051 * @param {String} str 1052 * @returns {Obj} <tt>{ side, pos }</tt> 1053 */ 1054 parsePosition: function(str) { 1055 var a, i, 1056 side = '', 1057 pos = ''; 1058 1059 str = str.trim(); 1060 if (str !== '') { 1061 a = str.split(/[ ,]+/); 1062 for (i = 0; i < a.length; i++) { 1063 if (a[i] === 'left' || a[i] === 'right') { 1064 side = a[i]; 1065 } else { 1066 pos = a[i]; 1067 } 1068 } 1069 } 1070 1071 return { 1072 side: side, 1073 pos: pos 1074 }; 1075 }, 1076 1077 /** 1078 * Extracts the keys of a given object. 1079 * @param object The object the keys are to be extracted 1080 * @param onlyOwn If true, hasOwnProperty() is used to verify that only keys are collected 1081 * the object owns itself and not some other object in the prototype chain. 1082 * @returns {Array} All keys of the given object. 1083 */ 1084 keys: function (object, onlyOwn) { 1085 var keys = [], 1086 property; 1087 1088 // the caller decides if we use hasOwnProperty 1089 /*jslint forin:true*/ 1090 for (property in object) { 1091 if (onlyOwn) { 1092 if (object.hasOwnProperty(property)) { 1093 keys.push(property); 1094 } 1095 } else { 1096 keys.push(property); 1097 } 1098 } 1099 /*jslint forin:false*/ 1100 1101 return keys; 1102 }, 1103 1104 /** 1105 * This outputs an object with a base class reference to the given object. This is useful if 1106 * you need a copy of an e.g. attributes object and want to overwrite some of the attributes 1107 * without changing the original object. 1108 * @param {Object} obj Object to be embedded. 1109 * @returns {Object} An object with a base class reference to <tt>obj</tt>. 1110 */ 1111 clone: function (obj) { 1112 var cObj = {}; 1113 1114 cObj.prototype = obj; 1115 1116 return cObj; 1117 }, 1118 1119 /** 1120 * Embeds an existing object into another one just like {@link #clone} and copies the contents of the second object 1121 * to the new one. Warning: The copied properties of obj2 are just flat copies. 1122 * @param {Object} obj Object to be copied. 1123 * @param {Object} obj2 Object with data that is to be copied to the new one as well. 1124 * @returns {Object} Copy of given object including some new/overwritten data from obj2. 1125 */ 1126 cloneAndCopy: function (obj, obj2) { 1127 var r, 1128 cObj = function () { 1129 return undefined; 1130 }; 1131 1132 cObj.prototype = obj; 1133 1134 // no hasOwnProperty on purpose 1135 /*jslint forin:true*/ 1136 /*jshint forin:true*/ 1137 1138 for (r in obj2) { 1139 cObj[r] = obj2[r]; 1140 } 1141 1142 /*jslint forin:false*/ 1143 /*jshint forin:false*/ 1144 1145 return cObj; 1146 }, 1147 1148 /** 1149 * Recursively merges obj2 into obj1 in-place. Contrary to {@link JXG#deepCopy} this won't create a new object 1150 * but instead will overwrite obj1. 1151 * <p> 1152 * In contrast to method JXG.mergeAttr, merge recurses into any kind of object, e.g. DOM object and JSXGraph objects. 1153 * So, please be careful. 1154 * @param {Object} obj1 1155 * @param {Object} obj2 1156 * @returns {Object} 1157 * @see JXG.mergeAttr 1158 * 1159 * @example 1160 * JXG.Options = JXG.merge(JXG.Options, { 1161 * board: { 1162 * showNavigation: false, 1163 * showInfobox: true 1164 * }, 1165 * point: { 1166 * face: 'o', 1167 * size: 4, 1168 * fillColor: '#eeeeee', 1169 * highlightFillColor: '#eeeeee', 1170 * strokeColor: 'white', 1171 * highlightStrokeColor: 'white', 1172 * showInfobox: 'inherit' 1173 * } 1174 * }); 1175 * 1176 * </pre><div id="JXGc5bf0f2a-bd5a-4612-97c2-09f17b1bbc6b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1177 * <script type="text/javascript"> 1178 * (function() { 1179 * var board = JXG.JSXGraph.initBoard('JXGc5bf0f2a-bd5a-4612-97c2-09f17b1bbc6b', 1180 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1181 * JXG.Options = JXG.merge(JXG.Options, { 1182 * board: { 1183 * showNavigation: false, 1184 * showInfobox: true 1185 * }, 1186 * point: { 1187 * face: 'o', 1188 * size: 4, 1189 * fillColor: '#eeeeee', 1190 * highlightFillColor: '#eeeeee', 1191 * strokeColor: 'white', 1192 * highlightStrokeColor: 'white', 1193 * showInfobox: 'inherit' 1194 * } 1195 * }); 1196 * 1197 * 1198 * })(); 1199 * 1200 * </script><pre> 1201 */ 1202 merge: function (obj1, obj2) { 1203 var i, j, o, oo; 1204 1205 for (i in obj2) { 1206 if (obj2.hasOwnProperty(i)) { 1207 o = obj2[i]; 1208 if (this.isArray(o)) { 1209 if (!obj1[i]) { 1210 obj1[i] = []; 1211 } 1212 1213 for (j = 0; j < o.length; j++) { 1214 oo = obj2[i][j]; 1215 if (typeof obj2[i][j] === 'object') { 1216 obj1[i][j] = this.merge(obj1[i][j], oo); 1217 } else { 1218 obj1[i][j] = obj2[i][j]; 1219 } 1220 } 1221 } else if (typeof o === 'object') { 1222 if (!obj1[i]) { 1223 obj1[i] = {}; 1224 } 1225 1226 obj1[i] = this.merge(obj1[i], o); 1227 } else { 1228 if (typeof obj1 === 'boolean') { 1229 // This is necessary in the following scenario: 1230 // lastArrow == false 1231 // and call of 1232 // setAttribute({lastArrow: {type: 7}}) 1233 obj1 = {}; 1234 } 1235 obj1[i] = o; 1236 } 1237 } 1238 } 1239 1240 return obj1; 1241 }, 1242 1243 /** 1244 * Creates a deep copy of an existing object, i.e. arrays or sub-objects are copied component resp. 1245 * element-wise instead of just copying the reference. If a second object is supplied, the two objects 1246 * are merged into one object. The properties of the second object have priority. 1247 * @param {Object} obj This object will be copied. 1248 * @param {Object} obj2 This object will merged into the newly created object 1249 * @param {Boolean} [toLower=false] If true the keys are convert to lower case. This is needed for visProp, see JXG#copyAttributes 1250 * @returns {Object} copy of obj or merge of obj and obj2. 1251 */ 1252 deepCopy: function (obj, obj2, toLower) { 1253 var c, i, prop, i2; 1254 1255 toLower = toLower || false; 1256 if (typeof obj !== 'object' || obj === null) { 1257 return obj; 1258 } 1259 1260 // Missing hasOwnProperty is on purpose in this function 1261 if (this.isArray(obj)) { 1262 c = []; 1263 for (i = 0; i < obj.length; i++) { 1264 prop = obj[i]; 1265 // Attention: typeof null === 'object' 1266 if (prop !== null && typeof prop === 'object') { 1267 // We certainly do not want to recurse into a JSXGraph object. 1268 // This would for sure result in an infinite recursion. 1269 // As alternative we copy the id of the object. 1270 if (this.exists(prop.board)) { 1271 c[i] = prop.id; 1272 } else { 1273 c[i] = this.deepCopy(prop, {}, toLower); 1274 } 1275 } else { 1276 c[i] = prop; 1277 } 1278 } 1279 } else { 1280 c = {}; 1281 for (i in obj) { 1282 if (obj.hasOwnProperty(i)) { 1283 i2 = toLower ? i.toLowerCase() : i; 1284 prop = obj[i]; 1285 if (prop !== null && typeof prop === 'object') { 1286 if (this.exists(prop.board)) { 1287 c[i2] = prop.id; 1288 } else { 1289 c[i2] = this.deepCopy(prop, {}, toLower); 1290 } 1291 } else { 1292 c[i2] = prop; 1293 } 1294 } 1295 } 1296 1297 for (i in obj2) { 1298 if (obj2.hasOwnProperty(i)) { 1299 i2 = toLower ? i.toLowerCase() : i; 1300 1301 prop = obj2[i]; 1302 if (prop !== null && typeof prop === 'object') { 1303 if (this.isArray(prop) || !this.exists(c[i2])) { 1304 c[i2] = this.deepCopy(prop, {}, toLower); 1305 } else { 1306 c[i2] = this.deepCopy(c[i2], prop, toLower); 1307 } 1308 } else { 1309 c[i2] = prop; 1310 } 1311 } 1312 } 1313 } 1314 1315 return c; 1316 }, 1317 1318 /** 1319 * In-place (deep) merging of attributes. Allows attributes like `{shadow: {enabled: true...}}` 1320 * <p> 1321 * In contrast to method JXG.merge, mergeAttr does not recurse into DOM objects and JSXGraph objects. Instead 1322 * handles (pointers) to these objects are used. 1323 * 1324 * @param {Object} attr Object with attributes - usually containing default options - that will be changed in-place. 1325 * @param {Object} special Special option values which overwrite (recursively) the default options 1326 * @param {Boolean} [toLower=true] If true the keys are converted to lower case. 1327 * @param {Boolean} [ignoreUndefinedSpecials=false] If true the values in special that are undefined are not used. 1328 * 1329 * @see JXG.merge 1330 * 1331 */ 1332 mergeAttr: function (attr, special, toLower, ignoreUndefinedSpecials) { 1333 var e, e2, o; 1334 1335 toLower = toLower || true; 1336 ignoreUndefinedSpecials = ignoreUndefinedSpecials || false; 1337 1338 for (e in special) { 1339 if (special.hasOwnProperty(e)) { 1340 e2 = (toLower) ? e.toLowerCase(): e; 1341 // Key already exists, but not in lower case 1342 if (e2 !== e && attr.hasOwnProperty(e)) { 1343 if (attr.hasOwnProperty(e2)) { 1344 // Lower case key already exists - this should not happen 1345 // We have to unify the two key-value pairs 1346 // It is not clear which has precedence. 1347 this.mergeAttr(attr[e2], attr[e], toLower); 1348 } else { 1349 attr[e2] = attr[e]; 1350 } 1351 delete attr[e]; 1352 } 1353 1354 o = special[e]; 1355 if (this.isObject(o) && o !== null && 1356 // Do not recurse into a document object or a JSXGraph object 1357 !this.isDocumentOrFragment(o) && !this.exists(o.board) && 1358 // Do not recurse if a string is provided as "new String(...)" 1359 typeof o.valueOf() !== 'string') { 1360 if (attr[e2] === undefined || attr[e2] === null || !this.isObject(attr[e2])) { 1361 // The last test handles the case: 1362 // attr.draft = false; 1363 // special.draft = { strokewidth: 4} 1364 attr[e2] = {}; 1365 } 1366 this.mergeAttr(attr[e2], o, toLower); 1367 } else if(!ignoreUndefinedSpecials || this.exists(o)) { 1368 // Flat copy 1369 // This is also used in the cases 1370 // attr.shadow = { enabled: true ...} 1371 // special.shadow = false; 1372 // and 1373 // special.anchor is a JSXGraph element 1374 attr[e2] = o; 1375 } 1376 } 1377 } 1378 }, 1379 1380 /** 1381 * Convert an object to a new object containing only 1382 * lower case properties. 1383 * 1384 * @param {Object} obj 1385 * @returns Object 1386 * @example 1387 * var attr = JXG.keysToLowerCase({radiusPoint: {visible: false}}); 1388 * 1389 * // return {radiuspoint: {visible: false}} 1390 */ 1391 keysToLowerCase: function (obj) { 1392 var key, val, 1393 keys = Object.keys(obj), 1394 n = keys.length, 1395 newObj = {}; 1396 1397 if (typeof obj !== 'object') { 1398 return obj; 1399 } 1400 1401 while (n--) { 1402 key = keys[n]; 1403 if (obj.hasOwnProperty(key)) { 1404 // We recurse into an object only if it is 1405 // neither a DOM node nor an JSXGraph object 1406 val = obj[key]; 1407 if (typeof val === 'object' && val !== null && 1408 !this.isArray(val) && 1409 !this.exists(val.nodeType) && 1410 !this.exists(val.board)) { 1411 newObj[key.toLowerCase()] = this.keysToLowerCase(val); 1412 } else { 1413 newObj[key.toLowerCase()] = val; 1414 } 1415 } 1416 } 1417 return newObj; 1418 }, 1419 1420 /** 1421 * Generates an attributes object that is filled with default values from the Options object 1422 * and overwritten by the user specified attributes. 1423 * @param {Object} attributes user specified attributes 1424 * @param {Object} options defaults options 1425 * @param {String} s variable number of strings, e.g. 'slider', subtype 'point1'. Must be provided in lower case! 1426 * @returns {Object} The resulting attributes object 1427 */ 1428 copyAttributes: function (attributes, options, s) { 1429 var a, arg, i, len, o, isAvail, 1430 primitives = { 1431 circle: 1, 1432 curve: 1, 1433 foreignobject: 1, 1434 image: 1, 1435 line: 1, 1436 point: 1, 1437 polygon: 1, 1438 text: 1, 1439 ticks: 1, 1440 integral: 1 1441 }; 1442 1443 len = arguments.length; 1444 if (len < 3 || primitives[s]) { 1445 // Default options from Options.elements 1446 a = JXG.deepCopy(options.elements, null, true); 1447 } else { 1448 a = {}; 1449 } 1450 1451 // Only the layer of the main element is set. 1452 if (len < 4 && this.exists(s) && this.exists(options.layer[s])) { 1453 a.layer = options.layer[s]; 1454 } 1455 1456 // Default options from the specific element like 'line' in 1457 // copyAttribute(attributes, board.options, 'line') 1458 // but also like in 1459 // Type.copyAttributes(attributes, board.options, 'view3d', 'az', 'slider'); 1460 o = options; 1461 isAvail = true; 1462 for (i = 2; i < len; i++) { 1463 arg = arguments[i]; 1464 if (this.exists(o[arg])) { 1465 o = o[arg]; 1466 } else { 1467 isAvail = false; 1468 break; 1469 } 1470 } 1471 if (isAvail) { 1472 a = JXG.deepCopy(a, o, true); 1473 } 1474 1475 // Merge the specific options given in the parameter 'attributes' 1476 // into the default options. 1477 // Additionally, we step into a sub-element of attribute like line.point1 - 1478 // in case it is supplied as in 1479 // copyAttribute(attributes, board.options, 'line', 'point1') 1480 // In this case we would merge attributes.point1 into the global line.point1 attributes. 1481 o = (typeof attributes === 'object') ? this.keysToLowerCase(attributes) : {}; 1482 isAvail = true; 1483 for (i = 3; i < len; i++) { 1484 arg = arguments[i].toLowerCase(); 1485 if (this.exists(o[arg])) { 1486 o = o[arg]; 1487 } else { 1488 isAvail = false; 1489 break; 1490 } 1491 } 1492 if (isAvail) { 1493 this.mergeAttr(a, o, true); 1494 } 1495 1496 if (arguments[2] === 'board') { 1497 // For board attributes we are done now. 1498 return a; 1499 } 1500 1501 // Special treatment of labels 1502 o = options; 1503 isAvail = true; 1504 for (i = 2; i < len; i++) { 1505 arg = arguments[i]; 1506 if (this.exists(o[arg])) { 1507 o = o[arg]; 1508 } else { 1509 isAvail = false; 1510 break; 1511 } 1512 } 1513 if (isAvail && this.exists(o.label)) { 1514 a.label = JXG.deepCopy(o.label, a.label, true); 1515 } 1516 a.label = JXG.deepCopy(options.label, a.label, true); 1517 1518 return a; 1519 }, 1520 1521 /** 1522 * Copy all prototype methods from object "superObject" to object 1523 * "subObject". The constructor of superObject will be available 1524 * in subObject as subObject.constructor[constructorName]. 1525 * @param {Object} subObj A JavaScript object which receives new methods. 1526 * @param {Object} superObj A JavaScript object which lends its prototype methods to subObject 1527 * @returns {String} constructorName Under this name the constructor of superObj will be available 1528 * in subObject. 1529 * @private 1530 */ 1531 copyPrototypeMethods: function (subObject, superObject, constructorName) { 1532 var key; 1533 1534 subObject.prototype[constructorName] = superObject.prototype.constructor; 1535 for (key in superObject.prototype) { 1536 if (superObject.prototype.hasOwnProperty(key)) { 1537 subObject.prototype[key] = superObject.prototype[key]; 1538 } 1539 } 1540 }, 1541 1542 /** 1543 * Create a stripped down version of a JSXGraph element for cloning to the background. 1544 * Used in {JXG.GeometryElement#cloneToBackground} for creating traces. 1545 * 1546 * @param {JXG.GeometryElement} el Element to be cloned 1547 * @returns Object Cloned element 1548 * @private 1549 */ 1550 getCloneObject: function(el) { 1551 var obj, key, 1552 copy = {}; 1553 1554 copy.id = el.id + "T" + el.numTraces; 1555 el.numTraces += 1; 1556 1557 copy.coords = el.coords; 1558 obj = this.deepCopy(el.visProp, el.visProp.traceattributes, true); 1559 copy.visProp = {}; 1560 for (key in obj) { 1561 if (obj.hasOwnProperty(key)) { 1562 if ( 1563 key.indexOf('aria') !== 0 && 1564 key.indexOf('highlight') !== 0 && 1565 key.indexOf('attractor') !== 0 && 1566 key !== 'label' && 1567 key !== 'needsregularupdate' && 1568 key !== 'infoboxdigits' 1569 ) { 1570 copy.visProp[key] = el.eval(obj[key]); 1571 } 1572 } 1573 } 1574 copy.evalVisProp = function(val) { 1575 return copy.visProp[val]; 1576 }; 1577 copy.eval = function(val) { 1578 return val; 1579 }; 1580 1581 copy.visProp.layer = el.board.options.layer.trace; 1582 copy.visProp.tabindex = null; 1583 copy.visProp.highlight = false; 1584 copy.board = el.board; 1585 copy.elementClass = el.elementClass; 1586 1587 this.clearVisPropOld(copy); 1588 copy.visPropCalc = { 1589 visible: el.evalVisProp('visible') 1590 }; 1591 1592 return copy; 1593 }, 1594 1595 /** 1596 * Converts a JavaScript object into a JSON string. 1597 * @param {Object} obj A JavaScript object, functions will be ignored. 1598 * @param {Boolean} [noquote=false] No quotes around the name of a property. 1599 * @returns {String} The given object stored in a JSON string. 1600 * @deprecated 1601 */ 1602 toJSON: function (obj, noquote) { 1603 var list, prop, i, s, val; 1604 1605 noquote = JXG.def(noquote, false); 1606 1607 // check for native JSON support: 1608 if (JSON !== undefined && JSON.stringify && !noquote) { 1609 try { 1610 s = JSON.stringify(obj); 1611 return s; 1612 } catch (e) { 1613 // if something goes wrong, e.g. if obj contains functions we won't return 1614 // and use our own implementation as a fallback 1615 } 1616 } 1617 1618 switch (typeof obj) { 1619 case "object": 1620 if (obj) { 1621 list = []; 1622 1623 if (this.isArray(obj)) { 1624 for (i = 0; i < obj.length; i++) { 1625 list.push(JXG.toJSON(obj[i], noquote)); 1626 } 1627 1628 return "[" + list.join(",") + "]"; 1629 } 1630 1631 for (prop in obj) { 1632 if (obj.hasOwnProperty(prop)) { 1633 try { 1634 val = JXG.toJSON(obj[prop], noquote); 1635 } catch (e2) { 1636 val = ""; 1637 } 1638 1639 if (noquote) { 1640 list.push(prop + ":" + val); 1641 } else { 1642 list.push('"' + prop + '":' + val); 1643 } 1644 } 1645 } 1646 1647 return "{" + list.join(",") + "} "; 1648 } 1649 return 'null'; 1650 case "string": 1651 return "'" + obj.replace(/(["'])/g, "\\$1") + "'"; 1652 case "number": 1653 case "boolean": 1654 return obj.toString(); 1655 } 1656 1657 return '0'; 1658 }, 1659 1660 /** 1661 * Resets visPropOld. 1662 * @param {JXG.GeometryElement} el 1663 * @returns {GeometryElement} 1664 */ 1665 clearVisPropOld: function (el) { 1666 el.visPropOld = { 1667 cssclass: "", 1668 cssdefaultstyle: "", 1669 cssstyle: "", 1670 fillcolor: "", 1671 fillopacity: "", 1672 firstarrow: false, 1673 fontsize: -1, 1674 lastarrow: false, 1675 left: -100000, 1676 linecap: "", 1677 shadow: false, 1678 strokecolor: "", 1679 strokeopacity: "", 1680 strokewidth: "", 1681 tabindex: -100000, 1682 transitionduration: 0, 1683 top: -100000, 1684 visible: null 1685 }; 1686 1687 return el; 1688 }, 1689 1690 /** 1691 * Checks if an object contains a key, whose value equals to val. 1692 * @param {Object} obj 1693 * @param val 1694 * @returns {Boolean} 1695 */ 1696 isInObject: function (obj, val) { 1697 var el; 1698 1699 for (el in obj) { 1700 if (obj.hasOwnProperty(el)) { 1701 if (obj[el] === val) { 1702 return true; 1703 } 1704 } 1705 } 1706 1707 return false; 1708 }, 1709 1710 /** 1711 * Replaces all occurences of & by &, > by >, and < by <. 1712 * @param {String} str 1713 * @returns {String} 1714 */ 1715 escapeHTML: function (str) { 1716 return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 1717 }, 1718 1719 /** 1720 * Eliminates all substrings enclosed by < and > and replaces all occurences of 1721 * & by &, > by >, and < by <. 1722 * @param {String} str 1723 * @returns {String} 1724 */ 1725 unescapeHTML: function (str) { 1726 // This regex is NOT insecure. We are replacing everything found with '' 1727 /*jslint regexp:true*/ 1728 return str 1729 .replace(/<\/?[^>]+>/gi, "") 1730 .replace(/&/g, "&") 1731 .replace(/</g, "<") 1732 .replace(/>/g, ">"); 1733 }, 1734 1735 /** 1736 * Makes a string lower case except for the first character which will be upper case. 1737 * @param {String} str Arbitrary string 1738 * @returns {String} The capitalized string. 1739 */ 1740 capitalize: function (str) { 1741 return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); 1742 }, 1743 1744 /** 1745 * Make numbers given as strings nicer by removing all unnecessary leading and trailing zeroes. 1746 * @param {String} str 1747 * @returns {String} 1748 */ 1749 trimNumber: function (str) { 1750 str = str.replace(/^0+/, ""); 1751 str = str.replace(/0+$/, ""); 1752 1753 if (str[str.length - 1] === "." || str[str.length - 1] === ",") { 1754 str = str.slice(0, -1); 1755 } 1756 1757 if (str[0] === "." || str[0] === ",") { 1758 str = "0" + str; 1759 } 1760 1761 return str; 1762 }, 1763 1764 /** 1765 * Filter an array of elements. 1766 * @param {Array} list 1767 * @param {Object|function} filter 1768 * @returns {Array} 1769 */ 1770 filterElements: function (list, filter) { 1771 var i, 1772 f, 1773 item, 1774 flower, 1775 value, 1776 visPropValue, 1777 pass, 1778 l = list.length, 1779 result = []; 1780 1781 if (this.exists(filter) && typeof filter !== "function" && typeof filter !== 'object') { 1782 return result; 1783 } 1784 1785 for (i = 0; i < l; i++) { 1786 pass = true; 1787 item = list[i]; 1788 1789 if (typeof filter === 'object') { 1790 for (f in filter) { 1791 if (filter.hasOwnProperty(f)) { 1792 flower = f.toLowerCase(); 1793 1794 if (typeof item[f] === 'function') { 1795 value = item[f](); 1796 } else { 1797 value = item[f]; 1798 } 1799 1800 if (item.visProp && typeof item.visProp[flower] === 'function') { 1801 visPropValue = item.visProp[flower](); 1802 } else { 1803 visPropValue = item.visProp && item.visProp[flower]; 1804 } 1805 1806 if (typeof filter[f] === 'function') { 1807 pass = filter[f](value) || filter[f](visPropValue); 1808 } else { 1809 pass = value === filter[f] || visPropValue === filter[f]; 1810 } 1811 1812 if (!pass) { 1813 break; 1814 } 1815 } 1816 } 1817 } else if (typeof filter === 'function') { 1818 pass = filter(item); 1819 } 1820 1821 if (pass) { 1822 result.push(item); 1823 } 1824 } 1825 1826 return result; 1827 }, 1828 1829 /** 1830 * Remove all leading and trailing whitespaces from a given string. 1831 * @param {String} str 1832 * @returns {String} 1833 */ 1834 trim: function (str) { 1835 // str = str.replace(/^\s+/, ''); 1836 // str = str.replace(/\s+$/, ''); 1837 // 1838 // return str; 1839 return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ""); 1840 }, 1841 1842 /** 1843 * Convert a floating point number to a string integer + fraction. 1844 * Returns either a string of the form '3 1/3' (in case of useTeX=false) 1845 * or '3 \\frac{1}{3}' (in case of useTeX=true). 1846 * 1847 * @param {Number} x 1848 * @param {Boolean} [useTeX=false] 1849 * @param {Number} [order=0.001] 1850 * @returns {String} 1851 * @see JXG.Math.decToFraction 1852 */ 1853 toFraction: function (x, useTeX, order) { 1854 var arr = Mat.decToFraction(x, order), 1855 str = ''; 1856 1857 if (arr[1] === 0 && arr[2] === 0) { 1858 // 0 1859 str += '0'; 1860 } else { 1861 // Sign 1862 if (arr[0] < 0) { 1863 str += '-'; 1864 } 1865 if (arr[2] === 0) { 1866 // Integer 1867 str += arr[1]; 1868 } else if (!(arr[2] === 1 && arr[3] === 1)) { 1869 // Proper fraction 1870 if (arr[1] !== 0) { 1871 // Absolute value larger than 1 1872 str += arr[1] + ' '; 1873 } 1874 // Add fractional part 1875 if (useTeX === true) { 1876 str += '\\frac{' + arr[2] + '}{' + arr[3] + '}'; 1877 } else { 1878 str += arr[2] + '/' + arr[3]; 1879 } 1880 } 1881 } 1882 return str; 1883 }, 1884 1885 /** 1886 * Concat array src to array dest. 1887 * Uses push instead of JavaScript concat, which is much 1888 * faster. 1889 * The array dest is changed in place. 1890 * <p><b>Attention:</b> if "dest" is an anonymous array, the correct result is returned from the function. 1891 * 1892 * @param {Array} dest 1893 * @param {Array} src 1894 * @returns Array 1895 */ 1896 concat: function(dest, src) { 1897 var i, 1898 le = src.length; 1899 for (i = 0; i < le; i++) { 1900 dest.push(src[i]); 1901 } 1902 return dest; 1903 }, 1904 1905 /** 1906 * Convert HTML tags to entities or use html_sanitize if the google caja html sanitizer is available. 1907 * @param {String} str 1908 * @param {Boolean} caja 1909 * @returns {String} Sanitized string 1910 */ 1911 sanitizeHTML: function (str, caja) { 1912 if (typeof html_sanitize === "function" && caja) { 1913 return html_sanitize( 1914 str, 1915 function () { 1916 return undefined; 1917 }, 1918 function (id) { 1919 return id; 1920 } 1921 ); 1922 } 1923 1924 if (str && typeof str === 'string') { 1925 str = str.replace(/</g, "<").replace(/>/g, ">"); 1926 } 1927 1928 return str; 1929 }, 1930 1931 /** 1932 * If <tt>s</tt> is a slider, it returns the sliders value, otherwise it just returns the given value. 1933 * @param {*} s 1934 * @returns {*} s.Value() if s is an element of type slider, s otherwise 1935 */ 1936 evalSlider: function (s) { 1937 if (s && s.type === Const.OBJECT_TYPE_GLIDER && typeof s.Value === 'function') { 1938 return s.Value(); 1939 } 1940 1941 return s; 1942 }, 1943 1944 /** 1945 * Convert a string containing a MAXIMA /STACK expression into a JSXGraph / JessieCode string 1946 * or an array of JSXGraph / JessieCode strings. 1947 * <p> 1948 * This function is meanwhile superseded by stack_jxg.stack2jsxgraph. 1949 * 1950 * @deprecated 1951 * 1952 * @example 1953 * console.log( JXG.stack2jsxgraph("%e**x") ); 1954 * // Output: 1955 * // "EULER**x" 1956 * 1957 * @example 1958 * console.log( JXG.stack2jsxgraph("[%pi*(x**2 - 1), %phi*(x - 1), %gamma*(x+1)]") ); 1959 * // Output: 1960 * // [ "PI*(x**2 - 1)", "1.618033988749895*(x - 1)", "0.5772156649015329*(x+1)" ] 1961 * 1962 * @param {String} str 1963 * @returns String 1964 */ 1965 stack2jsxgraph: function(str) { 1966 var t; 1967 1968 t = str. 1969 replace(/%pi/g, 'PI'). 1970 replace(/%e/g, 'EULER'). 1971 replace(/%phi/g, '1.618033988749895'). 1972 replace(/%gamma/g, '0.5772156649015329'). 1973 trim(); 1974 1975 // String containing array -> array containing strings 1976 if (t[0] === '[' && t[t.length - 1] === ']') { 1977 t = t.slice(1, -1).split(/\s*,\s*/); 1978 } 1979 1980 return t; 1981 } 1982 } 1983 ); 1984 1985 export default JXG; 1986