1 /*
  2     Copyright 2008-2013
  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 <http://www.gnu.org/licenses/>
 29     and <http://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 
 33 /*global JXG: true, define: true*/
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /* depends:
 37  jxg
 38  base/constants
 39  utils/type
 40  math/math
 41  math/geometry
 42  */
 43 
 44 define([
 45     'jxg', 'base/constants', 'utils/type'
 46 ], function (JXG, Const, Type) {
 47 
 48     "use strict";
 49 
 50     /**
 51      * Parser helper routines. The methods in here are for parsing expressions in Geonext Syntax.
 52      * @namespace
 53      */
 54     JXG.GeonextParser = {
 55         /**
 56          * Converts expression of the form <i>leftop^rightop</i> into <i>Math.pow(leftop,rightop)</i>.
 57          * @param {String} te Expression of the form <i>leftop^rightop</i>
 58          * @returns {String} Converted expression.
 59          */
 60         replacePow: function (te) {
 61             var count, pos, c,
 62                 leftop, rightop, pre, p, left, i, right, expr;
 63 
 64             // delete all whitespace immediately before and after all ^ operators
 65             te = te.replace(/(\s*)\^(\s*)/g, '^');
 66 
 67             //  Loop over all ^ operators
 68             i = te.indexOf('^');
 69             while (i >= 0) {
 70                 // left and right are the substrings before, resp. after the ^ character
 71                 left = te.slice(0, i);
 72                 right = te.slice(i + 1);
 73 
 74                 // If there is a ")" immediately before the ^ operator, it can be the end of a
 75                 // (i) term in parenthesis
 76                 // (ii) function call
 77                 // (iii) method  call
 78                 // In either case, first the corresponding opening parenthesis is searched.
 79                 // This is the case, when count==0
 80                 if (left.charAt(left.length - 1) === ')') {
 81                     count = 1;
 82                     pos = left.length - 2;
 83 
 84                     while (pos >= 0 && count > 0) {
 85                         c = left.charAt(pos);
 86                         if (c === ')') {
 87                             count++;
 88                         } else if (c === '(') {
 89                             count -= 1;
 90                         }
 91                         pos -= 1;
 92                     }
 93 
 94                     if (count === 0) {
 95                         // Now, we have found the opning parenthesis and we have to look
 96                         // if it is (i), or (ii), (iii).
 97                         leftop = '';
 98                         // Search for F or p.M before (...)^
 99                         pre = left.substring(0, pos + 1);
100                         p = pos;
101                         while (p >= 0 && pre.substr(p, 1).match(/([\w\.]+)/)) {
102                             leftop = RegExp.$1 + leftop;
103                             p -= 1;
104                         }
105                         leftop += left.substring(pos + 1, left.length);
106                         leftop = leftop.replace(/([\(\)\+\*\%\^\-\/\]\[])/g, '\\$1');
107                     } else {
108                         throw new Error("JSXGraph: Missing '(' in expression");
109                     }
110                 } else {
111                     // Otherwise, the operand has to be a constant (or variable).
112                     leftop = '[\\w\\.]+'; // former: \\w\\.
113                 }
114 
115                 // To the right of the ^ operator there also may be a function or method call
116                 // or a term in parenthesis. Alos, ere we search for the closing
117                 // parenthesis.
118                 if (right.match(/^([\w\.]*\()/)) {
119                     count = 1;
120                     pos = RegExp.$1.length;
121 
122                     while (pos < right.length && count > 0) {
123                         c = right.charAt(pos);
124 
125                         if (c === ')') {
126                             count -= 1;
127                         } else if (c === '(') {
128                             count += 1;
129                         }
130                         pos += 1;
131                     }
132 
133                     if (count === 0) {
134                         rightop = right.substring(0, pos);
135                         rightop = rightop.replace(/([\(\)\+\*\%\^\-\/\[\]])/g, '\\$1');
136                     } else {
137                         throw new Error("JSXGraph: Missing ')' in expression");
138                     }
139                 } else {
140                     // Otherwise, the operand has to be a constant (or variable).
141                     rightop = '[\\w\\.]+';
142                 }
143                 // Now, we have the two operands and replace ^ by JXG.Math.pow
144                 expr = new RegExp('(' + leftop + ')\\^(' + rightop + ')');
145                 //te = te.replace(expr, 'JXG.Math.pow($1,$2)');
146                 te = te.replace(expr, 'pow($1,$2)');
147                 i = te.indexOf('^');
148             }
149 
150             return te;
151         },
152 
153         /**
154          * Converts expression of the form <i>If(a,b,c)</i> into <i>(a)?(b):(c)/i>.
155          * @param {String} te Expression of the form <i>If(a,b,c)</i>
156          * @returns {String} Converted expression.
157          */
158         replaceIf: function (te) {
159             var left, right,
160                 i, pos, count, k1, k2, c, meat,
161                 s = '',
162                 first = null,
163                 second = null,
164                 third = null;
165 
166             i = te.indexOf('If(');
167             if (i < 0) {
168                 return te;
169             }
170 
171             // "" means not defined. Here, we replace it by 0
172             te = te.replace(/""/g, '0');
173             while (i >= 0) {
174                 left = te.slice(0, i);
175                 right = te.slice(i + 3);
176 
177                 // Search the end of the If() command and take out the meat
178                 count = 1;
179                 pos = 0;
180                 k1 = -1;
181                 k2 = -1;
182 
183                 while (pos < right.length && count > 0) {
184                     c = right.charAt(pos);
185 
186                     if (c === ')') {
187                         count -= 1;
188                     } else if (c === '(') {
189                         count += 1;
190                     } else if (c === ',' && count === 1) {
191                         if (k1 < 0) {
192                             // first komma
193                             k1 = pos;
194                         } else {
195                             // second komma
196                             k2 = pos;
197                         }
198                     }
199                     pos += 1;
200                 }
201                 meat = right.slice(0, pos - 1);
202                 right = right.slice(pos);
203 
204                 // Test the two kommas
205                 if (k1 < 0) {
206                     // , missing
207                     return '';
208                 }
209 
210                 if (k2 < 0) {
211                     // , missing
212                     return '';
213                 }
214 
215                 first = meat.slice(0, k1);
216                 second = meat.slice(k1 + 1, k2);
217                 third = meat.slice(k2 + 1);
218 
219                 // Recurse
220                 first = this.replaceIf(first);
221                 second = this.replaceIf(second);
222                 third = this.replaceIf(third);
223 
224                 s += left + '((' + first + ')?' + '(' + second + '):(' + third + '))';
225                 te = right;
226                 first = null;
227                 second = null;
228                 i = te.indexOf('If(');
229             }
230             s += right;
231             return s;
232         },
233 
234         /**
235          * Replace an element's name in terms by an element's id.
236          * @param {String} term Term containing names of elements.
237          * @param {JXG.Board} board Reference to the board the elements are on.
238          * @param {Boolean} [jc=false] If true, all id's will be surrounded by <tt>$('</tt> and <tt>')</tt>.
239          * @returns {String} The same string with names replaced by ids.
240          **/
241         replaceNameById: function (term, board, jc) {
242             var end, elName, el, i,
243                 pos = 0,
244                 funcs = ['X', 'Y', 'L', 'V'],
245 
246                 printId = function (id) {
247                     if (jc) {
248                         return '$(\'' + id + '\')';
249                     }
250 
251                     return id;
252                 };
253 
254             // Find X(el), Y(el), ...
255             // All functions declared in funcs
256             for (i = 0; i < funcs.length; i++) {
257                 pos = term.indexOf(funcs[i] + '(');
258 
259                 while (pos >= 0) {
260                     if (pos >= 0) {
261                         end = term.indexOf(')', pos + 2);
262                         if (end >= 0) {
263                             elName = term.slice(pos + 2, end);
264                             elName = elName.replace(/\\(['"])?/g, '$1');
265                             el = board.elementsByName[elName];
266 
267                             if (el) {
268                                 term = term.slice(0, pos + 2) + (jc ? '$(\'' : '') + printId(el.id) +  term.slice(end);
269                             }
270                         }
271                     }
272                     end = term.indexOf(')', pos + 2);
273                     pos = term.indexOf(funcs[i] + '(', end);
274                 }
275             }
276 
277             pos = term.indexOf('Dist(');
278             while (pos >= 0) {
279                 if (pos >= 0) {
280                     end = term.indexOf(',', pos + 5);
281                     if (end >= 0) {
282                         elName = term.slice(pos + 5, end);
283                         elName = elName.replace(/\\(['"])?/g, '$1');
284                         el = board.elementsByName[elName];
285 
286                         if (el) {
287                             term = term.slice(0, pos + 5) + printId(el.id) +  term.slice(end);
288                         }
289                     }
290                 }
291                 end = term.indexOf(',', pos + 5);
292                 pos = term.indexOf(',', end);
293                 end = term.indexOf(')', pos + 1);
294 
295                 if (end >= 0) {
296                     elName = term.slice(pos + 1, end);
297                     elName = elName.replace(/\\(['"])?/g, '$1');
298                     el = board.elementsByName[elName];
299 
300                     if (el) {
301                         term = term.slice(0, pos + 1) + printId(el.id) +  term.slice(end);
302                     }
303                 }
304                 end = term.indexOf(')', pos + 1);
305                 pos = term.indexOf('Dist(', end);
306             }
307 
308             funcs = ['Deg', 'Rad'];
309             for (i = 0; i < funcs.length; i++) {
310                 pos = term.indexOf(funcs[i] + '(');
311                 while (pos >= 0) {
312                     if (pos >= 0) {
313                         end = term.indexOf(',', pos + 4);
314                         if (end >= 0) {
315                             elName = term.slice(pos + 4, end);
316                             elName = elName.replace(/\\(['"])?/g, '$1');
317                             el = board.elementsByName[elName];
318 
319                             if (el) {
320                                 term = term.slice(0, pos + 4) + printId(el.id) +  term.slice(end);
321                             }
322                         }
323                     }
324 
325                     end = term.indexOf(',', pos + 4);
326                     pos = term.indexOf(',', end);
327                     end = term.indexOf(',', pos + 1);
328 
329                     if (end >= 0) {
330                         elName = term.slice(pos + 1, end);
331                         elName = elName.replace(/\\(['"])?/g, '$1');
332                         el = board.elementsByName[elName];
333 
334                         if (el) {
335                             term = term.slice(0, pos + 1) + printId(el.id) +  term.slice(end);
336                         }
337                     }
338 
339                     end = term.indexOf(',', pos + 1);
340                     pos = term.indexOf(',', end);
341                     end = term.indexOf(')', pos + 1);
342 
343                     if (end >= 0) {
344                         elName = term.slice(pos + 1, end);
345                         elName = elName.replace(/\\(['"])?/g, '$1');
346                         el = board.elementsByName[elName];
347                         if (el) {
348                             term = term.slice(0, pos + 1) + printId(el.id) +  term.slice(end);
349                         }
350                     }
351 
352                     end = term.indexOf(')', pos + 1);
353                     pos = term.indexOf(funcs[i] + '(', end);
354                 }
355             }
356 
357             return term;
358         },
359 
360         /**
361          * Replaces element ids in terms by element this.board.objects['id'].
362          * @param {String} term A GEONE<sub>x</sub>T function string with JSXGraph ids in it.
363          * @returns {String} The input string with element ids replaced by this.board.objects["id"].
364          **/
365         replaceIdByObj: function (term) {
366             // Search for expressions like "X(gi23)" or "Y(gi23A)" and convert them to objects['gi23'].X().
367             var expr = /(X|Y|L)\(([\w_]+)\)/g;
368             term = term.replace(expr, '$(\'$2\').$1()');
369 
370             expr = /(V)\(([\w_]+)\)/g;
371             term = term.replace(expr, '$(\'$2\').Value()');
372 
373             expr = /(Dist)\(([\w_]+),([\w_]+)\)/g;
374             term = term.replace(expr, 'dist($(\'$2\'), $(\'$3\'))');
375 
376             expr = /(Deg)\(([\w_]+),([ \w\[\w_]+),([\w_]+)\)/g;
377             term = term.replace(expr, 'deg($(\'$2\'),$(\'$3\'),$(\'$4\'))');
378 
379             // Search for Rad('gi23','gi24','gi25')
380             expr = /Rad\(([\w_]+),([\w_]+),([\w_]+)\)/g;
381             term = term.replace(expr, 'rad($(\'$1\'),$(\'$2\'),$(\'$3\'))');
382 
383             // it's ok, it will run through the jessiecode parser afterwards...
384             /*jslint regexp: true*/
385             expr = /N\((.+)\)/g;
386             term = term.replace(expr, '($1)');
387 
388             return term;
389         },
390 
391         /**
392          * Converts the given algebraic expression in GEONE<sub>x</sub>T syntax into an equivalent expression in JavaScript syntax.
393          * @param {String} term Expression in GEONExT syntax
394          * @param {JXG.Board} board
395          * @returns {String} Given expression translated to JavaScript.
396          */
397         geonext2JS: function (term, board) {
398             var expr, newterm, i,
399                 from = ['Abs', 'ACos', 'ASin', 'ATan', 'Ceil', 'Cos', 'Exp', 'Factorial', 'Floor',
400                     'Log', 'Max', 'Min', 'Random', 'Round', 'Sin', 'Sqrt', 'Tan', 'Trunc'],
401                 to =   ['abs', 'acos', 'asin', 'atan', 'ceil', 'cos',
402                     'exp', 'factorial', 'floor', 'log', 'max', 'min',
403                     'random', 'round', 'sin', 'sqrt', 'tan', 'ceil'];
404 
405             // Hacks, to enable not well formed XML, @see JXG.GeonextReader#replaceLessThan
406             term = term.replace(/</g, '<');
407             term = term.replace(/>/g, '>');
408             term = term.replace(/&/g, '&');
409 
410             // Umwandeln der GEONExT-Syntax in JavaScript-Syntax
411             newterm = term;
412             newterm = this.replaceNameById(newterm, board);
413             newterm = this.replaceIf(newterm);
414             // Exponentiations-Problem x^y -> Math(exp(x,y).
415             newterm = this.replacePow(newterm);
416             newterm = this.replaceIdByObj(newterm);
417 
418             for (i = 0; i < from.length; i++) {
419                 // sin -> Math.sin and asin -> Math.asin
420                 expr = new RegExp(['(\\W|^)(', from[i], ')'].join(''), 'ig');
421                 newterm = newterm.replace(expr, ['$1', to[i]].join(''));
422             }
423             newterm = newterm.replace(/True/g, 'true');
424             newterm = newterm.replace(/False/g, 'false');
425             newterm = newterm.replace(/fasle/g, 'false');
426             newterm = newterm.replace(/Pi/g, 'PI');
427             newterm = newterm.replace(/"/g, '\'');
428 
429             return newterm;
430         },
431 
432         /**
433          * Finds dependencies in a given term and resolves them by adding the
434          * dependent object to the found objects child elements.
435          * @param {JXG.GeometryElement} me Object depending on objects in given term.
436          * @param {String} term String containing dependencies for the given object.
437          * @param {JXG.Board} [board=me.board] Reference to a board
438          */
439         findDependencies: function (me, term, board) {
440             var elements, el, expr, elmask;
441 
442             if (!Type.exists(board)) {
443                 board = me.board;
444             }
445 
446             elements = board.elementsByName;
447 
448             for (el in elements) {
449                 if (elements.hasOwnProperty(el)) {
450                     if (el !== me.name) {
451                         if (elements[el].type === Const.OBJECT_TYPE_TEXT) {
452                             if (!elements[el].visProp.islabel) {
453                                 elmask = el.replace(/\[/g, '\\[');
454                                 elmask = elmask.replace(/\]/g, '\\]');
455 
456                                 // Searches (A), (A,B),(A,B,C)
457                                 expr = new RegExp("\\(([\\w\\[\\]'_ ]+,)*(" + elmask + ")(,[\\w\\[\\]'_ ]+)*\\)", 'g');
458 
459                                 if (term.search(expr) >= 0) {
460                                     elements[el].addChild(me);
461                                 }
462                             }
463                         } else {
464                             elmask = el.replace(/\[/g, '\\[');
465                             elmask = elmask.replace(/\]/g, '\\]');
466 
467                             // Searches (A), (A,B),(A,B,C)
468                             expr = new RegExp("\\(([\\w\\[\\]'_ ]+,)*(" + elmask + ")(,[\\w\\[\\]'_ ]+)*\\)", 'g');
469 
470                             if (term.search(expr) >= 0) {
471                                 elements[el].addChild(me);
472                             }
473                         }
474                     }
475                 }
476             }
477         },
478 
479         /**
480          * Converts the given algebraic expression in GEONE<sub>x</sub>T syntax into an equivalent expression in JessieCode syntax.
481          * @param {String} term Expression in GEONExT syntax
482          * @param {JXG.Board} board
483          * @returns {String} Given expression translated to JavaScript.
484          */
485         gxt2jc: function (term, board) {
486             var newterm,
487                 from = ['Sqrt'],
488                 to = ['sqrt'];
489 
490             // Hacks, to enable not well formed XML, @see JXG.GeonextReader#replaceLessThan
491             term = term.replace(/</g, '<');
492             term = term.replace(/>/g, '>');
493             term = term.replace(/&/g, '&');
494             newterm = term;
495             newterm = this.replaceNameById(newterm, board, true);
496             newterm = newterm.replace(/True/g, 'true');
497             newterm = newterm.replace(/False/g, 'false');
498             newterm = newterm.replace(/fasle/g, 'false');
499 
500             return newterm;
501         }
502     };
503 
504     return JXG.GeonextParser;
505 });
506