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