WeightedWaveletZTransformResultDialog.java

/**
 * VStar: a statistical analysis tool for variable star data.
 * Copyright (C) 2010  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.wwz;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GradientPaint;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JPanel;
import javax.swing.JTabbedPane;

import org.aavso.tools.vstar.plugin.PluginComponentFactory;
import org.aavso.tools.vstar.plugin.period.PeriodAnalysisDialogBase;
import org.aavso.tools.vstar.ui.NamedComponent;
import org.aavso.tools.vstar.ui.mediator.Mediator;
import org.aavso.tools.vstar.ui.mediator.message.PeriodAnalysisSelectionMessage;
import org.aavso.tools.vstar.ui.mediator.message.PeriodChangeMessage;
import org.aavso.tools.vstar.ui.model.list.WWZDataTableModel;
import org.aavso.tools.vstar.ui.model.plot.WWZ2DPlotModel;
import org.aavso.tools.vstar.ui.model.plot.WWZ3DPlotModel;
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.IPeriodAnalysisDatum;
import org.aavso.tools.vstar.util.period.wwz.WWZCoordinateType;
import org.aavso.tools.vstar.util.period.wwz.WWZStatistic;
import org.aavso.tools.vstar.util.period.wwz.WeightedWaveletZTransform;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.LookupPaintScale;
import org.jfree.chart.renderer.xy.XYBlockRenderer;
import org.jfree.chart.title.PaintScaleLegend;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets;
import org.math.plot.Plot3DPanel;

/**
 * This dialog class is used to visualise WWZ algorithm results.
 */
