PeriodAnalysisDialogBase.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.plugin.period;

import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JPanel;

import org.aavso.tools.vstar.ui.mediator.DocumentManager;
import org.aavso.tools.vstar.ui.mediator.Mediator;
import org.aavso.tools.vstar.util.IStartAndCleanup;
import org.aavso.tools.vstar.util.Tolerance;
import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.util.model.Harmonic;

/**
 * This can be used as the base class for period analysis dialogs.
 */
@SuppressWarnings("serial")
abstract public class PeriodAnalysisDialogBase extends JDialog implements
		IStartAndCleanup {

	protected JButton newPhasePlotButton;
	protected JButton findHarmonicsButton;
	protected boolean wantPhasePlotButton;
	protected boolean wantHarmonicsButton;

	private JPanel topPane;

	/**
	 * Constructor.
	 * 
	 * @param title
	 *            The dialog title.
	 * @param isModal
	 *            Should the dialog be modal?
	 * @param wantPhasePlotButton
	 *            Do we want a phase plot button?
	 */
	public PeriodAnalysisDialogBase(String title, boolean isModal,
			boolean wantPhasePlotButton, boolean wantHarmonicsButton) {
		super(DocumentManager.findActiveWindow());
		this.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
		this.addWindowListener(createWindowClosingListener());
		this.wantPhasePlotButton = wantPhasePlotButton;
		this.wantHarmonicsButton = wantHarmonicsButton;
		this.setTitle(title);
		this.setModal(isModal);
	}

	/**
	 * Constructor for a non-modal dialog that is not always on top.
	 * 
	 * @param title
	 *            The dialog title.
	 */
	public PeriodAnalysisDialogBase(String title) {
		this(title, false, true, true);
	}

	/**
	 * A subclass must invoke this when it wants to add the dialog's content and
	 * prepare it for visibility. It will in turn call createContent() and
	 * createButtonPanel().
	 */
	protected void prepareDialog() {
		topPane = new JPanel();
		topPane.setLayout(new BoxLayout(topPane, BoxLayout.PAGE_AXIS));
		topPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

		topPane.add(createContent());
		topPane.add(createButtonPanel());

		this.getContentPane().add(topPane);
		this.pack();
		this.setLocationRelativeTo(Mediator.getUI().getContentPane());
	}

	// Methods that must be overridden by subclasses.

	/**
	 * This method is invoked when the new-phase-plot button is clicked.
	 */
	abstract protected void newPhasePlotButtonAction();

	/**
	 * This method is invoked when the find-harmonics button is clicked.
	 */
	abstract protected void findHarmonicsButtonAction();

	/**
	 * Create the content to be added to the dialog's content pane.
	 */
	abstract protected Component createContent();

	// Protected methods for use by subclasses.

	protected JPanel createButtonPanel() {
		JPanel buttonPane = new JPanel(new FlowLayout());

		addNewPhasePlotButton(buttonPane);
		addFindHarmomicsButton(buttonPane);
		addDismissButton(buttonPane);

		return buttonPane;
	}

	protected void addNewPhasePlotButton(JPanel buttonPane) {
		if (wantPhasePlotButton) {
			newPhasePlotButton = new JButton(
					LocaleProps.get("NEW_PHASE_PLOT_BUTTON"));
			newPhasePlotButton
					.addActionListener(createNewPhasePlotButtonHandler());
			newPhasePlotButton.setEnabled(false);
			buttonPane.add(newPhasePlotButton);
		}
	}

	protected void addFindHarmomicsButton(JPanel buttonPane) {
		if (wantHarmonicsButton) {
			findHarmonicsButton = new JButton(
					LocaleProps.get("FIND_HARMONICS_BUTTON"));
			findHarmonicsButton
					.addActionListener(createFindHarmonicsButtonHandler());
			findHarmonicsButton.setEnabled(false);
			buttonPane.add(findHarmonicsButton);
		}
	}

	protected void addDismissButton(JPanel buttonPane) {
		JButton dismissButton = new JButton(LocaleProps.get("DISMISS_BUTTON"));
		dismissButton.addActionListener(createDismissButtonHandler());
		buttonPane.add(dismissButton);
	}

	/**
	 * Find all harmonics of the specified frequency in the data and return
	 * them, including the fundamental itself.
	 * 
	 * The first harmonic is just the frequency itself. This is a view expressed
	 * by Grant Foster which makes perfect sense to me; it generalises the
	 * notion of "harmonic", not making a special case of the fundamental.
	 * 
	 * Question: Is it an error to find the n-1th and n+1th harmonic in the data
	 * but not the nth? Is this just a by-product of trying to perform floating
	 * point comparison without tolerances?
	 * 
	 * @param freq
	 *            The frequency whose harmonics we seek.
	 * @param data
	 *            The data in which to search; assumed to be frequencies.
	 * @return A list of harmonic objects.
	 */
	protected List<Harmonic> findHarmonics(double freq, List<Double> data, double tolerance) {
		List<Harmonic> harmonics = new ArrayList<Harmonic>();
		
		//Do not assume that the fundamental frequency exists in the data.
		//harmonics.add(new Harmonic(freq, Harmonic.FUNDAMENTAL));
		//int n = Harmonic.FUNDAMENTAL + 1;
		
		double minData = Collections.min(data);
		double maxData = Collections.max(data);
		
		assert(freq > 0 || minData > 0);
		
		int n = Harmonic.FUNDAMENTAL;

		// Do not assume that data are sorted.
		while (freq * n <= maxData) {
			for (int i = 0; i < data.size(); i++) {
				double potentialHarmonic = data.get(i) / n;
				// Check if the data is a harmonic of the frequency within
				// a relative tolerance range
				if (Tolerance.areClose(potentialHarmonic, freq, tolerance, false)) {
					if (freq * n >= minData || freq * n <= maxData) {
						harmonics.add(new Harmonic(freq * n, n));
						break;
					}
				}
			}
			n++;
		}

		return harmonics;
	}

	/**
	 * Sets the enabled state of the new-phase-plot button.
	 * 
	 * @param state
	 *            The desired boolean state.
	 */
	public void setNewPhasePlotButtonState(boolean state) {
		this.newPhasePlotButton.setEnabled(state);
	}

	/**
	 * Sets the enabled state of the find-harmonics button.
	 * 
	 * @param state
	 *            The desired boolean state.
	 */
	public void setFindHarmonicsButtonState(boolean state) {
		if (wantHarmonicsButton) {
			this.findHarmonicsButton.setEnabled(state);
		}
	}

	// WindowClosing listener.
	private WindowAdapter createWindowClosingListener() {
		return new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				setVisible(false);
				cleanup();
				dispose();
			}
		};
	}

	// Dismiss button listener.
	private ActionListener createDismissButtonHandler() {
		return new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
				cleanup();
				dispose();
			}
		};
	}

	// Invoke concrete dialog class's handler when the new-phase-plot button is
	// clicked.
	private ActionListener createNewPhasePlotButtonHandler() {
		return new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				newPhasePlotButtonAction();
			}
		};
	}

	// Invoke concrete dialog class's handler when the find-harmomics button is
	// clicked.
	private ActionListener createFindHarmonicsButtonHandler() {
		return new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				findHarmonicsButtonAction();
			}
		};
	}
}