create changelog entry
[debian/openrocket] / core / src / net / sf / openrocket / simulation / customexpression / CustomExpression.java
1 package net.sf.openrocket.simulation.customexpression;
2
3 import java.util.List;
4 import java.util.regex.*;
5
6 import net.sf.openrocket.document.OpenRocketDocument;
7 import net.sf.openrocket.logging.LogHelper;
8 import net.sf.openrocket.simulation.FlightDataType;
9 import net.sf.openrocket.simulation.SimulationStatus;
10 import net.sf.openrocket.startup.Application;
11 import net.sf.openrocket.unit.FixedUnitGroup;
12 import net.sf.openrocket.unit.UnitGroup;
13 import net.sf.openrocket.util.ArrayList;
14 import de.congrace.exp4j.Calculable;
15 import de.congrace.exp4j.ExpressionBuilder;
16 import de.congrace.exp4j.UnknownFunctionException;
17 import de.congrace.exp4j.UnparsableExpressionException;
18 import de.congrace.exp4j.Variable;
19
20 /**
21  * Represents a single custom expression
22  * @author Richard Graham
23  *
24  */
25 public class CustomExpression implements Cloneable{
26         
27         private static final LogHelper log = Application.getLogger();
28         
29         private OpenRocketDocument doc;
30         private String name, symbol, unit;
31
32         protected String expression;
33         private ExpressionBuilder builder;
34         private List<CustomExpression> subExpressions = new ArrayList<CustomExpression>();
35         
36         public CustomExpression(OpenRocketDocument doc){
37                 this.doc = doc;
38                 
39                 setName("");
40                 setSymbol("");
41                 setUnit("");
42                 setExpression("");
43         }
44         
45         public CustomExpression(OpenRocketDocument doc, 
46                                                         String name, 
47                                                         String symbol, 
48                                                         String unit, 
49                                                         String expression) {
50                 this.doc = doc;
51                 
52                 setName(name);
53                 setSymbol(symbol);
54                 setUnit(unit);
55                 setExpression(expression);
56         }
57         
58         /*
59          * Sets the long name of this expression, e.g. 'Kinetic energy'  
60          */
61         public void setName(String name){
62                 this.name = name;
63         }
64         
65         /*
66          * Sets the string for the units of the result of this expression.
67          */
68         public void setUnit(String unit){
69                 this.unit = unit;
70         }
71         
72         /*
73          * Sets the symbol string. This is the short, locale independent symbol for this whole expression
74          */
75         public void setSymbol(String symbol){
76                 this.symbol = symbol;
77         }
78         
79         /*
80          * Sets the actual expression string for this expression
81          */
82         public void setExpression(String expression){
83                 
84                 // This is the expression as supplied
85                 this.expression = expression;
86                 
87                 // Replace any indexed variables
88                 expression = subTimeIndexes(expression);
89                 expression = subTimeRanges(expression);
90                 
91                 builder = new ExpressionBuilder(expression);
92                 for (String n : getAllSymbols()){
93                         builder.withVariable(new Variable(n));
94                 }
95                 for (CustomExpression exp : this.subExpressions){
96                         builder.withVariable(new Variable(exp.hash()));
97                 }
98         
99                 builder.withCustomFunctions(Functions.getInstance().getAllFunction());
100                 log.info("Built expression "+expression);
101         }
102         
103         /*
104          * Replaces expressions of the form:
105          *   a[x:y]  with a hash and creates an associated RangeExpression from x to y
106          */
107         private String subTimeRanges(String str){
108                 
109                 Pattern p = Pattern.compile(variableRegex()+"\\[[^\\]]*:.*?\\]");
110                 Matcher m = p.matcher(str);
111                 
112                 // for each match, make a new custom expression (in subExpressions) with a hashed name
113                 // and replace the expression and variable in the original expression string with [hash].
114                 while (m.find()){
115                         String match = m.group();
116                         
117                         int start = match.indexOf("[");
118                         int end = match.indexOf("]");
119                         int colon = match.indexOf(":");
120                         
121                         String startTime = match.substring(start+1, colon);
122                         String endTime = match.substring(colon+1, end);
123                         String variableType = match.substring(0, start);
124                         
125                         RangeExpression exp = new RangeExpression(doc, startTime, endTime, variableType);
126                         subExpressions.add( exp );
127                         str = str.replace(match, exp.hash());
128                 }
129                 return str;
130         }
131         
132         /*
133          * Replaces expressions of the form
134          *   a[x]    with a hash and creates an associated IndexExpression with x
135          */
136         private String subTimeIndexes(String str){
137                 
138                 // find any matches of the time-indexed variable notation, e.g. m[1.2] for mass at 1.2 sec
139                 Pattern p = Pattern.compile(variableRegex()+"\\[[^:]*?\\]");
140                 Matcher m = p.matcher(str);
141                                 
142                 // for each match, make a new custom expression (in subExpressions) with a hashed name
143                 // and replace the expression and variable in the original expression string with [hash].
144                 while (m.find()){
145                         String match = m.group();
146                         // just the index part (in the square brackets) :
147                         String indexText = match.substring(match.indexOf("[")+1, match.length()-1);
148                         // just the flight data type
149                         String typeText = match.substring(0, match.indexOf("[")); 
150                                                 
151                         // Do the replacement and add a corresponding new IndexExpression to the list
152                         IndexExpression exp = new IndexExpression(doc, indexText, typeText);
153                         subExpressions.add( exp );
154                         str = str.replace(match, exp.hash());
155                 }
156                 return str;
157         }
158         
159         /*
160          * Returns a string of the form (t|a| ... ) with all variable symbols available
161          * This is useful for regex evaluation
162          */
163         protected String variableRegex(){
164                 String regex = "(";
165                 for (String s : getAllSymbols()){
166                         regex = regex + s + "|";
167                 }
168                 regex = regex.substring(0, regex.length()-1) + ")";
169                 return regex;
170         }
171         
172         // get a list of all the names of all the available variables
173         protected ArrayList<String> getAllNames(){
174                 ArrayList<String> names = new ArrayList<String>();
175                 /*
176                 for (FlightDataType type : FlightDataType.ALL_TYPES)
177                         names.add(type.getName());
178
179                 if (doc != null){
180                         List<CustomExpression> expressions = doc.getCustomExpressions();
181                         for (CustomExpression exp : expressions ){
182                                 if (exp != this)
183                                         names.add(exp.getName());
184                         }
185                 }
186                 */
187                 for (FlightDataType type : doc.getFlightDataTypes()){
188                         String symb = type.getName();
189                         if (name == null) continue;
190                         
191                         if (!name.equals( this.getName() )){
192                                 names.add(symb);
193                         }
194                 }
195                 return names;
196         }
197         
198         // get a list of all the symbols of the available variables ignoring this one
199         protected ArrayList<String> getAllSymbols(){
200                 ArrayList<String> symbols = new ArrayList<String>();
201                 /*
202                 for (FlightDataType type : FlightDataType.ALL_TYPES)
203                         symbols.add(type.getSymbol());
204                 
205                 if (doc != null){
206                         for (CustomExpression exp : doc.getCustomExpressions() ){
207                                 if (exp != this)
208                                         symbols.add(exp.getSymbol());
209                         }
210                 }
211                 */
212                 for (FlightDataType type : doc.getFlightDataTypes()){
213                         String symb = type.getSymbol();                 
214                         if (!symb.equals( this.getSymbol() )){
215                                 symbols.add(symb);
216                         }
217                 }
218                 
219                 return symbols;
220         }
221         
222         public boolean checkSymbol(){
223                 if ("".equals(symbol.trim()))
224                         return false;
225                 
226                 // No bad characters
227                 for (char c : "0123456789.,()[]{}<>:#@%^&*$ ".toCharArray())
228                         if (symbol.indexOf(c) != -1 )
229                                 return false;
230                 
231                 // No operators (ignoring brackets)
232                 for (String s : Functions.AVAILABLE_OPERATORS.keySet()){
233                         if (symbol.equals(s.trim().replaceAll("\\(|\\)|\\]|\\[|:", "")))
234                                 return false;
235                 }
236                 
237                 // No already defined symbols
238                 ArrayList<String> symbols = getAllSymbols().clone();
239                 if (symbols.contains(symbol.trim())){
240                         int index = symbols.indexOf(symbol.trim());
241                         log.user("Symbol "+symbol+" already exists, found "+symbols.get(index));
242                         return false;
243                 }
244                 
245                 return true;
246         }
247         
248         public boolean checkName(){
249                 if ("".equals(name.trim()))
250                         return false;
251                 
252                 // No characters that could mess things up saving etc
253                 for (char c : ",()[]{}<>#$".toCharArray())
254                         if (name.indexOf(c) != -1 )
255                                 return false;
256                 
257                 ArrayList<String> names = getAllNames().clone();
258                 if (names.contains(name.trim())){
259                         int index = names.indexOf(name.trim());
260                         log.user("Name "+name+" already exists, found "+names.get(index));
261                         return false;
262                 }
263                 
264                 return true;
265         }
266         
267         // Currently no restrictions on unit
268         public boolean checkUnit(){
269                 return true;
270         }
271         
272         public boolean checkAll(){
273                 return checkUnit() && checkSymbol() && checkName() && checkExpression();
274         }
275         
276         public String getName(){
277                 return name;
278         }
279         
280         public String getSymbol(){
281                 return symbol;
282         }
283         
284         public String getUnit(){
285                 return unit;
286         }
287         
288         public String getExpressionString(){
289                 return expression;
290         }
291         
292         /**
293          * Performs a basic check to see if the current expression string is valid
294          * This includes checking for bad characters and balanced brackets and test
295          * building the expression.
296          */
297         public boolean checkExpression(){
298                 if ("".equals(expression.trim())){
299                         return false;
300                 }
301                 
302                 int round = 0, square = 0; // count of bracket openings
303                 for (char c : expression.toCharArray()){
304                         switch (c) {
305                                 case '(' : round++; break;
306                                 case ')' : round--; break;
307                                 case '[' : square++; break;
308                                 case ']' : square--; break;
309                                 case ':' : 
310                                         if (square <= 0){
311                                                 log.user(": found outside range expression");
312                                                 return false;
313                                         }
314                                         else break;
315                                 case '#' : return false;
316                                 case '$' : return false;
317                                 case '=' : return false;
318                         }
319                 }
320                 if (round != 0 || square != 0) {
321                         log.user("Expression has unballanced brackets");
322                         return false;
323                 }
324                 
325                 
326                 //// Define the available variables as empty
327                 // The built in data types
328                 /*
329                 for (FlightDataType type : FlightDataType.ALL_TYPES){
330                         builder.withVariable(new Variable(type.getSymbol()));
331                 }
332                 
333                 for (String symb : getAllSymbols()){
334                         builder.withVariable(new Variable(symb));
335                 }
336                 */
337                 for (FlightDataType type : doc.getFlightDataTypes()){
338                         builder.withVariable(new Variable(type.getSymbol()));
339                 }
340                 
341                 // Try to build
342                 try {
343                         builder.build();
344                 } catch (Exception e) {
345                         log.user("Custom expression " + this.toString() + " invalid : " + e.toString());
346                         return false;
347                 }
348                 
349                 
350                 // Otherwise, all OK
351                 return true;
352         }
353         
354         public Double evaluateDouble(SimulationStatus status){
355                 double result = evaluate(status).getDoubleValue();
356                 if (result == Double.NEGATIVE_INFINITY || result == Double.POSITIVE_INFINITY) result = Double.NaN;
357                 return result;
358         }
359         
360         /*
361          * Builds the expression, done automatically during evaluation. Logs any errors. Returns null in case of error.
362          */
363         protected Calculable buildExpression(){
364                 return buildExpression(builder);
365         }
366         
367         /*
368          * Builds a specified expression, log any errors and returns null in case of error.
369          */
370         protected Calculable buildExpression(ExpressionBuilder b){
371                 Calculable calc = null;
372                 try {
373                         calc = b.build();
374                 } catch (UnknownFunctionException e1) {
375                         log.user("Unknown function. Could not build custom expression "+this.toString());
376                         return null;
377                 } catch (UnparsableExpressionException e1) {
378                         log.user("Unparsable expression. Could not build custom expression "+this.toString()+". "+e1.getMessage());
379                         return null;
380                 }
381                 
382                 return calc;
383         }
384         
385         /*
386          * Evaluate the expression using the last variable values from the simulation status.
387          * Returns NaN on any error.
388          */
389         public Variable evaluate(SimulationStatus status){
390                 
391                 Calculable calc = buildExpression(builder);
392                 if (calc == null){
393                         return new Variable("Unknown");
394                 }
395                 
396                 // Evaluate any sub expressions and set associated variables in the calculable
397                 for (CustomExpression expr : this.subExpressions){
398                         calc.setVariable( expr.evaluate(status) );
399                 }
400                         
401                 // Set all the built-in variables. Strictly we surely won't need all of them
402                 // Going through and checking them to include only the ones used *might* give a speedup
403                 for (FlightDataType type : status.getFlightData().getTypes()){
404                         double value = status.getFlightData().getLast(type); 
405                         calc.setVariable( new Variable(type.getSymbol(), value ) );
406                 }
407                 
408                 double result = Double.NaN;
409                 try{
410                         result = calc.calculate().getDoubleValue();
411                 }
412                 catch (java.util.EmptyStackException e){
413                         log.user("Unable to calculate expression "+this.expression+" due to empty stack exception");
414                 }
415                         
416                 return new Variable(name, result);
417         }
418         
419         /*
420          * Returns the new flight data type corresponding to this calculated data
421          * If the unit matches a SI unit string then the datatype will have the corresponding unitgroup.
422          * Otherwise, a fixed unit group will be created
423          */
424         public FlightDataType getType(){
425                 
426                 
427                 UnitGroup ug = UnitGroup.SIUNITS.get(unit); 
428                 if ( ug == null ){
429                         log.debug("SI unit not found for "+unit+" in expression "+toString()+". Making a new fixed unit.");
430                         ug = new FixedUnitGroup(unit);
431                 }
432                 //UnitGroup ug = new FixedUnitGroup(unit);
433                 
434                 FlightDataType type = FlightDataType.getType(name, symbol, ug);
435                 
436                 //log.debug(this.getClass().getSimpleName()+" returned type "+type.getName()+" (" + type.getSymbol() + ")" );           
437                 
438                 return type;
439         }
440         
441         /*
442          * Add this expression to the document if valid and not in document already
443          */
444         public void addToDocument(){
445                 // Abort if exact expression already in
446                 List<CustomExpression> expressions = doc.getCustomExpressions();
447                 if ( !expressions.isEmpty() ) {
448                         // check if expression already exists
449                         if ( expressions.contains(this) ){
450                                 log.user("Expression already in document. This unit : "+this.getUnit()+", existing unit : "+expressions.get(0).getUnit());
451                                 return;
452                         }
453                 }
454                         
455                 if (this.checkAll()){
456                         log.user("Custom expression added to rocket document");
457                         doc.addCustomExpression( this );
458                 }
459         }
460         
461         /*
462          * Removes this expression from the document, replacing it with a given new expression
463          */
464         public void overwrite(CustomExpression newExpression){
465                 if (!doc.getCustomExpressions().contains(this)) 
466                         return;
467                 else {
468                         int index = doc.getCustomExpressions().indexOf(this);
469                         doc.getCustomExpressions().set(index, newExpression);
470                 }
471         }
472         
473         @Override
474         public String toString(){
475                 return "[Expression name="+this.name.toString()+ " expression=" + this.expression+" unit="+this.unit+"]";
476         }
477         
478         @Override
479         /*
480          * Clone method makes a deep copy of everything except the reference to the document.
481          * If you want to apply this to another simulation, set simulation manually after cloning.
482          * @see java.lang.Object#clone()
483          */
484         public Object clone() {
485               try {
486                   return super.clone();
487               }
488               catch( CloneNotSupportedException e )
489               {
490                   return new CustomExpression(  doc  , 
491                                 new String(this.getName()), 
492                                 new String(this.getSymbol()),
493                                 new String(this.getUnit()),
494                                 new String(this.getExpressionString()));
495               }
496         } 
497         
498         /*
499          * Returns a simple all upper case string hash code with a proceeding $ mark.
500          * Used for temporary substitution when evaluating index and range expressions.
501          */
502         public String hash(){
503                 Integer hashint = new Integer(this.getExpressionString().hashCode() + symbol.hashCode());
504                 String hash = "$";
505                 for (char c : hashint.toString().toCharArray()){
506                         if (c == '-') c = '0';
507                         char newc = (char) (c + 17);
508                         hash = hash + newc;
509                 }
510                 return hash;
511         }
512         
513         @Override
514         public boolean equals(Object obj){
515                 CustomExpression other = (CustomExpression) obj;
516                 
517                 return ( this.getName().equals( other.getName() ) && 
518                                  this.getSymbol().equals( other.getSymbol() ) &&
519                                  this.getExpressionString().equals( other.getExpressionString() ) &&
520                                  this.getUnit().equals( other.getUnit() )
521                                 );
522         }
523
524         @Override
525         public int hashCode() {
526                 return hash().hashCode();
527         }
528 }