Mediator.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.mediator;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.print.PrinterException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;

import javax.swing.JDialog;
import javax.swing.JTable.PrintMode;
import javax.swing.SwingWorker;
import javax.swing.filechooser.FileNameExtensionFilter;

import org.aavso.tools.vstar.data.InvalidObservation;
import org.aavso.tools.vstar.data.SeriesType;
import org.aavso.tools.vstar.data.ValidObservation;
import org.aavso.tools.vstar.data.ValidObservation.JDflavour;
import org.aavso.tools.vstar.exception.AuthenticationError;
import org.aavso.tools.vstar.exception.CancellationException;
import org.aavso.tools.vstar.exception.ConnectionException;
import org.aavso.tools.vstar.exception.ObservationReadError;
import org.aavso.tools.vstar.input.AbstractObservationRetriever;
import org.aavso.tools.vstar.input.database.Authenticator;
import org.aavso.tools.vstar.plugin.CustomFilterPluginBase;
import org.aavso.tools.vstar.plugin.ModelCreatorPluginBase;
import org.aavso.tools.vstar.plugin.ObservationSinkPluginBase;
import org.aavso.tools.vstar.plugin.ObservationSourcePluginBase;
import org.aavso.tools.vstar.plugin.ObservationToolPluginBase;
import org.aavso.tools.vstar.plugin.ObservationTransformerPluginBase;
import org.aavso.tools.vstar.plugin.period.PeriodAnalysisPluginBase;
import org.aavso.tools.vstar.ui.IMainUI;
import org.aavso.tools.vstar.ui.NamedComponent;
import org.aavso.tools.vstar.ui.TabbedDataPane;
import org.aavso.tools.vstar.ui.dialog.DelimitedFieldFileSaveChooser;
import org.aavso.tools.vstar.ui.dialog.DiscrepantReportDialog;
import org.aavso.tools.vstar.ui.dialog.DoubleField;
import org.aavso.tools.vstar.ui.dialog.IntegerField;
import org.aavso.tools.vstar.ui.dialog.LoadChooser;
import org.aavso.tools.vstar.ui.dialog.MessageBox;
import org.aavso.tools.vstar.ui.dialog.MultiEntryComponentDialog;
import org.aavso.tools.vstar.ui.dialog.ObservationDetailsDialog;
import org.aavso.tools.vstar.ui.dialog.PNGImageFileSaveChooser;
import org.aavso.tools.vstar.ui.dialog.PhaseDialog;
import org.aavso.tools.vstar.ui.dialog.PhaseParameterDialog;
import org.aavso.tools.vstar.ui.dialog.PlotControlDialog;
import org.aavso.tools.vstar.ui.dialog.SaveChooser;
import org.aavso.tools.vstar.ui.dialog.filter.ObservationFilterDialog;
import org.aavso.tools.vstar.ui.dialog.filter.ObservationFiltersDialog;
import org.aavso.tools.vstar.ui.dialog.model.ModelDialog;
import org.aavso.tools.vstar.ui.dialog.plugin.manager.PluginManagementOperation;
import org.aavso.tools.vstar.ui.dialog.series.MultipleSeriesSelectionDialog;
import org.aavso.tools.vstar.ui.dialog.series.SeriesVisibilityPane;
import org.aavso.tools.vstar.ui.dialog.series.SingleSeriesSelectionDialog;
import org.aavso.tools.vstar.ui.mediator.message.AnalysisTypeChangeMessage;
import org.aavso.tools.vstar.ui.mediator.message.DiscrepantObservationMessage;
import org.aavso.tools.vstar.ui.mediator.message.ExcludedObservationMessage;
import org.aavso.tools.vstar.ui.mediator.message.FilteredObservationMessage;
import org.aavso.tools.vstar.ui.mediator.message.HarmonicSearchResultMessage;
import org.aavso.tools.vstar.ui.mediator.message.MeanSourceSeriesChangeMessage;
import org.aavso.tools.vstar.ui.mediator.message.MessageBase;
import org.aavso.tools.vstar.ui.mediator.message.ModelCreationMessage;
import org.aavso.tools.vstar.ui.mediator.message.ModelSelectionMessage;
import org.aavso.tools.vstar.ui.mediator.message.MultipleObservationSelectionMessage;
import org.aavso.tools.vstar.ui.mediator.message.NewStarMessage;
import org.aavso.tools.vstar.ui.mediator.message.ObservationSelectionMessage;
import org.aavso.tools.vstar.ui.mediator.message.PanRequestMessage;
import org.aavso.tools.vstar.ui.mediator.message.PeriodAnalysisRefinementMessage;
import org.aavso.tools.vstar.ui.mediator.message.PeriodAnalysisSelectionMessage;
import org.aavso.tools.vstar.ui.mediator.message.PeriodChangeMessage;
import org.aavso.tools.vstar.ui.mediator.message.PhaseChangeMessage;
import org.aavso.tools.vstar.ui.mediator.message.PhaseSelectionMessage;
import org.aavso.tools.vstar.ui.mediator.message.ProgressInfo;
import org.aavso.tools.vstar.ui.mediator.message.ProgressType;
import org.aavso.tools.vstar.ui.mediator.message.SeriesCreationMessage;
import org.aavso.tools.vstar.ui.mediator.message.SeriesVisibilityChangeMessage;
import org.aavso.tools.vstar.ui.mediator.message.StopRequestMessage;
import org.aavso.tools.vstar.ui.mediator.message.UndoActionMessage;
import org.aavso.tools.vstar.ui.mediator.message.UndoableActionType;
import org.aavso.tools.vstar.ui.mediator.message.ZoomRequestMessage;
import org.aavso.tools.vstar.ui.model.list.AbstractMeanObservationTableModel;
import org.aavso.tools.vstar.ui.model.list.AbstractModelObservationTableModel;
import org.aavso.tools.vstar.ui.model.list.InvalidObservationTableModel;
import org.aavso.tools.vstar.ui.model.list.PhasePlotMeanObservationTableModel;
import org.aavso.tools.vstar.ui.model.list.RawDataMeanObservationTableModel;
import org.aavso.tools.vstar.ui.model.list.ValidObservationTableModel;
import org.aavso.tools.vstar.ui.model.plot.ContinuousModelFunction;
import org.aavso.tools.vstar.ui.model.plot.ISeriesInfoProvider;
import org.aavso.tools.vstar.ui.model.plot.InViewObservationFilter;
import org.aavso.tools.vstar.ui.model.plot.JDCoordSource;
import org.aavso.tools.vstar.ui.model.plot.JDTimeElementEntity;
import org.aavso.tools.vstar.ui.model.plot.ObservationAndMeanPlotModel;
import org.aavso.tools.vstar.ui.model.plot.PhaseTimeElementEntity;
import org.aavso.tools.vstar.ui.model.plot.PhasedObservationAndMeanPlotModel;
import org.aavso.tools.vstar.ui.model.plot.PreviousCyclePhaseCoordSource;
import org.aavso.tools.vstar.ui.model.plot.StandardPhaseCoordSource;
import org.aavso.tools.vstar.ui.pane.list.ObservationListPane;
import org.aavso.tools.vstar.ui.pane.list.SyntheticObservationListPane;
import org.aavso.tools.vstar.ui.pane.plot.ObservationAndMeanPlotPane;
import org.aavso.tools.vstar.ui.pane.plot.PhaseAndMeanPlotPane;
import org.aavso.tools.vstar.ui.pane.plot.TimeElementsInBinSettingPane;
import org.aavso.tools.vstar.ui.resources.ResourceAccessor;
import org.aavso.tools.vstar.ui.task.ModellingTask;
import org.aavso.tools.vstar.ui.task.NewStarFromObSourcePluginTask;
import org.aavso.tools.vstar.ui.task.NewStarFromObSourcePluginWithSuppliedFileTask;
import org.aavso.tools.vstar.ui.task.NewStarFromObSourcePluginWithSuppliedURLTask;
import org.aavso.tools.vstar.ui.task.ObsListFileSaveTask;
import org.aavso.tools.vstar.ui.task.PeriodAnalysisTask;
import org.aavso.tools.vstar.ui.task.PhasePlotTask;
import org.aavso.tools.vstar.ui.task.PluginManagerOperationTask;
import org.aavso.tools.vstar.ui.undo.IUndoableAction;
import org.aavso.tools.vstar.ui.undo.UndoableActionManager;
import org.aavso.tools.vstar.util.Triple;
import org.aavso.tools.vstar.util.comparator.JDComparator;
import org.aavso.tools.vstar.util.comparator.PreviousCyclePhaseComparator;
import org.aavso.tools.vstar.util.comparator.StandardPhaseComparator;
import org.aavso.tools.vstar.util.coords.DecInfo;
import org.aavso.tools.vstar.util.coords.EpochType;
import org.aavso.tools.vstar.util.coords.RAInfo;
import org.aavso.tools.vstar.util.date.AbstractHJDConverter;
import org.aavso.tools.vstar.util.discrepant.DiscrepantReport;
import org.aavso.tools.vstar.util.discrepant.IDiscrepantReporter;
import org.aavso.tools.vstar.util.discrepant.VSXWebServiceZapperLogger;
import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.util.model.IModel;
import org.aavso.tools.vstar.util.notification.Listener;
import org.aavso.tools.vstar.util.notification.Notifier;
import org.aavso.tools.vstar.util.prefs.ChartPropertiesPrefs;
import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs;
import org.aavso.tools.vstar.util.stats.BinningResult;
import org.aavso.tools.vstar.util.stats.PhaseCalcs;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;

/**
 * This class manages the creation of models and views and sends notifications
 * for changes to mode and analysis types.
 * 
 * This is a Singleton since only one mediator per application instance should
 * exist.
 * 
 * TODO: This is really 2 classes: a task manager and a message broker...
 */
public class Mediator {

    public static final String NOT_IMPLEMENTED_YET = "This feature is not implemented yet.";

    private static IMainUI ui;

    // Valid and invalid observation lists and series category maps.
    private List<ValidObservation> validObsList;
    private List<InvalidObservation> invalidObsList;

    // Note: it would be useful to update these with mean obs, excluded obs etc
    // so they could be used in places where currently the model must be
    // consulted instead; especially the first map, e.g. for period analysis.
    private Map<SeriesType, List<ValidObservation>> validObservationCategoryMap;
    private Map<SeriesType, List<ValidObservation>> phasedValidObservationCategoryMap;

    // Current observation and mean plot model.
    // Period search (TODO: did I mean ANOVA vs period search?) needs access to
    // this to determine the current mean source band.
    private ObservationAndMeanPlotModel obsAndMeanPlotModel;

    // Current observation table model.
    private ValidObservationTableModel validObsTableModel;

    // Current view viewMode.
    private ViewModeType viewMode;

    // Current analysis type.
    private AnalysisType analysisType;

    // The new star messages created and sent to listeners, most recent at
    // the highest index.
    private List<NewStarMessage> newStarMessageList;

    // The latest model selection message created and sent to listeners.
    private ModelSelectionMessage modelSelectionMessage;

    // Mapping from analysis type to the latest analysis change
    // messages created and sent to listeners.
    private Map<AnalysisType, AnalysisTypeChangeMessage> analysisTypeMap;

    // A file dialog for saving any kind of observation list.
    private DelimitedFieldFileSaveChooser obsListFileSaveDialog;

