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 118 * instance and def. 119 * @param {Object} instance 120 * @param {Object} def 121 * @param {String} pre 122 * @returns 123 */ 124 _minimizeSubObject: function(instance, def, pre) { 125 var p, pl, del, 126 deleteAll = true, 127 copy = instance; 128 129 for (p in def) { 130 if (def.hasOwnProperty(p)) { 131 pl = p.toLowerCase(); 132 // console.log(pre + 'Test', pl, typeof def[p]) 133 134 if ((def[p] === copy[pl]) || (!Type.exists(def[p]) && !Type.exists(copy[pl])) ) { 135 // Equality is determined for strings and numbers. 136 // For different arrays or objects, '===' is always false. 137 138 // console.log(pre + "\tdelete", p) 139 delete copy[pl]; 140 } else if (Type.isArray(def[p]) && Type.isArray(copy[pl])) { 141 // Compare two arrays 142 if (Type.cmpArrays(copy[pl], def[p])) { 143 // console.log(pre + "\t\tdelete ARR", p); 144 delete copy[pl]; 145 } else { 146 deleteAll = false; 147 } 148 } else { 149 if (Type.exists(def[p]) && typeof def[p] === 'object' && 150 Type.exists(copy[pl]) && typeof copy[pl] === 'object' 151 ) { 152 // Recursively compare two objects 153 del = this._minimizeSubObject(copy[pl], def[p], pre + '\t'); 154 if (del) { 155 // console.log(pre + "--> delete obj", p) 156 delete copy[pl]; 157 } else { 158 // console.log(pre + '|') 159 // console.log(def[p], copy[pl]) 160 deleteAll = false; 161 } 162 } else { 163 deleteAll = false; 164 } 165 } 166 } 167 } 168 if (deleteAll && Object.keys(def).length === 0 && Object.keys(copy).length !== 0) { 169 // If def is empty and copy is non-empty, we keep copy. 170 // This is the case if copy is filled with entries from an inherited element, 171 // like label is inherited from text. 172 deleteAll = false; 173 } 174 175 // console.log(pre + 'deleteAll', deleteAll) 176 return deleteAll; 177 }, 178 179 /** 180 * Eliminate default values given by {@link JXG.Options} from the attributes object. 181 * @param {Object} instance Attribute object of the element 182 * @param {Object} s Arbitrary number of objects <tt>instance</tt> will be compared to. Usually these are 183 * sub-objects of the {@link JXG.Board#options} structure. 184 * @returns {Object} Minimal attributes object 185 */ 186 minimizeObject: function (instance, s) { 187 var i, del, 188 def = {}, 189 copy = Type.deepCopy(instance), 190 defaults = []; 191 192 for (i = 1; i < arguments.length; i++) { 193 defaults.push(arguments[i]); 194 } 195 196 def = Type.deepCopy(def, JXG.Options.elements, true); 197 for (i = defaults.length - 1; i >= 0; i--) { 198 def = Type.deepCopy(def, defaults[i], true); 199 } 200 201 // console.log('element', copy) 202 // console.log('default', def) 203 del = this._minimizeSubObject(copy, def, ' '); 204 if (del === true) { 205 copy = {}; 206 } 207 208 /* 209 // Original 210 for (p in def) { 211 if (def.hasOwnProperty(p)) { 212 pl = p.toLowerCase(); 213 214 // Original. Does not work for gradient: null 215 // if (def[p] !== null && typeof def[p] !== "object" && def[p] === copy[pl]) { 216 // delete copy[pl]; 217 // } 218 } 219 } 220 */ 221 return copy; 222 }, 223 224 /** 225 * Prepare the attributes object for an element to be dumped as JavaScript or JessieCode code. 226 * @param {JXG.Board} board 227 * @param {JXG.GeometryElement} obj Geometry element which attributes object is generated 228 * @returns {Object} An attributes object. 229 */ 230 prepareAttributes: function (board, obj) { 231 var a, s, o; 232 233 o = JXG.Options[obj.elType] || {}; 234 a = this.minimizeObject(obj.getAttributes(), o); 235 236 for (s in obj.subs) { 237 if (obj.subs.hasOwnProperty(s)) { 238 a[s] = this.minimizeObject( 239 obj.subs[s].getAttributes(), 240 o[s], 241 JXG.Options[obj.subs[s].elType] || {} 242 ); 243 a[s].id = obj.subs[s].id; 244 a[s].name = obj.subs[s].name; 245 } 246 } 247 248 a.id = obj.id; 249 a.name = obj.name; 250 251 return a; 252 }, 253 254 setBoundingBox: function (methods, board, boardVarName) { 255 methods.push({ 256 obj: boardVarName, 257 method: "setBoundingBox", 258 params: [board.getBoundingBox(), board.keepaspectratio] 259 }); 260 261 return methods; 262 }, 263 264 /** 265 * Generate a save-able structure with all elements. This is used by {@link JXG.Dump#toJessie} and 266 * {@link JXG.Dump#toJavaScript} to generate the script. 267 * @param {JXG.Board} board 268 * @returns {Array} An array with all metadata necessary to save the construction. 269 */ 270 dump: function (board) { 271 var e, 272 obj, 273 element, 274 s, 275 props = [], 276 methods = [], 277 elementList = [], 278 len = board.objectsList.length; 279 280 this.addMarkers(board, "dumped", false); 281 282 for (e = 0; e < len; e++) { 283 obj = board.objectsList[e]; 284 element = {}; 285 286 if (!obj.dumped && obj.dump) { 287 element.type = obj.getType(); 288 element.parents = obj.getParents().slice(); 289 290 // Extract coordinates of a point 291 if (element.type === "point" && element.parents[0] === 1) { 292 element.parents = element.parents.slice(1); 293 } 294 295 for (s = 0; s < element.parents.length; s++) { 296 if ( 297 Type.isString(element.parents[s]) && 298 element.parents[s][0] !== "'" && 299 element.parents[s][0] !== '"' 300 ) { 301 element.parents[s] = '"' + element.parents[s] + '"'; 302 } else if (Type.isArray(element.parents[s])) { 303 element.parents[s] = "[" + element.parents[s].toString() + "]"; 304 } 305 } 306 307 element.attributes = this.prepareAttributes(board, obj); 308 if (element.type === "glider" && obj.onPolygon) { 309 props.push({ 310 obj: obj.id, 311 prop: "onPolygon", 312 val: true 313 }); 314 } 315 316 elementList.push(element); 317 } 318 } 319 320 this.deleteMarkers(board, 'dumped'); 321 322 return { 323 elements: elementList, 324 props: props, 325 methods: methods 326 }; 327 }, 328 329 /** 330 * Converts an array of different values into a parameter string that can be used by the code generators. 331 * @param {Array} a 332 * @param {function} converter A function that is used to transform the elements of <tt>a</tt>. Usually 333 * {@link JXG.toJSON} or {@link JXG.Dump.toJCAN} are used. 334 * @returns {String} 335 */ 336 arrayToParamStr: function (a, converter) { 337 var i, 338 s = []; 339 340 for (i = 0; i < a.length; i++) { 341 s.push(converter.call(this, a[i])); 342 } 343 344 return s.join(", "); 345 }, 346 347 /** 348 * Converts a JavaScript object into a JCAN (JessieCode Attribute Notation) string. 349 * @param {Object} obj A JavaScript object, functions will be ignored. 350 * @returns {String} The given object stored in a JCAN string. 351 */ 352 toJCAN: function (obj) { 353 var i, list, prop; 354 355 switch (typeof obj) { 356 case "object": 357 if (obj) { 358 list = []; 359 360 if (Type.isArray(obj)) { 361 for (i = 0; i < obj.length; i++) { 362 list.push(this.toJCAN(obj[i])); 363 } 364 365 return "[" + list.join(",") + "]"; 366 } 367 368 for (prop in obj) { 369 if (obj.hasOwnProperty(prop)) { 370 list.push(prop + ": " + this.toJCAN(obj[prop])); 371 } 372 } 373 374 return "<<" + list.join(", ") + ">> "; 375 } 376 return 'null'; 377 case "string": 378 return "'" + obj.replace(/\\/g, "\\\\").replace(/(["'])/g, "\\$1") + "'"; 379 case "number": 380 case "boolean": 381 return obj.toString(); 382 case "null": 383 return 'null'; 384 } 385 }, 386 387 /** 388 * Saves the construction in <tt>board</tt> to JessieCode. 389 * @param {JXG.Board} board 390 * @returns {String} JessieCode 391 */ 392 toJessie: function (board) { 393 var i, 394 elements, 395 id, 396 dump = this.dump(board), 397 script = []; 398 399 dump.methods = this.setBoundingBox(dump.methods, board, "$board"); 400 401 elements = dump.elements; 402 403 for (i = 0; i < elements.length; i++) { 404 if (elements[i].attributes.name.length > 0) { 405 script.push("// " + elements[i].attributes.name); 406 } 407 script.push( 408 "s" + i + " = " + elements[i].type + "(" + elements[i].parents.join(", ") + ") " + this.toJCAN(elements[i].attributes).replace(/\n/, "\\n") + ";" 409 ); 410 411 if (elements[i].type === 'axis') { 412 // Handle the case that remove[All]Ticks had been called. 413 id = elements[i].attributes.id; 414 if (board.objects[id].defaultTicks === null) { 415 script.push("s" + i + ".removeAllTicks();"); 416 } 417 } 418 script.push(""); 419 } 420 421 for (i = 0; i < dump.methods.length; i++) { 422 script.push( 423 dump.methods[i].obj + 424 "." + 425 dump.methods[i].method + 426 "(" + 427 this.arrayToParamStr(dump.methods[i].params, this.toJCAN) + 428 ");" 429 ); 430 script.push(""); 431 } 432 433 for (i = 0; i < dump.props.length; i++) { 434 script.push( 435 dump.props[i].obj + 436 "." + 437 dump.props[i].prop + 438 " = " + 439 this.toJCAN(dump.props[i].val) + 440 ";" 441 ); 442 script.push(""); 443 } 444 445 return script.join("\n"); 446 }, 447 448 /** 449 * Saves the construction in <tt>board</tt> to JavaScript. 450 * @param {JXG.Board} board 451 * @returns {String} JavaScript 452 */ 453 toJavaScript: function (board) { 454 var i, 455 elements, 456 id, 457 dump = this.dump(board), 458 script = []; 459 460 dump.methods = this.setBoundingBox(dump.methods, board, 'board'); 461 462 elements = dump.elements; 463 464 for (i = 0; i < elements.length; i++) { 465 script.push( 466 'board.create("' + 467 elements[i].type + 468 '", [' + 469 elements[i].parents.join(", ") + 470 "], " + 471 Type.toJSON(elements[i].attributes) + 472 ");" 473 ); 474 475 if (elements[i].type === 'axis') { 476 // Handle the case that remove[All]Ticks had been called. 477 id = elements[i].attributes.id; 478 if (board.objects[id].defaultTicks === null) { 479 script.push( 480 'board.objects["' + 481 id + 482 '"].removeTicks(board.objects["' + 483 id + 484 '"].defaultTicks);' 485 ); 486 } 487 } 488 } 489 490 for (i = 0; i < dump.methods.length; i++) { 491 script.push( 492 dump.methods[i].obj + 493 "." + 494 dump.methods[i].method + 495 "(" + 496 this.arrayToParamStr(dump.methods[i].params, Type.toJSON) + 497 ");" 498 ); 499 script.push(""); 500 } 501 502 for (i = 0; i < dump.props.length; i++) { 503 script.push( 504 dump.props[i].obj + 505 "." + 506 dump.props[i].prop + 507 " = " + 508 Type.toJSON(dump.props[i].val) + 509 ";" 510 ); 511 script.push(""); 512 } 513 514 return script.join("\n"); 515 } 516 }; 517 518 export default JXG.Dump; 519