1 /* 2 Copyright 2008-2026 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 dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true*/ 33 /*jslint nomen: true, plusplus: true*/ 34 35 /** 36 * @fileoverview The JXG.Dump namespace provides methods to save a board to javascript. 37 */ 38 39 import JXG from "../jxg.js"; 40 import Type from "./type.js"; 41 42 /** 43 * The JXG.Dump namespace provides classes and methods to save a board to javascript. 44 * @namespace 45 */ 46 JXG.Dump = { 47 /** 48 * Adds markers to every element of the board 49 * @param {JXG.Board} board 50 * @param {Array|String} markers 51 * @param {Array} values 52 */ 53 addMarkers: function (board, markers, values) { 54 var e, l, i; 55 56 if (!Type.isArray(markers)) { 57 markers = [markers]; 58 } 59 60 if (!Type.isArray(values)) { 61 values = [values]; 62 } 63 64 l = Math.min(markers.length, values.length); 65 66 markers.length = l; 67 values.length = l; 68 69 for (e in board.objects) { 70 if (board.objects.hasOwnProperty(e)) { 71 for (i = 0; i < l; i++) { 72 board.objects[e][markers[i]] = values[i]; 73 } 74 } 75 } 76 }, 77 78 /** 79 * Removes markers from every element on the board. 80 * @param {JXG.Board} board 81 * @param {Array|String} markers 82 */ 83 deleteMarkers: function (board, markers) { 84 var e, l, i; 85 86 if (!Type.isArray(markers)) { 87 markers = [markers]; 88 } 89 90 l = markers.length; 91 92 markers.length = l; 93 94 for (e in board.objects) { 95 if (board.objects.hasOwnProperty(e)) { 96 for (i = 0; i < l; i++) { 97 delete board.objects[e][markers[i]]; 98 } 99 } 100 } 101 }, 102 103 /** 104 * Stringifies a string, i.e. puts some quotation marks around <tt>s</tt> if it is of type string. 105 * @param {*} s 106 * @returns {String} " + s + " 107 */ 108 str: function (s) { 109 if (typeof s === "string" && s.slice(0, 7) !== 'function') { 110 s = '"' + s + '"'; 111 } 112 113 return s; 114 }, 115 116 /** 117 * Recursively determine the difference between objects instance and def. 118 * @param {Object} instance Attribute object of the element. Usually a copy is supplied 119 * @param {Object} def Default attributes, the instance is compared to 120 * @param {String} pre Helper string for debug output 121 * @returns 122 */ 123 _minimizeSubObject: function(instance, def, pre) { 124 var p, pl, del, 125 deleteAll = true, 126 copy = instance; 127 128 for (p in def) { 129 if (def.hasOwnProperty(p)) { 130 pl = p.toLowerCase(); 131 // console.log(pre + 'Test', pl, typeof def[p]) 132 133 if ((def[p] === copy[pl]) || (!Type.exists(def[p]) && !Type.exists(copy[pl])) ) { 134 // Equality is determined for strings and numbers. 135 // For different arrays or objects, '===' is always false. 136 137 // console.log(pre + "\tdelete", p) 138 delete copy[pl]; 139 } else if (Type.isArray(def[p]) && Type.isArray(copy[pl])) { 140 // Compare two arrays 141 // console.log(p, 'arrays', 'instance:', copy[pl], 'default:', def[p]) 142 if (Type.cmpArrays(copy[pl], def[p])) { 143 // console.log(pre + "\t\tdelete array", p); 144 delete copy[pl]; 145 } else { 146 // console.log(pre + '\t keep array') 147 deleteAll = false; 148 } 149 } else { 150 if (Type.exists(def[p]) && typeof def[p] === 'object' && 151 Type.exists(copy[pl]) && typeof copy[pl] === 'object' 152 ) { 153 // Recursively compare two objects 154 del = this._minimizeSubObject(copy[pl], def[p], pre + '\t'); 155 if (del) { 156 // console.log(pre + "--> delete obj", p) 157 delete copy[pl]; 158 } else { 159 // console.log(pre + '|', 'keep obj') 160 // console.log('default:', def[p], 'copy:', copy[pl]) 161 deleteAll = false; 162 } 163 } else { 164 // console.log(pre + '|', 'keep', pl) 165 deleteAll = false; 166 } 167 } 168 } 169 } 170 if (deleteAll && Object.keys(def).length === 0 && Object.keys(copy).length !== 0) { 171 // If def is empty and copy is non-empty, we keep copy. 172 // This is the case if copy is filled with entries from an inherited element, 173 // like label is inherited from text. 174 deleteAll = false; 175 } 176 177 // console.log(pre + 'deleteAll', deleteAll) 178 return deleteAll; 179 }, 180 181 /** 182 * Eliminate default values given by {@link JXG.Options} from the attributes object. 183 * @param {Object} instance Attribute object of the element 184 * @param {Object} s Arbitrary number of objects <tt>instance</tt> will be compared to. Usually these are 185 * sub-objects of the {@link JXG.Board#options} structure. 186 * @returns {Object} Minimal attributes object 187 */ 188 minimizeObject: function (instance, s) { 189 var i, del, 190 def = {}, 191 copy = Type.deepCopy(instance), 192 defaults = []; 193 194 for (i = 1; i < arguments.length; i++) { 195 defaults.push(arguments[i]); 196 } 197 198 // First, take the generic GeometryElement options ('elements') 199 def = Type.deepCopy(def, JXG.Options.elements, true); 200 // Second, take the options supplied as parameters 201 for (i = defaults.length - 1; i >= 0; i--) { 202 def = Type.deepCopy(def, defaults[i], true); 203 } 204 205 // console.log('element', copy) 206 // console.log('default', def) 207 // "copy" is a copy of the attribute object of the element 208 del = this._minimizeSubObject(copy, def, ' '); 209 // console.log('del', del) 210 if (del === true) { 211 copy = {}; 212 } 213 214 /* 215 // Original 216 for (p in def) { 217 if (def.hasOwnProperty(p)) { 218 pl = p.toLowerCase(); 219 220 // Original. Does not work for gradient: null 221 // if (def[p] !== null && typeof def[p] !== "object" && def[p] === copy[pl]) { 222 // delete copy[pl]; 223 // } 224 } 225 } 226 */ 227 return copy; 228 }, 229 230 /** 231 * Prepare the attributes object for an element to be dumped as JavaScript or JessieCode code. 232 * @param {JXG.Board} board 233 * @param {JXG.GeometryElement} obj Geometry element which attributes object is generated 234 * @returns {Object} An attributes object. 235 */ 236 prepareAttributes: function (board, obj) { 237 var a, s, o; 238 239 o = JXG.Options[obj.elType] || {}; 240 // console.log('prepareAttributes', obj.id, obj.getAttributes(), o) 241 a = this.minimizeObject(obj.getAttributes(), o); 242 243 for (s in obj.subs) { 244 if (obj.subs.hasOwnProperty(s)) { 245 // console.log('sub', s) 246 a[s] = this.minimizeObject( 247 obj.subs[s].getAttributes(), 248 o[s], 249 JXG.Options[obj.subs[s].elType] || {} 250 ); 251 a[s].id = obj.subs[s].id; 252 a[s].name = obj.subs[s].name; 253 } 254 } 255 256 // Handle label separately 257 if (Type.exists(a.label)) { 258 o = JXG.Options.label || {}; 259 a.label = this.minimizeObject(a.label, o); 260 if (Type.isEmpty(a.label)) { 261 delete a.label; 262 } 263 } 264 265 // Handle layer if it still exists 266 if (Type.exists(a.layer)) { 267 o = JXG.Options.layer || {}; 268 if (a.layer === o[obj.elType]) { 269 delete a.layer; 270 } 271 } 272 // Handle draft = false separately, see options.js - JXG.Validator 273 if (Type.exists(a.draft) && a.draft === false) { 274 delete a.draft; 275 } 276 277 a.id = obj.id; 278 a.name = obj.name; 279 280 return a; 281 }, 282 283 setBoundingBox: function (methods, board, boardVarName) { 284 methods.push({ 285 obj: boardVarName, 286 method: "setBoundingBox", 287 params: [board.getBoundingBox(), board.keepaspectratio] 288 }); 289 290 return methods; 291 }, 292 293 /** 294 * Generate a store-able structure with all elements. This is used by {@link JXG.Dump#toJessie} and 295 * {@link JXG.Dump#toJavaScript} to generate the script. 296 * @param {JXG.Board} board 297 * @param {Boolean} [json=false] If a JavaScript object for JSON should be returned then do 298 * not enclose strings in quotes. 299 * @returns {Array} An array with all metadata necessary to save the construction. 300 * @see JXG.Dump#toJSON 301 * @see JXG.Dump#toJavaScript 302 * @see JXG.Dump#toJessie 303 */ 304 dump: function (board, json) { 305 var e, 306 obj, 307 element, 308 s, 309 props = [], 310 methods = [], 311 elementList = [], 312 len = board.objectsList.length; 313 314 this.addMarkers(board, "dumped", false); 315 316 for (e = 0; e < len; e++) { 317 obj = board.objectsList[e]; 318 element = {}; 319 320 if (!obj.dumped && obj.dump) { 321 element.type = obj.getType(); 322 element.parents = obj.getParents().slice(); 323 element.children = []; 324 325 // Extract coordinates of a point 326 if (element.type === "point" && element.parents[0] === 1) { 327 element.parents = element.parents.slice(1); 328 } 329 330 for (s = 0; s < element.parents.length; s++) { 331 if ( 332 !(json === true) && // This is needed in Dump.toJSON() 333 Type.isString(element.parents[s]) && 334 element.parents[s][0] !== "'" && 335 element.parents[s][0] !== '"' 336 ) { 337 element.parents[s] = '"' + element.parents[s] + '"'; 338 } else if (Type.isArray(element.parents[s])) { 339 element.parents[s] = "[" + element.parents[s].toString() + "]"; 340 } 341 } 342 343 element.attributes = this.prepareAttributes(board, obj); 344 if (element.type === "glider" && obj.onPolygon) { 345 props.push({ 346 obj: obj.id, 347 prop: "onPolygon", 348 val: true 349 }); 350 } 351 352 elementList.push(element); 353 } 354 } 355 356 this.deleteMarkers(board, 'dumped'); 357 358 return { 359 elements: elementList, 360 props: props, 361 methods: methods 362 }; 363 }, 364 365 /** 366 * Converts an array of different values into a parameter string that can be used by the code generators. 367 * @param {Array} a 368 * @param {function} converter A function that is used to transform the elements of <tt>a</tt>. Usually 369 * {@link JXG.toJSON} or {@link JXG.Dump.toJCAN} are used. 370 * @returns {String} 371 */ 372 arrayToParamStr: function (a, converter) { 373 var i, 374 s = []; 375 376 for (i = 0; i < a.length; i++) { 377 s.push(converter.call(this, a[i])); 378 } 379 380 return s.join(", "); 381 }, 382 383 /** 384 * Converts a JavaScript object into a JCAN (JessieCode Attribute Notation) string. 385 * @param {Object} obj A JavaScript object, functions will be ignored. 386 * @returns {String} The given object stored in a JCAN string. 387 */ 388 toJCAN: function (obj) { 389 var i, list, prop; 390 391 switch (typeof obj) { 392 case "object": 393 if (obj) { 394 list = []; 395 396 if (Type.isArray(obj)) { 397 for (i = 0; i < obj.length; i++) { 398 list.push(this.toJCAN(obj[i])); 399 } 400 401 return "[" + list.join(",") + "]"; 402 } 403 404 for (prop in obj) { 405 if (obj.hasOwnProperty(prop)) { 406 list.push(prop + ": " + this.toJCAN(obj[prop])); 407 } 408 } 409 410 return "<<" + list.join(", ") + ">> "; 411 } 412 return 'null'; 413 case "string": 414 return "'" + obj.replace(/\\/g, "\\\\").replace(/(["'])/g, "\\$1") + "'"; 415 case "number": 416 case "boolean": 417 return obj.toString(); 418 case "null": 419 return 'null'; 420 } 421 }, 422 423 /** 424 * Exports the construction in <tt>board</tt> to JessieCode. 425 * @param {JXG.Board} board 426 * @param {Boolean} [noAttributes=false] If true, output contains no attributes beside 'id' and 'name' 427 * @returns {String} The construction as JessieCode code 428 * @see JXG.Dump#dump 429 * @see JXG.Dump#toJSON 430 * @see JXG.Dump#toJavaScript 431 */ 432 toJessie: function (board, noAttributes) { 433 var i, a, 434 elements, 435 id, 436 dump = this.dump(board), 437 script = []; 438 439 dump.methods = this.setBoundingBox(dump.methods, board, "$board"); 440 441 elements = dump.elements; 442 // Delete unwanted attributes 443 if (noAttributes === true) { 444 for (i = 0; i < elements.length; i++) { 445 for (a in elements[i].attributes) { 446 if (elements[i].attributes.hasOwnProperty(a) && a !== 'id' && a !== 'name') { 447 delete elements[i].attributes[a]; 448 } 449 } 450 } 451 } 452 453 for (i = 0; i < elements.length; i++) { 454 if (elements[i].attributes.name.length > 0) { 455 script.push("// " + elements[i].attributes.name); 456 } 457 script.push( 458 "s" + i + " = " + elements[i].type + 459 "(" + elements[i].parents.join(", ") + ") " + 460 this.toJCAN(elements[i].attributes).replace(/\n/, "\\n") + ";" 461 ); 462 463 if (elements[i].type === 'axis') { 464 // Handle the case that remove[All]Ticks had been called. 465 id = elements[i].attributes.id; 466 if (board.objects[id].defaultTicks === null) { 467 script.push("s" + i + ".removeAllTicks();"); 468 } 469 } 470 script.push(""); 471 } 472 473 for (i = 0; i < dump.methods.length; i++) { 474 script.push( 475 dump.methods[i].obj + 476 "." + 477 dump.methods[i].method + 478 "(" + 479 this.arrayToParamStr(dump.methods[i].params, this.toJCAN) + 480 ");" 481 ); 482 script.push(""); 483 } 484 485 for (i = 0; i < dump.props.length; i++) { 486 script.push( 487 dump.props[i].obj + 488 "." + 489 dump.props[i].prop + 490 " = " + 491 this.toJCAN(dump.props[i].val) + 492 ";" 493 ); 494 script.push(""); 495 } 496 497 return script.join("\n"); 498 }, 499 500 /** 501 * Export the construction as JSON string. Wrapper of {@link JXG.Dump#dump}, i.e. 502 * a store-able structure with all elements. 503 * 504 * @param {JXG.Board} board 505 * @param {Boolean} [asObj=false] If false, return a JSON string, else return a JavaScript object. 506 * @returns {String} The construction as JSON string (or object) 507 * 508 * @see JXG.Dump#dump 509 * @see JXG.Dump#toJessie 510 * @see JXG.Dump#toJavaScript 511 */ 512 toJSON: function(board, asObj) { 513 var dump = this.dump(board, true), 514 i, el, c, 515 elements = dump.elements; 516 517 for (i = 0; i < elements.length; i++) { 518 elements[i].properties = {}; 519 elements[i].ancestors = []; 520 521 el = board.objects[elements[i].attributes.id]; 522 if (Type.exists(el)) { 523 for (c in el.childElements) { 524 if (el.childElements.hasOwnProperty(c) && el.childElements[c].dump) { 525 elements[i].children.push(c); 526 } 527 } 528 elements[i].children = Type.uniqueArray(elements[i].children); 529 530 // For JSON: draggable is draggable AND visible 531 elements[i].properties.isDraggable = el.isDraggable && el.visPropCalc.visible; 532 elements[i].properties.elType = el.elType; 533 534 //console.log(el.ancestors) 535 for (c in el.ancestors) { 536 if (el.ancestors.hasOwnProperty(c) && el.ancestors[c].dump) { 537 elements[i].ancestors.push(c); 538 } 539 } 540 elements[i].ancestors = Type.uniqueArray(elements[i].ancestors); 541 } 542 } 543 dump.userLog = board.userLog.slice(); 544 545 if (asObj === true) { 546 return dump; 547 } 548 return JSON.stringify(dump); 549 }, 550 551 /** 552 * Exports the construction in <tt>board</tt> to JavaScript. 553 * @param {JXG.Board} board 554 * @returns {String} The construction as JavaScript code 555 * @see JXG.Dump#dump 556 * @see JXG.Dump#toJSON 557 * @see JXG.Dump#toJessie 558 */ 559 toJavaScript: function (board) { 560 var i, 561 elements, 562 id, 563 dump = this.dump(board), 564 script = []; 565 566 dump.methods = this.setBoundingBox(dump.methods, board, 'board'); 567 568 elements = dump.elements; 569 570 for (i = 0; i < elements.length; i++) { 571 script.push( 572 'board.create("' + 573 elements[i].type + 574 '", [' + 575 elements[i].parents.join(", ") + 576 "], " + 577 Type.toJSON(elements[i].attributes) + 578 ");" 579 ); 580 581 if (elements[i].type === 'axis') { 582 // Handle the case that remove[All]Ticks had been called. 583 id = elements[i].attributes.id; 584 if (board.objects[id].defaultTicks === null) { 585 script.push( 586 'board.objects["' + 587 id + 588 '"].removeTicks(board.objects["' + 589 id + 590 '"].defaultTicks);' 591 ); 592 } 593 } 594 } 595 596 for (i = 0; i < dump.methods.length; i++) { 597 script.push( 598 dump.methods[i].obj + 599 "." + 600 dump.methods[i].method + 601 "(" + 602 this.arrayToParamStr(dump.methods[i].params, Type.toJSON) + 603 ");" 604 ); 605 script.push(""); 606 } 607 608 for (i = 0; i < dump.props.length; i++) { 609 script.push( 610 dump.props[i].obj + 611 "." + 612 dump.props[i].prop + 613 " = " + 614 Type.toJSON(dump.props[i].val) + 615 ";" 616 ); 617 script.push(""); 618 } 619 620 return script.join("\n"); 621 } 622 }; 623 624 export default JXG.Dump; 625