    // A file dialog for saving an image, e.g. a plot.
    private PNGImageFileSaveChooser imageSaveDialog;

    // A helper object for loading a VeLa code file.
    private LoadChooser velaFileLoadDialog;

    // A helper object for saving a VeLa code file.
    private SaveChooser velaFileSaveDialog;

    // A helper object for loading an XML from VeLa Model XML file using respective
    // chooser.
    private LoadChooser velaXMLloadDialog;

    // An helper object for saving a VeLa model to XML file using respective
    // chooser.
    private SaveChooser velaXMLsaveDialog;

    // Persistent phase parameter dialog.
    private PhaseParameterDialog phaseParameterDialog;

    // Persistent observation filter dialog.
    private ObservationFilterDialog obsFilterDialog;

    // Model dialog.
    private ModelDialog modelDialog;

    // A dialog to manage phase plots.
    private PhaseDialog phaseDialog;

    // A dialog to manage filters.
    private ObservationFiltersDialog observationFiltersDialog;

    // Notifiers.
    private Notifier<AnalysisTypeChangeMessage> analysisTypeChangeNotifier;
    private Notifier<NewStarMessage> newStarNotifier;
    private Notifier<ProgressInfo> progressNotifier;
    // TODO: This next notifier could be used to mark the "document"
    // (the current star's dataset) associated with the valid obs
    // as being in need of saving (optional for now). See DocumentManager
    private Notifier<DiscrepantObservationMessage> discrepantObservationNotifier;
    private Notifier<ExcludedObservationMessage> excludedObservationNotifier;
    private Notifier<ObservationSelectionMessage> observationSelectionNotifier;
    private Notifier<MultipleObservationSelectionMessage> multipleObservationSelectionNotifier;
    private Notifier<PeriodAnalysisSelectionMessage> periodAnalysisSelectionNotifier;
    private Notifier<PeriodChangeMessage> periodChangeNotifier;
    private Notifier<PhaseChangeMessage> phaseChangeNotifier;
    private Notifier<PhaseSelectionMessage> phaseSelectionNotifier;
    private Notifier<PeriodAnalysisRefinementMessage> periodAnalysisRefinementNotifier;
    private Notifier<MeanSourceSeriesChangeMessage> meanSourceSeriesChangeNotifier;
    private Notifier<ZoomRequestMessage> zoomRequestNotifier;
    private Notifier<FilteredObservationMessage> filteredObservationNotifier;
    private Notifier<ModelSelectionMessage> modelSelectionNofitier;
    private Notifier<ModelCreationMessage> modelCreationNotifier;
    private Notifier<PanRequestMessage> panRequestNotifier;
    private Notifier<UndoActionMessage> undoActionNotifier;
    private Notifier<StopRequestMessage> stopRequestNotifier;
    private Notifier<SeriesVisibilityChangeMessage> seriesVisibilityChangeNotifier;
    private Notifier<HarmonicSearchResultMessage> harmonicSearchNotifier;
    private Notifier<SeriesCreationMessage> seriesCreationNotifier;

    private DocumentManager documentManager;

    private UndoableActionManager undoableActionManager;

    // Currently active task.
    private SwingWorker currTask;

    // Singleton fields, constructor, getter.

    private static Mediator mediator;

    /**
     * Private constructor.
     */
    private Mediator() {
        ui = null;

        this.analysisTypeChangeNotifier = new Notifier<AnalysisTypeChangeMessage>();
        this.newStarNotifier = new Notifier<NewStarMessage>();
        this.progressNotifier = new Notifier<ProgressInfo>();
        this.discrepantObservationNotifier = new Notifier<DiscrepantObservationMessage>();
        this.excludedObservationNotifier = new Notifier<ExcludedObservationMessage>();
        this.observationSelectionNotifier = new Notifier<ObservationSelectionMessage>();
        this.multipleObservationSelectionNotifier = new Notifier<MultipleObservationSelectionMessage>();
        this.periodAnalysisSelectionNotifier = new Notifier<PeriodAnalysisSelectionMessage>();
        this.periodChangeNotifier = new Notifier<PeriodChangeMessage>();
        this.phaseChangeNotifier = new Notifier<PhaseChangeMessage>();
        this.phaseSelectionNotifier = new Notifier<PhaseSelectionMessage>();
        this.periodAnalysisRefinementNotifier = new Notifier<PeriodAnalysisRefinementMessage>();
        this.meanSourceSeriesChangeNotifier = new Notifier<MeanSourceSeriesChangeMessage>();
        this.zoomRequestNotifier = new Notifier<ZoomRequestMessage>();
        this.filteredObservationNotifier = new Notifier<FilteredObservationMessage>();
        this.modelSelectionNofitier = new Notifier<ModelSelectionMessage>();
        this.modelCreationNotifier = new Notifier<ModelCreationMessage>();
        this.panRequestNotifier = new Notifier<PanRequestMessage>();
        this.undoActionNotifier = new Notifier<UndoActionMessage>();
        this.stopRequestNotifier = new Notifier<StopRequestMessage>();
        this.seriesVisibilityChangeNotifier = new Notifier<SeriesVisibilityChangeMessage>();
        this.harmonicSearchNotifier = new Notifier<HarmonicSearchResultMessage>();
        this.seriesCreationNotifier = new Notifier<SeriesCreationMessage>();

        this.obsListFileSaveDialog = new DelimitedFieldFileSaveChooser();
        this.imageSaveDialog = new PNGImageFileSaveChooser();
        {
            FileNameExtensionFilter[] extensionFilter = new FileNameExtensionFilter[1];
            extensionFilter[0] = new FileNameExtensionFilter("VeLa files (*.txt, *.vl, *.vela)", "txt", "vl", "vela");
            this.velaFileLoadDialog = new LoadChooser(extensionFilter, "Open VeLa File");
            this.velaFileSaveDialog = new SaveChooser(extensionFilter, "vela", "Save VeLa File");
        }
        {
            FileNameExtensionFilter[] extensionFilter = new FileNameExtensionFilter[1];
            extensionFilter[0] = new FileNameExtensionFilter("VeLa XML and Text files (*.vlmx, *.txt, *.vl, *.vela)",
                    "vlmx", "txt", "vl", "vela");
            this.velaXMLloadDialog = new LoadChooser(extensionFilter, "Open VeLa XML File");
            this.velaXMLsaveDialog = new SaveChooser(extensionFilter, "vlmx", "Save VeLa XML File");
        }

        // These (among other things) are created for each new star.
        this.validObsList = null;
        this.invalidObsList = null;
        this.validObservationCategoryMap = null;
        this.phasedValidObservationCategoryMap = null;
        this.obsAndMeanPlotModel = null;

        this.analysisTypeMap = new HashMap<AnalysisType, AnalysisTypeChangeMessage>();

        this.viewMode = ViewModeType.PLOT_OBS_MODE;
        this.analysisType = AnalysisType.RAW_DATA;

        this.newStarMessageList = new ArrayList<NewStarMessage>();
        this.modelSelectionMessage = null;

        this.periodChangeNotifier.addListener(createPeriodChangeListener());

        this.phaseSelectionNotifier.addListener(createPhaseSelectionListener());

        this.modelSelectionNofitier.addListener(createModelSelectionListener());
        this.filteredObservationNotifier.addListener(createFilteredObservationListener());

        this.seriesCreationNotifier.addListener(createSeriesCreationListener());
    }

    /**
     * Return the Singleton instance, optionally creating it first
     */
    public static synchronized Mediator getInstance() {
        if (mediator == null) {
            mediator = new Mediator();
        }
        return mediator;
    }

    /**
     * @param ui the ui to set
     */
    public void setUI(IMainUI ui) {
        this.ui = ui;
    }

    public static IMainUI getUI() {
        return ui;
    }

    /**
     * @return The current list of valid observations
     */
    public List<ValidObservation> getValidObsList() {
        return validObsList;
    }

    /**
     * @return the latest newStarMessage, or null if none present.
     */
    public NewStarMessage getLatestNewStarMessage() {
        NewStarMessage msg = null;

        if (!newStarMessageList.isEmpty()) {
            msg = newStarMessageList.get(newStarMessageList.size() - 1);
        }

        return msg;
    }

    /**
     * @return the newStarMessageList
     */
    public List<NewStarMessage> getNewStarMessageList() {
        return newStarMessageList;
    }

    /**
     * @return the validObservationCategoryMap
     */
    public Map<SeriesType, List<ValidObservation>> getValidObservationCategoryMap() {
        return validObservationCategoryMap;
    }

    /**
     * Given an analysis type, the plot pane for the specified analysis type.
     * 
     * @param type The analysis type.
     * 
     * @return The plot pane.
     */
    public ObservationAndMeanPlotPane getPlotPane(AnalysisType type) {
        return analysisTypeMap.get(type).getObsAndMeanChartPane();
    }

    /**
     * Given an analysis type, the observation list pane for the specified analysis
     * type.
     * 
     * @param type The analysis type.
     * 
     * @return The observation list pane.
     */
    public ObservationListPane getObservationListPane(AnalysisType type) {
        return analysisTypeMap.get(type).getObsListPane();
    }

    /**
     * @return the modelSelectionMessage
     */
    public ModelSelectionMessage getModelSelectionMessage() {
        return modelSelectionMessage;
    }

    /**
     * @return the analysisTypeChangeNotifier
     */
    public Notifier<AnalysisTypeChangeMessage> getAnalysisTypeChangeNotifier() {
        return analysisTypeChangeNotifier;
    }

    /**
     * @param analysisType the analysisType to set
     */
    public void setAnalysisType(AnalysisType analysisType) {
        this.analysisType = analysisType;
    }

    /**
     * @return the newStarNotifier
     */
    public Notifier<NewStarMessage> getNewStarNotifier() {
        return newStarNotifier;
    }

    /**
     * @return the progressNotifier
     */
    public Notifier<ProgressInfo> getProgressNotifier() {
        return progressNotifier;
    }

    /**
     * @return the discrepantObservationNotifier
     */
    public Notifier<DiscrepantObservationMessage> getDiscrepantObservationNotifier() {
        return discrepantObservationNotifier;
    }

    /**
     * @return the excludedObservationNotifier
     */
    public Notifier<ExcludedObservationMessage> getExcludedObservationNotifier() {
        return excludedObservationNotifier;
    }

    /**
     * @return the observationSelectionNotifier
     */
    public Notifier<ObservationSelectionMessage> getObservationSelectionNotifier() {
        return observationSelectionNotifier;
    }

    /**
     * @return the multipleObservationSelectionNotifier
     */
    public Notifier<MultipleObservationSelectionMessage> getMultipleObservationSelectionNotifier() {
        return multipleObservationSelectionNotifier;
    }

    /**
     * @return the periodAnalysisSelectionNotifier
     */
    public Notifier<PeriodAnalysisSelectionMessage> getPeriodAnalysisSelectionNotifier() {
        return periodAnalysisSelectionNotifier;
    }

    /**
     * @return the periodChangeNotifier
     */
    public Notifier<PeriodChangeMessage> getPeriodChangeNotifier() {
        return periodChangeNotifier;
    }

    /**
     * @return the phaseChangeNotifier
     */
    public Notifier<PhaseChangeMessage> getPhaseChangeNotifier() {
        return phaseChangeNotifier;
    }

