1 /* 2 Copyright 2008-2025 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. The original 708 * Array will be altered. 709 * @param {Array} arr 710 * @returns {Array} 711 */ 712 uniqueArray: function (arr) { 713 var i, 714 j, 715 isArray, 716 ret = []; 717 718 if (arr.length === 0) { 719 return []; 720 } 721 722 for (i = 0; i < arr.length; i++) { 723 isArray = this.isArray(arr[i]); 724 725 if (!this.exists(arr[i])) { 726 arr[i] = ""; 727 continue; 728 } 729 for (j = i + 1; j < arr.length; j++) { 730 if (isArray && JXG.cmpArrays(arr[i], arr[j])) { 731 arr[i] = []; 732 } else if (!isArray && arr[i] === arr[j]) { 733 arr[i] = ""; 734 } 735 } 736 } 737 738 j = 0; 739 740 for (i = 0; i < arr.length; i++) { 741 isArray = this.isArray(arr[i]); 742 743 if (!isArray && arr[i] !== "") { 744 ret[j] = arr[i]; 745 j++; 746 } else if (isArray && arr[i].length !== 0) { 747 ret[j] = arr[i].slice(0); 748 j++; 749 } 750 } 751 752 arr = ret; 753 return ret; 754 }, 755 756 toUniqueArrayFloat: function (arr, eps) { 757 var a, 758 i, le; 759 760 // if (false && Type.exists(arr.toSorted)) { 761 // a = arr.toSorted(function(a, b) { return a - b; }); 762 // } else { 763 // } 764 // Backwards compatibility 765 a = arr.slice(); 766 a.sort(function (a, b) { return a - b; }); 767 le = a.length; 768 for (i = le - 1; i > 0; i--) { 769 if (Math.abs(a[i] - a[i - 1]) < eps) { 770 a.splice(i, 1); 771 } 772 } 773 return a; 774 }, 775 776 777 /** 778 * Checks if an array contains an element equal to <tt>val</tt> but does not check the type! 779 * @param {Array} arr 780 * @param val 781 * @returns {Boolean} 782 */ 783 isInArray: function (arr, val) { 784 return JXG.indexOf(arr, val) > -1; 785 }, 786 787 /** 788 * Converts an array of {@link JXG.Coords} objects into a coordinate matrix. 789 * @param {Array} coords 790 * @param {Boolean} split 791 * @returns {Array} 792 */ 793 coordsArrayToMatrix: function (coords, split) { 794 var i, 795 x = [], 796 m = []; 797 798 for (i = 0; i < coords.length; i++) { 799 if (split) { 800 x.push(coords[i].usrCoords[1]); 801 m.push(coords[i].usrCoords[2]); 802 } else { 803 m.push([coords[i].usrCoords[1], coords[i].usrCoords[2]]); 804 } 805 } 806 807 if (split) { 808 m = [x, m]; 809 } 810 811 return m; 812 }, 813 814 /** 815 * Compare two arrays. 816 * @param {Array} a1 817 * @param {Array} a2 818 * @returns {Boolean} <tt>true</tt>, if the arrays coefficients are of same type and value. 819 */ 820 cmpArrays: function (a1, a2) { 821 var i; 822 823 // trivial cases 824 if (a1 === a2) { 825 return true; 826 } 827 828 if (a1.length !== a2.length) { 829 return false; 830 } 831 832 for (i = 0; i < a1.length; i++) { 833 if (this.isArray(a1[i]) && this.isArray(a2[i])) { 834 if (!this.cmpArrays(a1[i], a2[i])) { 835 return false; 836 } 837 } else if (a1[i] !== a2[i]) { 838 return false; 839 } 840 } 841 842 return true; 843 }, 844 845 /** 846 * Removes an element from the given array 847 * @param {Array} ar 848 * @param el 849 * @returns {Array} 850 */ 851 removeElementFromArray: function (ar, el) { 852 var i; 853 854 for (i = 0; i < ar.length; i++) { 855 if (ar[i] === el) { 856 ar.splice(i, 1); 857 return ar; 858 } 859 } 860 861 return ar; 862 }, 863 864 /** 865 * Truncate a number <tt>n</tt> after <tt>p</tt> decimals. 866 * @param {Number} n 867 * @param {Number} p 868 * @returns {Number} 869 */ 870 trunc: function (n, p) { 871 p = JXG.def(p, 0); 872 873 return this.toFixed(n, p); 874 }, 875 876 /** 877 * Decimal adjustment of a number. 878 * From https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Math/round 879 * 880 * @param {String} type The type of adjustment. 881 * @param {Number} value The number. 882 * @param {Number} exp The exponent (the 10 logarithm of the adjustment base). 883 * @returns {Number} The adjusted value. 884 * 885 * @private 886 */ 887 _decimalAdjust: function (type, value, exp) { 888 // If the exp is undefined or zero... 889 if (exp === undefined || +exp === 0) { 890 return Math[type](value); 891 } 892 893 value = +value; 894 exp = +exp; 895 // If the value is not a number or the exp is not an integer... 896 if (isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) { 897 return NaN; 898 } 899 900 // Shift 901 value = value.toString().split('e'); 902 value = Math[type](+(value[0] + "e" + (value[1] ? +value[1] - exp : -exp))); 903 904 // Shift back 905 value = value.toString().split('e'); 906 return +(value[0] + "e" + (value[1] ? +value[1] + exp : exp)); 907 }, 908 909 /** 910 * Round a number to given number of decimal digits. 911 * 912 * Example: JXG._toFixed(3.14159, -2) gives 3.14 913 * @param {Number} value Number to be rounded 914 * @param {Number} exp Number of decimal digits given as negative exponent 915 * @return {Number} Rounded number. 916 * 917 * @private 918 */ 919 _round10: function (value, exp) { 920 return this._decimalAdjust("round", value, exp); 921 }, 922 923 /** 924 * "Floor" a number to given number of decimal digits. 925 * 926 * Example: JXG._toFixed(3.14159, -2) gives 3.14 927 * @param {Number} value Number to be floored 928 * @param {Number} exp Number of decimal digits given as negative exponent 929 * @return {Number} "Floored" number. 930 * 931 * @private 932 */ 933 _floor10: function (value, exp) { 934 return this._decimalAdjust("floor", value, exp); 935 }, 936 937 /** 938 * "Ceil" a number to given number of decimal digits. 939 * 940 * Example: JXG._toFixed(3.14159, -2) gives 3.15 941 * @param {Number} value Number to be ceiled 942 * @param {Number} exp Number of decimal digits given as negative exponent 943 * @return {Number} "Ceiled" number. 944 * 945 * @private 946 */ 947 _ceil10: function (value, exp) { 948 return this._decimalAdjust("ceil", value, exp); 949 }, 950 951 /** 952 * Replacement of the default toFixed() method. 953 * It does a correct rounding (independent of the browser) and 954 * returns "0.00" for toFixed(-0.000001, 2) instead of "-0.00" which 955 * is returned by JavaScript's toFixed() 956 * 957 * @memberOf JXG 958 * @param {Number} num Number tp be rounded 959 * @param {Number} digits Decimal digits 960 * @return {String} Rounded number is returned as string 961 */ 962 toFixed: function (num, digits) { 963 return this._round10(num, -digits).toFixed(digits); 964 }, 965 966 /** 967 * Truncate a number <tt>val</tt> automatically. 968 * @memberOf JXG 969 * @param val 970 * @returns {Number} 971 */ 972 autoDigits: function (val) { 973 var x = Math.abs(val), 974 str; 975 976 if (x >= 0.1) { 977 str = this.toFixed(val, 2); 978 } else if (x >= 0.01) { 979 str = this.toFixed(val, 4); 980 } else if (x >= 0.0001) { 981 str = this.toFixed(val, 6); 982 } else { 983 str = val; 984 } 985 return str; 986 }, 987 988 /** 989 * Convert value v. If v has the form 990 * <ul> 991 * <li> 'x%': return floating point number x * percentOfWhat * 0.01 992 * <li> 'xfr': return floating point number x * percentOfWhat 993 * <li> 'xpx': return x * convertPx or convertPx(x) or x 994 * <li> x or 'x': return floating point number x 995 * </ul> 996 * @param {String|Number} v 997 * @param {Number} percentOfWhat 998 * @param {Function|Number|*} convertPx 999 * @returns {String|Number} 1000 */ 1001 parseNumber: function(v, percentOfWhat, convertPx) { 1002 var str; 1003 1004 if (this.isString(v) && v.indexOf('%') > -1) { 1005 str = v.replace(/\s+%\s+/, ''); 1006 return parseFloat(str) * percentOfWhat * 0.01; 1007 } 1008 if (this.isString(v) && v.indexOf('fr') > -1) { 1009 str = v.replace(/\s+fr\s+/, ''); 1010 return parseFloat(str) * percentOfWhat; 1011 } 1012 if (this.isString(v) && v.indexOf('px') > -1) { 1013 str = v.replace(/\s+px\s+/, ''); 1014 str = parseFloat(str); 1015 if(this.isFunction(convertPx)) { 1016 return convertPx(str); 1017 } else if(this.isNumber(convertPx)) { 1018 return str * convertPx; 1019 } else { 1020 return str; 1021 } 1022 } 1023 // Number or String containing no unit 1024 return parseFloat(v); 1025 }, 1026 1027 /** 1028 * Parse a string for label positioning of the form 'left pos' or 'pos right' 1029 * and return e.g. 1030 * <tt>{ side: 'left', pos: 'pos' }</tt>. 1031 * @param {String} str 1032 * @returns {Obj} <tt>{ side, pos }</tt> 1033 */ 1034 parsePosition: function(str) { 1035 var a, i, 1036 side = '', 1037 pos = ''; 1038 1039 str = str.trim(); 1040 if (str !== '') { 1041 a = str.split(/[ ,]+/); 1042 for (i = 0; i < a.length; i++) { 1043 if (a[i] === 'left' || a[i] === 'right') { 1044 side = a[i]; 1045 } else { 1046 pos = a[i]; 1047 } 1048 } 1049 } 1050 1051 return { 1052 side: side, 1053 pos: pos 1054 }; 1055 }, 1056 1057 /** 1058 * Extracts the keys of a given object. 1059 * @param object The object the keys are to be extracted 1060 * @param onlyOwn If true, hasOwnProperty() is used to verify that only keys are collected 1061 * the object owns itself and not some other object in the prototype chain. 1062 * @returns {Array} All keys of the given object. 1063 */ 1064 keys: function (object, onlyOwn) { 1065 var keys = [], 1066 property; 1067 1068 // the caller decides if we use hasOwnProperty 1069 /*jslint forin:true*/ 1070 for (property in object) { 1071 if (onlyOwn) { 1072 if (object.hasOwnProperty(property)) { 1073 keys.push(property); 1074 } 1075 } else { 1076 keys.push(property); 1077 } 1078 } 1079 /*jslint forin:false*/ 1080 1081 return keys; 1082 }, 1083 1084 /** 1085 * This outputs an object with a base class reference to the given object. This is useful if 1086 * you need a copy of an e.g. attributes object and want to overwrite some of the attributes 1087 * without changing the original object. 1088 * @param {Object} obj Object to be embedded. 1089 * @returns {Object} An object with a base class reference to <tt>obj</tt>. 1090 */ 1091 clone: function (obj) { 1092 var cObj = {}; 1093 1094 cObj.prototype = obj; 1095 1096 return cObj; 1097 }, 1098 1099 /** 1100 * Embeds an existing object into another one just like {@link #clone} and copies the contents of the second object 1101 * to the new one. Warning: The copied properties of obj2 are just flat copies. 1102 * @param {Object} obj Object to be copied. 1103 * @param {Object} obj2 Object with data that is to be copied to the new one as well. 1104 * @returns {Object} Copy of given object including some new/overwritten data from obj2. 1105 */ 1106 cloneAndCopy: function (obj, obj2) { 1107 var r, 1108 cObj = function () { 1109 return undefined; 1110 }; 1111 1112 cObj.prototype = obj; 1113 1114 // no hasOwnProperty on purpose 1115 /*jslint forin:true*/ 1116 /*jshint forin:true*/ 1117 1118 for (r in obj2) { 1119 cObj[r] = obj2[r]; 1120 } 1121 1122 /*jslint forin:false*/ 1123 /*jshint forin:false*/ 1124 1125 return cObj; 1126 }, 1127 1128 /** 1129 * Recursively merges obj2 into obj1 in-place. Contrary to {@link JXG#deepCopy} this won't create a new object 1130 * but instead will overwrite obj1. 1131 * <p> 1132 * In contrast to method JXG.mergeAttr, merge recurses into any kind of object, e.g. DOM object and JSXGraph objects. 1133 * So, please be careful. 1134 * @param {Object} obj1 1135 * @param {Object} obj2 1136 * @returns {Object} 1137 * @see JXG.mergeAttr 1138 * 1139 * @example 1140 * JXG.Options = JXG.merge(JXG.Options, { 1141 * board: { 1142 * showNavigation: false, 1143 * showInfobox: true 1144 * }, 1145 * point: { 1146 * face: 'o', 1147 * size: 4, 1148 * fillColor: '#eeeeee', 1149 * highlightFillColor: '#eeeeee', 1150 * strokeColor: 'white', 1151 * highlightStrokeColor: 'white', 1152 * showInfobox: 'inherit' 1153 * } 1154 * }); 1155 * 1156 * </pre><div id="JXGc5bf0f2a-bd5a-4612-97c2-09f17b1bbc6b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1157 * <script type="text/javascript"> 1158 * (function() { 1159 * var board = JXG.JSXGraph.initBoard('JXGc5bf0f2a-bd5a-4612-97c2-09f17b1bbc6b', 1160 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1161 * JXG.Options = JXG.merge(JXG.Options, { 1162 * board: { 1163 * showNavigation: false, 1164 * showInfobox: true 1165 * }, 1166 * point: { 1167 * face: 'o', 1168 * size: 4, 1169 * fillColor: '#eeeeee', 1170 * highlightFillColor: '#eeeeee', 1171 * strokeColor: 'white', 1172 * highlightStrokeColor: 'white', 1173 * showInfobox: 'inherit' 1174 * } 1175 * }); 1176 * 1177 * 1178 * })(); 1179 * 1180 * </script><pre> 1181 */ 1182 merge: function (obj1, obj2) { 1183 var i, j, o, oo; 1184 1185 for (i in obj2) { 1186 if (obj2.hasOwnProperty(i)) { 1187 o = obj2[i]; 1188 if (this.isArray(o)) { 1189 if (!obj1[i]) { 1190 obj1[i] = []; 1191 } 1192 1193 for (j = 0; j < o.length; j++) { 1194 oo = obj2[i][j]; 1195 if (typeof obj2[i][j] === 'object') { 1196 obj1[i][j] = this.merge(obj1[i][j], oo); 1197 } else { 1198 obj1[i][j] = obj2[i][j]; 1199 } 1200 } 1201 } else if (typeof o === 'object') { 1202 if (!obj1[i]) { 1203 obj1[i] = {}; 1204 } 1205 1206 obj1[i] = this.merge(obj1[i], o); 1207 } else { 1208 if (typeof obj1 === 'boolean') { 1209 // This is necessary in the following scenario: 1210 // lastArrow == false 1211 // and call of 1212 // setAttribute({lastArrow: {type: 7}}) 1213 obj1 = {}; 1214 } 1215 obj1[i] = o; 1216 } 1217 } 1218 } 1219 1220 return obj1; 1221 }, 1222 1223 /** 1224 * Creates a deep copy of an existing object, i.e. arrays or sub-objects are copied component resp. 1225 * element-wise instead of just copying the reference. If a second object is supplied, the two objects 1226 * are merged into one object. The properties of the second object have priority. 1227 * @param {Object} obj This object will be copied. 1228 * @param {Object} obj2 This object will merged into the newly created object 1229 * @param {Boolean} [toLower=false] If true the keys are convert to lower case. This is needed for visProp, see JXG#copyAttributes 1230 * @returns {Object} copy of obj or merge of obj and obj2. 1231 */ 1232 deepCopy: function (obj, obj2, toLower) { 1233 var c, i, prop, i2; 1234 1235 toLower = toLower || false; 1236 if (typeof obj !== 'object' || obj === null) { 1237 return obj; 1238 } 1239 1240 // Missing hasOwnProperty is on purpose in this function 1241 if (this.isArray(obj)) { 1242 c = []; 1243 for (i = 0; i < obj.length; i++) { 1244 prop = obj[i]; 1245 // Attention: typeof null === 'object' 1246 if (prop !== null && typeof prop === 'object') { 1247 // We certainly do not want to recurse into a JSXGraph object. 1248 // This would for sure result in an infinite recursion. 1249 // As alternative we copy the id of the object. 1250 if (this.exists(prop.board)) { 1251 c[i] = prop.id; 1252 } else { 1253 c[i] = this.deepCopy(prop, {}, toLower); 1254 } 1255 } else { 1256 c[i] = prop; 1257 } 1258 } 1259 } else { 1260 c = {}; 1261 for (i in obj) { 1262 if (obj.hasOwnProperty(i)) { 1263 i2 = toLower ? i.toLowerCase() : i; 1264 prop = obj[i]; 1265 if (prop !== null && typeof prop === 'object') { 1266 if (this.exists(prop.board)) { 1267 c[i2] = prop.id; 1268 } else { 1269 c[i2] = this.deepCopy(prop, {}, toLower); 1270 } 1271 } else { 1272 c[i2] = prop; 1273 } 1274 } 1275 } 1276 1277 for (i in obj2) { 1278 if (obj2.hasOwnProperty(i)) { 1279 i2 = toLower ? i.toLowerCase() : i; 1280 1281 prop = obj2[i]; 1282 if (prop !== null && typeof prop === 'object') { 1283 if (this.isArray(prop) || !this.exists(c[i2])) { 1284 c[i2] = this.deepCopy(prop, {}, toLower); 1285 } else { 1286 c[i2] = this.deepCopy(c[i2], prop, toLower); 1287 } 1288 } else { 1289 c[i2] = prop; 1290 } 1291 } 1292 } 1293 } 1294 1295 return c; 1296 }, 1297 1298 /** 1299 * In-place (deep) merging of attributes. Allows attributes like `{shadow: {enabled: true...}}` 1300 * <p> 1301 * In contrast to method JXG.merge, mergeAttr does not recurse into DOM objects and JSXGraph objects. Instead 1302 * handles (pointers) to these objects are used. 1303 * 1304 * @param {Object} attr Object with attributes - usually containing default options - that will be changed in-place. 1305 * @param {Object} special Special option values which overwrite (recursively) the default options 1306 * @param {Boolean} [toLower=true] If true the keys are converted to lower case. 1307 * @param {Boolean} [ignoreUndefinedSpecials=false] If true the values in special that are undefined are not used. 1308 * 1309 * @see JXG.merge 1310 * 1311 */ 1312 mergeAttr: function (attr, special, toLower, ignoreUndefinedSpecials) { 1313 var e, e2, o; 1314 1315 toLower = toLower || true; 1316 ignoreUndefinedSpecials = ignoreUndefinedSpecials || false; 1317 1318 for (e in special) { 1319 if (special.hasOwnProperty(e)) { 1320 e2 = (toLower) ? e.toLowerCase(): e; 1321 // Key already exists, but not in lower case 1322 if (e2 !== e && attr.hasOwnProperty(e)) { 1323 if (attr.hasOwnProperty(e2)) { 1324 // Lower case key already exists - this should not happen 1325 // We have to unify the two key-value pairs 1326 // It is not clear which has precedence. 1327 this.mergeAttr(attr[e2], attr[e], toLower); 1328 } else { 1329 attr[e2] = attr[e]; 1330 } 1331 delete attr[e]; 1332 } 1333 1334 o = special[e]; 1335 if (this.isObject(o) && o !== null && 1336 // Do not recurse into a document object or a JSXGraph object 1337 !this.isDocumentOrFragment(o) && !this.exists(o.board) && 1338 // Do not recurse if a string is provided as "new String(...)" 1339 typeof o.valueOf() !== 'string') { 1340 if (attr[e2] === undefined || attr[e2] === null || !this.isObject(attr[e2])) { 1341 // The last test handles the case: 1342 // attr.draft = false; 1343 // special.draft = { strokewidth: 4} 1344 attr[e2] = {}; 1345 } 1346 this.mergeAttr(attr[e2], o, toLower); 1347 } else if(!ignoreUndefinedSpecials || this.exists(o)) { 1348 // Flat copy 1349 // This is also used in the cases 1350 // attr.shadow = { enabled: true ...} 1351 // special.shadow = false; 1352 // and 1353 // special.anchor is a JSXGraph element 1354 attr[e2] = o; 1355 } 1356 } 1357 } 1358 }, 1359 1360 /** 1361 * Convert an object to a new object containing only 1362 * lower case properties. 1363 * 1364 * @param {Object} obj 1365 * @returns Object 1366 * @example 1367 * var attr = JXG.keysToLowerCase({radiusPoint: {visible: false}}); 1368 * 1369 * // return {radiuspoint: {visible: false}} 1370 */ 1371 keysToLowerCase: function (obj) { 1372 var key, val, 1373 keys = Object.keys(obj), 1374 n = keys.length, 1375 newObj = {}; 1376 1377 if (typeof obj !== 'object') { 1378 return obj; 1379 } 1380 1381 while (n--) { 1382 key = keys[n]; 1383 if (obj.hasOwnProperty(key)) { 1384 // We recurse into an object only if it is 1385 // neither a DOM node nor an JSXGraph object 1386 val = obj[key]; 1387 if (typeof val === 'object' && val !== null && 1388 !this.isArray(val) && 1389 !this.exists(val.nodeType) && 1390 !this.exists(val.board)) { 1391 newObj[key.toLowerCase()] = this.keysToLowerCase(val); 1392 } else { 1393 newObj[key.toLowerCase()] = val; 1394 } 1395 } 1396 } 1397 return newObj; 1398 }, 1399 1400 /** 1401 * Generates an attributes object that is filled with default values from the Options object 1402 * and overwritten by the user specified attributes. 1403 * @param {Object} attributes user specified attributes 1404 * @param {Object} options defaults options 1405 * @param {String} s variable number of strings, e.g. 'slider', subtype 'point1'. Must be provided in lower case! 1406 * @returns {Object} The resulting attributes object 1407 */ 1408 copyAttributes: function (attributes, options, s) { 1409 var a, arg, i, len, o, isAvail, 1410 primitives = { 1411 circle: 1, 1412 curve: 1, 1413 foreignobject: 1, 1414 image: 1, 1415 line: 1, 1416 point: 1, 1417 polygon: 1, 1418 text: 1, 1419 ticks: 1, 1420 integral: 1 1421 }; 1422 1423 len = arguments.length; 1424 if (len < 3 || primitives[s]) { 1425 // Default options from Options.elements 1426 a = JXG.deepCopy(options.elements, null, true); 1427 } else { 1428 a = {}; 1429 } 1430 1431 // Only the layer of the main element is set. 1432 if (len < 4 && this.exists(s) && this.exists(options.layer[s])) { 1433 a.layer = options.layer[s]; 1434 } 1435 1436 // Default options from the specific element like 'line' in 1437 // copyAttribute(attributes, board.options, 'line') 1438 // but also like in 1439 // Type.copyAttributes(attributes, board.options, 'view3d', 'az', 'slider'); 1440 o = options; 1441 isAvail = true; 1442 for (i = 2; i < len; i++) { 1443 arg = arguments[i]; 1444 if (this.exists(o[arg])) { 1445 o = o[arg]; 1446 } else { 1447 isAvail = false; 1448 break; 1449 } 1450 } 1451 if (isAvail) { 1452 a = JXG.deepCopy(a, o, true); 1453 } 1454 1455 // Merge the specific options given in the parameter 'attributes' 1456 // into the default options. 1457 // Additionally, we step into a sub-element of attribute like line.point1 - 1458 // in case it is supplied as in 1459 // copyAttribute(attributes, board.options, 'line', 'point1') 1460 // In this case we would merge attributes.point1 into the global line.point1 attributes. 1461 o = (typeof attributes === 'object') ? this.keysToLowerCase(attributes) : {}; 1462 isAvail = true; 1463 for (i = 3; i < len; i++) { 1464 arg = arguments[i].toLowerCase(); 1465 if (this.exists(o[arg])) { 1466 o = o[arg]; 1467 } else { 1468 isAvail = false; 1469 break; 1470 } 1471 } 1472 if (isAvail) { 1473 this.mergeAttr(a, o, true); 1474 } 1475 1476 if (arguments[2] === 'board') { 1477 // For board attributes we are done now. 1478 return a; 1479 } 1480 1481 // Special treatment of labels 1482 o = options; 1483 isAvail = true; 1484 for (i = 2; i < len; i++) { 1485 arg = arguments[i]; 1486 if (this.exists(o[arg])) { 1487 o = o[arg]; 1488 } else { 1489 isAvail = false; 1490 break; 1491 } 1492 } 1493 if (isAvail && this.exists(o.label)) { 1494 a.label = JXG.deepCopy(o.label, a.label, true); 1495 } 1496 a.label = JXG.deepCopy(options.label, a.label, true); 1497 1498 return a; 1499 }, 1500 1501 /** 1502 * Copy all prototype methods from object "superObject" to object 1503 * "subObject". The constructor of superObject will be available 1504 * in subObject as subObject.constructor[constructorName]. 1505 * @param {Object} subObj A JavaScript object which receives new methods. 1506 * @param {Object} superObj A JavaScript object which lends its prototype methods to subObject 1507 * @returns {String} constructorName Under this name the constructor of superObj will be available 1508 * in subObject. 1509 * @private 1510 */ 1511 copyPrototypeMethods: function (subObject, superObject, constructorName) { 1512 var key; 1513 1514 subObject.prototype[constructorName] = superObject.prototype.constructor; 1515 for (key in superObject.prototype) { 1516 if (superObject.prototype.hasOwnProperty(key)) { 1517 subObject.prototype[key] = superObject.prototype[key]; 1518 } 1519 } 1520 }, 1521 1522 /** 1523 * Create a stripped down version of a JSXGraph element for cloning to the background. 1524 * Used in {JXG.GeometryElement#cloneToBackground} for creating traces. 1525 * 1526 * @param {JXG.GeometryElement} el Element to be cloned 1527 * @returns Object Cloned element 1528 * @private 1529 */ 1530 getCloneObject: function(el) { 1531 var obj, key, 1532 copy = {}; 1533 1534 copy.id = el.id + "T" + el.numTraces; 1535 el.numTraces += 1; 1536 1537 copy.coords = el.coords; 1538 obj = this.deepCopy(el.visProp, el.visProp.traceattributes, true); 1539 copy.visProp = {}; 1540 for (key in obj) { 1541 if (obj.hasOwnProperty(key)) { 1542 if ( 1543 key.indexOf('aria') !== 0 && 1544 key.indexOf('highlight') !== 0 && 1545 key.indexOf('attractor') !== 0 && 1546 key !== 'label' && 1547 key !== 'needsregularupdate' && 1548 key !== 'infoboxdigits' 1549 ) { 1550 copy.visProp[key] = el.eval(obj[key]); 1551 } 1552 } 1553 } 1554 copy.evalVisProp = function(val) { 1555 return copy.visProp[val]; 1556 }; 1557 copy.eval = function(val) { 1558 return val; 1559 }; 1560 1561 copy.visProp.layer = el.board.options.layer.trace; 1562 copy.visProp.tabindex = null; 1563 copy.visProp.highlight = false; 1564 copy.board = el.board; 1565 copy.elementClass = el.elementClass; 1566 1567 this.clearVisPropOld(copy); 1568 copy.visPropCalc = { 1569 visible: el.evalVisProp('visible') 1570 }; 1571 1572 return copy; 1573 }, 1574 1575 /** 1576 * Converts a JavaScript object into a JSON string. 1577 * @param {Object} obj A JavaScript object, functions will be ignored. 1578 * @param {Boolean} [noquote=false] No quotes around the name of a property. 1579 * @returns {String} The given object stored in a JSON string. 1580 * @deprecated 1581 */ 1582 toJSON: function (obj, noquote) { 1583 var list, prop, i, s, val; 1584 1585 noquote = JXG.def(noquote, false); 1586 1587 // check for native JSON support: 1588 if (JSON !== undefined && JSON.stringify && !noquote) { 1589 try { 1590 s = JSON.stringify(obj); 1591 return s; 1592 } catch (e) { 1593 // if something goes wrong, e.g. if obj contains functions we won't return 1594 // and use our own implementation as a fallback 1595 } 1596 } 1597 1598 switch (typeof obj) { 1599 case "object": 1600 if (obj) { 1601 list = []; 1602 1603 if (this.isArray(obj)) { 1604 for (i = 0; i < obj.length; i++) { 1605 list.push(JXG.toJSON(obj[i], noquote)); 1606 } 1607 1608 return "[" + list.join(",") + "]"; 1609 } 1610 1611 for (prop in obj) { 1612 if (obj.hasOwnProperty(prop)) { 1613 try { 1614 val = JXG.toJSON(obj[prop], noquote); 1615 } catch (e2) { 1616 val = ""; 1617 } 1618 1619 if (noquote) { 1620 list.push(prop + ":" + val); 1621 } else { 1622 list.push('"' + prop + '":' + val); 1623 } 1624 } 1625 } 1626 1627 return "{" + list.join(",") + "} "; 1628 } 1629 return 'null'; 1630 case "string": 1631 return "'" + obj.replace(/(["'])/g, "\\$1") + "'"; 1632 case "number": 1633 case "boolean": 1634 return obj.toString(); 1635 } 1636 1637 return '0'; 1638 }, 1639 1640 /** 1641 * Resets visPropOld. 1642 * @param {JXG.GeometryElement} el 1643 * @returns {GeometryElement} 1644 */ 1645 clearVisPropOld: function (el) { 1646 el.visPropOld = { 1647 cssclass: "", 1648 cssdefaultstyle: "", 1649 cssstyle: "", 1650 fillcolor: "", 1651 fillopacity: "", 1652 firstarrow: false, 1653 fontsize: -1, 1654 lastarrow: false, 1655 left: -100000, 1656 linecap: "", 1657 shadow: false, 1658 strokecolor: "", 1659 strokeopacity: "", 1660 strokewidth: "", 1661 tabindex: -100000, 1662 transitionduration: 0, 1663 top: -100000, 1664 visible: null 1665 }; 1666 1667 return el; 1668 }, 1669 1670 /** 1671 * Checks if an object contains a key, whose value equals to val. 1672 * @param {Object} obj 1673 * @param val 1674 * @returns {Boolean} 1675 */ 1676 isInObject: function (obj, val) { 1677 var el; 1678 1679 for (el in obj) { 1680 if (obj.hasOwnProperty(el)) { 1681 if (obj[el] === val) { 1682 return true; 1683 } 1684 } 1685 } 1686 1687 return false; 1688 }, 1689 1690 /** 1691 * Replaces all occurences of & by &, > by >, and < by <. 1692 * @param {String} str 1693 * @returns {String} 1694 */ 1695 escapeHTML: function (str) { 1696 return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 1697 }, 1698 1699 /** 1700 * Eliminates all substrings enclosed by < and > and replaces all occurences of 1701 * & by &, > by >, and < by <. 1702 * @param {String} str 1703 * @returns {String} 1704 */ 1705 unescapeHTML: function (str) { 1706 // This regex is NOT insecure. We are replacing everything found with '' 1707 /*jslint regexp:true*/ 1708 return str 1709 .replace(/<\/?[^>]+>/gi, "") 1710 .replace(/&/g, "&") 1711 .replace(/</g, "<") 1712 .replace(/>/g, ">"); 1713 }, 1714 1715 /** 1716 * Makes a string lower case except for the first character which will be upper case. 1717 * @param {String} str Arbitrary string 1718 * @returns {String} The capitalized string. 1719 */ 1720 capitalize: function (str) { 1721 return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); 1722 }, 1723 1724 /** 1725 * Make numbers given as strings nicer by removing all unnecessary leading and trailing zeroes. 1726 * @param {String} str 1727 * @returns {String} 1728 */ 1729 trimNumber: function (str) { 1730 str = str.replace(/^0+/, ""); 1731 str = str.replace(/0+$/, ""); 1732 1733 if (str[str.length - 1] === "." || str[str.length - 1] === ",") { 1734 str = str.slice(0, -1); 1735 } 1736 1737 if (str[0] === "." || str[0] === ",") { 1738 str = "0" + str; 1739 } 1740 1741 return str; 1742 }, 1743 1744 /** 1745 * Filter an array of elements. 1746 * @param {Array} list 1747 * @param {Object|function} filter 1748 * @returns {Array} 1749 */ 1750 filterElements: function (list, filter) { 1751 var i, 1752 f, 1753 item, 1754 flower, 1755 value, 1756 visPropValue, 1757 pass, 1758 l = list.length, 1759 result = []; 1760 1761 if (this.exists(filter) && typeof filter !== "function" && typeof filter !== 'object') { 1762 return result; 1763 } 1764 1765 for (i = 0; i < l; i++) { 1766 pass = true; 1767 item = list[i]; 1768 1769 if (typeof filter === 'object') { 1770 for (f in filter) { 1771 if (filter.hasOwnProperty(f)) { 1772 flower = f.toLowerCase(); 1773 1774 if (typeof item[f] === 'function') { 1775 value = item[f](); 1776 } else { 1777 value = item[f]; 1778 } 1779 1780 if (item.visProp && typeof item.visProp[flower] === 'function') { 1781 visPropValue = item.visProp[flower](); 1782 } else { 1783 visPropValue = item.visProp && item.visProp[flower]; 1784 } 1785 1786 if (typeof filter[f] === 'function') { 1787 pass = filter[f](value) || filter[f](visPropValue); 1788 } else { 1789 pass = value === filter[f] || visPropValue === filter[f]; 1790 } 1791 1792 if (!pass) { 1793 break; 1794 } 1795 } 1796 } 1797 } else if (typeof filter === 'function') { 1798 pass = filter(item); 1799 } 1800 1801 if (pass) { 1802 result.push(item); 1803 } 1804 } 1805 1806 return result; 1807 }, 1808 1809 /** 1810 * Remove all leading and trailing whitespaces from a given string. 1811 * @param {String} str 1812 * @returns {String} 1813 */ 1814 trim: function (str) { 1815 // str = str.replace(/^\s+/, ''); 1816 // str = str.replace(/\s+$/, ''); 1817 // 1818 // return str; 1819 return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ""); 1820 }, 1821 1822 /** 1823 * Convert a floating point number to a string integer + fraction. 1824 * Returns either a string of the form '3 1/3' (in case of useTeX=false) 1825 * or '3 \\frac{1}{3}' (in case of useTeX=true). 1826 * 1827 * @param {Number} x 1828 * @param {Boolean} [useTeX=false] 1829 * @param {Number} [order=0.001] 1830 * @returns {String} 1831 * @see JXG.Math.decToFraction 1832 */ 1833 toFraction: function (x, useTeX, order) { 1834 var arr = Mat.decToFraction(x, order), 1835 str = ''; 1836 1837 if (arr[1] === 0 && arr[2] === 0) { 1838 // 0 1839 str += '0'; 1840 } else { 1841 // Sign 1842 if (arr[0] < 0) { 1843 str += '-'; 1844 } 1845 if (arr[2] === 0) { 1846 // Integer 1847 str += arr[1]; 1848 } else if (!(arr[2] === 1 && arr[3] === 1)) { 1849 // Proper fraction 1850 if (arr[1] !== 0) { 1851 // Absolute value larger than 1 1852 str += arr[1] + ' '; 1853 } 1854 // Add fractional part 1855 if (useTeX === true) { 1856 str += '\\frac{' + arr[2] + '}{' + arr[3] + '}'; 1857 } else { 1858 str += arr[2] + '/' + arr[3]; 1859 } 1860 } 1861 } 1862 return str; 1863 }, 1864 1865 /** 1866 * Concat array src to array dest. 1867 * Uses push instead of JavaScript concat, which is much 1868 * faster. 1869 * The array dest is changed in place. 1870 * <p><b>Attention:</b> if "dest" is an anonymous array, the correct result is returned from the function. 1871 * 1872 * @param {Array} dest 1873 * @param {Array} src 1874 * @returns Array 1875 */ 1876 concat: function(dest, src) { 1877 var i, 1878 le = src.length; 1879 for (i = 0; i < le; i++) { 1880 dest.push(src[i]); 1881 } 1882 return dest; 1883 }, 1884 1885 /** 1886 * Convert HTML tags to entities or use html_sanitize if the google caja html sanitizer is available. 1887 * @param {String} str 1888 * @param {Boolean} caja 1889 * @returns {String} Sanitized string 1890 */ 1891 sanitizeHTML: function (str, caja) { 1892 if (typeof html_sanitize === "function" && caja) { 1893 return html_sanitize( 1894 str, 1895 function () { 1896 return undefined; 1897 }, 1898 function (id) { 1899 return id; 1900 } 1901 ); 1902 } 1903 1904 if (str && typeof str === 'string') { 1905 str = str.replace(/</g, "<").replace(/>/g, ">"); 1906 } 1907 1908 return str; 1909 }, 1910 1911 /** 1912 * If <tt>s</tt> is a slider, it returns the sliders value, otherwise it just returns the given value. 1913 * @param {*} s 1914 * @returns {*} s.Value() if s is an element of type slider, s otherwise 1915 */ 1916 evalSlider: function (s) { 1917 if (s && s.type === Const.OBJECT_TYPE_GLIDER && typeof s.Value === 'function') { 1918 return s.Value(); 1919 } 1920 1921 return s; 1922 }, 1923 1924 /** 1925 * Convert a string containing a MAXIMA /STACK expression into a JSXGraph / JessieCode string 1926 * or an array of JSXGraph / JessieCode strings. 1927 * <p> 1928 * This function is meanwhile superseded by stack_jxg.stack2jsxgraph. 1929 * 1930 * @deprecated 1931 * 1932 * @example 1933 * console.log( JXG.stack2jsxgraph("%e**x") ); 1934 * // Output: 1935 * // "EULER**x" 1936 * 1937 * @example 1938 * console.log( JXG.stack2jsxgraph("[%pi*(x**2 - 1), %phi*(x - 1), %gamma*(x+1)]") ); 1939 * // Output: 1940 * // [ "PI*(x**2 - 1)", "1.618033988749895*(x - 1)", "0.5772156649015329*(x+1)" ] 1941 * 1942 * @param {String} str 1943 * @returns String 1944 */ 1945 stack2jsxgraph: function(str) { 1946 var t; 1947 1948 t = str. 1949 replace(/%pi/g, 'PI'). 1950 replace(/%e/g, 'EULER'). 1951 replace(/%phi/g, '1.618033988749895'). 1952 replace(/%gamma/g, '0.5772156649015329'). 1953 trim(); 1954 1955 // String containing array -> array containing strings 1956 if (t[0] === '[' && t[t.length - 1] === ']') { 1957 t = t.slice(1, -1).split(/\s*,\s*/); 1958 } 1959 1960 return t; 1961 } 1962 } 1963 ); 1964 1965 export default JXG; 1966