SeriesVisibilityPane.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.series;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JPanel;

import org.aavso.tools.vstar.data.SeriesType;
import org.aavso.tools.vstar.ui.mediator.AnalysisType;
import org.aavso.tools.vstar.ui.model.plot.ObservationAndMeanPlotModel;
import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.util.notification.Listener;
import org.aavso.tools.vstar.util.stats.BinningResult;

/**
 * This class represents a pane with checkboxes showing those series that are
 * rendered. The series to be displayed can be changed.
 *
 * TODO: rename as MultipleSeriesSelectionPane
 */
@SuppressWarnings("serial")
public class SeriesVisibilityPane extends JPanel {

	private ObservationAndMeanPlotModel obsPlotModel;
	private AnalysisType analysisType;

	private Map<Integer, Boolean> visibilityDeltaMap;

	private List<JCheckBox> checkBoxes;

	private JCheckBox discrepantCheckBox;
	private JCheckBox excludedCheckBox;

	private JCheckBox meanCheckBox;
	private JCheckBox filteredCheckBox;
	private JCheckBox modelCheckBox;
	private JCheckBox residualsCheckBox;

	private boolean includeSynthetic;
	private boolean modifyVisibility;

	/**
	 * Constructor.
	 * 
	 * @param obsPlotModel
	 *            The plot model.
	 * @param analysisType
	 *            The analysis type.
	 * @param includeSynthetic
	 *            Include synthetic series?
	 * @param modifyVisibility
	 *            Modify series visibility?
	 */
	public SeriesVisibilityPane(ObservationAndMeanPlotModel obsPlotModel,
			AnalysisType analysisType, boolean includeSynthetic,
			boolean modifyVisibility) {
		this(obsPlotModel, analysisType, includeSynthetic, modifyVisibility,
				true);
	}

	/**
	 * Constructor.
	 * 
	 * @param obsPlotModel
	 *            The plot model.
	 * @param analysisType
	 *            The analysis type.
	 * @param includeSynthetic
	 *            Include synthetic series?
	 * @param modifyVisibility
	 *            Modify series visibility?
	 * @param showVisibilityBorderTitle
	 *            show "Series Visibility" border title? For some uses of the
	 *            dialog, we just want to see the series checkboxes.
	 */
	public SeriesVisibilityPane(ObservationAndMeanPlotModel obsPlotModel,
			AnalysisType analysisType, boolean includeSynthetic,
			boolean modifyVisibility, boolean showVisibilityBorderTitle) {
		super();

		this.obsPlotModel = obsPlotModel;
		this.analysisType = analysisType;

		this.includeSynthetic = includeSynthetic;
		this.modifyVisibility = modifyVisibility;

		this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
		if (showVisibilityBorderTitle) {
			this.setBorder(BorderFactory.createTitledBorder(LocaleProps
					.get("VISIBILITY_TITLE")));
		}
		this.setToolTipText("Select or deselect series.");

		this.visibilityDeltaMap = new HashMap<Integer, Boolean>();

		this.checkBoxes = new ArrayList<JCheckBox>();

		filteredCheckBox = null;
		modelCheckBox = null;

		addSeriesCheckBoxes();

		obsPlotModel.getMeansChangeNotifier().addListener(
				createMeanObsChangeListener());

		addButtons();
	}

	/**
	 * Constructor for pane in which synthetic series are included and series
	 * visibility is modified.
	 * 
	 * @param obsPlotModel
	 *            The plot model.
	 * @param analysisType
	 *            The analysis type.
	 */
	public SeriesVisibilityPane(ObservationAndMeanPlotModel obsPlotModel,
			AnalysisType analysisType) {
		this(obsPlotModel, analysisType, true, true);
	}