    /**
     * @return the phaseSelectionNotifier
     */
    public Notifier<PhaseSelectionMessage> getPhaseSelectionNotifier() {
        return phaseSelectionNotifier;
    }

    /**
     * @return the periodAnalysisRefinementNotifier
     */
    public Notifier<PeriodAnalysisRefinementMessage> getPeriodAnalysisRefinementNotifier() {
        return periodAnalysisRefinementNotifier;
    }

    /**
     * @return the meanSourceSeriesChangeNotifier
     */
    public Notifier<MeanSourceSeriesChangeMessage> getMeanSourceSeriesChangeNotifier() {
        return meanSourceSeriesChangeNotifier;
    }

    /**
     * @return the zoomRequestNotifier
     */
    public Notifier<ZoomRequestMessage> getZoomRequestNotifier() {
        return zoomRequestNotifier;
    }

    /**
     * @return the filteredObservationNotifier
     */
    public Notifier<FilteredObservationMessage> getFilteredObservationNotifier() {
        return filteredObservationNotifier;
    }

    /**
     * @return the modelSelectionNofitier
     */
    public Notifier<ModelSelectionMessage> getModelSelectionNofitier() {
        return modelSelectionNofitier;
    }

    /**
     * @return the modelCreationNotifier
     */
    public Notifier<ModelCreationMessage> getModelCreationNotifier() {
        return modelCreationNotifier;
    }

    /**
     * @return the panRequestNotifier
     */
    public Notifier<PanRequestMessage> getPanRequestNotifier() {
        return panRequestNotifier;
    }

    /**
     * @return the undoActionNotifier
     */
    public Notifier<UndoActionMessage> getUndoActionNotifier() {
        return undoActionNotifier;
    }

    /**
     * @return the stopRequestNotifier
     */
    public Notifier<StopRequestMessage> getStopRequestNotifier() {
        return stopRequestNotifier;
    }

    /**
     * @return the seriesVisibilityChangeNotifier
     */
    public Notifier<SeriesVisibilityChangeMessage> getSeriesVisibilityChangeNotifier() {
        return seriesVisibilityChangeNotifier;
    }

    /**
     * @return the harmonicSearchNotifier
     */
    public Notifier<HarmonicSearchResultMessage> getHarmonicSearchNotifier() {
        return harmonicSearchNotifier;
    }

    /**
     * @return the seriesCreationNotifier
     */
    public Notifier<SeriesCreationMessage> getSeriesCreationNotifier() {
        return seriesCreationNotifier;
    }

    /**
     * @return the velaFileLoadDialog
     */
    public LoadChooser getVelaFileLoadDialog() {
        return velaFileLoadDialog;
    }

    /**
     * @return the velaFileSaveDialog
     */
    public SaveChooser getVelaFileSaveDialog() {
        return velaFileSaveDialog;
    }

    /**
     * @return the velaXMLloadDialog
     */
    public LoadChooser getVelaXMLloadDialog() {
        return velaXMLloadDialog;
    }

    /**
     * @return the velaXMLsaveDialog
     */
    public SaveChooser getVelaXMLsaveDialog() {
        return velaXMLsaveDialog;
    }

    /**
     * Create a mean observation change listener and return it. Whenever the mean
     * series source changes, listeners may want to perform a new period analysis or
     * change the max time increments for means binning.
     */
    private Listener<BinningResult> createMeanObsChangeListener(int initialSeriesNum) {
        final int initialSeriesNumFinal = initialSeriesNum;

        return new Listener<BinningResult>() {
            private int meanSourceSeriesNum = initialSeriesNumFinal;

            public boolean canBeRemoved() {
                return false;
            }

            public void update(BinningResult info) {
                // TODO: would removing this guard permit listeners
                // to do other things, e.g. compare old and new binning results,
                // e.g. for change to days-in-bin?
                if (this.meanSourceSeriesNum != obsAndMeanPlotModel.getMeanSourceSeriesNum()) {

                    this.meanSourceSeriesNum = obsAndMeanPlotModel.getMeanSourceSeriesNum();

                    SeriesType meanSourceSeriesType = obsAndMeanPlotModel.getSeriesNumToSrcTypeMap()
                            .get(this.meanSourceSeriesNum);

                    meanSourceSeriesChangeNotifier
                            .notifyListeners(new MeanSourceSeriesChangeMessage(this, meanSourceSeriesType));
                }
            }
        };
    }

