/* * Copyright (C) 2012 Jason Gedge <http://www.gedge.ca> * * This file is part of the OpGraph project. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * */ package ca.gedge.opgraph.nodes.math; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.math.BigDecimal; import java.text.ParseException; import java.util.ArrayList; import java.util.Properties; import java.util.logging.Logger; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SpinnerModel; import javax.swing.JFormattedTextField.AbstractFormatter; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.antlr.runtime.tree.CommonTreeNodeStream; import ca.gedge.opgraph.InputField; import ca.gedge.opgraph.OpContext; import ca.gedge.opgraph.OpNode; import ca.gedge.opgraph.OpNodeInfo; import ca.gedge.opgraph.OutputField; import ca.gedge.opgraph.app.GraphDocument; import ca.gedge.opgraph.app.GraphEditorModel; import ca.gedge.opgraph.app.edits.node.NodeSettingsEdit; import ca.gedge.opgraph.app.extensions.NodeSettings; import ca.gedge.opgraph.exceptions.ProcessingException; import ca.gedge.opgraph.nodes.math.parser.MathExpressionEval; import ca.gedge.opgraph.nodes.math.parser.MathExpressionLexer; import ca.gedge.opgraph.nodes.math.parser.MathExpressionParser; /** * A node that computes a value from a mathematical expression. */ @OpNodeInfo( name="Math Expression", description="Computes the value of a mathematical expression.", category="Math" ) public class MathExpressionNode extends OpNode implements NodeSettings { /** Logger */ private static final Logger LOGGER = Logger.getLogger(MathExpressionNode.class.getName()); /** Output field for the expression result */ public final OutputField RESULT_OUTPUT_FIELD = new OutputField("result", "expression result", true, Number.class); /** The math expression */ private String expression; /** The expression parser that parsed the expression when it was set */ private MathExpressionParser expressionParser; /** The parsed expression tree */ private Object expressionTree; /** The number of decimal places that are significant in the expression result */ private int significantDigits; /** The default number of decimal places that are significant the expression result */ private static final int DEFAULT_SIGNIFICANT_DIGITS = -1; /** * Constructs a math expression node with no expression. */ public MathExpressionNode() { this(null); } /** * Constructs a math expression node with a given expression. * * @param expression the math expression */ public MathExpressionNode(String expression) { setExpression(expression); setSignificantDigits(DEFAULT_SIGNIFICANT_DIGITS); putField(RESULT_OUTPUT_FIELD); putExtension(NodeSettings.class, this); } /** * Gets the math expression to evaluate. * * @return the math expression */ public String getExpression() { return expression; } /** * Sets the math expression to evaluate. * * @param expression the math expression */ public void setExpression(String expression) { this.expression = (expression == null ? "" : expression); final ANTLRStringStream stream = new ANTLRStringStream(this.expression); final MathExpressionLexer lexer = new MathExpressionLexer(stream); final CommonTokenStream tokens = new CommonTokenStream(lexer); expressionParser = new MathExpressionParser(tokens); try { expressionTree = expressionParser.prog().getTree(); if(expressionTree != null) LOGGER.info(((org.antlr.runtime.tree.CommonTree)expressionTree).toStringTree()); // Remove any input fields that correspond to non-existant variables final ArrayList<InputField> inputFieldsCopy = new ArrayList<InputField>(getInputFields()); for(InputField field : inputFieldsCopy) { if(!expressionParser.getVariables().contains(field.getKey())) removeField(field); } // Insert new input fields for(String variable : expressionParser.getVariables()) { if(getInputFieldWithKey(variable) == null) putField(new InputField(variable, "expression variable", false, true, Number.class)); } } catch(RecognitionException exc) { expressionParser = null; } } /** * Gets the number of decimal places that are significant in the expression * result. If negative, all decimal places are significant. If zero, the * result will always be an integer. * * @return the number of significant digits */ public int getSignificantDigits() { return significantDigits; } /** * Sets the number of decimal places that are significant in the expression * result. If negative, all decimal places are significant. If set to zero, * the result will always be an integer. * * @param significantDigits the number of significant digits. */ public void setSignificantDigits(int significantDigits) { this.significantDigits = significantDigits; } /** * Rounds a double to a specified number of significant digits past the * decimal place. Given a value <code>x</code>, the computed value * <code>x'</code> will satisfy: * <blockquote> * <code>Math.abs(x - x') < Math.pow(1, -significantDigits)</code> * </blockquote> * If the number of significant digits is negative then all decimal places * are significant, and the result is returned as-is. * * @param val the value * @param significantDigits the number of significant digits * * @return The value rounded to the given number of significant digits. * If the rounded value is an integer, an integral value is * returned, otherwise a decimal value is returned. */ private static Number roundToSignificantDigits(double val, int significantDigits) { // If negative significant digits, return value as-is if(significantDigits < 0) return val; // If zero significant digits, just return the value rounded if(significantDigits == 0) return Math.round(val); // Take advantage of the rounding facilities of BigDecimal final BigDecimal bigValue = new BigDecimal(val); final BigDecimal scaledBigValue = bigValue.setScale(significantDigits, BigDecimal.ROUND_HALF_UP); // Try to get an integer out of it Number retVal = scaledBigValue; try { retVal = scaledBigValue.toBigIntegerExact(); } catch(ArithmeticException exc) { } return retVal; } // // OpNode // @Override public void operate(OpContext context) throws ProcessingException { if(expressionParser == null || expressionTree == null) throw new NullPointerException("Math expression could not be parsed"); // final CommonTreeNodeStream stream = new CommonTreeNodeStream(expressionTree); final MathExpressionEval expressionEval = new MathExpressionEval(stream); // Add variable bindings for(String variable : expressionParser.getVariables()) expressionEval.putValue(variable, (Number)context.get(variable)); // Evaluate, and round to the number of significant decimal places try { expressionEval.prog(); final Number result = roundToSignificantDigits(expressionEval.getResult(), significantDigits); context.put(RESULT_OUTPUT_FIELD, result); } catch(RecognitionException exc) { throw new ProcessingException("Could not evaluate math expression", exc); } } // // NodeSettings // private static final String EXPRESSION_KEY = "expression"; private static final String SIGNIFICANT_DIGITS_KEY = "significantDigits"; /** * A formatter that checks whether or not a given math expression is valid. */ public static class MathExpressionFormatter extends AbstractFormatter { @Override public Object stringToValue(String text) throws ParseException { final ANTLRStringStream stream = new ANTLRStringStream(text); final MathExpressionLexer lexer = new MathExpressionLexer(stream); final CommonTokenStream tokens = new CommonTokenStream(lexer); final MathExpressionParser expressionParser = new MathExpressionParser(tokens); try { expressionParser.prog(); } catch(RecognitionException exc) { setEditValid(false); invalidEdit(); throw new ParseException(expressionParser.getErrorHeader(exc), 0); } if(expressionParser.getNumberOfSyntaxErrors() == 0) { setEditValid(true); } else { setEditValid(false); throw new ParseException("Could not parser expression: " + text, 0); } return text; } @Override public String valueToString(Object value) throws ParseException { return (value == null ? "" : value.toString()); } } /** * Constructs a math expression settings for the given node. */ public static class MathExpressionSettings extends JPanel { /** * Constructs this component for a given math expression node . * * @param node the math expression node */ public MathExpressionSettings(final MathExpressionNode node) { super(new GridBagLayout()); // A text field for the mathematical expression final JLabel expressionLabel = new JLabel("Expression: "); expressionLabel.setToolTipText("The mathematical expression (e.g., x+y)"); final JFormattedTextField expressionText = new JFormattedTextField(new MathExpressionFormatter()); expressionText.setValue(node.getExpression()); expressionText.addPropertyChangeListener("value", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent e) { final GraphDocument document = GraphEditorModel.getActiveDocument(); if(document != null) { final Properties settings = new Properties(); settings.put(EXPRESSION_KEY, e.getNewValue().toString()); document.getUndoSupport().postEdit(new NodeSettingsEdit(node, settings)); } } }); // An integer spinner for the number of significant digits in the result final JLabel significantDigitsLabel = new JLabel("Significant digits: "); significantDigitsLabel.setToolTipText("The number of significant decimal places to maintain in the result. If zero, the result will always be an integer. If negative, all decimal places are significant."); final SpinnerModel spinnerModel = new SpinnerNumberModel(node.getSignificantDigits(), -1, 100, 1); final JSpinner significantDigitsSpinner = new JSpinner(spinnerModel); significantDigitsSpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { final GraphDocument document = GraphEditorModel.getActiveDocument(); if(document != null) { final Properties settings = new Properties(); settings.put(EXPRESSION_KEY, spinnerModel.getValue()); document.getUndoSupport().postEdit(new NodeSettingsEdit(node, settings)); } } }); // Add components final GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.EAST; add(expressionLabel, gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.weightx = 1; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; add(expressionText, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.EAST; add(significantDigitsLabel, gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.weightx = 1; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; add(significantDigitsSpinner, gbc); } } // // NodeSettings // @Override public Component getComponent(GraphDocument document) { return new MathExpressionSettings(this); } @Override public Properties getSettings() { final Properties props = new Properties(); props.setProperty(EXPRESSION_KEY, getExpression()); props.setProperty(SIGNIFICANT_DIGITS_KEY, "" + getSignificantDigits()); return props; } @Override public void loadSettings(Properties properties) { if(properties.containsKey(EXPRESSION_KEY)) setExpression(properties.getProperty(EXPRESSION_KEY)); if(properties.containsKey(SIGNIFICANT_DIGITS_KEY)) setSignificantDigits(Integer.parseInt(properties.getProperty(SIGNIFICANT_DIGITS_KEY))); } }