package net.sf.openrocket.simulation.customexpression;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.logging.Markers;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.SimulationStatus;
import net.sf.openrocket.unit.FixedUnitGroup;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.ArrayList;
import net.sf.openrocket.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 Logger log = LoggerFactory.getLogger(CustomExpression.class);
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
subExpressions.clear();
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 (StringUtil.isEmpty(symbol)) {
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.info(Markers.USER_MARKER, "Symbol " + symbol + " already exists, found " + symbols.get(index));
return false;
}
return true;
}
public boolean checkName() {
if (StringUtil.isEmpty(name)) {
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.info(Markers.USER_MARKER, "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 (StringUtil.isEmpty(expression)) {
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.info(Markers.USER_MARKER, ": found outside range expression");
return false;
}
else
break;
case '#':
return false;
case '$':
return false;
case '=':
return false;
}
}
if (round != 0 || square != 0) {
log.info(Markers.USER_MARKER, "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.info(Markers.USER_MARKER, "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.info(Markers.USER_MARKER, "Unknown function. Could not build custom expression " + this.toString());
return null;
} catch (UnparsableExpressionException e1) {
log.info(Markers.USER_MARKER, "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.info(Markers.USER_MARKER, "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.info(Markers.USER_MARKER, "Expression already in document. This unit : " + this.getUnit() + ", existing unit : " + expressions.get(0).getUnit());
return;
}
}
if (this.checkAll()) {
log.info(Markers.USER_MARKER, "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);
log.debug("Overwriting custom expression already in document");
}
}
@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();
}
}