@SuppressWarnings("serial")
public class WeightedWaveletZTransformResultDialog extends
		PeriodAnalysisDialogBase {

	private String chartTitle;
	private IPeriodAnalysisDatum selectedDataPoint;
	private WeightedWaveletZTransform wwt;
	private WWZCoordinateType rangeType;

	List<IStartAndCleanup> startupAndCleanupComponents;

	private Listener<PeriodAnalysisSelectionMessage> periodAnalysisListener;

	/**
	 * Constructor.
	 * 
	 * @param title
	 *            The title for the dialog.
	 * @param chartTitle
	 *            The title for the chart.
	 * @param rangeType
	 *            The type of the range coordinate.
	 */
	public WeightedWaveletZTransformResultDialog(String title,
			String chartTitle, WeightedWaveletZTransform wwt,
			WWZCoordinateType rangeType) {
		super(title, false, true, false);

		this.chartTitle = chartTitle;
		this.wwt = wwt;
		this.rangeType = rangeType;

		selectedDataPoint = null;

		startupAndCleanupComponents = new ArrayList<IStartAndCleanup>();

		prepareDialog();

		startup();
	}

	/**
	 * @see org.aavso.tools.vstar.plugin.period.PeriodAnalysisDialogBase#createContent()
	 */
	@Override
	protected Component createContent() {
		return createTabs();
	}

	private JTabbedPane createTabs() {
		List<NamedComponent> namedComponents = new ArrayList<NamedComponent>();

		final String MAXIMAL_WWZ = "(" + LocaleProps.get("MAXIMAL") + " WWZ)";

		// Maximal period vs time plot.
		namedComponents.add(createChart(MAXIMAL_WWZ, new WWZ2DPlotModel(wwt
				.getMaximalStats(), WWZCoordinateType.TAU,
				WWZCoordinateType.PERIOD),
				getMinValue(WWZCoordinateType.PERIOD),
				getMaxValue(WWZCoordinateType.PERIOD)));

		// Maximal frequency vs time plot.
		namedComponents.add(createChart(MAXIMAL_WWZ, new WWZ2DPlotModel(wwt
				.getMaximalStats(), WWZCoordinateType.TAU,
				WWZCoordinateType.FREQUENCY),
				getMinValue(WWZCoordinateType.FREQUENCY),
				getMaxValue(WWZCoordinateType.FREQUENCY)));

		// Maximal semi-amplitude vs time plot.
		namedComponents.add(createChart(MAXIMAL_WWZ, new WWZ2DPlotModel(wwt
				.getMaximalStats(), WWZCoordinateType.TAU,
				WWZCoordinateType.SEMI_AMPLITUDE), wwt.getMinAmp(), wwt
				.getMaxAmp()));

		// Contour plot of time vs period vs WWZ.
		namedComponents.add(createContourChart("", new WWZ3DPlotModel(wwt
				.getStats(), WWZCoordinateType.TAU, WWZCoordinateType.PERIOD,
				WWZCoordinateType.WWZ), wwt.getStats().get(0).getTau(), wwt
				.getStats().get(wwt.getStats().size() - 1).getTau(),
				getMinValue(WWZCoordinateType.PERIOD),
				getMaxValue(WWZCoordinateType.PERIOD), wwt.getMinWWZ(), wwt
						.getMaxWWZ()));

		// 3D plot from maximal stats.
		namedComponents.add(create3DStatsPlot(MAXIMAL_WWZ,
				WWZCoordinateType.TAU, rangeType, WWZCoordinateType.WWZ, wwt
						.getMaximalStats()));

		// Tables for all and maximal statistics.
		WWZDataTablePane dataPane = new WWZDataTablePane(new WWZDataTableModel(
				wwt.getStats(), wwt));
		startupAndCleanupComponents.add(dataPane);
		namedComponents.add(new NamedComponent(LocaleProps.get("WWZ_RESULTS"),
				dataPane));

		WWZDataTablePane maximalPane = new WWZDataTablePane(
				new WWZDataTableModel(wwt.getMaximalStats(), wwt));
		startupAndCleanupComponents.add(maximalPane);
		namedComponents.add(new NamedComponent(LocaleProps
				.get("MAXIMAL_WWZ_RESULTS"), maximalPane));

		return PluginComponentFactory.createTabs(namedComponents);
	}

	protected JPanel createButtonPanel() {
		JPanel buttonPane = super.createButtonPanel();
		return buttonPane;
	}

	/**
	 * The new phase plot button will only be enabled when a period analysis
	 * selection message has been received, so we *know* without having to ask
	 * that there is a selected row in the data table.
	 */
	@Override
	protected void newPhasePlotButtonAction() {
		PeriodChangeMessage message = new PeriodChangeMessage(this,
				selectedDataPoint.getPeriod());
		message.setTag(this.getName());
		Mediator.getInstance().getPeriodChangeNotifier().notifyListeners(
				message);
	}

	// Note: The harmonics finder methods are not currently used here.
	// Need to assess the validity or suitability of that functionality
	// in the WWZ context.

	@Override
	protected void findHarmonicsButtonAction() {
//		List<Harmonic> harmonics = findHarmonicsFromWWZStats(selectedDataPoint
//				.getFrequency());
//		HarmonicSearchResultMessage msg = new HarmonicSearchResultMessage(this,
//				harmonics, selectedDataPoint);
//		msg.setName(this.getName());
//		Mediator.getInstance().getHarmonicSearchNotifier().notifyListeners(msg);
	}

/*
	// TODO: Refactor so that the base class method takes an interface
	// with a method that returns a frequency from a sequence. Then
	// List<Double> or List<WWZStatistic> can be wrapped in an object
	// that implements that interface. Then this method can go away.
	protected List<Harmonic> findHarmonicsFromWWZStats(double freq) {
		List<Harmonic> harmonics = new ArrayList<Harmonic>();
		harmonics.add(new Harmonic(freq, Harmonic.FUNDAMENTAL));
		int n = Harmonic.FUNDAMENTAL + 1;

		List<WWZStatistic> data = wwt.getStats();

		for (int i = 0; i < data.size(); i++) {
			double candidateFreq = data.get(i).getFrequency();
			// Try it both ways in case of round-off errors.
			if (candidateFreq / n == freq || candidateFreq == freq * n) {
				harmonics.add(new Harmonic(freq * n, n));
				n++;
			}
		}

		return harmonics;
	}
*/	

	// Enable the new phase plot button and store the selected
	// period analysis data point.
	private Listener<PeriodAnalysisSelectionMessage> createPeriodAnalysisListener() {
		return new Listener<PeriodAnalysisSelectionMessage>() {
			public void update(PeriodAnalysisSelectionMessage info) {
				if (!Mediator.isMsgForDialog(Mediator.getParentDialog(WeightedWaveletZTransformResultDialog.this), info))
					return;
				setNewPhasePlotButtonState(true);
				selectedDataPoint = info.getDataPoint();
			}

			public boolean canBeRemoved() {
				return true;
			}
		};
	}

	// Helpers

	private NamedComponent createChart(String suffix, WWZ2DPlotModel model,
			double minRange, double maxRange) {

		String name = model.getRangeType().toString() + " "
				+ LocaleProps.get("VERSUS") + " "
				+ model.getDomainType().toString() + " " + suffix;

		JFreeChart chart1 = ChartFactory
				.createXYLineChart(chartTitle + ": " + name, model
						.getDomainType().toString(), model.getRangeType()
						.toString(), model, PlotOrientation.VERTICAL, true,
						true, false);

		JFreeChart chart2 = ChartFactory
				.createScatterPlot(chartTitle + ": " + name, model
						.getDomainType().toString(), model.getRangeType()
						.toString(), model, PlotOrientation.VERTICAL, true,
						true, false);

		// Make a combined chart.
		chart2.getXYPlot().setDataset(1, model);
		chart2.getXYPlot().setRenderer(1, chart1.getXYPlot().getRenderer());
		chart2.getXYPlot().setDatasetRenderingOrder(
				DatasetRenderingOrder.FORWARD);

		double rangeMargin = ((maxRange - minRange) / 100) * 10;
		minRange -= rangeMargin;
		maxRange += rangeMargin;

		WWZPlotPane pane = new WWZPlotPane(chart2, model, minRange, maxRange);
		startupAndCleanupComponents.add(pane);

		return new NamedComponent(name, pane);
	}

	private NamedComponent createContourChart(String prefix,
			WWZ3DPlotModel model, double minDomain, double maxDomain,
			double minRange, double maxRange, double minZ, double maxZ) {

		XYBlockRenderer renderer = new XYBlockRenderer();
		renderer.setBlockWidth(10);
		// renderer.setBlockHeight(100);

		double increments = (maxZ - minZ) / 6;
		if (minZ == maxZ)
			maxZ++; // make sure the scale is valid
		LookupPaintScale scale = new LookupPaintScale(minZ, maxZ, Color.white);
		// PaintScale scale = new GrayPaintScale(minZ, maxZ);
		scale.add(minZ, Color.MAGENTA);
		scale.add(minZ + increments, Color.BLUE);
		scale.add(minZ + increments * 2, Color.GREEN);
		scale.add(minZ + increments * 3, Color.YELLOW);
		scale.add(minZ + increments * 4, Color.ORANGE);
		scale.add(minZ + increments * 5, Color.RED);
		renderer.setPaintScale(scale);

		NumberAxis xAxis = new NumberAxis(model.getDomainType().toString());
		xAxis.setLowerBound(minDomain);
		xAxis.setUpperBound(maxDomain);
		NumberAxis yAxis = new NumberAxis(model.getRangeType().toString());
		XYPlot plot = new XYPlot(model, xAxis, yAxis, renderer);
		plot.setBackgroundPaint(Color.white);
		plot.setDomainGridlinesVisible(false);

		plot.setRangeGridlinePaint(Color.white);
		plot.setRangeGridlinesVisible(false);

		plot.setDomainCrosshairVisible(true);
		plot.setRangeCrosshairVisible(true);
		plot.setDomainCrosshairValue(0);
		plot.setRangeCrosshairValue(0);

		plot.setAxisOffset(new RectangleInsets(5, 5, 5, 5));
		plot.setOutlinePaint(Color.blue);

		JFreeChart chart = new JFreeChart(plot);

		NumberAxis scaleAxis = new NumberAxis(model.getZType().toString());
		scaleAxis.setAxisLinePaint(Color.white);
		scaleAxis.setTickMarkPaint(Color.white);
		scaleAxis.setTickLabelFont(new Font("Dialog", Font.PLAIN, 7));

		PaintScaleLegend legend = new PaintScaleLegend(scale, scaleAxis);
		legend.setStripOutlineVisible(false);
		legend.setSubdivisionCount(20);
		legend.setAxisLocation(AxisLocation.BOTTOM_OR_LEFT);
		legend.setAxisOffset(5.0);
		legend.setMargin(new RectangleInsets(5, 5, 5, 5));
		legend.setFrame(new BlockBorder(Color.red));
		legend.setPadding(new RectangleInsets(10, 10, 10, 10));
		legend.setStripWidth(10);
		legend.setPosition(RectangleEdge.BOTTOM);
		chart.removeLegend();
		chart.addSubtitle(legend);
		chart.setBackgroundPaint(new GradientPaint(0, 0, Color.WHITE, 0, 1000,
				Color.WHITE));
		ChartUtils.applyCurrentTheme(chart);

		double rangeMargin = ((maxRange - minRange) / 100) * 25;
		minRange -= rangeMargin;
		maxRange += rangeMargin;

		String name = prefix + model.getRangeType().toString() + " "
				+ LocaleProps.get("VERSUS") + " "
				+ model.getDomainType().toString() + " "
				+ LocaleProps.get("VERSUS") + " " + model.getZType().toString()
				+ " " + LocaleProps.get("CONTOUR");

		WWZPlotPane pane = new WWZPlotPane(chart, model, minRange, maxRange);
		startupAndCleanupComponents.add(pane);

		return new NamedComponent(name, pane);
	}

	/**
	 * Create a 3D plot of 3 WWZ coordinates, e.g. of of time, period, wwz from
	 * maximal stats.
	 * 
	 * @return A named component suitable for adding to dialog.
	 */
	private NamedComponent create3DStatsPlot(String suffix,
			WWZCoordinateType xType, WWZCoordinateType yType,
			WWZCoordinateType zType, List<WWZStatistic> stats) {
		Plot3DPanel plot = new Plot3DPanel();
		plot
				.setAxisLabels(xType.toString(), yType.toString(), zType
						.toString());

		int size = stats.size();
		double[][] xyz = new double[3][size];

		for (int i = 0; i < size; i++) {
			WWZStatistic stat = stats.get(i);
			xyz[0][i] = stat.getValue(xType);
			xyz[1][i] = stat.getValue(yType);
			xyz[2][i] = stat.getValue(zType);
		}

		plot.addBarPlot(LocaleProps.get("WWZ_STATISTICS_3D_PLOT"), Color.GREEN,
				xyz);

		String name = yType.toString() + " " + LocaleProps.get("VERSUS") + " "
				+ xType.toString() + " " + LocaleProps.get("VERSUS") + " "
				+ zType.toString() + " 3D " + suffix;

		return new NamedComponent(name, plot);
	}

	private double getMaxValue(WWZCoordinateType type) {
		double value = 0;

		switch (type) {
		case PERIOD:
			value = wwt.getMaxPeriod();
			break;
		case FREQUENCY:
			value = wwt.getMaxFreq();
			break;
		case SEMI_AMPLITUDE:
			value = wwt.getMaxAmp();
			break;
		case WWZ:
			value = wwt.getMaxWWZ();
			break;
		default:
			throw new IllegalArgumentException("Invalid type: "
					+ type.toString());
		}

		return value;
	}

	private double getMinValue(WWZCoordinateType type) {
		double value = 0;

		switch (type) {
		case PERIOD:
			value = wwt.getMinPeriod();
			break;
		case FREQUENCY:
			value = wwt.getMinFreq();
			break;
		case SEMI_AMPLITUDE:
			value = wwt.getMinAmp();
			break;
		case WWZ:
			value = wwt.getMinWWZ();
			break;
		default:
			throw new IllegalArgumentException("Invalid type: "
					+ type.toString());
		}

		return value;
	}

	@Override
	public void startup() {
		periodAnalysisListener = this.createPeriodAnalysisListener();

		Mediator.getInstance().getPeriodAnalysisSelectionNotifier()
				.addListener(periodAnalysisListener);

		for (IStartAndCleanup component : startupAndCleanupComponents) {
			component.startup();
		}
	}

	@Override
	public void cleanup() {
		Mediator.getInstance().getPeriodAnalysisSelectionNotifier()
				.removeListenerIfWilling(periodAnalysisListener);

		for (IStartAndCleanup component : startupAndCleanupComponents) {
			component.cleanup();
		}
	}
}