package gov.nasa.arc.mct.fastplot.settings.controls;
import gov.nasa.arc.mct.fastplot.bridge.PlotConstants;
import gov.nasa.arc.mct.fastplot.bridge.PlotterPlot;
import gov.nasa.arc.mct.fastplot.settings.PlotConfiguration;
import gov.nasa.arc.mct.fastplot.settings.PlotSettingsPanel;
import gov.nasa.arc.mct.fastplot.settings.PlotSettingsSubPanel;
import gov.nasa.arc.mct.fastplot.view.NumericTextField;
import gov.nasa.arc.mct.fastplot.view.PlotViewManifestation;
import gov.nasa.arc.mct.fastplot.view.TimeDuration;
import gov.nasa.arc.mct.fastplot.view.TimeSpanTextField;
import gov.nasa.arc.mct.fastplot.view.TimeTextField;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionListener;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.ResourceBundle;
import java.util.TimeZone;
import javax.swing.AbstractButton;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.text.MaskFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class PlotSettingsAxisGroup extends PlotSettingsPanel implements ActionListener {
private static final long serialVersionUID = -6810586939806488596L;
// Names, for automatic naming of nested Swing components
protected static final String MIN_SUFFIX = "Min";
protected static final String MAX_SUFFIX = "Max";
protected static final String SPAN_SUFFIX = "Span";
protected static final String AUTO_SUFFIX = "Auto";
protected static final String CURRENT_SUFFIX = "Current";
protected static final String MANUAL_SUFFIX = "Manual";
protected static final String VALUE_SUFFIX = "Value";
// Access bundle file where externalized strings are defined.
private static final ResourceBundle BUNDLE =
ResourceBundle.getBundle("gov.nasa.arc.mct.fastplot.view.Bundle");
// Logger
private final static Logger logger = LoggerFactory.getLogger(PlotSettingsAxisGroup.class);
// Other constants
private static final Double NONTIME_AXIS_SPAN_INIT_VALUE = Double.valueOf(30);
private static final int JTEXTFIELD_COLS = 8;
private static final int NUMERIC_TEXTFIELD_COLS1 = 12;
private static final String MANUAL_LABEL = BUNDLE.getString("Manual.label");
private static final int INTERCONTROL_HORIZONTAL_SPACING = 0;
private static final DecimalFormat PARENTHESIZED_LABEL_FORMAT = new DecimalFormat("###.###");
private static final int SPACE_BETWEEN_ROW_ITEMS = 3;
// Fields
private AxisBoundsPanel minControls;
private AxisBoundsPanel maxControls;
private AxisSpanCluster spanControls;
private String minText = "Min";
private String maxText = "Max";
private String title = "Axis";
private boolean temporal;
private boolean manualUpdates = true; // Does manual update with current value?
private Runnable autoControlsCallback = new Runnable() {
@Override
public void run() {
if (!temporal) {
minControls.auto.setEnabled(!maxControls.auto.isSelected());
minControls.autoValue.setEnabled(!maxControls.auto.isSelected());
maxControls.auto.setEnabled(!minControls.auto.isSelected());
maxControls.autoValue.setEnabled(!minControls.auto.isSelected());
if (!maxControls.auto.isSelected() && !minControls.auto.isSelected()) {
spanControls.setSpanValue(getValue(maxControls) - getValue(minControls));
}
if (!maxControls.auto.isSelected()) {
minControls.autoValue.setValue(getValue(maxControls) - spanControls.getSpanValue());
}
if (!minControls.auto.isSelected()) {
maxControls.autoValue.setValue(getValue(minControls) + spanControls.getSpanValue());
}
} else {
maxControls.updateAuto(getValue(minControls) + spanControls.getSpanValue());
}
}
};
public PlotSettingsAxisGroup(boolean temporal) {
this.temporal = temporal;
this.minControls = new AxisBoundsPanel(false);
this.maxControls = new AxisBoundsPanel(true);
this.spanControls = new AxisSpanCluster();
addSubPanel(minControls);
addSubPanel(maxControls);
addSubPanel(spanControls);
minControls.addCallback(autoControlsCallback);
maxControls.addCallback(autoControlsCallback);
}
public JPanel getMinControls() {
return minControls;
}
public JPanel getMaxControls() {
return maxControls;
}
public JPanel getSpanControls() {
return spanControls;
}
public String getMinText() {
return minText;
}
public String getMaxText() {
return maxText;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public void setName(String name) {
super.setName(name);
minControls.setName(name + MIN_SUFFIX);
maxControls.setName(name + MAX_SUFFIX);
spanControls.setName(name + SPAN_SUFFIX);
}
public abstract void setBounds(PlotConfiguration settings, double min, double max);
public abstract double getBoundMinimum(PlotConfiguration settings);
public abstract double getBoundMaximum(PlotConfiguration settings);
public abstract double getActualMinimum(PlotViewManifestation view);
public abstract double getActualMaximum(PlotViewManifestation view);
public void updateFrom(PlotViewManifestation view) {
if (temporal) {
minControls.updateCurrent(getActualMinimum(view));
maxControls.updateCurrent(getActualMaximum(view));
minControls.updateAuto((double) view.getCurrentMCTTime());
maxControls.updateAuto(getValue(minControls) + spanControls.getSpanValue());
if (!maxControls.auto.isSelected()) {
spanControls.setSpanValue(getValue(maxControls) - getValue(minControls));
}
} else {
minControls.currentValue.setValue(getActualMinimum(view));
maxControls.currentValue.setValue(getActualMaximum(view));
}
}
private double getValue(AxisBoundsPanel panel) {
try {
boolean maximum = panel == maxControls;
AxisBoundsPanel other = maximum ? minControls : maxControls;
if (temporal) {
TimeSpanTextField field = (TimeSpanTextField) spanControls.spanValue;
if (panel.auto.isSelected()) {
return maximum ? (field.getDurationInMillis() + getValue(other)) :
panel.autoValue.getValue(); // Now
} else if (panel.current.isSelected()) {
return panel.currentValue.getValue();
} else if (panel.manual.isSelected()) {
return ((ManualTimeEntryArea)panel.manualValue).getValue();
}
} else {
NumericTextField field = (NumericTextField) spanControls.spanValue;
if (panel.auto.isSelected()) {
return getValue(other) + (maximum ? 1 : -1) * field.getDoubleValue();
} else if (panel.current.isSelected()) {
return panel.currentValue.getValue();
} else if (panel.manual.isSelected()) {
field = (NumericTextField) panel.manualValue;
return field.getDoubleValue();
}
}
} catch (ParseException pe) {
logger.error("Parse exception in axis bounds panel.");
}
logger.error("Could not interpret user input from axis bounds panel.");
return Double.NaN;
}
// Non-time axis Maximums panel
class AxisBoundsPanel extends PlotSettingsSubPanel {
private static final long serialVersionUID = -768623994853270825L;
private JRadioButton current;
private ParenthesizedLabel currentValue;
private JRadioButton manual;
private JComponent manualValue;
private JRadioButton auto;
private ParenthesizedLabel autoValue;
private double cachedManualValue;
private boolean maximal;
public AxisBoundsPanel(boolean maximal) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
current = new JRadioButton(BUNDLE.getString( temporal ?
(maximal ? "CurrentMax.label" : "Currentmin.label") :
(maximal ? "CurrentLargestDatum.label" : "CurrentSmallestDatum.label")
), true);
currentValue = temporal ? new ParenthesizedTimeLabel(current) : new ParenthesizedNumericLabel(current);
manual = new JRadioButton( MANUAL_LABEL, false);
manualValue = temporal ? getTimeManualValue() : getNonTimeManualValue();
auto = new JRadioButton(BUNDLE.getString(temporal ?
(maximal ? "MinPlusSpan.label" : "Now.label" ) :
(maximal ? "MinPlusSpan.label" : "MaxMinusSpan.label")
), false);
autoValue = temporal ? new ParenthesizedTimeLabel(current) : new ParenthesizedNumericLabel(auto);
ButtonGroup maximumsGroup = new ButtonGroup();
maximumsGroup.add(manual);
maximumsGroup.add(current);
maximumsGroup.add(auto);
manual.addActionListener(this);
current.addActionListener(this);
auto.addActionListener(this);
// Layout
add(createMultiItemRow(current, currentValue));
add(createMultiItemRow(manual, manualValue) );
add(createMultiItemRow(auto, autoValue) );
// Note the maximality
this.maximal = maximal;
//TODO: Tooltips, instrumentation
}
private JComponent getTimeManualValue() {
GregorianCalendar calendar = new GregorianCalendar();
ManualTimeEntryArea entryArea = new ManualTimeEntryArea(calendar);
entryArea.addFocusListener(this);
entryArea.addActionListener(this);
return entryArea;
}
private JComponent getNonTimeManualValue() {
DecimalFormat format = new DecimalFormat("###.######");
format.setParseIntegerOnly(false);
manualValue = new NumericTextField(NUMERIC_TEXTFIELD_COLS1, format);
((NumericTextField)manualValue).addFocusListener(this);
((NumericTextField)manualValue).addActionListener(this);
((JTextField)manualValue).setColumns(JTEXTFIELD_COLS);
return manualValue;
}
public void updateCurrent(double value) {
currentValue.setValue(value);
if (temporal && manualUpdates) {
updateManual(value);
}
}
public void updateManual(double value) {
if (temporal) ((ManualTimeEntryArea) manualValue).setValue(value);
else ((NumericTextField) manualValue).setValue(value);
}
public void updateAuto(double value) {
autoValue.setValue(value);
}
public void addActionListener(ActionListener actionListener) {
current.addActionListener(actionListener);
manual.addActionListener(actionListener);
auto.addActionListener(actionListener);
}
public void removeActionListener(ActionListener actionListener) {
current.removeActionListener(actionListener);
manual.removeActionListener(actionListener);
auto.removeActionListener(actionListener);
}
@Override
public void populate(PlotConfiguration settings) {
// All handled in the span control - nothing to do here
}
@Override
public void reset(PlotConfiguration settings, boolean hard) {
if (temporal) {
if (hard) {
current.setSelected(true);
manualUpdates = true;
} else {
if (maximal && !auto.isSelected()) {
spanControls.setSpanValue(getValue(maxControls) - getValue(minControls));
}
manualUpdates &= !manual.isSelected();
}
} else {
NumericTextField field = (NumericTextField) manualValue;
if (hard) {
manual.setSelected(true);
cachedManualValue = maximal ? getBoundMaximum(settings) : getBoundMinimum(settings);
field.setValue(cachedManualValue);
}
// Prevent empty manual value field
if (field.getText().isEmpty()) {
field.setValue(cachedManualValue);
}
autoControlsCallback.run(); // Update enabled state
}
}
@Override
public boolean isDirty() {
try {
return !(temporal ? current : manual).isSelected() ||
(temporal ? false :
((NumericTextField) manualValue).getDoubleValue() != cachedManualValue
);
} catch (ParseException pe) {
return false;
}
}
@Override
public boolean isValidated() {
return true;
}
@Override
public void setName(String name) {
auto.setName(name + AUTO_SUFFIX);
current.setName(name + CURRENT_SUFFIX);
manual.setName(name + MANUAL_SUFFIX);
autoValue.setName(name + AUTO_SUFFIX + VALUE_SUFFIX);
currentValue.setName(name + CURRENT_SUFFIX + VALUE_SUFFIX);
manualValue.setName(name + MANUAL_SUFFIX + VALUE_SUFFIX);
}
}
// Panel holding span controls
class AxisSpanCluster extends PlotSettingsSubPanel {
private static final long serialVersionUID = -3947426156383446643L;
private JTextField spanValue;
private JLabel spanTag;
public AxisSpanCluster() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
spanTag = new JLabel(BUNDLE.getString("SpanTextField.label"));
// Create a text field with a ddd/hh:mm:ss format for time
spanValue = temporal ? getTimeSpanTextFieldFormat() : getNonTimeTextField();
spanValue.addActionListener(this);
spanValue.addFocusListener(this);
add(spanTag);
add(Box.createHorizontalStrut(INTERCONTROL_HORIZONTAL_SPACING));
add(spanValue);
if (temporal) {
add(Box.createHorizontalStrut(INTERCONTROL_HORIZONTAL_SPACING + 3));
add(new JLabel(BUNDLE.getString("YearSpan")));
add(Box.createHorizontalStrut(INTERCONTROL_HORIZONTAL_SPACING + 3));
add(((TimeSpanTextField)spanValue).getYearSpanValue());
}
// Instrument
spanTag.setName("spanTag");
}
JTextField getNonTimeTextField() {
NumericTextField nonTimeSpanValue = new NumericTextField(NUMERIC_TEXTFIELD_COLS1, PARENTHESIZED_LABEL_FORMAT);
nonTimeSpanValue.setColumns(JTEXTFIELD_COLS);
nonTimeSpanValue.setValue(NONTIME_AXIS_SPAN_INIT_VALUE);
return nonTimeSpanValue;
}
JTextField getTimeSpanTextFieldFormat() {
MaskFormatter formatter = null;
try {
formatter = new MaskFormatter("###/##:##:##");
formatter.setPlaceholderCharacter('0');
} catch (ParseException e) {
logger.error("Error in creating a mask formatter", e);
}
return new TimeSpanTextField(formatter);
}
public void setName(String name) {
super.setName(name);
spanValue.setName(name + VALUE_SUFFIX);
}
public double getSpanValue() {
try {
if (temporal) {
TimeSpanTextField field = (TimeSpanTextField) spanValue;
return field.getDurationInMillis();
} else {
NumericTextField field = (NumericTextField) spanValue;
return field.getDoubleValue();
}
} catch (ParseException pe) {
return Double.NaN;
}
}
public void setSpanValue(double value) {
if (value > 0) {
if (temporal) {
TimeSpanTextField field = (TimeSpanTextField) spanValue;
field.setTime(new TimeDuration((long)value));
} else {
NumericTextField field = (NumericTextField) spanValue;
field.setValue(value);
}
}
}
@Override
public void populate(PlotConfiguration settings) {
PlotSettingsAxisGroup.this.setBounds(settings, getValue(minControls), getValue(maxControls));
}
@Override
public void reset(PlotConfiguration settings, boolean hard) {
if (temporal) {
spanValue.setEnabled(maxControls.auto.isSelected());
} else {
spanValue.setEnabled(minControls.auto.isSelected() || maxControls.auto.isSelected());
NumericTextField field = (NumericTextField) spanValue;
field.setValue(getBoundMaximum(settings) - getBoundMinimum(settings));
}
}
@Override
public boolean isDirty() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isValidated() {
return getValue(maxControls) > getValue(minControls);
}
}
private class ManualTimeEntryArea extends JPanel {
private static final long serialVersionUID = 5096738756047815979L;
private JComboBox yearBox;
private TimeTextField timeField;
public ManualTimeEntryArea(Calendar calendar) {
Integer[] years = new Integer[10];
for (int i = 0 ; i < 10; i++ ) {
years[i] = new Integer(calendar.get(Calendar.YEAR) - i);
}
yearBox = new JComboBox(years);
yearBox.setEditable(true);
MaskFormatter formatter = null;
try {
formatter = new MaskFormatter("###/##:##:##");
formatter.setPlaceholderCharacter('0');
} catch (ParseException e) {
logger.error("Parse error in creating time field", e);
}
timeField = new TimeTextField(formatter, calendar.get(Calendar.YEAR));
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
add(timeField);
add(yearBox);
yearBox.setPreferredSize(new Dimension(60,timeField.getPreferredSize().height - 1));
}
public void addActionListener(ActionListener al) {
timeField.addActionListener(al);
yearBox.addActionListener(al);
}
public void addFocusListener(FocusListener fl) {
timeField.addFocusListener(fl);
yearBox.addFocusListener(fl);
}
public void setValue(double d) {
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeZone(TimeZone.getTimeZone(PlotConstants.DEFAULT_TIME_ZONE));
cal.setTimeInMillis((long) d);
int year = cal.get(Calendar.YEAR);
timeField.setTime(cal);
yearBox.getModel().setSelectedItem(Integer.valueOf(year));
yearBox.revalidate();
}
public long getValue() {
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(timeField.getValueInMillis());
cal.set(Calendar.YEAR, (Integer) yearBox.getSelectedItem());
return cal.getTimeInMillis();
}
}
private abstract class ParenthesizedLabel extends JLabel {
private static final long serialVersionUID = 4908562204337928432L;
abstract void setValue(Double value);
abstract double getValue();
}
private class ParenthesizedTimeLabel extends ParenthesizedLabel {
private static final long serialVersionUID = -6004293775277749905L;
private GregorianCalendar timeInMillis = new GregorianCalendar();
private JRadioButton companionButton;
// TODO: This should be the user-selected date
private DateFormat dateFormat = new SimpleDateFormat(PlotConstants.DEFAULT_TIME_FORMAT);
public ParenthesizedTimeLabel(JRadioButton button) {
dateFormat.setTimeZone(TimeZone.getTimeZone(PlotConstants.DEFAULT_TIME_ZONE));
companionButton = button;
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
companionButton.setSelected(true);
companionButton.requestFocusInWindow();
//TODO : Fire callbacks!
}
});
}
public void setValue (Double value) {
timeInMillis.setTimeInMillis(value.longValue());
setText("(" + dateFormat.format(timeInMillis.getTime()) + " "
+ timeInMillis.get(Calendar.YEAR) + ")");
}
public double getValue() {
return (double) timeInMillis.getTimeInMillis();
}
}
private class ParenthesizedNumericLabel extends ParenthesizedLabel {
private static final long serialVersionUID = 3403375470853249483L;
private JRadioButton companionButton;
public ParenthesizedNumericLabel(JRadioButton button) {
companionButton = button;
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
companionButton.setSelected(true);
companionButton.requestFocusInWindow();
//TODO: Fire callbacks!
}
});
}
public double getValue() {
String data = getText();
if (data == null) {
return Double.NaN;
}
if (data.length() < 3) {
logger.error("Numeric label in plot settings contained invalid content [" + data + "]");
return Double.NaN;
}
Double result = null;
try {
result = Double.parseDouble(data.substring(1, data.length() - 1));
} catch(NumberFormatException e) {
logger.error("Could not parse numeric value from ["+ data.substring(1, data.length() - 1) + "]");
}
return result;
}
public void setValue(Double input) {
String formatNum = PARENTHESIZED_LABEL_FORMAT.format(input);
if ( (input.doubleValue() >= PlotConstants.MILLION_VALUES) || (input.doubleValue() <= PlotConstants.NEGATIVE_MILLION_VALUES) ) {
formatNum = PlotterPlot.getNumberFormatter(input.doubleValue()).format(input.doubleValue());
}
if (formatNum.equals("0"))
formatNum = "0.0";
if (formatNum.equals("1"))
formatNum = "1.0";
setText("(" + formatNum + ")");
}
}
// Convenience method for populating and applying a standard layout for multi-item rows
private JPanel createMultiItemRow(final JComponent button, JComponent secondItem) {
JPanel panel = new JPanel();
panel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.BASELINE_LEADING;
if (button != null && button instanceof JRadioButton) {
((AbstractButton) button).setSelected(false);
panel.add(button, gbc);
} else if (button != null && button instanceof JComboBox){
panel.add(button, gbc);
}
if (secondItem != null) {
gbc.gridx = 1;
gbc.insets = new Insets(0, SPACE_BETWEEN_ROW_ITEMS, 0, 0);
gbc.weightx = 1;
panel.add(secondItem, gbc);
}
panel.setName("multiItemRow");
return panel;
}
}