--- /dev/null
+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();
+ }
+}