1 /* 2 Copyright 2008-2011 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software: you can redistribute it and/or modify 13 it under the terms of the GNU Lesser General Public License as published by 14 the Free Software Foundation, either version 3 of the License, or 15 (at your option) any later version. 16 17 JSXGraph is distributed in the hope that it will be useful, 18 but WITHOUT ANY WARRANTY; without even the implied warranty of 19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 GNU Lesser General Public License for more details. 21 22 You should have received a copy of the GNU Lesser General Public License 23 along with JSXGraph. If not, see <http://www.gnu.org/licenses/>. 24 */ 25 26 /** 27 * @fileoverview The JSXGraph object is defined in this file. JXG.JSXGraph controls all boards. 28 * It has methods to create, save, load and free boards. Additionally some helper functions are 29 * defined in this file directly in the JXG namespace. 30 * @version 0.83 31 */ 32 33 34 // We need the following two methods "extend" and "shortcut" to create the JXG object via JXG.extend. 35 36 /** 37 * Copy all properties of the <tt>extension</tt> object to <tt>object</tt>. 38 * @param {Object} object 39 * @param {Object} extension 40 * @param {Boolean} [onlyOwn=false] Only consider properties that belong to extension itself, not any inherited properties. 41 * @param {Boolean} [toLower=false] If true the keys are convert to lower case. This is needed for visProp, see JXG#copyAttributes 42 */ 43 JXG.extend = function (object, extension, onlyOwn, toLower) { 44 var e, e2; 45 46 onlyOwn = onlyOwn || false; 47 toLower = toLower || false; 48 49 for(e in extension) { 50 if (!onlyOwn || (onlyOwn && extension.hasOwnProperty(e))) { 51 if (toLower) { 52 e2 = e.toLowerCase(); 53 } else { 54 e2 = e; 55 } 56 57 object[e2] = extension[e]; 58 } 59 } 60 }; 61 62 /** 63 * Creates a shortcut to a method, e.g. {@link JXG.Board#create} is a shortcut to {@link JXG.Board#createElement}. 64 * Sometimes the target is undefined by the time you want to define the shortcut so we need this little helper. 65 * @param {Object} object The object the method we want to create a shortcut for belongs to. 66 * @param {Function} fun The method we want to create a shortcut for. 67 * @returns {Function} A function that calls the given method. 68 */ 69 JXG.shortcut = function (object, fun) { 70 return function () { 71 return object[fun].apply(this, arguments); 72 }; 73 }; 74 75 76 JXG.extend(JXG, /** @lends JXG */ { 77 78 /** 79 * Detect browser support for VML. 80 * @returns {Boolean} True, if the browser supports VML. 81 */ 82 supportsVML: function () { 83 // From stackoverflow.com 84 return !!document.namespaces; 85 }, 86 87 /** 88 * Detect browser support for SVG. 89 * @returns {Boolean} True, if the browser supports SVG. 90 */ 91 supportsSVG: function () { 92 return document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"); 93 }, 94 95 /** 96 * Detect browser support for Canvas. 97 * @returns {Boolean} True, if the browser supports HTML canvas. 98 */ 99 supportsCanvas: function () { 100 return !!document.createElement('canvas').getContext; 101 }, 102 103 /** 104 * Detects if the user is using an Android powered device. 105 * @returns {Boolean} 106 */ 107 isAndroid: function () { 108 return navigator.userAgent.toLowerCase().search("android") > -1; 109 }, 110 111 /** 112 * Detects if the user is using the default Webkit browser on an Android powered device. 113 * @returns {Boolean} 114 */ 115 isWebkitAndroid: function () { 116 return this.isAndroid() && navigator.userAgent.search(" AppleWebKit/") > -1; 117 }, 118 119 /** 120 * Detects if the user is using a Apple iPad / iPhone. 121 * @returns {Boolean} 122 */ 123 isApple: function () { 124 return (navigator.userAgent.search(/iPad/) != -1 || navigator.userAgent.search(/iPhone/) != -1); 125 }, 126 127 /** 128 * Detects if the user is using Safari on an Apple device. 129 * @returns {Boolean} 130 */ 131 isWebkitApple: function () { 132 return this.isApple() && (navigator.userAgent.search(/Mobile *.*Safari/) > -1); 133 }, 134 135 /** 136 * Resets visPropOld of <tt>el</tt> 137 * @param {JXG.GeometryElement} el 138 */ 139 clearVisPropOld: function (el) { 140 el.visPropOld = { 141 strokecolor: '', 142 strokeopacity: '', 143 strokewidth: '', 144 fillcolor: '', 145 fillopacity: '', 146 shadow: false, 147 firstarrow: false, 148 lastarrow: false 149 }; 150 }, 151 152 /** 153 * Internet Explorer version. Works only for IE > 4. 154 * @type Number 155 */ 156 ieVersion: (function() { 157 var undef, 158 v = 3, 159 div = document.createElement('div'), 160 all = div.getElementsByTagName('i'); 161 162 while ( 163 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><' + '/i><![endif]-->', 164 all[0] 165 ); 166 167 return v > 4 ? v : undef; 168 169 }()), 170 171 /** 172 * s may be a string containing the name or id of an element or even a reference 173 * to the element itself. This function returns a reference to the element. Search order: id, name. 174 * @param {JXG.Board} board Reference to the board the element belongs to. 175 * @param {String} s String or reference to a JSXGraph element. 176 * @returns {Object} Reference to the object given in parameter object 177 */ 178 getReference: function (board, s) { 179 if (typeof(s) == 'string') { 180 if (JXG.exists(board.objects[s])) { // Search by ID 181 s = board.objects[s]; 182 } else if (JXG.exists(board.elementsByName[s])) { // Search by name 183 s = board.elementsByName[s]; 184 } else if (JXG.exists(board.groups[s])) { // Search by group ID 185 s = board.groups[s]; 186 } 187 } 188 189 return s; 190 }, 191 192 /** 193 * This is just a shortcut to {@link JXG.getReference}. 194 */ 195 getRef: JXG.shortcut(JXG, 'getReference'), 196 197 /** 198 * Checks if the given string is an id within the given board. 199 * @param {JXG.Board} board 200 * @param {String} s 201 * @returns {Boolean} 202 */ 203 isId: function (board, s) { 204 return typeof(s) == 'string' && !!board.objects[s]; 205 }, 206 207 /** 208 * Checks if the given string is a name within the given board. 209 * @param {JXG.Board} board 210 * @param {String} s 211 * @returns {Boolean} 212 */ 213 isName: function (board, s) { 214 return typeof(s) == 'string' && !!board.elementsByName[s]; 215 }, 216 217 /** 218 * Checks if the given string is a group id within the given board. 219 * @param {JXG.Board} board 220 * @param {String} s 221 * @returns {Boolean} 222 */ 223 isGroup: function (board, s) { 224 return typeof(s) == 'string' && !!board.groups[s]; 225 }, 226 227 /** 228 * Checks if the value of a given variable is of type string. 229 * @param v A variable of any type. 230 * @returns {Boolean} True, if v is of type string. 231 */ 232 isString: function (v) { 233 return typeof v === "string"; 234 }, 235 236 /** 237 * Checks if the value of a given variable is of type number. 238 * @param v A variable of any type. 239 * @returns {Boolean} True, if v is of type number. 240 */ 241 isNumber: function (v) { 242 return typeof v === "number"; 243 }, 244 245 /** 246 * Checks if a given variable references a function. 247 * @param v A variable of any type. 248 * @returns {Boolean} True, if v is a function. 249 */ 250 isFunction: function (v) { 251 return typeof v === "function"; 252 }, 253 254 /** 255 * Checks if a given variable references an array. 256 * @param v A variable of any type. 257 * @returns {Boolean} True, if v is of type array. 258 */ 259 isArray: function (v) { 260 // Borrowed from prototype.js 261 return v !== null && typeof v === "object" && 'splice' in v && 'join' in v; 262 }, 263 264 /** 265 * Checks if a given variable is a reference of a JSXGraph Point element. 266 * @param v A variable of any type. 267 * @returns {Boolean} True, if v is of type JXG.Point. 268 */ 269 isPoint: function (v) { 270 if (typeof v == 'object') { 271 return (v.elementClass == JXG.OBJECT_CLASS_POINT); 272 } 273 274 return false; 275 }, 276 277 /** 278 * Checks if a given variable is neither undefined nor null. You should not use this together with global 279 * variables! 280 * @param v A variable of any type. 281 * @returns {Boolean} True, if v is neither undefined nor null. 282 */ 283 exists: (function (undefined) { 284 return function (v) { 285 return !(v === undefined || v === null); 286 } 287 })(), 288 289 /** 290 * Handle default parameters. 291 * @param {%} v Given value 292 * @param {%} d Default value 293 * @returns {%} <tt>d</tt>, if <tt>v</tt> is undefined or null. 294 */ 295 def: function (v, d) { 296 if (JXG.exists(v)) { 297 return v; 298 } else { 299 return d; 300 } 301 }, 302 303 /** 304 * Converts a string containing either <strong>true</strong> or <strong>false</strong> into a boolean value. 305 * @param {String} s String containing either <strong>true</strong> or <strong>false</strong>. 306 * @returns {Boolean} String typed boolean value converted to boolean. 307 */ 308 str2Bool: function (s) { 309 if (!JXG.exists(s)) { 310 return true; 311 } 312 if (typeof s == 'boolean') { 313 return s; 314 } 315 return (s.toLowerCase()=='true'); 316 }, 317 318 /** 319 * Shortcut for {@link JXG.JSXGraph.initBoard}. 320 */ 321 _board: function (box, attributes) { 322 return JXG.JSXGraph.initBoard(box, attributes); 323 }, 324 325 /** 326 * Convert a String, a number or a function into a function. This method is used in Transformation.js 327 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 328 * by a GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 329 * values is of type string. 330 * @param {Array} param An array containing strings, numbers, or functions. 331 * @param {Number} n Length of <tt>param</tt>. 332 * @returns {Function} A function taking one parameter k which specifies the index of the param element 333 * to evaluate. 334 */ 335 createEvalFunction: function (board, param, n) { 336 // convert GEONExT syntax into function 337 var f = [], i, str; 338 339 for (i=0;i<n;i++) { 340 if (typeof param[i] == 'string') { 341 str = JXG.GeonextParser.geonext2JS(param[i],board); 342 str = str.replace(/this\.board\./g,'board.'); 343 f[i] = new Function('','return ' + (str) + ';'); 344 } 345 } 346 347 return function (k) { 348 var a = param[k]; 349 if (typeof a == 'string') { 350 return f[k](); 351 } else if (typeof a=='function') { 352 return a(); 353 } else if (typeof a=='number') { 354 return a; 355 } 356 return 0; 357 }; 358 }, 359 360 /** 361 * Convert a String, number or function into a function. 362 * @param term A variable of type string, function or number. 363 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 364 * by a GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 365 * values is of type string. 366 * @param {String} variableName Only required if evalGeonext is set to true. Describes the variable name 367 * of the variable in a GEONE<sub>X</sub>T string given as term. 368 * @param {Boolean} evalGeonext Set this true, if term should be treated as a GEONE<sub>X</sub>T string. 369 * @returns {Function} A function evaluation the value given by term or null if term is not of type string, 370 * function or number. 371 */ 372 createFunction: function (term, board, variableName, evalGeonext) { 373 var f = null; 374 375 if ((!JXG.exists(evalGeonext) || evalGeonext) && JXG.isString(term)) { 376 // Convert GEONExT syntax into JavaScript syntax 377 //newTerm = JXG.GeonextParser.geonext2JS(term, board); 378 //return new Function(variableName,'return ' + newTerm + ';'); 379 f = board.jc.snippet(term, true, variableName, true); 380 } else if (JXG.isFunction(term)) { 381 f = term; 382 } else if (JXG.isNumber(term)) { 383 f = function () { return term; }; 384 } else if (JXG.isString(term)) { // In case of string function like fontsize 385 f = function () { return term; }; 386 } 387 388 if (f !== null) { 389 f.origin = term; 390 } 391 392 return f; 393 }, 394 395 /** 396 * Checks given parents array against several expectations. 397 * @param {String} element The name of the element to be created 398 * @param {Array} parents A parents array 399 * @param {Array} expects Each possible parents array types combination is given as 400 * an array of element type constants containing the types or names of elements that are 401 * accepted and in what order they are accepted. Strings can be given for basic data types 402 * are <em>number, string, array, function, object</em>. We accept non element JSXGraph 403 * types like <em>coords</em>, too. 404 * @returns {Array} A new parents array prepared for the use within a create* method 405 */ 406 checkParents: function (element, parents, expects) { 407 // some running variables 408 var i, j, k, len, 409 410 // collects the parent elements that already got verified 411 new_parents = [], 412 413 // in case of multiple parent array type combinations we may have to start over again 414 // so hold the parents array in an temporary array in case we need the original one back 415 tmp_parents = parents.slice(0), 416 417 // test the given parent element against what we expect 418 is = function (expect, parent) { 419 // we basically got three cases: 420 // expect is of type 421 // number => test for parent.elementClass and parent.type 422 // lucky us elementClass and type constants don't overlap \o/ 423 // string => here we have two sub cases depending on the value of expect 424 // string, object, function, number => make a simple typeof 425 // array => check via isArray 426 427 var type_expect = (typeof expect).toLowerCase(); 428 429 if (type_expect === 'number') { 430 return parent && ((parent.type && parent.type === expect) || (parent.elementClass && parent.elementClass === expect)) 431 } else { 432 switch(expect.toLowerCase()) { 433 case 'string': 434 case 'object': 435 case 'function': 436 case 'number': 437 return (typeof parent).toLowerCase() === expect.toLowerCase(); 438 break; 439 case 'array': 440 return JXG.isArray(parent); 441 break; 442 } 443 } 444 445 446 return false; 447 }; 448 449 450 for(i = 0; i < expects.length; i++) { 451 // enter the next loop only if parents has enough elements 452 for(j = 0; j < expects[i].length && parents.length >= expects[i].length; j++) { 453 k = 0; 454 while (k < tmp_parents.length && !is(expects[i][j], tmp_parents[k])) 455 k++; 456 457 if (k<tmp_parents.length) { 458 new_parents.push(tmp_parents.splice(len-k-1, 1)[0]); 459 } 460 } 461 462 // if there are still elements left in the parents array we need to 463 // rebuild the original parents array and start with the next expect array 464 if (tmp_parents.length) { 465 tmp_parents = parents.slice(0); 466 new_parents = []; 467 } else // yay, we found something \o/ 468 return new_parents; 469 } 470 }, 471 472 /** 473 * Reads the configuration parameter of an attribute of an element from a {@link JXG.Options} object 474 * @param {JXG.Options} options Reference to an instance of JXG.Options. You usually want to use the 475 * options property of your board. 476 * @param {String} element The name of the element which options you wish to read, e.g. 'point' or 477 * 'elements' for general attributes. 478 * @param {String} key The name of the attribute to read, e.g. 'strokeColor' or 'withLabel' 479 * @returns The value of the selected configuration parameter. 480 * @see JXG.Options 481 */ 482 readOption: function (options, element, key) { 483 var val = options.elements[key]; 484 485 if (JXG.exists(options[element][key])) 486 val = options[element][key]; 487 488 return val; 489 }, 490 491 /** 492 * Checks an attributes object and fills it with default values if there are properties missing. 493 * @param {Object} attributes 494 * @param {Object} defaults 495 * @returns {Object} The given attributes object with missing properties added via the defaults object. 496 * @deprecated replaced by JXG#copyAttributes 497 */ 498 checkAttributes: function (attributes, defaults) { 499 var key; 500 501 // Make sure attributes is an object. 502 if (!JXG.exists(attributes)) { 503 attributes = {}; 504 } 505 506 // Go through all keys of defaults and check for their existence 507 // in attributes. If one doesn't exist, it is created with the 508 // same value as in defaults. 509 for (key in defaults) { 510 if (!JXG.exists(attributes[key])) { 511 attributes[key] = defaults[key]; 512 } 513 } 514 515 return attributes; 516 }, 517 518 /** 519 * Generates an attributes object that is filled with default values from the Options object 520 * and overwritten by the user speciified attributes. 521 * @param {Object} attributes user specified attributes 522 * @param {Object} options defaults options 523 * @param {String} % variable number of strings, e.g. 'slider', subtype 'point1'. 524 * @returns {Object} The resulting attributes object 525 */ 526 copyAttributes: function (attributes, options) { 527 var a, i, len, o, isAvail; 528 529 a = this.deepCopy(options.elements, null, true); // default options from Options.elements 530 len = arguments.length; 531 532 // Only the layer of the main element is set. 533 if (len < 4 && this.exists(arguments[2]) && this.exists(options.layer[arguments[2]])) { 534 a.layer = options.layer[arguments[2]]; 535 } 536 537 o = options; // default options from specific elements 538 isAvail = true; 539 for (i = 2; i < len; i++) { 540 if (JXG.exists(o[arguments[i]])) { 541 o = o[arguments[i]]; 542 } else { 543 isAvail = false; 544 break; 545 } 546 } 547 if (isAvail) { 548 a = this.deepCopy(a, o, true); 549 } 550 551 o = attributes; // options from attributes 552 isAvail = true; 553 for (i=3;i<len;i++) { 554 if (JXG.exists(o[arguments[i]])) { 555 o = o[arguments[i]]; 556 } else { 557 isAvail = false; 558 break; 559 } 560 } 561 if (isAvail) { 562 this.extend(a, o, null, true); 563 } 564 565 /** 566 * Special treatment of labels 567 */ 568 o = options; 569 isAvail = true; 570 for (i = 2; i < len; i++) { 571 if (JXG.exists(o[arguments[i]])) { 572 o = o[arguments[i]]; 573 } else { 574 isAvail = false; 575 break; 576 } 577 } 578 if (isAvail) { 579 a.label = JXG.deepCopy(o.label, a.label); 580 } 581 a.label = JXG.deepCopy(options.label, a.label); 582 583 return a; 584 }, 585 586 /** 587 * Reads the width and height of an HTML element. 588 * @param {String} elementId The HTML id of an HTML DOM node. 589 * @returns {Object} An object with the two properties width and height. 590 */ 591 getDimensions: function (elementId) { 592 var element, display, els, originalVisibility, originalPosition, 593 originalDisplay, originalWidth, originalHeight; 594 595 // Borrowed from prototype.js 596 element = document.getElementById(elementId); 597 if (!JXG.exists(element)) { 598 throw new Error("\nJSXGraph: HTML container element '" + (elementId) + "' not found."); 599 } 600 601 display = element.style.display; 602 if (display != 'none' && display != null) {// Safari bug 603 return {width: element.offsetWidth, height: element.offsetHeight}; 604 } 605 606 // All *Width and *Height properties give 0 on elements with display set to none, 607 // hence we show the element temporarily 608 els = element.style; 609 610 // save style 611 originalVisibility = els.visibility; 612 originalPosition = els.position; 613 originalDisplay = els.display; 614 615 // show element 616 els.visibility = 'hidden'; 617 els.position = 'absolute'; 618 els.display = 'block'; 619 620 // read the dimension 621 originalWidth = element.clientWidth; 622 originalHeight = element.clientHeight; 623 624 // restore original css values 625 els.display = originalDisplay; 626 els.position = originalPosition; 627 els.visibility = originalVisibility; 628 629 return { 630 width: originalWidth, 631 height: originalHeight 632 }; 633 }, 634 635 /** 636 * Adds an event listener to a DOM element. 637 * @param {Object} obj Reference to a DOM node. 638 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 639 * @param {Function} fn The function to call when the event is triggered. 640 * @param {Object} owner The scope in which the event trigger is called. 641 */ 642 addEvent: function (obj, type, fn, owner) { 643 var el = function () { 644 return fn.apply(owner, arguments); 645 }; 646 647 el.origin = fn; 648 owner['x_internal'+type] = owner['x_internal'+type] || []; 649 owner['x_internal'+type].push(el); 650 651 if (JXG.exists(obj) && JXG.exists(obj.addEventListener)) { // Non-IE browser 652 obj.addEventListener(type, el, false); 653 } else { // IE 654 obj.attachEvent('on'+type, el); 655 } 656 }, 657 658 /** 659 * Removes an event listener from a DOM element. 660 * @param {Object} obj Reference to a DOM node. 661 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 662 * @param {Function} fn The function to call when the event is triggered. 663 * @param {Object} owner The scope in which the event trigger is called. 664 */ 665 removeEvent: function (obj, type, fn, owner) { 666 var em = 'JSXGraph: Can\'t remove event listener on' + type + ': ' + owner['x_internal' + type], 667 i, j = -1, l; 668 669 if ((!JXG.exists(owner) || !JXG.exists(owner['x_internal' + type])) && !JXG.isArray(owner['x_internal' + type])) { 670 //JXG.debug(em); 671 return; 672 } 673 674 l = owner['x_internal' + type].length; 675 for (i = 0; i < l; i++) { 676 if (owner['x_internal' + type][i].origin === fn) { 677 j = i; 678 break; 679 } 680 } 681 682 if (j === -1) { 683 //JXG.debug(em + '; Event listener not found.'); 684 return; 685 } 686 687 try { 688 if (JXG.exists(obj.addEventListener)) { // Non-IE browser 689 obj.removeEventListener(type, owner['x_internal'+type][j], false); 690 } else { // IE 691 obj.detachEvent('on'+type, owner['x_internal'+type][j]); 692 } 693 694 JXG.removeElementFromArray(owner['x_internal'+type], owner['x_internal'+type][j]); 695 } catch(e) { 696 //JXG.debug('JSXGraph: Can\'t remove event listener on' + type + ': ' + owner['x_internal' + type]); 697 } 698 }, 699 700 /** 701 * Removes all events of the given type from a given DOM node; Use with caution and do not use it on a container div 702 * of a {@link JXG.Board} because this might corrupt the event handling system. 703 * @param {Object} obj Reference to a DOM node. 704 * @param {String} type The event to catch, without leading 'on', e.g. 'mousemove' instead of 'onmousemove'. 705 * @param {Object} owner The scope in which the event trigger is called. 706 */ 707 removeAllEvents: function(obj, type, owner) { 708 if (owner['x_internal' + type]) { 709 while (owner['x_internal' + type].length > 0) { 710 JXG.removeEvent(obj, type, owner['x_internal' + type][0].origin, owner); 711 } 712 } 713 }, 714 715 /** 716 * Generates a function which calls the function fn in the scope of owner. 717 * @param {Function} fn Function to call. 718 * @param {Object} owner Scope in which fn is executed. 719 * @returns {Function} A function with the same signature as fn. 720 */ 721 bind: function (fn, owner) { 722 return function () { 723 return fn.apply(owner,arguments); 724 }; 725 }, 726 727 /** 728 * Removes an element from the given array 729 * @param {Array} ar 730 * @param {%} el 731 */ 732 removeElementFromArray: function(ar, el) { 733 var i; 734 735 for (i = 0; i < ar.length; i++) { 736 if (ar[i] === el) { 737 ar.splice(i, 1); 738 return ar; 739 } 740 } 741 742 return ar; 743 }, 744 745 /** 746 * Cross browser mouse / touch coordinates retrieval relative to the board's top left corner. 747 * @param {Object} [e] The browsers event object. If omitted, <tt>window.event</tt> will be used. 748 * @param {Number} [index] If <tt>e</tt> is a touch event, this provides the index of the touch coordinates, i.e. it determines which finger. 749 * @returns {Array} Contains the position as x,y-coordinates in the first resp. second component. 750 */ 751 getPosition: function (e, index) { 752 var posx = 0, 753 posy = 0; 754 755 if (!e) { 756 e = window.event; 757 } 758 759 if (JXG.exists(index)) { 760 e = e.targetTouches[index]; 761 } 762 763 if (e.pageX || e.pageY) { 764 posx = e.pageX; 765 posy = e.pageY; 766 } else if (e.clientX || e.clientY) { 767 posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 768 posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 769 } 770 771 return [posx,posy]; 772 }, 773 774 /** 775 * Calculates recursively the offset of the DOM element in which the board is stored. 776 * @param {Object} obj A DOM element 777 * @returns {Array} An array with the elements left and top offset. 778 */ 779 getOffset: function (obj) { 780 var o = obj, 781 o2 = obj, 782 l =o.offsetLeft - o.scrollLeft, 783 t =o.offsetTop - o.scrollTop; 784 785 /* 786 * In Mozilla and Webkit: offsetParent seems to jump at least to the next iframe, 787 * if not to the body. In IE and if we are in an position:absolute environment 788 * offsetParent walks up the DOM hierarchy. 789 * In order to walk up the DOM hierarchy also in Mozilla and Webkit 790 * we need the parentNode steps. 791 */ 792 while (o=o.offsetParent) { 793 l+=o.offsetLeft; 794 t+=o.offsetTop; 795 if (o.offsetParent) { 796 l+=o.clientLeft - o.scrollLeft; 797 t+=o.clientTop - o.scrollTop; 798 } 799 o2 = o2.parentNode; 800 while (o2!=o) { 801 l += o2.clientLeft - o2.scrollLeft; 802 t += o2.clientTop - o2.scrollTop; 803 o2 = o2.parentNode; 804 } 805 } 806 return [l,t]; 807 }, 808 809 /** 810 * Access CSS style sheets. 811 * @param {Object} obj A DOM element 812 * @param {String} stylename The CSS property to read. 813 * @returns The value of the CSS property and <tt>undefined</tt> if it is not set. 814 */ 815 getStyle: function (obj, stylename) { 816 var r; 817 818 if (window.getComputedStyle) { 819 // Non-IE 820 r = document.defaultView.getComputedStyle(obj, null).getPropertyValue(stylename); 821 } else if (obj.currentStyle && JXG.ieVersion >= 9) { 822 // IE 823 r = obj.currentStyle[stylename]; 824 } else { 825 if (obj.style) { 826 // make stylename lower camelcase 827 stylename = stylename.replace(/-([a-z]|[0-9])/ig, function (all, letter) { 828 return ( letter + "" ).toUpperCase(); 829 }); 830 r = obj.style[stylename] 831 } 832 } 833 834 return r; 835 }, 836 837 /** 838 * Extracts the keys of a given object. 839 * @param object The object the keys are to be extracted 840 * @param onlyOwn If true, hasOwnProperty() is used to verify that only keys are collected 841 * the object owns itself and not some other object in the prototype chain. 842 * @returns {Array} All keys of the given object. 843 */ 844 keys: function (object, onlyOwn) { 845 var keys = [], property; 846 847 for (property in object) { 848 if (onlyOwn) { 849 if (object.hasOwnProperty(property)) { 850 keys.push(property); 851 } 852 } else { 853 keys.push(property); 854 } 855 } 856 return keys; 857 }, 858 859 /** 860 * Search an array for a given value. 861 * @param {Array} array 862 * @param {%} value 863 * @returns {Number} The index of the first appearance of the given value, or 864 * <tt>-1</tt> if the value was not found. 865 */ 866 indexOf: function (array, value) { 867 var i; 868 869 if (Array.indexOf) { 870 return array.indexOf(value); 871 } 872 873 for (i = 0; i < array.length; i++) { 874 if (array[i] === value) { 875 return i; 876 } 877 } 878 879 return -1; 880 }, 881 882 /** 883 * Replaces all occurences of & by &, > by >, and < by <. 884 * @param str 885 */ 886 escapeHTML: function (str) { 887 return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 888 }, 889 890 /** 891 * Eliminates all substrings enclosed by < and > and replaces all occurences of 892 * & by &, > by >, and < by <. 893 * @param str 894 */ 895 unescapeHTML: function (str) { 896 return str.replace(/<\/?[^>]+>/gi, '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 897 }, 898 899 /** 900 * This outputs an object with a base class reference to the given object. This is useful if 901 * you need a copy of an e.g. attributes object and want to overwrite some of the attributes 902 * without changing the original object. 903 * @param {Object} obj Object to be embedded. 904 * @returns {Object} An object with a base class reference to <tt>obj</tt>. 905 */ 906 clone: function (obj) { 907 var cObj = {}; 908 cObj.prototype = obj; 909 return cObj; 910 }, 911 912 /** 913 * Embeds an existing object into another one just like {@link #clone} and copies the contents of the second object 914 * to the new one. Warning: The copied properties of obj2 are just flat copies. 915 * @param {Object} obj Object to be copied. 916 * @param {Object} obj2 Object with data that is to be copied to the new one as well. 917 * @returns {Object} Copy of given object including some new/overwritten data from obj2. 918 */ 919 cloneAndCopy: function (obj, obj2) { 920 var cObj = function(){}, r; 921 cObj.prototype = obj; 922 //cObj = obj; 923 for(r in obj2) 924 cObj[r] = obj2[r]; 925 926 return cObj; 927 }, 928 929 /** 930 * Creates a deep copy of an existing object, i.e. arrays or sub-objects are copied component resp. 931 * element-wise instead of just copying the reference. If a second object is supplied, the two objects 932 * are merged into one object. The properties of the second object have priority. 933 * @param {Object} obj This object will be copied. 934 * @param {Object} obj2 This object will merged into the newly created object 935 * @param {Boolean} [toLower=false] If true the keys are convert to lower case. This is needed for visProp, see JXG#copyAttributes 936 * @returns {Object} copy of obj or merge of obj and obj2. 937 */ 938 deepCopy: function (obj, obj2, toLower) { 939 var c, i, prop, j, i2; 940 941 toLower = toLower || false; 942 943 if (typeof obj !== 'object' || obj == null) { 944 return obj; 945 } 946 947 if (this.isArray(obj)) { 948 c = []; 949 for (i = 0; i < obj.length; i++) { 950 prop = obj[i]; 951 if (typeof prop == 'object') { 952 c[i] = this.deepCopy(prop); 953 } else { 954 c[i] = prop; 955 } 956 } 957 } else { 958 c = {}; 959 for (i in obj) { 960 i2 = toLower ? i.toLowerCase() : i; 961 962 prop = obj[i]; 963 if (typeof prop == 'object') { 964 c[i2] = this.deepCopy(prop); 965 } else { 966 c[i2] = prop; 967 } 968 } 969 970 for (i in obj2) { 971 i2 = toLower ? i.toLowerCase() : i; 972 973 prop = obj2[i]; 974 if (typeof prop == 'object') { 975 if (JXG.isArray(prop) || !JXG.exists(c[i2])) { 976 c[i2] = this.deepCopy(prop); 977 } else { 978 c[i2] = this.deepCopy(c[i2], prop, toLower); 979 } 980 } else { 981 c[i2] = prop; 982 } 983 } 984 } 985 return c; 986 }, 987 988 /** 989 * Converts a JavaScript object into a JSON string. 990 * @param {Object} obj A JavaScript object, functions will be ignored. 991 * @param {Boolean} [noquote=false] No quotes around the name of a property. 992 * @returns {String} The given object stored in a JSON string. 993 */ 994 toJSON: function (obj, noquote) { 995 var s; 996 997 if (!JXG.exists(noquote)) { 998 noquote = false; 999 } 1000 1001 // check for native JSON support: 1002 if (window.JSON && window.JSON.stringify && !noquote) { 1003 try { 1004 s = JSON.stringify(obj); 1005 return s; 1006 } catch(e) { 1007 // if something goes wrong, e.g. if obj contains functions we won't return 1008 // and use our own implementation as a fallback 1009 } 1010 } 1011 1012 switch (typeof obj) { 1013 case 'object': 1014 if (obj) { 1015 var list = []; 1016 if (obj instanceof Array) { 1017 for (var i=0;i < obj.length;i++) { 1018 list.push(JXG.toJSON(obj[i], noquote)); 1019 } 1020 return '[' + list.join(',') + ']'; 1021 } else { 1022 for (var prop in obj) { 1023 if (noquote) { 1024 list.push(prop + ':' + JXG.toJSON(obj[prop], noquote)); 1025 } else { 1026 list.push('"' + prop + '":' + JXG.toJSON(obj[prop], noquote)); 1027 } 1028 } 1029 return '{' + list.join(',') + '} '; 1030 } 1031 } else { 1032 return 'null'; 1033 } 1034 case 'string': 1035 return '\'' + obj.replace(/(["'])/g, '\\$1') + '\''; 1036 case 'number': 1037 case 'boolean': 1038 return new String(obj); 1039 } 1040 }, 1041 1042 /** 1043 * Makes a string lower case except for the first character which will be upper case. 1044 * @param {String} str Arbitrary string 1045 * @returns {String} The capitalized string. 1046 */ 1047 capitalize: function (str) { 1048 return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); 1049 }, 1050 1051 /** 1052 * Process data in timed chunks. Data which takes long to process, either because it is such 1053 * a huge amount of data or the processing takes some time, causes warnings in browsers about 1054 * irresponsive scripts. To prevent these warnings, the processing is split into smaller pieces 1055 * called chunks which will be processed in serial order. 1056 * Copyright 2009 Nicholas C. Zakas. All rights reserved. MIT Licensed 1057 * @param {Array} items to do 1058 * @param {Function} process Function that is applied for every array item 1059 * @param {Object} context The scope of function process 1060 * @param {Function} callback This function is called after the last array element has been processed. 1061 */ 1062 timedChunk: function (items, process, context, callback) { 1063 var todo = items.concat(); //create a clone of the original 1064 setTimeout(function (){ 1065 var start = +new Date(); 1066 do { 1067 process.call(context, todo.shift()); 1068 } while (todo.length > 0 && (+new Date() - start < 300)); 1069 if (todo.length > 0){ 1070 setTimeout(arguments.callee, 1); 1071 } else { 1072 callback(items); 1073 } 1074 }, 1); 1075 }, 1076 1077 /** 1078 * Make numbers given as strings nicer by removing all unnecessary leading and trailing zeroes. 1079 * @param {String} str 1080 * @returns {String} 1081 */ 1082 trimNumber: function (str) { 1083 str = str.replace(/^0+/, ""); 1084 str = str.replace(/0+$/, ""); 1085 if (str[str.length-1] == '.' || str[str.length-1] == ',') { 1086 str = str.slice(0, -1); 1087 } 1088 if (str[0] == '.' || str[0] == ',') { 1089 str = "0" + str; 1090 } 1091 1092 return str; 1093 }, 1094 1095 /** 1096 * Remove all leading and trailing whitespaces from a given string. 1097 * @param {String} str 1098 * @returns {String} 1099 */ 1100 trim: function (str) { 1101 str = str.replace(/^\s+/, ""); 1102 str = str.replace(/\s+$/, ""); 1103 1104 return str; 1105 }, 1106 1107 /** 1108 * If <tt>val</tt> is a function, it will be evaluated without giving any parameters, else the input value 1109 * is just returned. 1110 * @param val Could be anything. Preferably a number or a function. 1111 * @returns If <tt>val</tt> is a function, it is evaluated and the result is returned. Otherwise <tt>val</tt> is returned. 1112 */ 1113 evaluate: function (val) { 1114 if (JXG.isFunction(val)) { 1115 return val(); 1116 } else { 1117 return val; 1118 } 1119 }, 1120 1121 /** 1122 * Eliminates duplicate entries in an array. 1123 * @param {Array} a An array 1124 * @returns {Array} The array with duplicate entries eliminated. 1125 */ 1126 eliminateDuplicates: function (a) { 1127 var i, len = a.length, 1128 result = [], 1129 obj = {}; 1130 1131 for (i = 0; i < len; i++) { 1132 obj[a[i]] = 0; 1133 } 1134 1135 for (i in obj) { 1136 if (obj.hasOwnProperty(i)) { 1137 result.push(i); 1138 } 1139 } 1140 1141 return result; 1142 }, 1143 1144 /** 1145 * Compare two arrays. 1146 * @param {Array} a1 1147 * @param {Array} a2 1148 * @returns {Boolean} <tt>true</tt>, if the arrays coefficients are of same type and value. 1149 */ 1150 cmpArrays: function (a1, a2) { 1151 var i; 1152 1153 // trivial cases 1154 if (a1 === a2) { 1155 return true; 1156 } 1157 1158 if (a1.length !== a2.length) { 1159 return false; 1160 } 1161 1162 for (i = 0; i < a1.length; i++) { 1163 if ((typeof a1[i] !== typeof a2[i]) || (a1[i] !== a2[i])) { 1164 return false; 1165 } 1166 } 1167 1168 return true; 1169 }, 1170 1171 /** 1172 * Truncate a number <tt>n</tt> after <tt>p</tt> decimals. 1173 * @param n 1174 * @param p 1175 * @returns {Number} 1176 */ 1177 trunc: function (n, p) { 1178 p = JXG.def(p, 0); 1179 1180 if (p == 0) { 1181 n = ~~n; 1182 } else { 1183 n = n.toFixed(p); 1184 } 1185 1186 return n; 1187 }, 1188 1189 /** 1190 * Add something to the debug log. If available a JavaScript debug console is used. Otherwise 1191 * we're looking for a HTML div with id "debug". If this doesn't exist, too, the output is omitted. 1192 * @param {%} An arbitrary number of parameters. 1193 */ 1194 debug: function (s) { 1195 var i; 1196 1197 for(i = 0; i < arguments.length; i++) { 1198 s = arguments[i]; 1199 if (window.console && console.log) { 1200 //if (typeof s === 'string') s = s.replace(/<\S[^><]*>/g, ""); 1201 console.log(s); 1202 } else if (document.getElementById('debug')) { 1203 document.getElementById('debug').innerHTML += s + "<br/>"; 1204 } 1205 // else: do nothing 1206 } 1207 }, 1208 1209 debugWST: function (s) { 1210 var e; 1211 JXG.debug(s); 1212 1213 if (window.console && console.log) { 1214 e = new Error(); 1215 if (e && e.stack) { 1216 console.log('stacktrace'); 1217 console.log(e.stack.split('\n').slice(1).join('\n')); 1218 } 1219 } 1220 } 1221 }); 1222 1223 // JessieScript startup: Search for script tags of type text/jessiescript and interpret them. 1224 JXG.addEvent(window, 'load', function () { 1225 var scripts = document.getElementsByTagName('script'), type, 1226 i, j, div, board, width, height, bbox, axis, grid; 1227 1228 for(i=0;i<scripts.length;i++) { 1229 type = scripts[i].getAttribute('type', false); 1230 if (!JXG.exists(type)) continue; 1231 if (type.toLowerCase() === 'text/jessiescript' || type.toLowerCase === 'jessiescript') { 1232 width = scripts[i].getAttribute('width', false) || '500px'; 1233 height = scripts[i].getAttribute('height', false) || '500px'; 1234 bbox = scripts[i].getAttribute('boundingbox', false) || '-5, 5, 5, -5'; 1235 bbox = bbox.split(','); 1236 if (bbox.length!==4) { 1237 bbox = [-5, 5, 5, -5]; 1238 } else { 1239 for(j=0;j<bbox.length;j++) { 1240 bbox[j] = parseFloat(bbox[j]); 1241 } 1242 } 1243 axis = JXG.str2Bool(scripts[i].getAttribute('axis', false) || 'false'); 1244 grid = JXG.str2Bool(scripts[i].getAttribute('grid', false) || 'false'); 1245 1246 div = document.createElement('div'); 1247 div.setAttribute('id', 'jessiescript_autgen_jxg_'+i); 1248 div.setAttribute('style', 'width:'+width+'; height:'+height+'; float:left'); 1249 div.setAttribute('class', 'jxgbox'); 1250 document.body.insertBefore(div, scripts[i]); 1251 1252 board = JXG.JSXGraph.initBoard('jessiescript_autgen_jxg_'+i, {boundingbox: bbox, keepaspectratio:true, grid: grid, axis: axis}); 1253 board.construct(scripts[i].innerHTML); 1254 } 1255 } 1256 }, window); 1257