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