	/**
	 * Return the complete set of selected series.
	 * 
	 * @return The selected series.
	 */
	public Set<SeriesType> getSelectedSeries() {
		Set<SeriesType> series = new TreeSet<SeriesType>();

		for (JCheckBox checkBox : checkBoxes) {
			if (checkBox.isSelected()) {
				String desc = checkBox.getText();
				series.add(SeriesType.getSeriesFromDescription(desc));
			}
		}

		return series;
	}

	// Create a checkbox for each series, grouped by type.
	private void addSeriesCheckBoxes() {
		JPanel panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
		panel.add(createDataSeriesCheckboxes());
		if (includeSynthetic) {
			panel.add(createDerivedSeriesCheckboxes());
		}
        JPanel userPanel = createUserDefinedSeriesCheckboxes();
        if (userPanel != null) {
            panel.add(userPanel);
        }
		this.add(panel);
	}

	private JPanel createDataSeriesCheckboxes() {
		JPanel panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
		panel.setBorder(BorderFactory.createTitledBorder(LocaleProps
				.get("DATA_TITLE")));

		// Ensure the panel is always wide enough.
		this.add(Box.createRigidArea(new Dimension(75, 1)));

		for (SeriesType series : this.obsPlotModel.getSeriesKeys()) {
			// We treat derived and user-defined series separately from data
			// series.
			if (!series.isSynthetic() && !series.isUserDefined()) {
				String seriesName = series.getDescription();
				JCheckBox checkBox = new JCheckBox(seriesName);

				checkBox.addActionListener(createSeriesVisibilityCheckBoxListener());

				// Enable/disable the series.
				boolean vis = obsPlotModel.getSeriesVisibilityMap().get(series);
				checkBox.setSelected(vis);

				panel.add(checkBox);
				panel.add(Box.createRigidArea(new Dimension(3, 3)));

				checkBoxes.add(checkBox);

				Integer seriesNum = obsPlotModel.getSrcTypeToSeriesNumMap()
						.get(series);

				// Listeners need access to discrepant and excluded checkboxes.
				// We also set the initial state for these checkboxes
				// conditionally, depending upon whether any observations are
				// present in these series. If they are already selected, it is
				// not disabled, e.g. catering for the case where a single
				// discrepant or excluded observation is undone.
				if (series == SeriesType.DISCREPANT) {
					discrepantCheckBox = checkBox;
					if (!discrepantCheckBox.isSelected()
							&& obsPlotModel.getSeriesNumToObSrcListMap()
									.get(seriesNum).isEmpty()) {
						setInitialCheckBoxState(series, discrepantCheckBox);
					}
				} else if (series == SeriesType.Excluded) {
					excludedCheckBox = checkBox;
					if (!excludedCheckBox.isSelected()
							&& obsPlotModel.getSeriesNumToObSrcListMap()
									.get(seriesNum).isEmpty()) {
						setInitialCheckBoxState(series, excludedCheckBox);
					}
				}
			}
		}

		return panel;
	}

