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