PeriodAnalysis2DChartPane.java
/**
* VStar: a statistical analysis tool for variable star data.
* Copyright (C) 2009 AAVSO (http://www.aavso.org/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.aavso.tools.vstar.ui.dialog.period;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JPanel;
import org.aavso.tools.vstar.ui.dialog.MessageBox;
import org.aavso.tools.vstar.ui.dialog.model.HarmonicInfoDialog;
import org.aavso.tools.vstar.ui.mediator.Mediator;
import org.aavso.tools.vstar.ui.mediator.message.HarmonicSearchResultMessage;
import org.aavso.tools.vstar.ui.mediator.message.PeriodAnalysisRefinementMessage;
import org.aavso.tools.vstar.ui.mediator.message.PeriodAnalysisSelectionMessage;
import org.aavso.tools.vstar.ui.model.plot.PeriodAnalysis2DPlotModel;
import org.aavso.tools.vstar.util.IStartAndCleanup;
import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.util.notification.Listener;
import org.aavso.tools.vstar.util.period.dcdft.PeriodAnalysisDataPoint;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYLineAnnotation;
import org.jfree.chart.entity.XYItemEntity;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.general.DatasetChangeEvent;
import org.jfree.data.general.DatasetChangeListener;
import org.jfree.data.xy.XYDataset;
/**
* This class represents a chart panel.
*/
@SuppressWarnings("serial")
public class PeriodAnalysis2DChartPane extends JPanel implements
ChartMouseListener, DatasetChangeListener, IStartAndCleanup {
public static final int TOP_HIT_SERIES = 0;
public static final int DATA_SERIES = 1;
private static int DEFAULT_CHART_PANEL_WIDTH = 680;
private static int DEFAULT_CHART_PANEL_HEIGHT = 420;
private ChartPanel chartPanel;
private JFreeChart chart;
private PeriodAnalysis2DPlotModel model;
private boolean permitLogarithmic;
private Listener<PeriodAnalysisSelectionMessage> periodAnalysisSelectionListener;
private Listener<PeriodAnalysisRefinementMessage> periodAnalysisRefinementListener;
private Listener<HarmonicSearchResultMessage> harmonicSearchListener;
private String chartPaneID = null;
/**
* Constructor
*
* @param chart
* The JFreeChart chart.
* @param model
* The plot model.
* @param permitLogarithmic
* Should it be possible to toggle the plot between a normal and
* logarithmic range?
*/
public PeriodAnalysis2DChartPane(JFreeChart chart,
PeriodAnalysis2DPlotModel model, boolean permitLogarithmic) {
super();
this.chart = chart;
this.model = model;
this.permitLogarithmic = permitLogarithmic;
this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
this.setBorder(BorderFactory.createEtchedBorder());
chartPanel = new ChartPanel(chart);
this.add(chartPanel);
this.add(createControlPanel());
configureChart();
chartPanel.addChartMouseListener(this);
model.addChangeListener(this);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(DEFAULT_CHART_PANEL_WIDTH, DEFAULT_CHART_PANEL_HEIGHT);
}
public void setChartPaneID(String chartPaneID) {
this.chartPaneID = chartPaneID;
}
public String getChartPaneID() {
return chartPaneID;
}
/**
* @return the chart
*/
public JFreeChart getChart() {
return chart;
}
/**
* @return the model
*/
public PeriodAnalysis2DPlotModel getModel() {
return model;
}
// Create and return a component that permits the "is logarithmic" and
// "show top hits" properties of the model to be toggled.
private JPanel createControlPanel() {
JPanel panel = new JPanel();
if (permitLogarithmic) {
final JCheckBox logarithmicCheckBox = new JCheckBox(LocaleProps
.get("LOGARITHMIC_CHECKBOX"));
logarithmicCheckBox.setSelected(model.isLogarithmic());
logarithmicCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
for (int modelNum = 0; modelNum < chart.getXYPlot()
.getDatasetCount(); modelNum++) {
XYDataset dataset = chart.getXYPlot().getDataset(
modelNum);
PeriodAnalysis2DPlotModel plotModel = (PeriodAnalysis2DPlotModel) dataset;
plotModel.setLogarithmic(logarithmicCheckBox
.isSelected());
plotModel.refresh();
}
}
});
panel.add(logarithmicCheckBox, BorderLayout.CENTER);
}
// Add a checkbox to toggle top hits series visibility.
final JCheckBox showTopHitsCheckBox = new JCheckBox(LocaleProps
.get("SHOW_TOP_HITS_CHECKBOX"));
showTopHitsCheckBox.setSelected(true);
final XYItemRenderer renderer = chart.getXYPlot().getRenderer();
renderer.setSeriesVisible(TOP_HIT_SERIES, true);
showTopHitsCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
for (int modelNum = 0; modelNum < chart.getXYPlot()
.getDatasetCount(); modelNum++) {
if (modelNum == TOP_HIT_SERIES) {
boolean enabled = showTopHitsCheckBox.isSelected();
renderer.setSeriesVisible(TOP_HIT_SERIES, enabled);
}
}
}
});
panel.add(showTopHitsCheckBox);
return panel;
}
private void configureChart() {
chart.getXYPlot().setBackgroundPaint(Color.WHITE);
chart.getXYPlot().setDomainCrosshairValue(0);
chart.getXYPlot().setRangeCrosshairValue(0);
chart.getXYPlot().setDomainCrosshairVisible(true);
chart.getXYPlot().setRangeCrosshairVisible(true);
}
public void chartMouseClicked(ChartMouseEvent event) {
// The cross-hair appears if the user clicks the chart even if there is no
// 'entity' under the mouse. This means that the cross-hair selection
// does not always coincide with the selected data point.
// Let's show the cross-hair only if an 'entity' is behind it.
XYPlot plot = event.getChart().getXYPlot();
plot.setDomainCrosshairVisible(false);
plot.setRangeCrosshairVisible(false);
if (event.getEntity() instanceof XYItemEntity) {
XYItemEntity entity = (XYItemEntity) event.getEntity();
int item = entity.getItem();
PeriodAnalysisDataPoint dataPoint = null;
for (int modelNum = 0; modelNum < chart.getXYPlot()
.getDatasetCount(); modelNum++) {
if (dataPoint == null) {
XYDataset dataset = chart.getXYPlot().getDataset(modelNum);
PeriodAnalysis2DPlotModel plotModel = (PeriodAnalysis2DPlotModel) dataset;
dataPoint = plotModel.getDataPointFromItem(item);
}
}
plot.setDomainCrosshairVisible(true);
plot.setRangeCrosshairVisible(true);
PeriodAnalysisSelectionMessage message = new PeriodAnalysisSelectionMessage(
this, dataPoint, item);
if (message != null) {
message.setTag(Mediator.getParentDialogName(this));
Mediator.getInstance().getPeriodAnalysisSelectionNotifier()
.notifyListeners(message);
}
}
}
/**
* Set the cross hair to the specified x,y coordinate.
*
* @param x
* The x coordinate.
* @param y
* The y coordinate.
*/
public void setCrossHair(double x, double y) {
chart.getXYPlot().setDomainCrosshairValue(x);
chart.getXYPlot().setRangeCrosshairValue(y);
chart.getXYPlot().setDomainCrosshairVisible(true);
chart.getXYPlot().setRangeCrosshairVisible(true);
}
public void chartMouseMoved(ChartMouseEvent event) {
// Nothing to do.
}
@Override
public void datasetChanged(DatasetChangeEvent event) {
// Set series colors if dataset changes.
// XYItemRenderer renderer = chart.getXYPlot().getRenderer();
for (int seriesNum = 0; seriesNum < model.getSeriesCount(); seriesNum++) {
switch (seriesNum) {
case DATA_SERIES:
// renderer.setSeriesPaint(seriesNum, Color.PINK);
break;
case TOP_HIT_SERIES:
// renderer.setSeriesPaint(seriesNum, Color.GREEN);
break;
}
}
}
/**
* Update the crosshairs according to the selected data point.
*/
protected Listener<PeriodAnalysisSelectionMessage> createPeriodAnalysisListener() {
final Component parent = this;
return new Listener<PeriodAnalysisSelectionMessage>() {
@Override
public void update(PeriodAnalysisSelectionMessage info) {
if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysis2DChartPane.this), info))
return;
if (info.getSource() != parent) {
double x = info.getDataPoint().getValue(model.getDomainType());
double y = info.getDataPoint().getValue(model.getRangeType());
if (x != Double.NaN && y != Double.NaN) {
XYPlot plot = chart.getXYPlot();
plot.setDomainCrosshairValue(x);
plot.setRangeCrosshairValue(y);
plot.setDomainCrosshairVisible(true);
plot.setRangeCrosshairVisible(true);
}
}
}
@Override
public boolean canBeRemoved() {
return true;
}
};
}
// Create a period analysis refinement listener which sets annotations on
// the plot according to the domain and range types.
private Listener<PeriodAnalysisRefinementMessage> createRefinementListener() {
return new Listener<PeriodAnalysisRefinementMessage>() {
@Override
public void update(PeriodAnalysisRefinementMessage info) {
if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysis2DChartPane.this), info))
return;
chart.getXYPlot().clearAnnotations();
for (PeriodAnalysisDataPoint dataPoint : info.getNewTopHits()) {
// if (model.getRangeType() ==
// PeriodAnalysisCoordinateType.POWER) {
double x = dataPoint.getValue(model.getDomainType());
double y = dataPoint.getValue(model.getRangeType());
XYLineAnnotation line = new XYLineAnnotation(x, 0, x, y);
chart.getXYPlot().addAnnotation(line);
// }
model.refresh();
}
}
@Override
public boolean canBeRemoved() {
return true;
}
};
}
private Listener<HarmonicSearchResultMessage> createHarmonicSearchListener() {
final PeriodAnalysis2DChartPane pane = this;
return new Listener<HarmonicSearchResultMessage>() {
@Override
public void update(HarmonicSearchResultMessage info) {
if (!Mediator.isMsgForDialog(Mediator.getParentDialog(pane), info))
return;
String id = pane.getChartPaneID();
String currentID = info.getIDstring();
if (currentID != null && currentID.equals(id)) {
if (info.getHarmonics().size() > 0) {
new HarmonicInfoDialog(info, pane);
} else {
MessageBox.showMessageDialog("Harmonics", "No top hit for this frequency");
}
}
}
@Override
public boolean canBeRemoved() {
return true;
}
};
}
@Override
public void startup() {
// We listen for and generate period analysis selection messages.
periodAnalysisSelectionListener = createPeriodAnalysisListener();
Mediator.getInstance().getPeriodAnalysisSelectionNotifier()
.addListener(periodAnalysisSelectionListener);
// We listen for period analysis refinement messages.
periodAnalysisRefinementListener = createRefinementListener();
Mediator.getInstance().getPeriodAnalysisRefinementNotifier()
.addListener(periodAnalysisRefinementListener);
// if (model.getRangeType() == PeriodAnalysisCoordinateType.POWER) {
// We listen for harmonic search result messages if this is a power
// spectrum.
harmonicSearchListener = createHarmonicSearchListener();
Mediator.getInstance().getHarmonicSearchNotifier().addListener(
harmonicSearchListener);
// } else {
// harmonicSearchListener = null;
// }
}
@Override
public void cleanup() {
Mediator.getInstance().getPeriodAnalysisSelectionNotifier()
.removeListenerIfWilling(periodAnalysisSelectionListener);
Mediator.getInstance().getPeriodAnalysisRefinementNotifier()
.removeListenerIfWilling(periodAnalysisRefinementListener);
if (harmonicSearchListener != null) {
Mediator.getInstance().getHarmonicSearchNotifier()
.removeListenerIfWilling(harmonicSearchListener);
}
}
}