	private JPanel createDerivedSeriesCheckboxes() {
		JPanel panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
		panel.setBorder(BorderFactory.createTitledBorder(LocaleProps
				.get("ANALYSIS_TITLE")));

		// Mean series.
		meanCheckBox = new JCheckBox(SeriesType.MEANS.getDescription());
		meanCheckBox
				.addActionListener(createSeriesVisibilityCheckBoxListener());
		setInitialCheckBoxState(SeriesType.MEANS, meanCheckBox);
		panel.add(meanCheckBox);
		panel.add(Box.createRigidArea(new Dimension(3, 3)));
		checkBoxes.add(meanCheckBox);

		// Filtered series.
		filteredCheckBox = new JCheckBox(SeriesType.Filtered.getDescription());
		filteredCheckBox
				.addActionListener(createSeriesVisibilityCheckBoxListener());
		setInitialCheckBoxState(SeriesType.Filtered, filteredCheckBox);
		panel.add(filteredCheckBox);
		panel.add(Box.createRigidArea(new Dimension(3, 3)));
		checkBoxes.add(filteredCheckBox);

		JPanel subPanel = new JPanel();
		subPanel.setLayout(new BoxLayout(subPanel, BoxLayout.PAGE_AXIS));
		// TODO: why bother with this panel? Just use parent Analysis panel!
		subPanel.setBorder(BorderFactory.createTitledBorder(LocaleProps
				.get("MODEL_TITLE")));
		subPanel.add(Box.createRigidArea(new Dimension(75, 1)));

		// Model series.
		modelCheckBox = new JCheckBox(SeriesType.Model.getDescription());
		modelCheckBox
				.addActionListener(createSeriesVisibilityCheckBoxListener());
		setInitialCheckBoxState(SeriesType.Model, modelCheckBox);
		subPanel.add(modelCheckBox);
		subPanel.add(Box.createRigidArea(new Dimension(3, 3)));
		checkBoxes.add(modelCheckBox);

		// Residuals series.
		residualsCheckBox = new JCheckBox(SeriesType.Residuals.getDescription());
		residualsCheckBox
				.addActionListener(createSeriesVisibilityCheckBoxListener());
		setInitialCheckBoxState(SeriesType.Residuals, residualsCheckBox);
		subPanel.add(residualsCheckBox);
		checkBoxes.add(residualsCheckBox);

		panel.add(subPanel);

		return panel;
	}

	private JPanel createUserDefinedSeriesCheckboxes() {
		JPanel panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
		panel.setBorder(BorderFactory.createTitledBorder(LocaleProps
				.get("USER_DEFINED_TITLE")));

		// Ensure the panel is always wide enough.
		this.add(Box.createRigidArea(new Dimension(75, 1)));

		boolean anyObs = false;

		for (SeriesType series : this.obsPlotModel.getSeriesKeys()) {

			if (series.isUserDefined()) {
				// Ignore user-defined series with no corresponding data in the
				// current dataset.
				Integer seriesNum = obsPlotModel.getSrcTypeToSeriesNumMap()
						.get(series);

				if (obsPlotModel.getSeriesNumToObSrcListMap().get(seriesNum)
						.isEmpty()) {
					continue;
				} else {
					if (!anyObs) {
						anyObs = true;
					}
				}

				String seriesName = series.getDescription();
				JCheckBox checkBox = new JCheckBox(seriesName);

				checkBox.addActionListener(createSeriesVisibilityCheckBoxListener());

				// Enable/disable the series.
				boolean vis = obsPlotModel.getSeriesVisibilityMap().get(series);
				checkBox.setSelected(vis);

				panel.add(checkBox);
				panel.add(Box.createRigidArea(new Dimension(3, 3)));

				checkBoxes.add(checkBox);
			}
		}

		if (!anyObs) {
			panel = null;
		}

		return panel;
	}

	/**
	 * Set the enabled and selected states of the checkbox corresponding to the
	 * specified series type according to the plot model's visibility map.
	 * 
	 * @param seriesType
	 *            The series type in question.
	 * @param checkbox
	 *            The checkbox whose state is to be set.
	 */
	protected void setInitialCheckBoxState(SeriesType seriesType,
			JCheckBox checkbox) {
		Integer seriesNum = obsPlotModel.getSrcTypeToSeriesNumMap().get(
				seriesType);

		// This series exists and has obs (or not), so allow it to be selected
		// (or not).
		boolean hasObs = obsPlotModel.getSeriesNumToObSrcListMap().containsKey(
				seriesNum)
				&& !obsPlotModel.getSeriesNumToObSrcListMap().get(seriesNum)
						.isEmpty();
		checkbox.setEnabled(hasObs);

		// The series is (or is not) marked as being visible so also select
		// (or don't) the checkbox. A series visibility may not have been set in
		// the map yet, unless it has been selected by default (e.g. visual
		// bands) or by the user.
		boolean visible = obsPlotModel.getSeriesVisibilityMap().containsKey(
				seriesType)
				&& obsPlotModel.getSeriesVisibilityMap().get(seriesType);
		checkbox.setSelected(visible);
	}

