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