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