edu.cmu.tetradapp.editor.GeneralizedExpressionEditor Maven / Gradle / Ivy
///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below. //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, //
// 2007, 2008, 2009, 2010, 2014, 2015, 2022 by Peter Spirtes, Richard //
// Scheines, Joseph Ramsey, and Clark Glymour. //
// //
// 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 2 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, write to the Free Software //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //
///////////////////////////////////////////////////////////////////////////////
package edu.cmu.tetradapp.editor;
import edu.cmu.tetrad.calculator.parser.ExpressionParser;
import edu.cmu.tetrad.graph.Node;
import edu.cmu.tetrad.graph.NodeType;
import edu.cmu.tetrad.sem.GeneralizedSemPm;
import edu.cmu.tetrad.util.MillisecondTimes;
import javax.swing.*;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.text.ParseException;
import java.util.List;
import java.util.*;
import java.util.prefs.Preferences;
/**
* Edits an expression for a node in the generalized SEM PM.
*
* @author josephramsey
*/
class GeneralizedExpressionEditor extends JComponent {
/**
* The color that selected text is being rendered. Either black or red.
*/
private Color color = Color.BLACK;
/**
* The start index of selected text.
*/
private int start;
/**
* The width of the selected text.
*/
private int stringWidth;
/**
* The time that the selectded text should be colored. (Must do this all indirectly using a thread because
* we cannot listen to the text pane.
*/
private long recolorTime = MillisecondTimes.timeMillis();
/**
* The text pane in which parsed text is rendered, typed, and colored.
*/
private final JTextPane expressionTextPane;
/**
* The generalized SEM PM that's being edited.
*/
private final GeneralizedSemPm semPm;
/**
* The latest parser that was used to parse the expression in expressionTextPane
. Needed to
* get the most up to date list of parameters.
*/
private ExpressionParser latestParser;
/**
* The node that's being edited, if a node is being edited; otherwise null.
*/
private Node node;
/**
* The error node for node
.
*/
private final Node errorNode;
/**
* The parameter that's being edited, if a parameter is being edited; otherwise null.
*/
private String parameter;
/**
* A display showing the equation or distribution that would result from taking the most recent parsable text
* from expressionTextPane
, writing the variable in front of it with = or ~, and appending the
* error term if it's not already in the expression.
*/
private final JTextArea resultTextPane;
/**
* The string described for resultTextPane
, without the "variable =" or "parameter ~".
*/
private String expressionString;
/**
* If a node is being edited, this is the list of variables other than the node and its parents.
*/
private final Set otherVariables;
/**
* A label listing the parameters referenced by expressionText
, according to the latest parser.
* This list changes as the expression is edited.
*/
private final JLabel referencedParametersLabel;
private final JCheckBox errorTermCheckBox;
//============================================CONSTRUCTORS==================================================//
/**
* Constructs an editor for a node in a generalized SEM PM.
*
* @param semPm The GeneralizedSemPm that's being edited, containing node
.
* @param node The node in semPm
that's being edited.
*/
public GeneralizedExpressionEditor(GeneralizedSemPm semPm, Node node) {
if (semPm == null) {
throw new NullPointerException("SEM PM must be provided.");
}
if (node == null) {
throw new NullPointerException("Node must be provided.");
}
if (!semPm.getNodes().contains(node)) {
throw new IllegalArgumentException("The node provided must be in the graph of the SEM PM.");
}
this.semPm = semPm;
this.node = node;
this.errorNode = semPm.getErrorNode(node);
this.expressionString = semPm.getNodeExpressionString(node);
StyleContext sc = new StyleContext();
DefaultStyledDocument doc = new DefaultStyledDocument(sc);
this.expressionTextPane = new JTextPane(doc);
this.resultTextPane = new JTextArea(semPm.getNodeExpressionString(node));
try {
// Add the text to the document
doc.insertString(0, semPm.getNodeExpressionString(node), null);
} catch (BadLocationException e) {
throw new RuntimeException("Couldn't construct editor", e);
}
this.otherVariables = new LinkedHashSet<>();
for (Node _node : semPm.getNodes()) {
if (semPm.getParents(node).contains(_node)) {
continue;
}
this.otherVariables.add(_node.getName());
}
ExpressionParser parser = new ExpressionParser(this.otherVariables, ExpressionParser.RestrictionType.MAY_NOT_CONTAIN);
latestParser = parser;
try {
parser.parseExpression(expressionString);
} catch (ParseException e) {
throw new RuntimeException("Cannot parser the stored expression.", e);
}
resultTextPane.setEditable(false);
resultTextPane.setBackground(Color.LIGHT_GRAY);
Map expressionsMap = this.getExpressionMap(semPm, node);
String[] expressionTokens = this.getExpressionTokens(semPm, node, expressionsMap);
JComboBox expressionsBox = new JComboBox(expressionTokens);
expressionsBox.setMaximumSize(expressionsBox.getPreferredSize());
JButton insertButton = new JButton("Insert");
insertButton.addActionListener(actionEvent -> {
String token = (String) expressionsBox.getSelectedItem();
String signature;
if ("-New Parameter-".equals(token)) {
signature = GeneralizedExpressionEditor.this.nextParameterName();
} else {
signature = expressionsMap.get(token);
}
while (signature.contains("%")) {
signature = signature.replaceFirst("%", GeneralizedExpressionEditor.this.nextParameterName());
}
expressionTextPane.replaceSelection(signature);
});
errorTermCheckBox = new JCheckBox("Automatically add error term");
errorTermCheckBox.setSelected(Preferences.userRoot().getBoolean("automaticallyAddErrorTerms", true));
errorTermCheckBox.addActionListener(event -> {
JCheckBox box = (JCheckBox) event.getSource();
Preferences.userRoot().putBoolean("automaticallyAddErrorTerms", box.isSelected());
});
Box b = Box.createVerticalBox();
Box b1 = Box.createHorizontalBox();
b1.add(new JLabel("Parents of " + node + ": " + this.niceParentsList(semPm.getParents(node))));
b1.add(Box.createHorizontalGlue());
b.add(b1);
b.add(Box.createVerticalStrut(5));
Box b2 = Box.createHorizontalBox();
referencedParametersLabel = new JLabel("Parameters: " + this.parameterString(parser));
b2.add(referencedParametersLabel);
b2.add(Box.createHorizontalGlue());
b.add(b2);
b.add(Box.createVerticalStrut(5));
Box b4 = Box.createHorizontalBox();
b4.add(new JLabel("Type Expression:"));
b4.add(Box.createHorizontalGlue());
b4.add(expressionsBox);
b4.add(Box.createHorizontalStrut(5));
b4.add(insertButton);
b.add(b4);
b.add(Box.createVerticalStrut(5));
JScrollPane expressionScroll = new JScrollPane(expressionTextPane);
expressionScroll.setPreferredSize(new Dimension(500, 50));
Box b5 = Box.createHorizontalBox();
b5.add(expressionScroll);
b.add(b5);
b.add(Box.createVerticalStrut(5));
Box b6 = Box.createHorizontalBox();
b6.add(new JLabel("Result:"));
b6.add(Box.createHorizontalGlue());
b.add(b6);
b.add(Box.createVerticalStrut(5));
JScrollPane resultScroll = new JScrollPane(resultTextPane);
resultScroll.setPreferredSize(new Dimension(500, 50));
Box b7 = Box.createHorizontalBox();
b7.add(resultScroll);
b.add(b7);
b.add(Box.createVerticalStrut(5));
Box b8 = Box.createHorizontalBox();
b8.add(errorTermCheckBox);
b8.add(Box.createHorizontalGlue());
b8.add(new JLabel("* Parameter appears in other expressions."));
b.add(b8);
this.setLayout(new BorderLayout());
this.add(b, BorderLayout.CENTER);
doc.addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent documentEvent) {
GeneralizedExpressionEditor.this.listen();
}
public void removeUpdate(DocumentEvent documentEvent) {
GeneralizedExpressionEditor.this.listen();
}
public void changedUpdate(DocumentEvent documentEvent) {
GeneralizedExpressionEditor.this.listen();
}
});
class ColorThread extends Thread {
private boolean stop;
@Override
public void run() {
StyledDocument document = (StyledDocument) expressionTextPane.getDocument();
Style red = expressionTextPane.addStyle("Red", null);
StyleConstants.setForeground(red, Color.RED);
Style black = expressionTextPane.addStyle("Black", null);
StyleConstants.setForeground(black, Color.BLACK);
while (!stop) {
if (MillisecondTimes.timeMillis() < recolorTime) {
continue;
}
if (color == Color.RED) {
document.setCharacterAttributes(start, stringWidth, expressionTextPane.getStyle("Red"), true);
} else if (color == Color.BLACK) {
document.setCharacterAttributes(start, stringWidth, expressionTextPane.getStyle("Black"), true);
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void scheduleStop() {
stop = true;
}
}
ColorThread thread = new ColorThread();
thread.start();
this.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent componentEvent) {
thread.scheduleStop();
}
});
expressionTextPane.setCaretPosition(expressionTextPane.getText().length());
// When the dialog closes, we want to make sure the expression gets parsed and set.
this.addAncestorListener(new AncestorListener() {
public void ancestorAdded(AncestorEvent ancestorEvent) {
}
public void ancestorRemoved(AncestorEvent ancestorEvent) {
GeneralizedExpressionEditor.this.listen();
}
public void ancestorMoved(AncestorEvent ancestorEvent) {
}
});
this.setFocusCycleRoot(true);
expressionTextPane.grabFocus();
}
/**
* Constructs an editor for a parameter in a generalized SEM PM.
*
* @param semPm The GeneralizedSemPm that's being edited, containing parameter
.
* @param parameter The parameter in semPm
that's being edited.
*/
public GeneralizedExpressionEditor(GeneralizedSemPm semPm, String parameter) {
if (semPm == null) {
throw new NullPointerException("SEM PM must be provided.");
}
if (parameter == null) {
throw new NullPointerException("Parameter must be provided.");
}
if (!semPm.getParameters().contains(parameter)) {
throw new IllegalArgumentException("The parameter provided must be in the graph of the SEM PM.");
}
this.semPm = semPm;
this.parameter = parameter;
errorNode = null;
expressionString = semPm.getParameterExpressionString(parameter);
StyleContext sc = new StyleContext();
DefaultStyledDocument doc = new DefaultStyledDocument(sc);
expressionTextPane = new JTextPane(doc);
resultTextPane = new JTextArea(semPm.getParameterExpressionString(parameter));
try {
try {
// Add the text to the document
doc.insertString(0, semPm.getParameterExpressionString(parameter), null);
} catch (BadLocationException ignored) {
}
} catch (Exception e) {
System.exit(1);
}
otherVariables = new LinkedHashSet<>();
for (Node _node : semPm.getNodes()) {
otherVariables.add(_node.getName());
}
ExpressionParser parser = new ExpressionParser(otherVariables, ExpressionParser.RestrictionType.MAY_NOT_CONTAIN);
try {
parser.parseExpression(expressionString);
} catch (ParseException e) {
throw new RuntimeException("Cannot parser the stored expression.", e);
}
resultTextPane.setEditable(false);
resultTextPane.setBackground(Color.LIGHT_GRAY);
Map expressionsMap = this.getExpressionMap(semPm, node);
String[] expressionTokens = this.getExpressionTokens(semPm, node, expressionsMap);
JComboBox expressionsBox = new JComboBox(expressionTokens);
expressionsBox.setMaximumSize(expressionsBox.getPreferredSize());
JButton insertButton = new JButton("Insert");
insertButton.addActionListener(actionEvent -> {
String token = (String) expressionsBox.getSelectedItem();
String signature;
if ("-New Parameter-".equals(token)) {
signature = GeneralizedExpressionEditor.this.nextParameterName();
} else {
signature = expressionsMap.get(token);
}
while (signature.contains("%")) {
signature = signature.replaceFirst("%", GeneralizedExpressionEditor.this.nextParameterName());
}
expressionTextPane.replaceSelection(signature);
});
errorTermCheckBox = new JCheckBox("Automatically add error term");
errorTermCheckBox.setSelected(Preferences.userRoot().getBoolean("automaticallyAddErrorTerms", true));
Box b = Box.createVerticalBox();
Box b2 = Box.createHorizontalBox();
referencedParametersLabel = new JLabel("Parameters: " + this.parameterString(parser));
b2.add(referencedParametersLabel);
b2.add(Box.createHorizontalGlue());
b.add(b2);
b.add(Box.createVerticalStrut(5));
Box b4 = Box.createHorizontalBox();
b4.add(new JLabel("Type Expression:"));
b4.add(Box.createHorizontalGlue());
b4.add(expressionsBox);
b4.add(Box.createHorizontalStrut(5));
b4.add(insertButton);
b.add(b4);
b.add(Box.createVerticalStrut(5));
JScrollPane expressionScroll = new JScrollPane(expressionTextPane);
expressionScroll.setPreferredSize(new Dimension(500, 50));
Box b5 = Box.createHorizontalBox();
b5.add(expressionScroll);
b.add(b5);
b.add(Box.createVerticalStrut(5));
Box b6 = Box.createHorizontalBox();
b6.add(new JLabel("Result:"));
b6.add(Box.createHorizontalGlue());
b.add(b6);
b.add(Box.createVerticalStrut(5));
JScrollPane resultScroll = new JScrollPane(resultTextPane);
resultScroll.setPreferredSize(new Dimension(500, 50));
Box b7 = Box.createHorizontalBox();
b7.add(resultScroll);
b.add(b7);
b.add(Box.createVerticalStrut(5));
Box b8 = Box.createHorizontalBox();
b8.add(errorTermCheckBox);
b8.add(Box.createHorizontalGlue());
b8.add(new JLabel("* Parameter appears in other expressions."));
b.add(b8);
this.setLayout(new BorderLayout());
this.add(b, BorderLayout.CENTER);
doc.addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent documentEvent) {
GeneralizedExpressionEditor.this.listen();
}
public void removeUpdate(DocumentEvent documentEvent) {
GeneralizedExpressionEditor.this.listen();
}
public void changedUpdate(DocumentEvent documentEvent) {
GeneralizedExpressionEditor.this.listen();
}
});
class ColorThread extends Thread {
private boolean stop;
@Override
public void run() {
StyledDocument document = (StyledDocument) expressionTextPane.getDocument();
Style red = expressionTextPane.addStyle("Red", null);
StyleConstants.setForeground(red, Color.RED);
Style black = expressionTextPane.addStyle("Black", null);
StyleConstants.setForeground(black, Color.BLACK);
while (!stop) {
if (MillisecondTimes.timeMillis() < recolorTime) {
continue;
}
if (color == Color.RED) {
document.setCharacterAttributes(start, stringWidth, expressionTextPane.getStyle("Red"), true);
} else if (color == Color.BLACK) {
document.setCharacterAttributes(start, stringWidth, expressionTextPane.getStyle("Black"), true);
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void scheduleStop() {
stop = true;
}
}
ColorThread thread = new ColorThread();
thread.start();
this.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent componentEvent) {
thread.scheduleStop();
}
});
this.setFocusCycleRoot(true);
expressionTextPane.grabFocus();
}
//==================================================PUBLIC METHODS==========================================//
/**
* @return the expression string (that is, the edited string, with error term appended if necessary, without
* "variable = " or "parameter ~". This is the final product of the editing.
*/
public String getExpressionString() {
return expressionString;
}
/**
* @return the next parameter in the sequence base1, base2,... that's not already being used by the SEM PM.
*/
private String nextParameterName() {
Set parameters = semPm.getParameters();
parameters.addAll(latestParser.getParameters());
System.out.println("*" + parameters);
System.out.println(latestParser.getParameters());
// Names should start with "1."
int i = 0;
loop:
while (true) {
String name = "b" + (++i);
for (String parameter : parameters) {
if (parameter.equals(name)) {
continue loop;
}
}
break;
}
return "b" + i;
}
//==================================================PRIVATE METHODS=========================================//
private String niceParentsList(List nodes) {
List nodeNames = new ArrayList<>();
for (Node node : nodes) {
nodeNames.add(node.getName());
}
List _nodeNames = new ArrayList<>(nodeNames);
StringBuilder buf = new StringBuilder();
for (int i = 0; i < _nodeNames.size(); i++) {
buf.append(_nodeNames.get(i));
if (i < nodeNames.size() - 1) {
buf.append(", ");
}
}
return buf.toString();
}
private void listen() {
String expressionString = expressionTextPane.getText();
String valueExpressionString;
ExpressionParser parser = new ExpressionParser(otherVariables, ExpressionParser.RestrictionType.MAY_NOT_CONTAIN);
try {
if (!"".equals(expressionString)) {
parser.parseExpression(expressionString);
}
this.color = Color.BLACK;
this.start = 0;
this.stringWidth = expressionString.length();
this.recolorTime = MillisecondTimes.timeMillis();
valueExpressionString = expressionString;
} catch (ParseException e) {
this.color = Color.RED;
this.start = e.getErrorOffset();
this.stringWidth = parser.getNextOffset() - e.getErrorOffset();
this.recolorTime = MillisecondTimes.timeMillis();
valueExpressionString = null;
}
if (valueExpressionString != null) {
String formula = this.expressionTextPane.getText();
if (this.node != null) {
if (this.node.getNodeType() != NodeType.ERROR && !formula.contains(this.errorNode.getName())
&& this.errorTermCheckBox.isSelected()) {
if (!formula.trim().endsWith("+") && !"".equals(formula)) {
formula += " + ";
}
formula += this.errorNode.getName();
}
this.expressionString = formula;
if (this.node.getNodeType() == NodeType.ERROR) {
this.resultTextPane.setText(this.node + " ~ " + formula);
} else {
this.resultTextPane.setText(this.node + " = " + formula);
}
} else if (this.parameter != null) {
this.expressionString = formula;
this.resultTextPane.setText(this.parameter + " ~ " + formula);
}
this.referencedParametersLabel.setText("Parameters: " + parameterString(parser));
}
this.latestParser = parser;
}
private String parameterString(ExpressionParser parser) {
Set parameters = new LinkedHashSet<>(parser.getParameters());
for (Node _node : this.semPm.getNodes()) {
parameters.remove(_node.getName());
}
List parametersList = new ArrayList<>(parameters);
StringBuilder buf = new StringBuilder();
for (int i = 0; i < parametersList.size(); i++) {
buf.append(parametersList.get(i));
Set referencingNodes = this.semPm.getReferencingNodes(parametersList.get(i));
referencingNodes.remove(this.node);
if (referencingNodes.size() > 0) {
buf.append("*");
}
if (i < parametersList.size() - 1) {
buf.append(", ");
}
}
return buf.toString();
}
private String[] getExpressionTokens(GeneralizedSemPm semPm, Node node, Map expressionsMap) {
List _tokens = new ArrayList<>(expressionsMap.keySet());
if (node != null) {
_tokens.add(semPm.getParents(node).size(), "-New Parameter-");
}
String[] expressionTokens = new String[_tokens.size()];
int i = -1;
for (String token : _tokens) {
expressionTokens[++i] = token;
}
return expressionTokens;
}
private Map getExpressionMap(GeneralizedSemPm semPm, Node node) {
// These are the expressions the user can choose from. The display form is on the left, and the template
// form is on the. Obviously you use a % for a new parameter. In case you want to change it.
String[][] expressions = {
{"+", " + "},
{"-", " - "},
{"*", " * "},
{"/", " / "},
{"^", "^"},
// {"+(a, b, ...)", "+(%, %)"},
// {"*(a, b, ...)", "*(%, %)"},
{"pow(a, b)", "pow(%, %)"},
{"sqrt(a)", "sqrt(%)"},
{"sin(a)", "sin(%)"},
{"cos(a)", "cos(%)"},
{"tan(a)", "tan(%)"},
{"asin(a)", "asin(%)"},
{"acos(a)", "acos(%)"},
{"atan(a)", "atan(%)"},
{"sinh(a)", "sinh(%)"},
{"tanh(a)", "tanh(%)"},
{"ln(a)", "ln(%)"},
{"log10(a)", "log10(%)"},
{"round(a)", "round(%)"},
{"ceil(a)", "ceil(%)"},
{"floor(a)", "floor(%)"},
{"abs(a)", "abs(%)"},
{"max(a, b, ...)", "max(%, %)"},
{"min(a, b, ...)", "min(%, %)"},
{"AND(a, b)", "AND(%, %)"},
{"OR(a, b)", "OR(%, %)"},
{"XOR(a, b)", "XOR(%, %)"},
{"IF(a, b, c)", "IF(%, %, %)"},
{"<", " < "},
{"<=", " <= "},
{"=", " = "},
{">=", " >= "},
{">", " > "},
{"Normal(mean, sd)", "Normal(%, %)"},
{"TruncNormal(mean, sd, low, high)", "TruncNormal(%, %, %, %)"},
{"Uniform(low, high)", "Uniform(%, %)"},
{"StudentT(df)", "StudentT(%)"},
{"Beta(alpha, beta)", "Beta(%, %)"},
{"Gamma(alpha, lambda)", "Gamma(%, %)"},
{"ChiSquare(df)", "ChiSquare(%)"},
{"Hyperbolic(alpha, beta)", "Hyperbolic(%, %)"},
{"Poisson(lambda)", "Poisson(%)"},
{"ExponentialPower(tau)", "ExponentialPower(%)"},
{"Exponential(lambda)", "ExponentialLambda(%)"},
{"VonMises(freedom)", "VonMises(%)"},
{"Split(a1, b1, a2, b2, ...)", "Split(%, %, %, %)"},
{"Discrete(a1, a2, a3, a4, ...)", "Discrete(%, %, %, %)"},
{"Indicator(p)", "Indicator(.5)"},
{"Mixture(a1, dist1, b1, dist2, ...)", "Mixture(%, Normal(%, %), %, Normal(%, %))"},
};
Map expressionsMap = new LinkedHashMap<>();
if (node != null) {
List parents = semPm.getParents(node);
for (Node parent : parents) {
expressionsMap.put(parent.getName(), parent.getName());
}
}
for (String[] expression : expressions) {
expressionsMap.put(expression[0], expression[1]);
}
return expressionsMap;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy