package com.ibm.nmon.gui.chart;
import org.slf4j.Logger;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.MenuElement;
import javax.swing.JMenuItem;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.ChartTransferable;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.Annotation;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.plot.Marker;
import org.jfree.ui.ExtensionFileFilter;
import com.ibm.nmon.gui.chart.data.*;
import com.ibm.nmon.gui.main.NMONVisualizerGui;
import com.ibm.nmon.gui.file.GUIFileChooser;
import com.ibm.nmon.util.CSVWriter;
public class BaseChartPanel extends ChartPanel implements PropertyChangeListener {
private static final long serialVersionUID = -1342720624336005568L;
protected final Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
protected final NMONVisualizerGui gui;
protected final JFrame parent;
// used by subclasses for adding annotations
protected java.awt.Point clickLocation = null;
private int saveHeight;
private int saveWidth;
protected BaseChartPanel(NMONVisualizerGui gui, JFrame parent) {
// With multiple charts for each ReportPanel and one report panel per DataSet, there could
// be a large number of active charts. To keep memory usage down, disable using a buffered
// image despite any performance benefit.
// Unfortunately, chartBuffer in ChartPanel is private so there is no way to clear the
// memory used when the chart is not being displayed.
super(null, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MINIMUM_DRAW_WIDTH, DEFAULT_MINIMUM_DRAW_HEIGHT,
DEFAULT_MAXIMUM_DRAW_WIDTH, DEFAULT_MAXIMUM_DRAW_HEIGHT, false, true, false, false, false, true);
setBackground(java.awt.Color.WHITE);
// keep the chart for resizing when empty
setPreferredSize(null);
// disable zooming
removeMouseMotionListener(this);
// no need for mouse listener when there is no chart
removeMouseListener(this);
this.gui = gui;
this.parent = parent;
this.saveWidth = 1920 / 2;
this.saveHeight = 1080 / 2;
setEnabled(false);
clearChart();
setEnforceFileExtensions(true);
}
public final void setChart(JFreeChart chart) {
JFreeChart old = getChart();
if (chart != old) {
super.setChart(chart);
if (chart != null) {
// keep in sync with clearChart(); only add a single mouse listener
if (old == null) {
addMouseListener(this);
}
firePropertyChange("chart", null, this);
}
else {
// fix memory leak; rendering info holds ChartEntitiy objects that reference the
// chart's Dataset
getChartRenderingInfo().clear();
firePropertyChange("chart", null, null);
}
}
}
public void clearChart() {
if (getChart() != null) {
setChart(null);
// do not display the popup menu when there is no chart
removeMouseListener(this);
}
}
/**
* <p>
* Visually indicate on the that the element (bar or line) corresponding to the <code>Dataset's</code> given row and
* column.
* </p>
*
* <p>
* This method <em>should not</em> fire a property change event since this is meant to be called in response to
* other user input. This function should toggle already highlighted elements to an unhighlighted state.
* </p>
*
* @param row the chart's dataset row
* @param column the chart's dataset column
*/
public void highlightElement(int row, int column) {}
/**
* Remove all highlights from chart.
*/
public void clearHighlightedElements() {}
public void setElementVisible(int row, int column, boolean visible) {};
/**
* Get the dataset associated with this chart or <code>null</code> if there the chart does not have one.
*/
public final DataTupleDataset getDataset() {
if (getChart() == null) {
return null;
}
Plot plot = getChart().getPlot();
if (plot instanceof XYPlot) {
XYPlot xyPlot = getChart().getXYPlot();
DataTupleDataset dataset = (DataTupleDataset) xyPlot.getDataset(0);
if (xyPlot.getDatasetCount() == 1) {
return dataset;
}
else {
// assume only 2 datasets / 2 axes
if (dataset instanceof DataTupleXYDataset) {
return ((DataTupleXYDataset) dataset).merge((DataTupleXYDataset) xyPlot.getDataset(1));
}
else if (dataset instanceof DataTupleHistogramDataset) {
return ((DataTupleHistogramDataset) dataset)
.merge((DataTupleHistogramDataset) xyPlot.getDataset(1));
}
else {
logger.warn("unknown DataTupleDataset class {}, returning null", dataset.getClass().getName());
return null;
}
}
}
else if (plot instanceof CategoryPlot) {
CategoryPlot categoryPlot = (CategoryPlot) plot;
DataTupleCategoryDataset dataset = (DataTupleCategoryDataset) categoryPlot.getDataset(0);
if (categoryPlot.getDatasetCount() == 1) {
return dataset;
}
else {
return dataset.merge((DataTupleCategoryDataset) categoryPlot.getDataset(1));
}
}
else {
return null;
}
}
// update the base class methods to put out images of a fixed size rather than the current size
// of the chart in the gui
@Override
public final void doCopy() {
Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
ChartTransferable selection = new ChartTransferable(getChart(), saveWidth, saveHeight);
systemClipboard.setContents(selection, null);
}
public final void doCopyDataset() {
DataTupleDataset data = getDataset();
if (data != null) {
StringWriter writer = new StringWriter(2048);
try {
CSVWriter.write(data, writer);
Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
systemClipboard.setContents(new StringSelection(writer.toString()), null);
}
catch (IOException ioe) {
logger.error("error copying dataset", ioe);
}
}
}
@Override
public final void doSaveAs() throws IOException {
String directory = gui.getPreferences().get("lastSaveDirectory", "./");
String filename = validateSaveFileName(null);
File chartFile = new File(directory, filename);
GUIFileChooser fileChooser = new GUIFileChooser(gui, "Select Save Location");
fileChooser.setSelectedFile(chartFile);
ExtensionFileFilter filter = new ExtensionFileFilter(localizationResources.getString("PNG_Image_Files"),
".png");
fileChooser.addChoosableFileFilter(filter);
if (fileChooser.showDialog(this, "Save") == JFileChooser.APPROVE_OPTION) {
chartFile = fileChooser.getSelectedFile();
if (isEnforceFileExtensions()) {
if (!chartFile.getName().endsWith(".png")) {
chartFile = new File(chartFile.getAbsolutePath() + ".png");
}
}
if (chartFile.exists()) {
int result = JOptionPane.showConfirmDialog(gui.getMainFrame(),
"File '" + chartFile.getName() + "' already exists.\nDo you want to overwrite it?",
"Overwrite?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
if (result != JOptionPane.OK_OPTION) {
return;
}
}
ChartUtilities.saveChartAsPNG(chartFile, getChart(), saveWidth, saveHeight);
}
// save the last directory whether or not the file is actually saved
gui.getPreferences().put("lastSaveDirectory", chartFile.getParentFile().getAbsolutePath());
}
public final void setSaveSize(int width, int height) {
if (width < 1) {
throw new IllegalArgumentException("width" + "must be greater than 0");
}
if (height < 1) {
throw new IllegalArgumentException("height" + "must be greater than 0");
}
saveWidth = width;
saveHeight = height;
}
public final void saveChart(String directory, String filename) {
filename = validateSaveFileName(filename);
File chartFile = new File(directory, filename);
try {
ChartUtilities.saveChartAsPNG(chartFile, getChart(), saveWidth, saveHeight);
}
catch (IOException ioe) {
logger.error("could not save chart '" + filename + "' to directory '" + directory + "'", ioe);
}
}
public void addAnnotations(List<Annotation> annotations) {}
public void addMarkers(List<Marker> markers) {}
protected String validateSaveFileName(String filename) {
if ((filename == null) || "".equals(filename)) {
String title = getChart().getTitle().getText();
if ((title == null) || "".equals(title)) {
filename = "chart_" + this.hashCode();
}
else {
filename = title;
}
}
if (isEnforceFileExtensions() && !filename.endsWith(".png")) {
filename += ".png";
}
filename = filename.replace('\n', ' ');
filename = filename.replace(' ', '_');
return filename;
}
@Override
protected JPopupMenu createPopupMenu(boolean properties, boolean copy, boolean save, boolean print, boolean zoom) {
JPopupMenu popup = super.createPopupMenu(properties, copy, save, print, zoom);
int n = 0;
// find the existing 'Copy' menu item and add an option to copy chart data after that
for (MenuElement element : popup.getSubElements()) {
JMenuItem item = (JMenuItem) element;
if (item.getText().equals("Copy")) {
JMenuItem copyData = new JMenuItem("Copy Chart Data");
copyData.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
doCopyDataset();
}
});
// after separator, after copy => + 2
popup.add(copyData, n + 2);
popup.add(new JPopupMenu.Separator(), n + 3);
}
n++;
}
// create Save Chart item
// note that the default 'Save as' item is no present since false was passed into the
// BaseChartPanel constructor when creating this class' instance
JMenuItem savePNG = new JMenuItem("Save Chart...");
savePNG.setActionCommand("SAVE_AS_PNG");
savePNG.addActionListener(this);
popup.add(savePNG);
return popup;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {}
// disable mouse movement to avoid constantly searching for ChartEntity objects when there are
// ChartMouseListeners
// assume subclasses will not need the functionality (zooming / panning) provided in the super
// class
@Override
public void mouseMoved(MouseEvent e) {}
@Override
public void mouseDragged(MouseEvent e) {}
// track location of the mouse event for annotations
@Override
public void mousePressed(MouseEvent event) {
if (event.isPopupTrigger()) {
clickLocation = new Point(event.getX(), event.getY());
}
super.mousePressed(event);
}
@Override
public void mouseReleased(MouseEvent event) {
if (event.isPopupTrigger()) {
clickLocation = new Point(event.getX(), event.getY());
}
super.mouseReleased(event);
}
@Override
public void paintComponent(Graphics g) {
if (logger.isTraceEnabled()) {
long start = System.nanoTime();
super.paintComponent(g);
String title = "<no title>";
if ((getChart() != null) && (getChart().getTitle()) != null) {
title = getChart().getTitle().getText();
}
logger.trace("painted chart '{}' in {} ms", title, (System.nanoTime() - start) / 1000000.0d);
}
else {
super.paintComponent(g);
}
}
}