    // When the period changes, create a new phase plot passing the pre-existing
    // series visibility map if a previous phase plot was created.
    //
    // TODO: actually, it should only be necessary to a. set the phases with the
    // new period and epoch (need to include the epoch in the message), and b.
    // update the plot and table models.
    private Listener<PeriodChangeMessage> createPeriodChangeListener() {
        return new Listener<PeriodChangeMessage>() {
            public void update(PeriodChangeMessage info) {
                PhaseParameterDialog phaseDialog = getPhaseParameterDialog();
                phaseDialog.setPeriodField(info.getPeriod());
                phaseDialog.showDialog();

                if (!phaseDialog.isCancelled()) {
                    double period = phaseDialog.getPeriod();
                    double epoch = phaseDialog.getEpoch();

                    AnalysisTypeChangeMessage lastPhasePlotMsg = analysisTypeMap.get(AnalysisType.PHASE_PLOT);

                    Map<SeriesType, Boolean> seriesVisibilityMap = null;

                    if (lastPhasePlotMsg != null) {
                        // Use the last phase plot's series visibility map.
                        seriesVisibilityMap = lastPhasePlotMsg.getObsAndMeanChartPane().getObsModel()
                                .getSeriesVisibilityMap();
                    } else {
                        // There has been no phase plot yet, so use the
                        // light curve's series visibility map.
                        AnalysisTypeChangeMessage lightCurveMsg = analysisTypeMap.get(AnalysisType.RAW_DATA);
                        seriesVisibilityMap = lightCurveMsg.getObsAndMeanChartPane().getObsModel()
                                .getSeriesVisibilityMap();
                    }

                    performPhasePlot(period, epoch, seriesVisibilityMap);
                }
            }

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

    // Returns a phase selection message listener, the purpose of which is to
    // recreate a previous phase plot.
    protected Listener<PhaseSelectionMessage> createPhaseSelectionListener() {
        final Mediator me = this;
        return new Listener<PhaseSelectionMessage>() {
            @Override
            public void update(PhaseSelectionMessage info) {
                if (info.getSource() != me) {
                    performPhasePlot(info.getPeriod(), info.getEpoch(), info.getSeriesVisibilityMap());
                }
            }

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

    /**
     * Removes phase plot if exists.
     * 
     */
    public void dropPhasePlotAnalysis() {
        assert (analysisType != AnalysisType.PHASE_PLOT);
        analysisTypeMap.remove(AnalysisType.PHASE_PLOT);
    }

    /**
     * Create a phase plot, first asking for period and epoch.
     * 
     * The series visibility map for the phase plot is taken from the currently
     * visible plot (raw data or phase plot).
     */
    public void createPhasePlot() {
        PhaseParameterDialog phaseDialog = getPhaseParameterDialog();
        phaseDialog.showDialog();
        if (!phaseDialog.isCancelled()) {
            double period = phaseDialog.getPeriod();
            double epoch = phaseDialog.getEpoch();

            Map<SeriesType, Boolean> seriesVisibilityMap = analysisTypeMap.get(analysisType).getObsAndMeanChartPane()
                    .getObsModel().getSeriesVisibilityMap();

            performPhasePlot(period, epoch, seriesVisibilityMap);
        }
    }

    /**
     * Create a phase plot, given the period and epoch.
     * 
     * The series visibility map for the phase plot is taken from the currently
     * visible plot (raw data or phase plot).
     * 
     * @param period The requested period of the phase plot.
     * @param epoch  The epoch (first Julian Date) for the phase plot.
     */
    public void createPhasePlot(double period, double epoch) {
        Map<SeriesType, Boolean> seriesVisibilityMap = analysisTypeMap.get(analysisType).getObsAndMeanChartPane()
                .getObsModel().getSeriesVisibilityMap();

        performPhasePlot(period, epoch, seriesVisibilityMap);
    }

    /**
     * Common phase plot handler.
     * 
     * @param period              The requested period of the phase plot.
     * @param epoch               The epoch (first Julian Date) for the phase plot.
     * @param seriesVisibilityMap A mapping from series number to visibility status.
     */
    public void performPhasePlot(double period, double epoch, Map<SeriesType, Boolean> seriesVisibilityMap) {

        PhasePlotTask task = new PhasePlotTask(period, epoch, seriesVisibilityMap);

        try {
            currTask = task;
            task.execute();
        } catch (Exception e) {
            Mediator.getUI().setCursor(null);
            MessageBox.showErrorDialog(Mediator.getUI().getComponent(), "New Phase Plot", e);
        }
    }

    /**
     * Perform plugin manager operation.
     */
    public void performPluginManagerOperation(PluginManagementOperation op) {
        PluginManagerOperationTask task = new PluginManagerOperationTask(op);

        try {
            currTask = task;
            task.execute();
        } catch (Exception e) {
            Mediator.getUI().setCursor(null);
            MessageBox.showErrorDialog(Mediator.getUI().getComponent(), "Plugin Manager", e.getLocalizedMessage());
        }
    }

    // ************************************************************************
    // The following listener methods ensure that the obs category map is kept
    // up to date with models, filters, new series. We handle means separately,
    // although it would be more consistent if we did not. For example, perhaps
    // we should just reconstruct this map each time it is required, from the
    // obs model. For user-defined series, the obs list is also updated.
    // Deriving this from the map would also be worth consideration.
    // ************************************************************************

    // Returns a model selection listener that updates the observation
    // category map with model and residuals series.
    private Listener<ModelSelectionMessage> createModelSelectionListener() {
        return new Listener<ModelSelectionMessage>() {
            @Override
            public void update(ModelSelectionMessage info) {
                validObservationCategoryMap.put(SeriesType.Model, info.getModel().getFit());

                validObservationCategoryMap.put(SeriesType.Residuals, info.getModel().getResiduals());

                modelSelectionMessage = info;
            }

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

    // Returns a filtered observation listener that updates the observation
    // category map with the filtered series.
    protected Listener<FilteredObservationMessage> createFilteredObservationListener() {
        return new Listener<FilteredObservationMessage>() {
            @Override
            public void update(FilteredObservationMessage info) {
                if (info == FilteredObservationMessage.NO_FILTER) {
                    validObservationCategoryMap.remove(SeriesType.Filtered);
                } else {
                    // First, copy the set of filtered observations to a list.
                    List<ValidObservation> obs = new ArrayList<ValidObservation>();
                    for (ValidObservation ob : info.getFilteredObs()) {
                        obs.add(ob);
                    }
                    validObservationCategoryMap.put(SeriesType.Filtered, obs);
                }
            }

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

    // Returns a series creation listener that updates the observation
    // category map and observation list with the series.
    protected Listener<SeriesCreationMessage> createSeriesCreationListener() {
        return new Listener<SeriesCreationMessage>() {
            @Override
            public void update(SeriesCreationMessage msg) {
                try {
                    StarInfo info = getLatestNewStarMessage().getStarInfo();
                    info.getRetriever().collectAllObservations(msg.getObs(), info.getRetriever().getSourceName());
                } catch (ObservationReadError e) {
                    MessageBox.showErrorDialog("New Series", "Invalid observations");
                }
            }

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

    /**
     * Change the mode of VStar's focus (i.e what is to be presented to the user).
     * 
     * @param viewMode The mode to change to.
     */
    public void changeViewMode(ViewModeType viewMode) {
        if (viewMode != this.viewMode) {
            this.viewMode = viewMode;
        }
    }

    public ViewModeType getViewMode() {
        return viewMode;
    }

    // Dialog singleton getters

    public PhaseParameterDialog getPhaseParameterDialog() {
        if (phaseParameterDialog == null) {
            phaseParameterDialog = new PhaseParameterDialog();
            newStarNotifier.addListener(phaseParameterDialog, true);
        }

        return phaseParameterDialog;
    }

    public ObservationFilterDialog getObsFilterDialog() {
        if (obsFilterDialog == null) {
            obsFilterDialog = new ObservationFilterDialog();
            newStarNotifier.addListener(obsFilterDialog.createNewStarListener(), true);
            observationSelectionNotifier.addListener(obsFilterDialog.createObservationSelectionListener(), true);
        }

        return obsFilterDialog;
    }

    public ModelDialog getModelDialog() {
        if (modelDialog == null) {
            modelDialog = new ModelDialog();
            newStarNotifier.addListener(modelDialog.createNewStarListener(), true);
            modelCreationNotifier.addListener(modelDialog.createModelCreationListener(), true);
        }

        return modelDialog;
    }

    public PhaseDialog getPhaseDialog() {
        if (phaseDialog == null) {
            phaseDialog = new PhaseDialog();
            newStarNotifier.addListener(phaseDialog.createNewStarListener(), true);
            phaseChangeNotifier.addListener(phaseDialog.createPhaseChangeListener(), true);
        }

        return phaseDialog;
    }

    public ObservationFiltersDialog getObservationFiltersDialog() {
        if (observationFiltersDialog == null) {
            observationFiltersDialog = new ObservationFiltersDialog();
            newStarNotifier.addListener(observationFiltersDialog.createNewStarListener(), true);
            filteredObservationNotifier.addListener(observationFiltersDialog.createFilterListener(), true);
        }

        return observationFiltersDialog;
    }

    public DocumentManager getDocumentManager() {
        if (documentManager == null) {
            documentManager = new DocumentManager();
            phaseChangeNotifier.addListener(documentManager.createPhaseChangeListener(), true);
        }

        return documentManager;
    }

    public UndoableActionManager getUndoableActionManager() {
        if (undoableActionManager == null) {
            undoableActionManager = new UndoableActionManager();
            newStarNotifier.addListener(undoableActionManager.createNewStarListener(), true);
            observationSelectionNotifier.addListener(undoableActionManager.createObservationSelectionListener(), true);
            multipleObservationSelectionNotifier
                    .addListener(undoableActionManager.createMultipleObservationSelectionListener(), true);
        }

        return undoableActionManager;
    }

    /**
     * Get the object that has information about available series and observations
     * pertaining thereto.
     * 
     * @return The series information provider.
     */
    public ISeriesInfoProvider getSeriesInfoProvider() {
        return analysisTypeMap.get(AnalysisType.RAW_DATA).getObsAndMeanChartPane().getObsModel();
    }

    /**
     * Get the observation plot model for the specified analysis type.
     * 
     * @return The observation plot model.
     */
    public ObservationAndMeanPlotModel getObservationPlotModel(AnalysisType type) {
        return analysisTypeMap.get(type).getObsAndMeanChartPane().getObsModel();
    }

    /**
     * Change the analysis type. If the old and new types are the same, there will
     * be no effect.
     * 
     * @param analysisType The analysis type to change to.
     */
    public AnalysisType changeAnalysisType(AnalysisType analysisType) {
        if (this.analysisType != analysisType) {
            try {
                AnalysisTypeChangeMessage msg;

                switch (analysisType) {
                case RAW_DATA:
                    // Create or retrieve raw plots and data tables.
                    // There has to be observations loaded already in order
                    // to be able to switch to raw data analysis mode.
                    msg = this.analysisTypeMap.get(AnalysisType.RAW_DATA);

                    if (msg != null) {
                        this.analysisType = analysisType;
                        this.analysisTypeChangeNotifier.notifyListeners(msg);
                        String statusMsg = "Raw data mode ("
                                + this.getLatestNewStarMessage().getStarInfo().getDesignation() + ")";
                        Mediator.getUI().getStatusPane().setMessage(statusMsg);
                    }
                    break;

                case PHASE_PLOT:
                    // Create or retrieve phase plots and data tables passing
                    // the light curve's series visibility map for the first
                    // phase plot.
                    msg = this.analysisTypeMap.get(AnalysisType.PHASE_PLOT);

                    if (msg == null) {
                        createPhasePlot();
                    } else {
                        // Change to the existing phase plot.
                        this.analysisType = analysisType;
                        this.analysisTypeChangeNotifier.notifyListeners(msg);
                        setPhasePlotStatusMessage();
                    }
                    break;
                }
            } catch (Exception e) {
                MessageBox.showErrorDialog(Mediator.getUI().getComponent(), "Analysis Type Change", e);
            }
        }

        return this.analysisType;
    }

    /**
     * Set the status bar to display phase plot information.
     */
    public void setPhasePlotStatusMessage() {
        String statusMsg = "Phase plot mode (" + this.getLatestNewStarMessage().getStarInfo().getDesignation() + ")";
        Mediator.getUI().getStatusPane().setMessage(statusMsg);
    }

    /**
     * @return the analysisType
     */
    public AnalysisType getAnalysisType() {
        return analysisType;
    }

    /**
     * Remove messages and all listeners that are willing, from all notifiers, to
     * ensure that no old, unnecessary listeners/messages remain from one new-star
     * load to another. Such listeners could receive notifications that make no
     * sense (e.g. location of an observation within a dataset) and guard against
     * memory leaks.
     */
    private void freeListeners() {
        analysisTypeChangeNotifier.cleanup();
        newStarNotifier.cleanup();
        progressNotifier.cleanup();
        discrepantObservationNotifier.cleanup();
        excludedObservationNotifier.cleanup();
        observationSelectionNotifier.cleanup();
        multipleObservationSelectionNotifier.cleanup();
        periodAnalysisSelectionNotifier.cleanup();
        periodChangeNotifier.cleanup();
        phaseChangeNotifier.cleanup();
        phaseSelectionNotifier.cleanup();
        periodAnalysisRefinementNotifier.cleanup();
        meanSourceSeriesChangeNotifier.cleanup();
        zoomRequestNotifier.cleanup();
        filteredObservationNotifier.cleanup();
        modelSelectionNofitier.cleanup();
        modelCreationNotifier.cleanup();
        panRequestNotifier.cleanup();
        undoActionNotifier.cleanup();
        stopRequestNotifier.cleanup();
        seriesVisibilityChangeNotifier.cleanup();
        harmonicSearchNotifier.cleanup();
        observationSelectionNotifier.cleanup();
    }

    /**
     * Creates and executes a background task to handle
     * new-star-from-external-source-plugin.
     * 
     * @param obSourcePlugin The plugin that will be used to obtain observations.
     */
    public void createObservationArtefactsFromObSourcePlugin(ObservationSourcePluginBase obSourcePlugin) {

        this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);
        this.getProgressNotifier().notifyListeners(ProgressInfo.BUSY_PROGRESS);

        NewStarFromObSourcePluginTask task = new NewStarFromObSourcePluginTask(obSourcePlugin);
        this.currTask = task;
        task.configure();
        if (task.isConfigured()) {
            task.execute();
        }
        task.done();
    }

    /**
     * Creates and executes a background task to handle
     * new-star-from-external-source-plugin when a file is supplied.
     * 
     * @param obSourcePlugin The plugin that will be used to obtain observations.
     * @param file           The file to used as input.
     * @param isAdditive     Is this an additive load?
     */
    public void createObservationArtefactsFromObSourcePlugin(ObservationSourcePluginBase obSourcePlugin, File file,
            boolean isAdditive) throws IOException, ObservationReadError {

        this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);
        this.getProgressNotifier().notifyListeners(ProgressInfo.BUSY_PROGRESS);

        NewStarFromObSourcePluginWithSuppliedFileTask task = new NewStarFromObSourcePluginWithSuppliedFileTask(
                obSourcePlugin, file, isAdditive);
        this.currTask = task;
        task.execute();
    }

    /**
     * Creates and executes a background task to handle
     * new-star-from-external-source-plugin when a URL is supplied.
     * 
     * @param obSourcePlugin The plugin that will be used to obtain observations.
     * @param url            The URL to used as input.
     * @param isAdditive     Is this an additive load?
     */
    public void createObservationArtefactsFromObSourcePlugin(ObservationSourcePluginBase obSourcePlugin, URL url,
            boolean isAdditive) throws IOException, ObservationReadError {

        this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);
        this.getProgressNotifier().notifyListeners(ProgressInfo.BUSY_PROGRESS);

        NewStarFromObSourcePluginWithSuppliedURLTask task = new NewStarFromObSourcePluginWithSuppliedURLTask(
                obSourcePlugin, url, isAdditive);
        this.currTask = task;
        task.execute();
    }

    /**
     * Create observation artefacts (models, GUI elements) on the assumption that a
     * valid observation list and category map have already been created.
     * 
     * @param newStarType               The new star enum type.
     * @param starInfo                  Information about the star, e.g. name
     *                                  (designation), AUID (for AID), period,
     *                                  epoch, including observation retriever.
     * @param obsArtefactProgressAmount The amount the progress bar should be
     *                                  incremented by, a value corresponding to a
     *                                  portion of the overall task of which this is
     *                                  just a part.
     * @param addObs                    Should the observations be added to the
     *                                  existing loaded dataset?
     */
    public void createNewStarObservationArtefacts(NewStarType newStarType, StarInfo starInfo,
            int obsArtefactProgressAmount, boolean addObs) throws ObservationReadError {

        // Given raw valid and invalid observation data, create observation
        // table and plot models, along with corresponding GUI components.

        // Handle additive load if requested and observations are already
        // loaded, otherwise, initialise the document manager.
        if (addObs && getLatestNewStarMessage() != null) {
            // convertObsToHJD(starInfo);

            starInfo.getRetriever().collectAllObservations(validObsList, starInfo.getRetriever().getSourceName());

            starInfo.getRetriever().addAllInvalidObservations(invalidObsList);

            // If any loaded data source type is different from the current data
            // source type, use arbitrary data source type that accommodates any
            // data source type. We should change this so that all data sources
            // have the same type!
            for (NewStarMessage msg : getNewStarMessageList()) {
                if (msg.getNewStarType() != newStarType) {
                    newStarType = NewStarType.NEW_STAR_FROM_ARBITRARY_SOURCE;
                    break;
                }
            }
        } else {
            getDocumentManager().init();
        }

        List<ValidObservation> validObsList = starInfo.getRetriever().getValidObservations();

        List<InvalidObservation> invalidObsList = starInfo.getRetriever().getInvalidObservations();

        Map<SeriesType, List<ValidObservation>> newObsCategoryMap = starInfo.getRetriever()
                .getValidObservationCategoryMap();

        // Table models.
        validObsTableModel = null;
        InvalidObservationTableModel invalidObsTableModel = null;
        RawDataMeanObservationTableModel meanObsTableModel = null;

        // Plot models.
        obsAndMeanPlotModel = null;

        // GUI table and chart components.
        ObservationListPane obsListPane = null;
        SyntheticObservationListPane<AbstractMeanObservationTableModel> meansListPane = null;
        ObservationAndMeanPlotPane obsAndMeanChartPane = null;

        if (!validObsList.isEmpty()) {

            freeListeners();

            if (!addObs) {
                newStarMessageList.clear();
            } else {
                // Exclude all but the most recent new star message if the newly
                // loaded dataset's series+observations are the same as that
                // of any previously loaded dataset.
                List<NewStarMessage> dupMessages = new ArrayList<NewStarMessage>();

                for (NewStarMessage msg : getNewStarMessageList()) {
                    // We need to check that both SeriesType and the corresponding observations are
                    // the same.
                    if (newObsCategoryMap.entrySet().equals(msg.getObsCategoryMap().entrySet())) {
                        dupMessages.add(msg);
                    }
                }

                for (NewStarMessage msg : dupMessages) {
                    newStarMessageList.remove(msg);
                }
            }

            // Create a message to notify whoever is listening that a new star
            // has been loaded.
            NewStarMessage newStarMsg = new NewStarMessage(newStarType, starInfo, validObsList, newObsCategoryMap,
                    starInfo.getRetriever().getMinMag(), starInfo.getRetriever().getMaxMag(),
                    starInfo.getRetriever().getSourceName());

            newStarMessageList.add(newStarMsg);

            // This is a specific fix for tracker 3007948.
            this.discrepantObservationNotifier = new Notifier<DiscrepantObservationMessage>();

            // Observation table and plot.
            validObsTableModel = new ValidObservationTableModel(newObsCategoryMap, validObsList,
                    newStarType.getRawDataTableColumnInfoSource());

            // Observation-and-mean table and plot.
            obsAndMeanPlotModel = new ObservationAndMeanPlotModel(newObsCategoryMap, JDCoordSource.instance,
                    JDComparator.instance, JDTimeElementEntity.instance, null);

            if (false) {
                // Record initial ANOVA information and make the document
                // manager
                // listen to changes to ANOVA via new binning results.
                getDocumentManager().updateAnovaInfo(obsAndMeanPlotModel.getBinningResult());
            }

            obsAndMeanPlotModel.getMeansChangeNotifier().addListener(getDocumentManager().createBinChangeListener());

            getDocumentManager().addStatsInfo("Confidence Interval",
                    "Mean error bars denote 95% Confidence Interval (twice Standard Error)");

            String plotName;
            String designation = starInfo.getDesignation();
            // If star's designation starts with '^', do not prefix it with "Light Curve
            // for"
            if (designation != null && designation.length() > 0 && "^".equals(designation.substring(0, 1)))
                plotName = designation.substring(1);
            else
                plotName = LocaleProps.get("LIGHT_CURVE") + " " + LocaleProps.get("FOR") + " " + designation;
            obsAndMeanChartPane = createObservationAndMeanPlotPane(plotName, null, obsAndMeanPlotModel,
                    starInfo.getRetriever());

            obsAndMeanPlotModel.getMeansChangeNotifier()
                    .addListener(createMeanObsChangeListener(obsAndMeanPlotModel.getMeanSourceSeriesNum()));

            // The mean observation table model must listen to the plot
            // model to know when the means data has changed. We also pass
            // the initial means data obtained from the plot model to
            // the mean observation table model.
            meanObsTableModel = new RawDataMeanObservationTableModel(obsAndMeanPlotModel.getMeanObsList());

            obsAndMeanPlotModel.getMeansChangeNotifier().addListener(meanObsTableModel);

            if (obsArtefactProgressAmount > 0) {
                // Update progress.
                getProgressNotifier()
                        .notifyListeners(new ProgressInfo(ProgressType.INCREMENT_PROGRESS, obsArtefactProgressAmount));
            }

            if (!invalidObsList.isEmpty()) {
                invalidObsTableModel = new InvalidObservationTableModel(invalidObsList);
            }

            // The observation table pane contains valid and potentially
            // invalid data components. Tell the valid data table to have
            // a horizontal scrollbar if there will be too many columns.

            boolean enableColumnAutoResize = newStarType == NewStarType.NEW_STAR_FROM_SIMPLE_FILE
                    || (newStarType == NewStarType.NEW_STAR_FROM_ARBITRARY_SOURCE && !addObs);

            obsListPane = new ObservationListPane(starInfo.getDesignation(), validObsTableModel, invalidObsTableModel,
                    enableColumnAutoResize, obsAndMeanPlotModel.getVisibleSeries(), AnalysisType.RAW_DATA);

            // We also create the means list pane.
            meansListPane = new SyntheticObservationListPane<AbstractMeanObservationTableModel>(meanObsTableModel,
                    null);

            // Create a message to notify whoever is listening that the analysis
            // type has changed (we could have been viewing a phase plot for a
            // different star before now) passing GUI components in the message.
            analysisType = AnalysisType.RAW_DATA;

            AnalysisTypeChangeMessage analysisTypeMsg = new AnalysisTypeChangeMessage(analysisType, obsAndMeanChartPane,
                    obsListPane, meansListPane, ViewModeType.PLOT_OBS_MODE);

            // Commit to using the new observation lists and category map,
            // first making old values available for garbage collection.
            // TODO: It would be worth considering doing this at the start
            // of this method, not at the end, so more memory is free.

            if (this.validObsList != null) {
                this.validObsList.clear();
            }

            if (this.invalidObsList != null) {
                this.invalidObsList.clear();
            }

            if (this.validObservationCategoryMap != null) {
                this.validObservationCategoryMap.clear();
            }

            if (this.phasedValidObservationCategoryMap != null) {
                // In case we did a phase plot, free this up.
                this.phasedValidObservationCategoryMap.clear();
                this.phasedValidObservationCategoryMap = null;
            }

            // Throw away old artefacts from raw and phase plot,
            // if there was (at least) one.
            analysisTypeMap.clear();
            analysisTypeMap.put(analysisType, analysisTypeMsg);

            // Suggest garbage collection.
            System.gc();

            // Store new data.
            this.validObsList = validObsList;
            this.invalidObsList = invalidObsList;
            this.validObservationCategoryMap = newObsCategoryMap;

            // Notify listeners of new star and analysis type.
            newStarNotifier.notifyListeners(getLatestNewStarMessage());
            analysisTypeChangeNotifier.notifyListeners(analysisTypeMsg);
        }
    }

    // Request the J2000.0 RA in HH:MM:SS.n
    public RAInfo requestRA(RAInfo ra) {
        Integer h = null;
        Integer m = null;
        Double s = null;

        if (ra != null) {
            Triple<Integer, Integer, Double> hms = ra.toHMS();
            h = hms.first;
            m = hms.second;
            s = hms.third;
        }

        IntegerField raHours = new IntegerField("Hours", 0, 23, h);
        IntegerField raMinutes = new IntegerField("Minutes", 0, 59, m);
        DoubleField raSeconds = new DoubleField("Seconds", 0.0, 59.99, s);
        MultiEntryComponentDialog dialog = new MultiEntryComponentDialog("RA (" + EpochType.J2000 + ")", raHours,
                raMinutes, raSeconds);

        RAInfo raInfo = null;
        if (!dialog.isCancelled()) {
            raInfo = new RAInfo(EpochType.J2000, raHours.getValue(), raMinutes.getValue(), raSeconds.getValue());
        }

        return raInfo;
    }

    // Request the J2000.0 RA in HH:MM:SS.n
    public RAInfo requestRA() {
        return requestRA(null);
    }

    // Request the J2000.0 Dec in DD:MM:SS.n
    public DecInfo requestDec(DecInfo dec) {
        Integer d = null;
        Integer m = null;
        Double s = null;

        if (dec != null) {
            Triple<Integer, Integer, Double> dms = dec.toDMS();
            d = dms.first;
            m = dms.second;
            s = dms.third;
        }

        IntegerField decDegrees = new IntegerField("Degrees", -90, 90, d);
        IntegerField decMinutes = new IntegerField("Minutes", 0, 59, m);
        DoubleField decSeconds = new DoubleField("Seconds", 0.0, 59.99, s);

        DecInfo decInfo;
        while (true) {
            decInfo = null;
            MultiEntryComponentDialog dialog = new MultiEntryComponentDialog("Dec (" + EpochType.J2000 + ")",
                    decDegrees, decMinutes, decSeconds);
            if (dialog.isCancelled())
                break;
            decInfo = new DecInfo(EpochType.J2000, decDegrees.getValue(), decMinutes.getValue(), decSeconds.getValue());

            double degrees = decInfo.toDegrees();

            // If Degrees = 90 and Min or Sec > 0, the resulted value is out of range.
            if (degrees >= -90.0 && degrees <= 90.0)
                break;

            MessageBox.showErrorDialog(Mediator.getUI().getComponent(), "Error",
                    "Please check input: Dec must be between -90.0 and 90.0");
        }

        return decInfo;
    }

    // Request the J2000.0 Dec in DD:MM:SS.n
    public DecInfo requestDec() {
        return requestDec(null);
    }

    /**
     * Convert the specified observations to use HJD (if not already) rather than
     * JD.
     * 
     * @param obs The list of observations to be converted.
     * @param ra  The RA for the object.
     * @param dec The Dec for the object.
     * @return The number of observations converted.
     */
    public int convertObsToHJD(List<ValidObservation> obs, RAInfo ra, DecInfo dec) {
        int count = 0;

        AbstractHJDConverter converter = AbstractHJDConverter.getInstance(ra.getEpoch());

        for (ValidObservation ob : obs) {
            if (ob.getJDflavour() == JDflavour.JD) {
                ob.setJD(converter.convert(ob.getJD(), ra, dec));
                ob.setJDflavour(JDflavour.HJD);
                count++;
            }
        }

        return count;
    }

    /**
     * Create phase plot artefacts, adding them to the analysis type map and
     * returning this message.
     * 
     * @param period              The requested period of the phase plot.
     * @param epoch               The epoch (first Julian Date) for the phase plot.
     * @param seriesVisibilityMap A mapping from series number to visibility status.
     * @return An analysis type message consisting of phase plot artefacts.
     */
    public AnalysisTypeChangeMessage createPhasePlotArtefacts(double period, double epoch,
            Map<SeriesType, Boolean> seriesVisibilityMap) throws Exception {
        String objName = getLatestNewStarMessage().getStarInfo().getDesignation();

        String subTitle = "";
        String periodAndEpochStr = String.format(
                LocaleProps.get("PERIOD") + ": %s, " + LocaleProps.get("EPOCH") + ": %s",
                NumericPrecisionPrefs.formatOther(period), NumericPrecisionPrefs.formatTime(epoch));

        if (this.getLatestNewStarMessage().getNewStarType() == NewStarType.NEW_STAR_FROM_DATABASE) {
            Date now = Calendar.getInstance().getTime();
            String formattedDate = DateFormat.getDateInstance().format(now);
            subTitle = formattedDate + " (" + LocaleProps.get("DATABASE") + "), " + periodAndEpochStr;
        } else {
            subTitle = periodAndEpochStr;
        }

        // Here we modify the underlying ValidObservation objects which will
        // affect both validObsList and validObservationCategoryMap. Some
        // series are not in the main observation list, only in the map
        // (e.g. model, residuals, filtered obs), so we handle those separately.
        PhaseCalcs.setPhases(validObsList, epoch, period);
        setPhasesForSeries(SeriesType.Model, epoch, period);
        setPhasesForSeries(SeriesType.Residuals, epoch, period);
        setPhasesForSeries(SeriesType.Filtered, epoch, period);

        // We duplicate the valid observation category map
        // so that it can vary from the main plot's over time.
        // TODO: but is it ever mutated in the plot models? is it enough to
        // duplicate and sort means?
        Map<SeriesType, List<ValidObservation>> phasedValidObservationCategoryMap = new TreeMap<SeriesType, List<ValidObservation>>();

        for (SeriesType series : validObservationCategoryMap.keySet()) {
            List<ValidObservation> obs = validObservationCategoryMap.get(series);

            List<ValidObservation> phasedObs = new ArrayList<ValidObservation>(obs);

            Collections.sort(phasedObs, StandardPhaseComparator.instance);

            phasedValidObservationCategoryMap.put(series, phasedObs);
        }

        // TODO:
        // o fix occurrences of obs doubling and just copy and sort
        // o indeed: is this needed now anyway? see plot model/pane code

        // Table and plot models.
        ValidObservationTableModel validObsTableModel = new ValidObservationTableModel(
                phasedValidObservationCategoryMap, validObsList, // TODO: phased obs?
                getLatestNewStarMessage().getNewStarType().getPhasePlotTableColumnInfoSource());

        // Observation-and-mean plot and table.
        ContinuousModelFunction rawModelFuncModel = obsAndMeanPlotModel.getModelFunction();

        ContinuousModelFunction prevCyclePhaseModelFuncModel = null;
        ContinuousModelFunction stdPhaseModelFuncModel = null;
        int modelFuncSeriesNum = ObservationAndMeanPlotModel.NO_SERIES;

        if (rawModelFuncModel != null) {
            // Use sorted fit from category map; this will also be compatible,
            // order-wise, with previous cycle phase.
            List<ValidObservation> phasedFit = phasedValidObservationCategoryMap.get(SeriesType.Model);

            prevCyclePhaseModelFuncModel = new ContinuousModelFunction(rawModelFuncModel.getFunction(), phasedFit,
                    rawModelFuncModel.getZeroPoint(), PreviousCyclePhaseCoordSource.instance);

            stdPhaseModelFuncModel = new ContinuousModelFunction(rawModelFuncModel.getFunction(), phasedFit,
                    rawModelFuncModel.getZeroPoint(), StandardPhaseCoordSource.instance);

            modelFuncSeriesNum = obsAndMeanPlotModel.getModelFunctionSeriesNum();
        }

        PhasedObservationAndMeanPlotModel obsAndMeanPlotModel1 = new PhasedObservationAndMeanPlotModel(
                phasedValidObservationCategoryMap, PreviousCyclePhaseCoordSource.instance,
                PreviousCyclePhaseComparator.instance, PhaseTimeElementEntity.instance, seriesVisibilityMap,
                prevCyclePhaseModelFuncModel, modelFuncSeriesNum);

        if (prevCyclePhaseModelFuncModel != null) {
            prevCyclePhaseModelFuncModel.setPpModel(obsAndMeanPlotModel1);
        }

        PhasedObservationAndMeanPlotModel obsAndMeanPlotModel2 = new PhasedObservationAndMeanPlotModel(
                phasedValidObservationCategoryMap, StandardPhaseCoordSource.instance, StandardPhaseComparator.instance,
                PhaseTimeElementEntity.instance, seriesVisibilityMap, stdPhaseModelFuncModel, modelFuncSeriesNum);

        if (stdPhaseModelFuncModel != null) {
            stdPhaseModelFuncModel.setPpModel(obsAndMeanPlotModel2);
        }

        // Select an arbitrary model for mean.
        obsAndMeanPlotModel = obsAndMeanPlotModel1;

        // The mean observation table model must listen to the plot
        // model to know when the means data has changed. We also pass
        // the initial means data obtained from the plot model to
        // the mean observation table model.
        PhasePlotMeanObservationTableModel meanObsTableModel = new PhasePlotMeanObservationTableModel(
                obsAndMeanPlotModel1.getMeanObsList());

        obsAndMeanPlotModel1.getMeansChangeNotifier().addListener(meanObsTableModel);

        obsAndMeanPlotModel2.getMeansChangeNotifier().addListener(meanObsTableModel);

        // If star's name starts with '^', remove the first char (see "LIGHT_CURVE")
        if (objName != null && objName.length() > 0 && "^".equals(objName.substring(0, 1)))
            objName = objName.substring(1);
        PhaseAndMeanPlotPane obsAndMeanChartPane = createPhaseAndMeanPlotPane(
                LocaleProps.get("PHASE_PLOT") + " " + LocaleProps.get("FOR") + " " + objName, subTitle,
                obsAndMeanPlotModel1, obsAndMeanPlotModel2, epoch, period,
                getLatestNewStarMessage().getStarInfo().getRetriever());

        // The observation table pane contains valid and potentially
        // invalid data components but for phase plot purposes, we only
        // display valid data, as opposed to the raw data view in which
        // both are shown. Tell the valid data table to have a horizontal
        // scrollbar if there will be too many columns.
        boolean enableColumnAutoResize = getLatestNewStarMessage()
                .getNewStarType() == NewStarType.NEW_STAR_FROM_SIMPLE_FILE
                || getLatestNewStarMessage().getNewStarType() == NewStarType.NEW_STAR_FROM_ARBITRARY_SOURCE;

        ObservationListPane obsListPane = new ObservationListPane(objName, validObsTableModel, null,
                enableColumnAutoResize, obsAndMeanPlotModel1.getVisibleSeries(), AnalysisType.PHASE_PLOT);

        SyntheticObservationListPane<AbstractMeanObservationTableModel> meansListPane = new SyntheticObservationListPane<AbstractMeanObservationTableModel>(
                meanObsTableModel, null);

        // Create a phase change message so that existing plot and tables can
        // update their GUI components and/or models accordingly. Also,
        // recording the series visibility map permits the existence of a phase
        // change creation listener that collects phase change messages for the
        // purpose of later being able to re-create the same phase plot.
        PhaseChangeMessage phaseChangeMessage = new PhaseChangeMessage(this, period, epoch, seriesVisibilityMap);
        phaseChangeNotifier.notifyListeners(phaseChangeMessage);

        // Observation-and-mean table and plot.
        AnalysisTypeChangeMessage phasePlotMsg = new AnalysisTypeChangeMessage(AnalysisType.PHASE_PLOT,
                obsAndMeanChartPane, obsListPane, meansListPane, ViewModeType.PLOT_OBS_MODE);

        analysisTypeMap.put(AnalysisType.PHASE_PLOT, phasePlotMsg);

        analysisTypeChangeNotifier.notifyListeners(phasePlotMsg);

        return phasePlotMsg;
    }

    /**
     * Set the phases for a particular series in the observation category map.
     * 
     * @param type   The series type of the observations whose phases are to be set.
     * @param epoch  The epoch to use for the phase calculation.
     * @param period The period to use for the phase calculation.
     */
    public void setPhasesForSeries(SeriesType type, double epoch, double period) {
        if (validObservationCategoryMap.containsKey(type)) {
            List<ValidObservation> obs = validObservationCategoryMap.get(type);
            PhaseCalcs.setPhases(obs, epoch, period);
        }
    }

    /**
     * Block, waiting for a job to complete. We only want to block if there is a
     * concurrent task in progress.
     */
    public void waitForJobCompletion() {
        if (currTask != null && !currTask.isDone()) {
            try {
                currTask.get();
            } catch (InterruptedException e) {
            } catch (ExecutionException e) {
            }
        }
    }

    /**
     * Attempt to stop the current task.
     */
    public void stopCurrentTask() {
        if (this.currTask != null) {
            this.currTask.cancel(true);
        }
    }

    /**
     * Clear the current task if not already cleared.
     */
    public void clearCurrentTask() {
        if (this.currTask != null) {
            this.currTask = null;
        }
    }

    /**
     * Create the observation-and-mean plot pane for the current list of valid
     * observations.
     */
    private ObservationAndMeanPlotPane createObservationAndMeanPlotPane(String plotName, String subTitle,
            ObservationAndMeanPlotModel obsAndMeanPlotModel, AbstractObservationRetriever retriever) {

        Dimension bounds = new Dimension((int) (TabbedDataPane.WIDTH * 0.9), (int) (TabbedDataPane.HEIGHT * 0.9));

        return new ObservationAndMeanPlotPane(plotName, subTitle, obsAndMeanPlotModel, bounds, retriever, AnalysisType.RAW_DATA);
    }

    /**
     * Create the observation-and-mean phase plot pane for the current list of valid
     * observations.
     */
    private PhaseAndMeanPlotPane createPhaseAndMeanPlotPane(String plotName, String subTitle,
            PhasedObservationAndMeanPlotModel obsAndMeanPlotModel1,
            PhasedObservationAndMeanPlotModel obsAndMeanPlotModel2, double epoch, double period,
            AbstractObservationRetriever retriever) {

        Dimension bounds = new Dimension((int) (TabbedDataPane.WIDTH * 0.9), (int) (TabbedDataPane.HEIGHT * 0.9));

        return new PhaseAndMeanPlotPane(plotName, subTitle, bounds, epoch, period, retriever, obsAndMeanPlotModel1,
                obsAndMeanPlotModel2);
    }

    /**
     * Create a period analysis dialog after the analysis is done. It only makes
     * sense to apply the observations to a single band as per this Q & A between
     * Matt Templeton and I:<br/>
     * DB: Like mean curve creation in VStar, should we only apply DC DFT to a
     * single band, e.g. visual? MT: Yes, because of two things: 1) The different
     * bands will have different mean values, and 2) The different bands will have
     * different amplitudes or frequencies depending on what is physically causing
     * the variation. Variability caused by temperature changes can have wildly
     * different amplitudes in U or B versus Rc or Ic.
     */
    public void performPeriodAnalysis(PeriodAnalysisPluginBase plugin) {
        try {
            if (getLatestNewStarMessage() != null && validObsList != null) {
                SingleSeriesSelectionDialog dialog = new SingleSeriesSelectionDialog(obsAndMeanPlotModel);

                if (!dialog.isCancelled()) {
                    SeriesType type = dialog.getSeries();

                    List<ValidObservation> obs = getSeriesInfoProvider().getObservations(type);

                    this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);
                    this.getProgressNotifier().notifyListeners(ProgressInfo.BUSY_PROGRESS);

                    PeriodAnalysisTask task = new PeriodAnalysisTask(plugin, type, obs);

                    this.currTask = task;
                    task.execute();
                }
            }
        } catch (Exception e) {
            MessageBox.showErrorDialog(Mediator.getUI().getComponent(), LocaleProps.get("PERIOD_ANALYSIS"), e);

            // TODO: why not ProgressInfo.COMPLETE_PROGRESS then
            // ProgressInfo.CLEAR_PROGRESS?
            this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

            Mediator.getUI().getStatusPane().setMessage("");
        }
    }

    /**
     * Open the plot control dialog relevant to the current analysis mode.<br/>
     */
    public void showPlotControlDialog() {
        String title = null;
        ObservationAndMeanPlotPane plotPane = analysisTypeMap.get(analysisType).getObsAndMeanChartPane();
        TimeElementsInBinSettingPane binSettingPane = null;
        NamedComponent extra = null;

        if (analysisType == AnalysisType.RAW_DATA) {
            title = LocaleProps.get("LIGHT_CURVE_CONTROL_DLG_TITLE");
            binSettingPane = new TimeElementsInBinSettingPane(LocaleProps.get("DAYS_PER_MEAN_SERIES_BIN"), plotPane,
                    JDTimeElementEntity.instance);
        } else if (analysisType == AnalysisType.PHASE_PLOT) {
            title = LocaleProps.get("PHASE_PLOT_CONTROL_DLG_TITLE");
            binSettingPane = new TimeElementsInBinSettingPane(LocaleProps.get("PHASE_STEPS_PER_MEAN_SERIES_BIN"),
                    plotPane, PhaseTimeElementEntity.instance);
        }

        PlotControlDialog dialog = new PlotControlDialog(title, plotPane, binSettingPane, extra, analysisType);
        dialog.setVisible(true);
    }

    /**
     * Open the model dialog.
     */
    public void showModelDialog() {
        getModelDialog().showDialog();
    }

    /**
     * Open the phase plots dialog.
     */
    public void showPhaseDialog() {
        getPhaseDialog().showDialog();
    }

    /**
     * Opens the filters dialog.
     */
    public void showFiltersDialog() {
        getObservationFiltersDialog().showDialog();
    }

    /**
     * Perform a plugin based modeling operation.
     */
    public void performModellingOperation(ModelCreatorPluginBase plugin) {
        try {
            if (getLatestNewStarMessage() != null && validObsList != null) {
                SingleSeriesSelectionDialog seriesDialog = new SingleSeriesSelectionDialog(obsAndMeanPlotModel);

                if (!seriesDialog.isCancelled()) {
                    SeriesType type = seriesDialog.getSeries();

                    List<ValidObservation> obs = getSeriesInfoProvider().getObservations(type);

                    // TODO: possibly need to add: getUI()/invokeDialog()
                    // compare with modelling and obs src plugins

                    IModel model = plugin.getModel(obs);

                    if (model != null) {
                        ModellingTask task = new ModellingTask(model);

                        this.currTask = task;

                        this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);
                        this.getProgressNotifier().notifyListeners(ProgressInfo.BUSY_PROGRESS);

                        task.execute();
                    }
                }
            }
        } catch (Exception e) {
            MessageBox.showErrorDialog(Mediator.getUI().getComponent(), "Modelling Error", e);

            this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

            Mediator.getUI().getStatusPane().setMessage("");
        }
    }

    /**
     * Perform a non-plugin based modeling operation.
     * 
     * @param model The model object for this plugin whose execute() method can be
     *              invoked to create the model artifacts.
     */
    public void performModellingOperation(IModel model) {
        try {
            ModellingTask task = new ModellingTask(model);

            this.currTask = task;

            this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);
            this.getProgressNotifier().notifyListeners(ProgressInfo.BUSY_PROGRESS);

            task.execute();
        } catch (Exception e) {
            MessageBox.showErrorDialog(Mediator.getUI().getComponent(), "Modelling Error", e);

            this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

            Mediator.getUI().getStatusPane().setMessage("");
        }
    }

    /**
     * Perform an observation transformation plugin operation.
     */
    public void performObservationTransformationOperation(ObservationTransformerPluginBase plugin) {
        try {
            if (getLatestNewStarMessage() != null && validObsList != null) {
                SeriesVisibilityPane seriesVisibilityPane = new SeriesVisibilityPane(obsAndMeanPlotModel,
                        this.getAnalysisType(), false, false, false);

                MultipleSeriesSelectionDialog seriesDialog = new MultipleSeriesSelectionDialog(seriesVisibilityPane);

                if (!seriesDialog.isCancelled()) {
                    IUndoableAction action = plugin.createAction(Mediator.getInstance().getSeriesInfoProvider(),
                            seriesDialog.getSelectedSeries());

                    currTask = getUndoableActionManager().performUndoableAction(action, UndoableActionType.DO);
                }
            }
        } catch (Exception e) {
            MessageBox.showErrorDialog(Mediator.getUI().getComponent(), "Observation Transformartion Error",
                    e.getLocalizedMessage());

            this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

            Mediator.getUI().getStatusPane().setMessage("");
        }
    }

    /**
     * Fire data model change events.<br/>
     * This method should be called after observation plot or table model changes.
     */
    public void updatePlotsAndTables() {
        getObservationPlotModel(AnalysisType.RAW_DATA).update();

        if (analysisTypeMap.containsKey(AnalysisType.PHASE_PLOT)) {
            ObservationAndMeanPlotModel phase_model = getObservationPlotModel(AnalysisType.PHASE_PLOT);
            phase_model.update();
        }

        validObsTableModel.fireTableDataChanged();
    }

    /**
     * Invokes a tool plugin with the currently loaded observations.
     * 
     * @param plugin The tool plugin to be invoked.
     */
    public void invokeTool(ObservationToolPluginBase plugin) {
        if (validObservationCategoryMap != null) {
            try {
                // getSeriesInfoProvider() always returns series info for RAW_DATA.
                // PHASE_PLOT can have other series visibility, so it is more logical to pass
                // series info for the active analysis.
                ISeriesInfoProvider seriesInfo = analysisTypeMap.get(analysisType).getObsAndMeanChartPane()
                        .getObsModel();
                plugin.invoke(seriesInfo);
                // plugin.invoke(getSeriesInfoProvider());
            } catch (Throwable t) {
                MessageBox.showErrorDialog("Tool Error", t);
            }
        } else {
            MessageBox.showMessageDialog(Mediator.getUI().getComponent(), "Tool Error",
                    "There are no observations loaded.");
        }
    }

    /**
     * Apply the custom filter plug-in to the currently loaded observation set.
     * 
     * @param plugin The tool plug-in to be invoked.
     */
    public void applyCustomFilterToCurrentObservations(CustomFilterPluginBase plugin) {
        if (validObsList != null) {
            try {
                plugin.apply(validObsList);
            } catch (Throwable t) {
                MessageBox.showErrorDialog("Custom Filter Error", t);
            }
        } else {
            MessageBox.showMessageDialog(Mediator.getUI().getComponent(), "Custom Filter",
                    "There are no observations loaded.");
        }
    }

    /**
     * Create a filter from the current plot view.
     */
    public void createFilterFromPlot() {
        InViewObservationFilter filter = new InViewObservationFilter();
        filter.execute();
    }

    /**
     * Save the artefact corresponding to the current viewMode.
     * 
     * @param parent The parent component to be used in dialogs.
     */
    public void saveCurrentMode(Component parent) {
        List<ValidObservation> obs = null;

        switch (viewMode) {
        case PLOT_OBS_MODE:
            savePlotToFile(parent);
            break;
        case LIST_OBS_MODE:
            saveObsListToFile(parent);
            break;
        case LIST_MEANS_MODE:
            obs = analysisTypeMap.get(analysisType).getMeansListPane().getObsTableModel().getObs();
            saveSyntheticObsListToFile(parent, obs);
            break;
        case MODEL_MODE:
            if (modelSelectionMessage != null) {
                obs = modelSelectionMessage.getModel().getFit();
                saveSyntheticObsListToFile(parent, obs);
            }
            break;
        case RESIDUALS_MODE:
            if (modelSelectionMessage != null) {
                obs = modelSelectionMessage.getModel().getResiduals();
                saveSyntheticObsListToFile(parent, obs);
            }
            break;
        }
    }

    /**
     * Save the current plot (as a PNG) to the specified file.<br/>
     * Used by VStar scripting API.
     * 
     * @param path   The file to write the PNG image to.
     * @param width  The desired width of the image.
     * @param height The desired height of the image.
     */
    public void saveCurrentPlotToFile(File file, int width, int height) {
        ChartPanel chart = analysisTypeMap.get(analysisType).getObsAndMeanChartPane().getChartPanel();

        try {
            //ChartUtils.saveChartAsPNG(file, chart.getChart(), width, height);
        	savePlotToFileHelper(file, chart.getChart(), width, height, ChartPropertiesPrefs.getScaleFactor());
        } catch (IOException e) {
            MessageBox.showErrorDialog("Save plot to file", "Cannot save plot to " + "'" + file.getPath() + "'.");
        }
    }

    /**
     * Save observation list to a file in a separate thread. Note that we want to
     * save just those observations that are in view in the observation list
     * currently.
     * 
     * Used by the VStar Scripting API.
     * 
     * @param parent    The parent component to be used in dialogs.
     * @param plugin    The observation sink plugin.
     * @param path      The path of the file to save to.
     * @param delimiter The delimiter between data items.
     */
    public void saveObsListToFile(Component parent, ObservationSinkPluginBase plugin, File path, String delimiter) {
        List<ValidObservation> obs = this.analysisTypeMap.get(analysisType).getObsListPane().getObservationsInView();

        if (!obs.isEmpty()) {
            this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

            this.getProgressNotifier().notifyListeners(new ProgressInfo(ProgressType.MAX_PROGRESS, obs.size()));

            ObsListFileSaveTask task = new ObsListFileSaveTask(plugin, obs, path, delimiter);

            this.currTask = task;
            task.execute();
        } else {
            MessageBox.showMessageDialog(parent, "Save Observations", "There are no visible observations to save.");
        }
    }

    /**
     * Save synthetic observation list (means, model, residuals) to a file in a
     * separate thread.<br/>
     * 
     * Used by VStar scripting API.
     * 
     * @param parent    The parent component to be used in dialogs.
     * @param plugin    The observation sink plugin.
     * @param mode      The current synthetic view mode.
     * @param path      The path of the file to save to.
     * @param delimiter The delimiter between data items.
     */
    public void saveSyntheticObsListToFile(Component parent, ObservationSinkPluginBase plugin, ViewModeType mode,
            File path, String delimiter) {

        List<ValidObservation> obs = null;

        switch (mode) {
        case LIST_MEANS_MODE:
            obs = analysisTypeMap.get(analysisType).getMeansListPane().getObsTableModel().getObs();
            break;
        case MODEL_MODE:
            if (modelSelectionMessage != null) {
                obs = modelSelectionMessage.getModel().getFit();
            } else {
                MessageBox.showMessageDialog(parent, "Save Observations", "There are no observations to save.");
            }
            break;
        case RESIDUALS_MODE:
            if (modelSelectionMessage != null) {
                obs = modelSelectionMessage.getModel().getResiduals();
            } else {
                MessageBox.showMessageDialog(parent, "Save Observations", "There are no observations to save.");
            }
            // Note: we include these for completeness otherwise the type
            // checker will complain. We assert that we should never arrive
            // here. We could of course merge this method and
            // saveObsListToFile().
        case LIST_OBS_MODE:
        case PLOT_OBS_MODE:
            assert (false);
            break;
        }

        if (!obs.isEmpty()) {
            this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

            this.getProgressNotifier().notifyListeners(new ProgressInfo(ProgressType.MAX_PROGRESS, obs.size()));

            // We re-use the same observation list file save task as
            // elsewhere but specify simple file type to match the fact that
            // we are only going to save JD, magnitude, and uncertainty
            // (for means).
            ObsListFileSaveTask task = new ObsListFileSaveTask(plugin, obs, path, obsListFileSaveDialog.getDelimiter());

            this.currTask = task;
            task.execute();
        } else {
            MessageBox.showMessageDialog(parent, "Save Observations", "There are no observations to save.");
        }
    }

    /**
     * Save the currently visible mode's plot. The file is requested from the user
     * via a dialog.
     * 
     * @param parent The parent component of the file dialog.
     */
    private void savePlotToFile(Component parent) {
        try {
            ChartPanel chartPanel = analysisTypeMap.get(analysisType).getObsAndMeanChartPane().getChartPanel();

            if (imageSaveDialog.showDialog(parent)) {
                File path = imageSaveDialog.getSelectedFile();
                if (path.exists() && path.isFile() && !MessageBox.showConfirmDialog(LocaleProps.get("FILE_MENU_SAVE"),
                        LocaleProps.get("SAVE_OVERWRITE"))) {
                    return;
                }
                JFreeChart chart = chartPanel.getChart();
                int width = chartPanel.getWidth();
                int height = chartPanel.getHeight();

                //ChartUtils.saveChartAsPNG(path, chart, width, height);
                savePlotToFileHelper(path, chart, width, height, ChartPropertiesPrefs.getScaleFactor());
            }
        } catch (IOException ex) {
            MessageBox.showErrorDialog(parent, "Save Observation and Means Plot", ex.getMessage());
        }
    }

    private void savePlotToFileHelper(File path, JFreeChart chart, int width, int height, int scale)
    		throws FileNotFoundException, IOException {
    	try (FileOutputStream out = new FileOutputStream(path)) {
            ChartUtils.writeScaledChartAsPNG(out, chart, width, height, scale, scale);
        }
    }
    
    /**
     * Save observation list to a file in a separate thread. Note that we want to
     * save just those observations that are in view in the observation list
     * currently. The file is requested from the user via a dialog.
     * 
     * @param parent The parent component to be used in dialogs.
     */
    private void saveObsListToFile(Component parent) {
        List<ValidObservation> obs = this.analysisTypeMap.get(analysisType).getObsListPane().getObservationsInView();

        if (!obs.isEmpty()) {
            if (obsListFileSaveDialog.showDialog(parent)) {
                File outFile = obsListFileSaveDialog.getSelectedFile();

                if (outFile.exists() && outFile.isFile() && !MessageBox
                        .showConfirmDialog(LocaleProps.get("FILE_MENU_SAVE"), LocaleProps.get("SAVE_OVERWRITE"))) {
                    return;
                }

                this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

                this.getProgressNotifier().notifyListeners(new ProgressInfo(ProgressType.MAX_PROGRESS, obs.size()));

                ObsListFileSaveTask task = new ObsListFileSaveTask(obsListFileSaveDialog.getSelectedPlugin(), obs,
                        outFile, obsListFileSaveDialog.getDelimiter());

                this.currTask = task;
                task.execute();
            }
        } else {
            MessageBox.showMessageDialog(parent, "Save Observations", "There are no visible observations to save.");
        }
    }

    /**
     * Save synthetic observation list (means, model, residuals) to a file in a
     * separate thread.
     * 
     * @param parent The parent component to be used in dialogs.
     * @param plugin The observation sink plugin.
     * @param obs    The list of observations to be saved.
     */
    private void saveSyntheticObsListToFile(Component parent, List<ValidObservation> obs) {

        if (!obs.isEmpty()) {
            if (obsListFileSaveDialog.showDialog(parent)) {
                File outFile = obsListFileSaveDialog.getSelectedFile();

                if (outFile.exists() && outFile.isFile() && !MessageBox
                        .showConfirmDialog(LocaleProps.get("FILE_MENU_SAVE"), LocaleProps.get("SAVE_OVERWRITE"))) {
                    return;
                }

                this.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);

                this.getProgressNotifier().notifyListeners(new ProgressInfo(ProgressType.MAX_PROGRESS, obs.size()));

                // We re-use the same observation list file save task as
                // above but specify simple file type to match the fact that
                // we are only going to save JD, magnitude, and uncertainty
                // (for means).
                ObsListFileSaveTask task = new ObsListFileSaveTask(obsListFileSaveDialog.getSelectedPlugin(), obs,
                        outFile, obsListFileSaveDialog.getDelimiter());

                this.currTask = task;
                task.execute();
            }
        } else {
            MessageBox.showMessageDialog(parent, "Save Observations", "There are no observations to save.");
        }
    }

    /**
     * Print the artefact corresponding to the current mode.
     * 
     * @param parent The parent component to be used by an error dialog.
     */
    public void printCurrentMode(Component parent) {
        switch (viewMode) {
        case PLOT_OBS_MODE:
            this.analysisTypeMap.get(analysisType).getObsAndMeanChartPane().getChartPanel().createChartPrintJob();
            break;

        case LIST_OBS_MODE:
            try {
                ObservationListPane obsListPane = this.analysisTypeMap.get(analysisType).getObsListPane();

                obsListPane.getValidDataTable().print(PrintMode.FIT_WIDTH);

                if (obsListPane.getInvalidDataTable() != null) {
                    obsListPane.getInvalidDataTable().print(PrintMode.FIT_WIDTH);
                }
            } catch (PrinterException e) {
                MessageBox.showErrorDialog(parent, "Print Observations", e.getMessage());
            }
            break;

        case LIST_MEANS_MODE:
            try {
                SyntheticObservationListPane<AbstractMeanObservationTableModel> meanObsListPane = this.analysisTypeMap
                        .get(analysisType).getMeansListPane();

                meanObsListPane.getObsTable().print(PrintMode.FIT_WIDTH);
            } catch (PrinterException e) {
                MessageBox.showErrorDialog(parent, "Print Mean Values", e.getMessage());
            }
            break;
        case MODEL_MODE:
            if (modelSelectionMessage != null) {
                try {
                    SyntheticObservationListPane<AbstractModelObservationTableModel> modelListPane = getDocumentManager()
                            .getModelListPane(analysisType, modelSelectionMessage.getModel());

                    modelListPane.getObsTable().print(PrintMode.FIT_WIDTH);
                } catch (PrinterException e) {
                    MessageBox.showErrorDialog(parent, "Print Model Values", e.getMessage());
                }
            }
            break;
        case RESIDUALS_MODE:
            if (modelSelectionMessage != null) {
                try {
                    SyntheticObservationListPane<AbstractModelObservationTableModel> residualsListPane = getDocumentManager()
                            .getResidualsListPane(analysisType, modelSelectionMessage.getModel());

                    residualsListPane.getObsTable().print(PrintMode.FIT_WIDTH);
                } catch (PrinterException e) {
                    MessageBox.showErrorDialog(parent, "Print Residual Values", e.getMessage());
                }
            }
            break;
        }
    }

    /**
     * Show the details of the currently selected observation in the current view
     * mode (plot or table).
     */
    public void showObservationDetails() {
        ValidObservation ob = null;

        switch (viewMode) {
        case PLOT_OBS_MODE:
            ob = this.analysisTypeMap.get(analysisType).getObsAndMeanChartPane().getLastObSelected();
            break;
        case LIST_OBS_MODE:
            ob = this.analysisTypeMap.get(analysisType).getObsListPane().getLastObSelected();
            break;
        case LIST_MEANS_MODE:
            ob = this.analysisTypeMap.get(analysisType).getMeansListPane().getLastObSelected();
            break;
        case MODEL_MODE:
            ob = getDocumentManager().getModelListPane(analysisType, modelSelectionMessage.getModel())
                    .getLastObSelected();
            break;
        case RESIDUALS_MODE:
            ob = getDocumentManager().getResidualsListPane(analysisType, modelSelectionMessage.getModel())
                    .getLastObSelected();
            break;
        }

        if (ob != null) {
            new ObservationDetailsDialog(ob);
        } else {
            MessageBox.showWarningDialog("Observation Details", "No observation selected");
        }
    }

    private void updateChartPropertiesForAnalysisType(AnalysisType type) {
        AnalysisTypeChangeMessage m = analysisTypeMap.get(type);
        if (m != null) {
            ObservationAndMeanPlotPane pane = m.getObsAndMeanChartPane();
            if (pane != null) {
                pane.updateChartProperties();
            }
        }
    }

    /**
     * 
     * Updates properties for charts (light curve, phase plot)
     * 
     */
    public void updateChartProperties() {
        List<AnalysisType> list = Arrays.asList(AnalysisType.values());
        for (AnalysisType type : list) {
            updateChartPropertiesForAnalysisType(type);
        }
    }

    /**
     * Report a discrepant observation to AAVSO (if the dataset was AID-downloaded).
     * 
     * @param ob     The observation to be reported.
     * @param dialog A parent dialog to set non-visible and dispose. May be null.
     */
    public void reportDiscrepantObservation(ValidObservation ob, JDialog dialog)
            throws AuthenticationError, CancellationException, ConnectionException {
        // If the dataset was loaded from AID and the change was
        // to mark this observation as discrepant, we ask the user
        // whether to report this to AAVSO.
        if (ob.isDiscrepant() && getLatestNewStarMessage().getNewStarType() == NewStarType.NEW_STAR_FROM_DATABASE) {

            String auid = getLatestNewStarMessage().getStarInfo().getAuid();
            String name = ob.getName();
            int uniqueId = ob.getRecordNumber();

            DiscrepantReportDialog reportDialog = new DiscrepantReportDialog(auid, ob);

            if (!reportDialog.isCancelled()) {
                try {
                    Mediator.getUI().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

                    Authenticator.getInstance().authenticate();

                    String userName = ResourceAccessor.getLoginInfo().getUserName();

                    String editor = "vstar:" + userName;

                    if (dialog != null) {
                        dialog.setVisible(false);
                    }

                    // Create and submit the discrepant report.
                    DiscrepantReport report = new DiscrepantReport(auid, name, uniqueId, editor,
                            reportDialog.getComments());

                    IDiscrepantReporter reporter = new VSXWebServiceZapperLogger();

                    reporter.lodge(report);

                    getUI().setCursor(null);

                    if (dialog != null) {
                        dialog.dispose();
                    }
                } finally {
                    Mediator.getUI().setCursor(null);

                }
            }
        }
    }

    /**
     * Exit VStar.
     */
    public void quit() {
        // TODO: do other cleanup, e.g. if file needs saving;
        // need a document model including undo for this;
        // defer to Mediator.
        System.exit(0);
    }

    public static JDialog getParentDialog(Component c) {
        while (c != null) {
            if (c instanceof JDialog) {
                return (JDialog) c;
            }
            c = c.getParent();
        }
        return null;
    }

    public static String getParentDialogName(Component c) {
        JDialog dlg = getParentDialog(c);
        if (dlg != null) {
            return dlg.getName();
        } else {
            return null;
        }
    }

    public static boolean isMsgForDialog(JDialog dlg, MessageBase msg) {
        if (dlg == null || msg == null || msg.getTag() == null)
            return false;
        return msg.getTag().equals(dlg.getName());
    }

}