	// Return a listener for the series visibility checkboxes.
	private ActionListener createSeriesVisibilityCheckBoxListener() {
		return new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (modifyVisibility) {
					JCheckBox checkBox = (JCheckBox) e.getSource();
					updateSeriesVisibilityMap(checkBox);
					seriesVisibilityChange(getVisibilityDeltaMap());
				}
			}
		};
	}

	// Create buttons for en-masse selection/deselection
	// of visibility checkboxes and an apply button.
	private void addButtons() {
		JPanel panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
		panel.setBorder(BorderFactory.createEtchedBorder());

		JButton selectAllButton = new JButton(
				LocaleProps.get("SELECT_ALL_BUTTON"));
		selectAllButton
				.addActionListener(createEnMasseSelectionButtonListener(true));
		panel.add(selectAllButton, BorderLayout.LINE_START);

		JButton deSelectAllButton = new JButton(
				LocaleProps.get("DESELECT_ALL_BUTTON"));
		deSelectAllButton
				.addActionListener(createEnMasseSelectionButtonListener(false));
		panel.add(deSelectAllButton, BorderLayout.LINE_END);

		this.add(panel, BorderLayout.CENTER);
	}

	/**
	 * Was there a change in the series visibility? Some callers may want to
	 * invoke this only for its side effects, while others may also want to know
	 * the result.
	 * 
	 * @param deltaMap
	 *            A mapping from series number to whether or not each series'
	 *            visibility was changed.
	 * 
	 * @return Was there a change in the visibility of any series?
	 */
	protected boolean seriesVisibilityChange(Map<Integer, Boolean> deltaMap) {
		boolean delta = false;

		for (int seriesNum : deltaMap.keySet()) {
			boolean visibility = deltaMap.get(seriesNum);
			delta |= obsPlotModel.changeSeriesVisibility(seriesNum, visibility);
		}

		return delta;
	}

	/**
	 * Return a listener for the "select/deselect all" checkbox.
	 * 
	 * @param target
	 *            The target check-button state.
	 * @return The button listener.
	 */
	private ActionListener createEnMasseSelectionButtonListener(
			final boolean target) {
		return new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				for (JCheckBox checkBox : checkBoxes) {
					if (checkBox.isEnabled()) {
						checkBox.setSelected(target);
						if (modifyVisibility) {
						    updateSeriesVisibilityMap(checkBox);
						}
					}
				}

                if (modifyVisibility) {
                    seriesVisibilityChange(getVisibilityDeltaMap());
                }
			}
		};
	}

	/**
	 * Update the series visibility map according to the state of the
	 * checkboxes.
	 * 
	 * @param checkBox
	 *            The checkbox whose state we want to update from.
	 */
	private void updateSeriesVisibilityMap(JCheckBox checkBox) {
		String seriesName = checkBox.getText();
		int seriesNum = obsPlotModel.getSrcTypeToSeriesNumMap().get(
				SeriesType.getSeriesFromDescription(seriesName));
		visibilityDeltaMap.put(seriesNum, checkBox.isSelected());
	}

	/**
	 * @return the visibilityDeltaMap
	 */
	public Map<Integer, Boolean> getVisibilityDeltaMap() {
		return visibilityDeltaMap;
	}

	// Return a mean observation change listener to ensure that the
	// mean series checkbox is selected if a new binning operation takes
	// place that also set the mean series as visible, assuming it was not
	// already.
	private Listener<BinningResult> createMeanObsChangeListener() {
		return new Listener<BinningResult>() {
			@Override
			public void update(BinningResult info) {
				// Check that the series was actually marked as visible in the
				// model!
				boolean meanSeriesVisible = obsPlotModel
						.getSeriesVisibilityMap().get(SeriesType.MEANS);
				if (meanSeriesVisible) {
					meanCheckBox.setSelected(true);
				}
			}

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