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