Merge commit '42b2e5ca519766e37ce6941ba4faecc9691cc403' into upstream
[debian/openrocket] / core / src / net / sf / openrocket / simulation / customexpression / CustomExpression.java
diff --git a/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java b/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java
new file mode 100644 (file)
index 0000000..e233def
--- /dev/null
@@ -0,0 +1,528 @@
+package net.sf.openrocket.simulation.customexpression;
+
+import java.util.List;
+import java.util.regex.*;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.simulation.FlightDataType;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.unit.FixedUnitGroup;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.ArrayList;
+import de.congrace.exp4j.Calculable;
+import de.congrace.exp4j.ExpressionBuilder;
+import de.congrace.exp4j.UnknownFunctionException;
+import de.congrace.exp4j.UnparsableExpressionException;
+import de.congrace.exp4j.Variable;
+
+/**
+ * Represents a single custom expression
+ * @author Richard Graham
+ *
+ */
+public class CustomExpression implements Cloneable{
+       
+       private static final LogHelper log = Application.getLogger();
+       
+       private OpenRocketDocument doc;
+       private String name, symbol, unit;
+
+       protected String expression;
+       private ExpressionBuilder builder;
+       private List<CustomExpression> subExpressions = new ArrayList<CustomExpression>();
+       
+       public CustomExpression(OpenRocketDocument doc){
+               this.doc = doc;
+               
+               setName("");
+               setSymbol("");
+               setUnit("");
+               setExpression("");
+       }
+       
+       public CustomExpression(OpenRocketDocument doc, 
+                                                       String name, 
+                                                       String symbol, 
+                                                       String unit, 
+                                                       String expression) {
+               this.doc = doc;
+               
+               setName(name);
+               setSymbol(symbol);
+               setUnit(unit);
+               setExpression(expression);
+       }
+       
+       /*
+        * Sets the long name of this expression, e.g. 'Kinetic energy'  
+        */
+       public void setName(String name){
+               this.name = name;
+       }
+       
+       /*
+        * Sets the string for the units of the result of this expression.
+        */
+       public void setUnit(String unit){
+               this.unit = unit;
+       }
+       
+       /*
+        * Sets the symbol string. This is the short, locale independent symbol for this whole expression
+        */
+       public void setSymbol(String symbol){
+               this.symbol = symbol;
+       }
+       
+       /*
+        * Sets the actual expression string for this expression
+        */
+       public void setExpression(String expression){
+               
+               // This is the expression as supplied
+               this.expression = expression;
+               
+               // Replace any indexed variables
+               expression = subTimeIndexes(expression);
+               expression = subTimeRanges(expression);
+               
+               builder = new ExpressionBuilder(expression);
+               for (String n : getAllSymbols()){
+                       builder.withVariable(new Variable(n));
+               }
+               for (CustomExpression exp : this.subExpressions){
+                       builder.withVariable(new Variable(exp.hash()));
+               }
+       
+               builder.withCustomFunctions(Functions.getInstance().getAllFunction());
+               log.info("Built expression "+expression);
+       }
+       
+       /*
+        * Replaces expressions of the form:
+        *   a[x:y]  with a hash and creates an associated RangeExpression from x to y
+        */
+       private String subTimeRanges(String str){
+               
+               Pattern p = Pattern.compile(variableRegex()+"\\[[^\\]]*:.*?\\]");
+               Matcher m = p.matcher(str);
+               
+               // for each match, make a new custom expression (in subExpressions) with a hashed name
+               // and replace the expression and variable in the original expression string with [hash].
+               while (m.find()){
+                       String match = m.group();
+                       
+                       int start = match.indexOf("[");
+                       int end = match.indexOf("]");
+                       int colon = match.indexOf(":");
+                       
+                       String startTime = match.substring(start+1, colon);
+                       String endTime = match.substring(colon+1, end);
+                       String variableType = match.substring(0, start);
+                       
+                       RangeExpression exp = new RangeExpression(doc, startTime, endTime, variableType);
+                       subExpressions.add( exp );
+                       str = str.replace(match, exp.hash());
+               }
+               return str;
+       }
+       
+       /*
+        * Replaces expressions of the form
+        *   a[x]    with a hash and creates an associated IndexExpression with x
+        */
+       private String subTimeIndexes(String str){
+               
+               // find any matches of the time-indexed variable notation, e.g. m[1.2] for mass at 1.2 sec
+               Pattern p = Pattern.compile(variableRegex()+"\\[[^:]*?\\]");
+               Matcher m = p.matcher(str);
+                               
+               // for each match, make a new custom expression (in subExpressions) with a hashed name
+               // and replace the expression and variable in the original expression string with [hash].
+               while (m.find()){
+                       String match = m.group();
+                       // just the index part (in the square brackets) :
+                       String indexText = match.substring(match.indexOf("[")+1, match.length()-1);
+                       // just the flight data type
+                       String typeText = match.substring(0, match.indexOf("[")); 
+                                               
+                       // Do the replacement and add a corresponding new IndexExpression to the list
+                       IndexExpression exp = new IndexExpression(doc, indexText, typeText);
+                       subExpressions.add( exp );
+                       str = str.replace(match, exp.hash());
+               }
+               return str;
+       }
+       
+       /*
+        * Returns a string of the form (t|a| ... ) with all variable symbols available
+        * This is useful for regex evaluation
+        */
+       protected String variableRegex(){
+               String regex = "(";
+               for (String s : getAllSymbols()){
+                       regex = regex + s + "|";
+               }
+               regex = regex.substring(0, regex.length()-1) + ")";
+               return regex;
+       }
+       
+       // get a list of all the names of all the available variables
+       protected ArrayList<String> getAllNames(){
+               ArrayList<String> names = new ArrayList<String>();
+               /*
+               for (FlightDataType type : FlightDataType.ALL_TYPES)
+                       names.add(type.getName());
+
+               if (doc != null){
+                       List<CustomExpression> expressions = doc.getCustomExpressions();
+                       for (CustomExpression exp : expressions ){
+                               if (exp != this)
+                                       names.add(exp.getName());
+                       }
+               }
+               */
+               for (FlightDataType type : doc.getFlightDataTypes()){
+                       String symb = type.getName();
+                       if (name == null) continue;
+                       
+                       if (!name.equals( this.getName() )){
+                               names.add(symb);
+                       }
+               }
+               return names;
+       }
+       
+       // get a list of all the symbols of the available variables ignoring this one
+       protected ArrayList<String> getAllSymbols(){
+               ArrayList<String> symbols = new ArrayList<String>();
+               /*
+               for (FlightDataType type : FlightDataType.ALL_TYPES)
+                       symbols.add(type.getSymbol());
+               
+               if (doc != null){
+                       for (CustomExpression exp : doc.getCustomExpressions() ){
+                               if (exp != this)
+                                       symbols.add(exp.getSymbol());
+                       }
+               }
+               */
+               for (FlightDataType type : doc.getFlightDataTypes()){
+                       String symb = type.getSymbol();                 
+                       if (!symb.equals( this.getSymbol() )){
+                               symbols.add(symb);
+                       }
+               }
+               
+               return symbols;
+       }
+       
+       public boolean checkSymbol(){
+               if ("".equals(symbol.trim()))
+                       return false;
+               
+               // No bad characters
+               for (char c : "0123456789.,()[]{}<>:#@%^&*$ ".toCharArray())
+                       if (symbol.indexOf(c) != -1 )
+                               return false;
+               
+               // No operators (ignoring brackets)
+               for (String s : Functions.AVAILABLE_OPERATORS.keySet()){
+                       if (symbol.equals(s.trim().replaceAll("\\(|\\)|\\]|\\[|:", "")))
+                               return false;
+               }
+               
+               // No already defined symbols
+               ArrayList<String> symbols = getAllSymbols().clone();
+               if (symbols.contains(symbol.trim())){
+                       int index = symbols.indexOf(symbol.trim());
+                       log.user("Symbol "+symbol+" already exists, found "+symbols.get(index));
+                       return false;
+               }
+               
+               return true;
+       }
+       
+       public boolean checkName(){
+               if ("".equals(name.trim()))
+                       return false;
+               
+               // No characters that could mess things up saving etc
+               for (char c : ",()[]{}<>#$".toCharArray())
+                       if (name.indexOf(c) != -1 )
+                               return false;
+               
+               ArrayList<String> names = getAllNames().clone();
+               if (names.contains(name.trim())){
+                       int index = names.indexOf(name.trim());
+                       log.user("Name "+name+" already exists, found "+names.get(index));
+                       return false;
+               }
+               
+               return true;
+       }
+       
+       // Currently no restrictions on unit
+       public boolean checkUnit(){
+               return true;
+       }
+       
+       public boolean checkAll(){
+               return checkUnit() && checkSymbol() && checkName() && checkExpression();
+       }
+       
+       public String getName(){
+               return name;
+       }
+       
+       public String getSymbol(){
+               return symbol;
+       }
+       
+       public String getUnit(){
+               return unit;
+       }
+       
+       public String getExpressionString(){
+               return expression;
+       }
+       
+       /**
+        * Performs a basic check to see if the current expression string is valid
+        * This includes checking for bad characters and balanced brackets and test
+        * building the expression.
+        */
+       public boolean checkExpression(){
+               if ("".equals(expression.trim())){
+                       return false;
+               }
+               
+               int round = 0, square = 0; // count of bracket openings
+               for (char c : expression.toCharArray()){
+                       switch (c) {
+                               case '(' : round++; break;
+                               case ')' : round--; break;
+                               case '[' : square++; break;
+                               case ']' : square--; break;
+                               case ':' : 
+                                       if (square <= 0){
+                                               log.user(": found outside range expression");
+                                               return false;
+                                       }
+                                       else break;
+                               case '#' : return false;
+                               case '$' : return false;
+                               case '=' : return false;
+                       }
+               }
+               if (round != 0 || square != 0) {
+                       log.user("Expression has unballanced brackets");
+                       return false;
+               }
+               
+               
+               //// Define the available variables as empty
+               // The built in data types
+               /*
+               for (FlightDataType type : FlightDataType.ALL_TYPES){
+                       builder.withVariable(new Variable(type.getSymbol()));
+               }
+               
+               for (String symb : getAllSymbols()){
+                       builder.withVariable(new Variable(symb));
+               }
+               */
+               for (FlightDataType type : doc.getFlightDataTypes()){
+                       builder.withVariable(new Variable(type.getSymbol()));
+               }
+               
+               // Try to build
+               try {
+                       builder.build();
+               } catch (Exception e) {
+                       log.user("Custom expression " + this.toString() + " invalid : " + e.toString());
+                       return false;
+               }
+               
+               
+               // Otherwise, all OK
+               return true;
+       }
+       
+       public Double evaluateDouble(SimulationStatus status){
+               double result = evaluate(status).getDoubleValue();
+               if (result == Double.NEGATIVE_INFINITY || result == Double.POSITIVE_INFINITY) result = Double.NaN;
+               return result;
+       }
+       
+       /*
+        * Builds the expression, done automatically during evaluation. Logs any errors. Returns null in case of error.
+        */
+       protected Calculable buildExpression(){
+               return buildExpression(builder);
+       }
+       
+       /*
+        * Builds a specified expression, log any errors and returns null in case of error.
+        */
+       protected Calculable buildExpression(ExpressionBuilder b){
+               Calculable calc = null;
+               try {
+                       calc = b.build();
+               } catch (UnknownFunctionException e1) {
+                       log.user("Unknown function. Could not build custom expression "+this.toString());
+                       return null;
+               } catch (UnparsableExpressionException e1) {
+                       log.user("Unparsable expression. Could not build custom expression "+this.toString()+". "+e1.getMessage());
+                       return null;
+               }
+               
+               return calc;
+       }
+       
+       /*
+        * Evaluate the expression using the last variable values from the simulation status.
+        * Returns NaN on any error.
+        */
+       public Variable evaluate(SimulationStatus status){
+               
+               Calculable calc = buildExpression(builder);
+               if (calc == null){
+                       return new Variable("Unknown");
+               }
+               
+               // Evaluate any sub expressions and set associated variables in the calculable
+               for (CustomExpression expr : this.subExpressions){
+                       calc.setVariable( expr.evaluate(status) );
+               }
+                       
+               // Set all the built-in variables. Strictly we surely won't need all of them
+               // Going through and checking them to include only the ones used *might* give a speedup
+               for (FlightDataType type : status.getFlightData().getTypes()){
+                       double value = status.getFlightData().getLast(type); 
+                       calc.setVariable( new Variable(type.getSymbol(), value ) );
+               }
+               
+               double result = Double.NaN;
+               try{
+                       result = calc.calculate().getDoubleValue();
+               }
+               catch (java.util.EmptyStackException e){
+                       log.user("Unable to calculate expression "+this.expression+" due to empty stack exception");
+               }
+                       
+               return new Variable(name, result);
+       }
+       
+       /*
+        * Returns the new flight data type corresponding to this calculated data
+        * If the unit matches a SI unit string then the datatype will have the corresponding unitgroup.
+        * Otherwise, a fixed unit group will be created
+        */
+       public FlightDataType getType(){
+               
+               
+               UnitGroup ug = UnitGroup.SIUNITS.get(unit); 
+               if ( ug == null ){
+                       log.debug("SI unit not found for "+unit+" in expression "+toString()+". Making a new fixed unit.");
+                       ug = new FixedUnitGroup(unit);
+               }
+               //UnitGroup ug = new FixedUnitGroup(unit);
+               
+               FlightDataType type = FlightDataType.getType(name, symbol, ug);
+               
+               //log.debug(this.getClass().getSimpleName()+" returned type "+type.getName()+" (" + type.getSymbol() + ")" );           
+               
+               return type;
+       }
+       
+       /*
+        * Add this expression to the document if valid and not in document already
+        */
+       public void addToDocument(){
+               // Abort if exact expression already in
+               List<CustomExpression> expressions = doc.getCustomExpressions();
+               if ( !expressions.isEmpty() ) {
+                       // check if expression already exists
+                       if ( expressions.contains(this) ){
+                               log.user("Expression already in document. This unit : "+this.getUnit()+", existing unit : "+expressions.get(0).getUnit());
+                               return;
+                       }
+               }
+                       
+               if (this.checkAll()){
+                       log.user("Custom expression added to rocket document");
+                       doc.addCustomExpression( this );
+               }
+       }
+       
+       /*
+        * Removes this expression from the document, replacing it with a given new expression
+        */
+       public void overwrite(CustomExpression newExpression){
+               if (!doc.getCustomExpressions().contains(this)) 
+                       return;
+               else {
+                       int index = doc.getCustomExpressions().indexOf(this);
+                       doc.getCustomExpressions().set(index, newExpression);
+               }
+       }
+       
+       @Override
+       public String toString(){
+               return "[Expression name="+this.name.toString()+ " expression=" + this.expression+" unit="+this.unit+"]";
+       }
+       
+       @Override
+       /*
+        * Clone method makes a deep copy of everything except the reference to the document.
+        * If you want to apply this to another simulation, set simulation manually after cloning.
+        * @see java.lang.Object#clone()
+        */
+       public Object clone() {
+             try {
+                 return super.clone();
+             }
+             catch( CloneNotSupportedException e )
+             {
+                 return new CustomExpression(  doc  , 
+                               new String(this.getName()), 
+                               new String(this.getSymbol()),
+                               new String(this.getUnit()),
+                               new String(this.getExpressionString()));
+             }
+       } 
+       
+       /*
+        * Returns a simple all upper case string hash code with a proceeding $ mark.
+        * Used for temporary substitution when evaluating index and range expressions.
+        */
+       public String hash(){
+               Integer hashint = new Integer(this.getExpressionString().hashCode() + symbol.hashCode());
+               String hash = "$";
+               for (char c : hashint.toString().toCharArray()){
+                       if (c == '-') c = '0';
+                       char newc = (char) (c + 17);
+                       hash = hash + newc;
+               }
+               return hash;
+       }
+       
+       @Override
+       public boolean equals(Object obj){
+               CustomExpression other = (CustomExpression) obj;
+               
+               return ( this.getName().equals( other.getName() ) && 
+                                this.getSymbol().equals( other.getSymbol() ) &&
+                                this.getExpressionString().equals( other.getExpressionString() ) &&
+                                this.getUnit().equals( other.getUnit() )
+                               );
+       }
+
+       @Override
+       public int hashCode() {
+               return hash().hashCode();
+       }
+}