package com.ibm.nmon.gui.chart.annotate;
import com.ibm.nmon.gui.main.NMONVisualizerGui;
import com.ibm.nmon.gui.GUIDialog;
import com.ibm.nmon.gui.chart.LineChartPanel;
import java.awt.BorderLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Point;
import java.awt.Insets;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import java.util.Calendar;
import java.util.Date;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerDateModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.JSpinner.DateEditor;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.ui.RectangleInsets;
import org.jfree.ui.TextAnchor;
import com.ibm.nmon.gui.Styles;
public final class LineChartAnnotationDialog extends GUIDialog {
private static final long serialVersionUID = 6545047405002972062L;
private final ButtonGroup lineType;
private final JTextField annotation;
private final JFormattedTextField yAxisValue;
// JTextField for value axis or JSpinner for time axis
private final JFormattedTextField xAxisValue;
private final JSpinner xAxisTime;
private boolean useTime;
private final JCheckBox useYAxisValue;
private final JCheckBox useXAxisValue;
private final XYPlot xyPlot;
public LineChartAnnotationDialog(LineChartPanel lineChartPanel, NMONVisualizerGui gui, JFrame parent,
Point clickLocation) {
super(gui, parent, "Annotate Line Chart");
setLayout(new BorderLayout());
setModal(true);
// calculate graph's x, y coordinates from the mouse click position
xyPlot = lineChartPanel.getChart().getXYPlot();
addPropertyChangeListener(lineChartPanel);
java.awt.geom.Rectangle2D dataArea = lineChartPanel.getChartRenderingInfo().getPlotInfo().getDataArea();
double x = xyPlot.getDomainAxis().java2DToValue(clickLocation.getX(), dataArea, xyPlot.getDomainAxisEdge());
double y = xyPlot.getRangeAxis().java2DToValue(clickLocation.getY(), dataArea, xyPlot.getRangeAxisEdge());
if (x < xyPlot.getDomainAxis().getLowerBound()) {
x = xyPlot.getDomainAxis().getLowerBound();
}
if (x > xyPlot.getDomainAxis().getUpperBound()) {
x = xyPlot.getDomainAxis().getUpperBound();
}
if (y < xyPlot.getRangeAxis().getLowerBound()) {
y = xyPlot.getRangeAxis().getLowerBound();
}
if (y > xyPlot.getRangeAxis().getUpperBound()) {
y = xyPlot.getRangeAxis().getUpperBound();
}
useTime = xyPlot.getDomainAxis() instanceof org.jfree.chart.axis.DateAxis;
// radio buttons at top to select line style
JLabel lineStyleLabel = new JLabel("Line Style:");
lineStyleLabel.setFont(Styles.LABEL);
lineStyleLabel.setHorizontalAlignment(SwingConstants.TRAILING);
JRadioButton vertical = new JRadioButton("Vertical");
JRadioButton horizontal = new JRadioButton("Horizontal");
JRadioButton none = new JRadioButton("None");
vertical.setActionCommand("Vertical");
horizontal.setActionCommand("Horizontal");
none.setActionCommand("None");
lineType = new ButtonGroup();
lineType.add(vertical);
lineType.add(horizontal);
lineType.add(none);
// annotation text with ability to set using x or y axis values
JLabel annotationLabel = new JLabel("Annotation:");
annotationLabel.setFont(Styles.LABEL);
annotationLabel.setHorizontalAlignment(SwingConstants.TRAILING);
annotation = new JTextField();
annotation.setColumns(5);
// x axis value as time
JLabel xAxisLabel = new JLabel("xAxis Location:");
xAxisLabel.setFont(Styles.LABEL);
xAxisLabel.setHorizontalAlignment(SwingConstants.TRAILING);
// for time
JButton addMinute = null;
JButton subtractMinute = null;
JButton roundMinute = null;
// for values
JButton roundX = null;
JButton addOneX = null;
JButton subtractOneX = null;
UpdateAnnotationAction annotationUpdater = new UpdateAnnotationAction();
if (useTime) {
xAxisTime = new JSpinner(new SpinnerDateModel(new Date((long) x), null, null, Calendar.MINUTE));
((DateEditor) xAxisTime.getEditor()).getFormat().setTimeZone(gui.getDisplayTimeZone());
xAxisTime.setEditor(new DateEditor(xAxisTime, Styles.DATE_FORMAT_STRING_WITH_YEAR));
xAxisTime.addChangeListener(annotationUpdater);
xAxisValue = null;
addMinute = new JButton("Add 1 Min");
subtractMinute = new JButton("Subtract 1 Min");
roundMinute = new JButton("Round");
addMinute.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
long time = ((Date) xAxisTime.getValue()).getTime();
xAxisTime.setValue(new Date(time + 60 * 1000));
}
});
subtractMinute.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
long time = ((Date) xAxisTime.getValue()).getTime();
xAxisTime.setValue(new Date(time - 60 * 1000));
}
});
roundMinute.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
long time = ((Date) xAxisTime.getValue()).getTime();
long mod = time % (60 * 1000);
if (mod >= (30 * 1000)) {
time += 60 * 1000;
}
xAxisTime.setValue(new Date(time / (60 * 1000) * (60 * 1000)));
}
});
}
else {
xAxisTime = null;
xAxisValue = new JFormattedTextField(Styles.NUMBER_FORMAT);
xAxisValue.setValue(x);
xAxisValue.addPropertyChangeListener(annotationUpdater);
roundX = new JButton("Round");
addOneX = new JButton("Add 1");
subtractOneX = new JButton("Subtract 1");
roundX.addActionListener(new RoundAction(xAxisValue));
addOneX.addActionListener(new AddAction(xAxisValue, 1));
subtractOneX.addActionListener(new AddAction(xAxisValue, -1));
}
// y axis value as formatted double
JLabel yAxisLabel = new JLabel("yAxis Location:");
yAxisLabel.setFont(Styles.LABEL);
yAxisLabel.setHorizontalAlignment(SwingConstants.TRAILING);
yAxisValue = new JFormattedTextField(Styles.NUMBER_FORMAT);
yAxisValue.setValue(y);
yAxisValue.addPropertyChangeListener(annotationUpdater);
JButton roundY = new JButton("Round");
JButton addOneY = new JButton("Add 1");
JButton subtractOneY = new JButton("Subtract 1");
roundY.addActionListener(new RoundAction(yAxisValue));
addOneY.addActionListener(new AddAction(yAxisValue, 1));
subtractOneY.addActionListener(new AddAction(yAxisValue, -1));
useXAxisValue = new JCheckBox("Use xAxis");
useYAxisValue = new JCheckBox("Use yAxis");
// OK button at bottom
JButton ok = new JButton("OK");
ok.addActionListener(doAnnotation);
JPanel okPanel = new JPanel();
okPanel.add(ok);
// pull question icon from JOptionPane
JLabel icon = new JLabel((Icon) UIManager.get("OptionPane.questionIcon"));
icon.setVerticalAlignment(SwingUtilities.TOP);
icon.setBorder(BorderFactory.createEmptyBorder(15, 15, 0, 25));
JPanel centerPanel = new JPanel(new GridBagLayout());
GridBagConstraints labelConstraints = new GridBagConstraints();
GridBagConstraints fieldConstraints = new GridBagConstraints();
GridBagConstraints buttonConstraints = new GridBagConstraints();
labelConstraints.gridx = 0;
fieldConstraints.gridx = 1;
labelConstraints.gridy = 0;
fieldConstraints.gridy = 0;
buttonConstraints.gridy = 0;
labelConstraints.insets = new Insets(5, 0, 5, 2);
fieldConstraints.insets = new Insets(5, 0, 5, 0);
buttonConstraints.insets = new Insets(5, 2, 5, 2);
labelConstraints.fill = GridBagConstraints.HORIZONTAL;
fieldConstraints.fill = GridBagConstraints.HORIZONTAL;
buttonConstraints.fill = GridBagConstraints.HORIZONTAL;
// labelConstraints.anchor = GridBagConstraints.BASELINE_TRAILING;
// fieldConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
buttonConstraints.anchor = GridBagConstraints.CENTER;
centerPanel.add(lineStyleLabel, labelConstraints);
buttonConstraints.gridx = 2;
centerPanel.add(vertical, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(horizontal, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(none, buttonConstraints);
++labelConstraints.gridy;
++fieldConstraints.gridy;
++buttonConstraints.gridy;
// annotation field is wider than other fields
fieldConstraints.gridwidth = 2;
buttonConstraints.gridx = 3;
centerPanel.add(annotationLabel, labelConstraints);
centerPanel.add(annotation, fieldConstraints);
centerPanel.add(useXAxisValue, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(useYAxisValue, buttonConstraints);
++labelConstraints.gridy;
++fieldConstraints.gridy;
++buttonConstraints.gridy;
fieldConstraints.gridwidth = 1;
buttonConstraints.gridx = 2;
centerPanel.add(xAxisLabel, labelConstraints);
if (useTime) {
centerPanel.add(xAxisTime, fieldConstraints);
centerPanel.add(addMinute, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(subtractMinute, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(roundMinute, buttonConstraints);
// the yAxis line will have one more button; make all buttons the same size
// addOneY.setPreferredSize(subtractMinute.getPreferredSize());
}
else {
centerPanel.add(xAxisValue, fieldConstraints);
centerPanel.add(addOneX, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(subtractOneX, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(roundX, buttonConstraints);
}
++labelConstraints.gridy;
++fieldConstraints.gridy;
++buttonConstraints.gridy;
buttonConstraints.gridx = 2;
centerPanel.add(yAxisLabel, labelConstraints);
centerPanel.add(yAxisValue, fieldConstraints);
centerPanel.add(addOneY, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(subtractOneY, buttonConstraints);
++buttonConstraints.gridx;
centerPanel.add(roundY, buttonConstraints);
add(centerPanel, BorderLayout.CENTER);
add(icon, BorderLayout.LINE_START);
add(okPanel, BorderLayout.PAGE_END);
useXAxisValue.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
useYAxisValue.setSelected(false);
if (useXAxisValue.isSelected()) {
if (useTime) {
annotation.setText(((DateEditor) xAxisTime.getEditor()).getFormat()
.format(xAxisTime.getValue()));
}
else {
annotation.setText(xAxisValue.getText());
}
annotation.setEnabled(false);
}
else {
annotation.setEnabled(true);
annotation.selectAll();
annotation.requestFocus();
}
}
});
useYAxisValue.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
useXAxisValue.setSelected(false);
if (useYAxisValue.isSelected()) {
annotation.setText(yAxisValue.getText());
annotation.setEnabled(false);
}
else {
annotation.setEnabled(true);
annotation.selectAll();
annotation.requestFocus();
}
}
});
horizontal.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
useYAxisValue.setEnabled(true);
useXAxisValue.setEnabled(false);
if (!useYAxisValue.isSelected()) {
useYAxisValue.doClick();
}
}
});
vertical.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
useYAxisValue.setEnabled(false);
useXAxisValue.setEnabled(true);
if (!useXAxisValue.isSelected()) {
useXAxisValue.doClick();
}
}
});
none.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
useYAxisValue.setEnabled(true);
useXAxisValue.setEnabled(true);
if (useXAxisValue.isSelected()) {
useXAxisValue.doClick();
}
if (useYAxisValue.isSelected()) {
useYAxisValue.doClick();
}
}
});
getRootPane().setDefaultButton(ok);
// default to vertical line and time annotations
vertical.setSelected(true);
useYAxisValue.setEnabled(false);
// set annotation assuming vertical line as the default
useXAxisValue.doClick();
addWindowListener(new WindowAdapter() {
@Override
public void windowOpened(WindowEvent e) {
// request focus so user can hit spacebar to immediately add a different annotation
useXAxisValue.requestFocus();
}
});
}
private ActionListener doAnnotation = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String line = lineType.getSelection().getActionCommand();
String text = annotation.getText();
if (text != null) {
text = text.trim();
}
if ("Vertical".equals(line)) {
ValueMarker marker = new DomainValueMarker(getX());
marker.setLabelOffset(new RectangleInsets(5, 5, 5, 5));
marker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
marker.setLabel(text);
formatMarker(marker);
if (marker != null) {
xyPlot.addDomainMarker(marker);
firePropertyChange("annotation", null, marker);
}
}
else if ("Horizontal".equals(line)) {
ValueMarker marker = new RangeValueMarker(getY());
marker.setLabelTextAnchor(TextAnchor.BASELINE_LEFT);
marker.setLabel(text);
formatMarker(marker);
if (marker != null) {
xyPlot.addRangeMarker(marker);
firePropertyChange("annotation", null, marker);
}
}
else if ("None".equals(line)) {
if (!"".equals(text)) {
XYTextAnnotation annotation = new XYTextAnnotation(text, getX(), getY());
annotation.setFont(Styles.ANNOTATION_FONT);
annotation.setPaint(Styles.ANNOTATION_COLOR);
if (annotation != null) {
xyPlot.addAnnotation(annotation);
firePropertyChange("annotation", null, annotation);
}
}
// else no annotation needed if no text
}
else {
throw new IllegalStateException("unknown annotation line type");
}
dispose();
}
private double getX() {
if (useTime) {
return ((Date) xAxisTime.getValue()).getTime();
}
else {
return ((Number) xAxisValue.getValue()).doubleValue();
}
}
private double getY() {
return ((Number) yAxisValue.getValue()).doubleValue();
}
private void formatMarker(ValueMarker marker) {
marker.setStroke(Styles.ANNOTATION_STROKE);
marker.setPaint(Styles.ANNOTATION_COLOR);
marker.setLabelFont(Styles.ANNOTATION_FONT);
marker.setLabelPaint(Styles.ANNOTATION_COLOR);
}
};
private final class UpdateAnnotationAction implements PropertyChangeListener, ChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("value".equals(evt.getPropertyName()) || "editValid".equals(evt.getPropertyName())) {
if (useXAxisValue.isSelected()) {
if (xAxisValue != null) {
annotation.setText(xAxisValue.getText());
}
// else xAxisTime handled by stateChanged()
}
else if (useYAxisValue.isSelected()) {
annotation.setText(yAxisValue.getText());
}
// else do not update the annotation
}
}
@Override
public void stateChanged(ChangeEvent e) {
// for xAxisTime
if (useXAxisValue.isSelected()) {
annotation.setText(((DateEditor) xAxisTime.getEditor()).getFormat().format(xAxisTime.getValue()));
}
}
}
// use Number in these functions since JFormattedTextField can return Long or Double
private final class RoundAction implements ActionListener {
private final JFormattedTextField textField;
RoundAction(JFormattedTextField textField) {
this.textField = textField;
}
@Override
public void actionPerformed(ActionEvent e) {
long rounded = Math.round(((Number) textField.getValue()).doubleValue());
textField.setValue((double) rounded);
}
}
private final class AddAction implements ActionListener {
private final JFormattedTextField textField;
private final double toAdd;
AddAction(JFormattedTextField textField, double toAdd) {
this.textField = textField;
this.toAdd = toAdd;
}
@Override
public void actionPerformed(ActionEvent e) {
textField.setValue(((Number) textField.getValue()).doubleValue() + toAdd);
}
}
}