ObservationAndMeanPlotModel.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.model.plot;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

import org.aavso.tools.vstar.data.SeriesType;
import org.aavso.tools.vstar.data.ValidObservation;
import org.aavso.tools.vstar.ui.mediator.Mediator;
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.ModelSelectionMessage;
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.stats.BinningResult;
import org.aavso.tools.vstar.util.stats.DescStats;

/**
 * This class is a model that represents a series of valid variable star
 * observations, e.g. for different bands (or from different sources) along with
 * a means series that can change over time.
 */
@SuppressWarnings("serial")
public class ObservationAndMeanPlotModel extends ObservationPlotModel {

	public static final int NO_SERIES = -1;

	// The series number of the series that is the source of the
	// means series.
	protected int meanSourceSeriesNum;

	// The series number of the means series.
	protected int meansSeriesNum;

	// An observation time source.
	protected ITimeElementEntity timeElementEntity;

	// The number of time elements in a means series bin.
	protected double timeElementsInBin;

	// The observations that constitute the means series.
	protected List<ValidObservation> meanObsList;

	// The binning result associated with this mean observation list.
	protected BinningResult binningResult;

	protected Notifier<BinningResult> meansChangeNotifier;

	// The current model function series number; may be NO_SERIES.
	protected int modelFunctionSeriesNum;

	// The current model function (polynomial fit, DCDFT, ...); may be
	// null.
	protected ContinuousModelFunction modelFunction;

	/**
	 * Constructor
	 * 
	 * We add named observation source lists to unique series numbers. Then we
	 * add the initial mean-based series.
	 * 
	 * @param obsSourceListMap
	 *            A mapping from source series to lists of observation sources.
	 * @param coordSrc
	 *            coordinate and error source.
	 * @param obComparator
	 *            A valid observation comparator (e.g. by JD or phase).
	 * @param timeElementEntity
	 *            A time element source for observations.
	 * @param seriesVisibilityMap
	 *            A mapping from series type to visibility status.
	 */
	public ObservationAndMeanPlotModel(
			Map<SeriesType, List<ValidObservation>> obsSourceListMap,
			ICoordSource coordSrc, Comparator<ValidObservation> obComparator,
			ITimeElementEntity timeElementEntity,
			Map<SeriesType, Boolean> seriesVisibilityMap) {

		super(obsSourceListMap, coordSrc, obComparator, seriesVisibilityMap);

		this.meansSeriesNum = NO_SERIES;

		this.timeElementEntity = timeElementEntity;

		this.timeElementsInBin = this.timeElementEntity
				.getDefaultTimeElementsInBin();

		this.meansChangeNotifier = new Notifier<BinningResult>();

		this.meanSourceSeriesNum = determineMeanSeriesSource();

		this.modelFunctionSeriesNum = NO_SERIES;
		this.modelFunction = null;

		// It doesn't actually matter whether we pass true or false here
		// since the parameter won't apply to the first mean series created.
		this.setMeanSeries(false);

		Mediator.getInstance().getDiscrepantObservationNotifier()
				.addListener(createDiscrepantChangeListener());

		Mediator.getInstance().getExcludedObservationNotifier()
				.addListener(createExcludedChangeListener());

		Mediator.getInstance().getModelSelectionNofitier()
				.addListener(createModelSelectionListener());

		Mediator.getInstance().getFilteredObservationNotifier()
				.addListener(createFilteredObservationListener());
	}

	/**
	 * @return the timeElementEntity
	 */
	public ITimeElementEntity getTimeElementEntity() {
		return timeElementEntity;
	}

	/**
	 * Set the mean-based series.
	 * 
	 * This method creates a new means series based upon the current mean source
	 * series index and time-elements-in-bin. It then updates the view and any
	 * listeners.
	 * 
	 * @param updateAfterInitial
	 *            Should the mean series be made visible after the initial
	 *            series is replaced by another? Even if this is set to false,
	 *            if the series is already visible, it will remain so, and any
	 *            change to the mean curve will be visible more or less
	 *            immediately.
	 */
	public boolean setMeanSeries(boolean updateAfterInitial) {

		boolean changed = true;

		// Does this rely upon mean source series being sorted to determine time
		// range?
		// Perhaps the difference between when mean is selected via plot control
		// dialog vs model listener below?

		binningResult = DescStats.createSymmetricBinnedObservations(
				seriesNumToObSrcListMap.get(meanSourceSeriesNum),
				timeElementEntity, timeElementsInBin);

		meanObsList = binningResult.getMeanObservations();

		if (meanObsList != Collections.EMPTY_LIST) {
			// As long as there were enough observations to create a means list
			// to make a "means" series, we do so.
			boolean found = false;

			// TODO: do something like this instead of what follows below.
			// Is this the first time the means series has been added?
			// if (this.meansSeriesNum != NO_SERIES) {
			// // Replace the means series with the new one.
			// this.seriesNumToObSrcListMap.put(this.meanSourceSeriesNum,
			// meanObsList);
			// this.fireDatasetChanged();
			//
			// // The mean series has been changed after the initial one. If it
			// // is not visible, make it so since the user has updated it and
			// // probably wants to see it right away.
			// if (updateAfterInitial) {
			// changeSeriesVisibility(this.meansSeriesNum, true);
			// }
			// } else {
			// // Create the means series.
			// this.meansSeriesNum = addObservationSeries(SeriesType.MEANS,
			// meanObsList);
			//
			// // Mean series not rendered by default.
			// getSeriesVisibilityMap().put(SeriesType.MEANS, false);
			// }

			for (Map.Entry<Integer, SeriesType> entry : this.seriesNumToSrcTypeMap
					.entrySet()) {
				if (SeriesType.MEANS.equals(entry.getValue())) {
					int series = entry.getKey();
					this.seriesNumToObSrcListMap.put(series, meanObsList);
					this.fireDatasetChanged();
					found = true;
					break;
				}
			}

			// Is this the first time the means series has been added?
			if (!found) {
				this.meansSeriesNum = addObservationSeries(SeriesType.MEANS,
						meanObsList);

				// Mean series not rendered by default.
				getSeriesVisibilityMap().put(SeriesType.MEANS, false);
			} else {
				// The mean series has been changed after the initial one. If it
				// is not visible, make it so since the user has updated it and
				// probably wants to see it right away.
				if (updateAfterInitial) {
					changeSeriesVisibility(this.meansSeriesNum, true);
				}
			}

			// Notify listeners.
			this.meansChangeNotifier.notifyListeners(binningResult);
		} else {
			changed = false;
		}

		return changed;
	}

	/**
	 * Attempt to create a new mean series with the specified number of time
	 * elements per bin.
	 * 
	 * @param timeElementsInBin
	 *            The number of days or phase steps to be created per bin.
	 * @return Whether or not the series was changed.
	 */
	public boolean changeMeansSeries(double timeElementsInBin) {
		this.timeElementsInBin = timeElementsInBin;
		return this.setMeanSeries(true);
	}

	/**
	 * @see org.aavso.tools.vstar.ui.model.plot.ObservationPlotModel#changeSeriesVisibility(int,
	 *      boolean)
	 */
	public boolean changeSeriesVisibility(int seriesNum, boolean visibility) {
		return super.changeSeriesVisibility(seriesNum, visibility);
	}

	/**
	 * Which series' elements should be joined visually (e.g. with lines)?
	 * 
	 * @return A collection of series numbers for series whose elements should
	 *         be joined visually.
	 */
	public Collection<Integer> getSeriesWhoseElementsShouldBeJoinedVisually() {
		List<Integer> seriesNumList = new ArrayList<Integer>();

		// See TODO in setMeanSeries() which also applies here.
		for (Map.Entry<Integer, SeriesType> entry : this.seriesNumToSrcTypeMap
				.entrySet()) {
			if (SeriesType.MEANS == entry.getValue()) {
				seriesNumList.add(entry.getKey());
				break;
			}
		}

		return seriesNumList;
	}

	/**
	 * Return the error associated with the magnitude. We skip the series and
	 * item legality check to improve performance on the assumption that this
	 * has been checked already when calling getMagAsYCoord(). So this is a
	 * precondition of calling the current function.
	 * 
	 * @param series
	 *            The series number.
	 * @param item
	 *            The item number within the series.
	 * @return The error value associated with the mean.
	 */
	public double getMagError(int series, int item) {
		if (series != this.meansSeriesNum) {
			// The series is something other than the means series
			// so just default to the superclass behaviour.
			return super.getMagError(series, item);
		} else {
			// For the means series, we store the mean magnitude error
			// value as the magnitude's uncertainty, and we are only interested
			// in this (not HQ vs user-specified, since only one value exists
			// for
			// a mean observation).

			// For mean observations we double the error value to show the 95%
			// Confidence Interval, as suggested to me by Grant Foster. See his
			// book
			// "Analyzing Light Curves" re: this.
			return this.seriesNumToObSrcListMap.get(series).get(item)
					.getMagnitude().getUncertainty() * 2;
		}
	}

	/**
	 * @return the meanSourceSeriesNum
	 */
	public int getMeanSourceSeriesNum() {
		return meanSourceSeriesNum;
	}

	/**
	 * @param meanSourceSeriesNum
	 *            the meanSourceSeriesNum to set
	 */
	public void setMeanSourceSeriesNum(int meanSourceSeriesNum) {
		this.meanSourceSeriesNum = meanSourceSeriesNum;
	}

	/**
	 * @return the means series number
	 */
	public int getMeansSeriesNum() {
		return meansSeriesNum;
	}

	/**
	 * @return the timeElementsInBin
	 */
	public double getTimeElementsInBin() {
		return timeElementsInBin;
	}

	/**
	 * @param timeElementsInBin
	 *            the timeElementsInBin to set
	 */
	public void setTimeElementsInBin(double timeElementsInBin) {
		this.timeElementsInBin = timeElementsInBin;
	}

	/**
	 * @return the meanObsList
	 */
	public List<ValidObservation> getMeanObsList() {
		return meanObsList;
	}

	/**
	 * @return the binningResult
	 */
	public BinningResult getBinningResult() {
		return binningResult;
	}

	/**
	 * @return the meansChangeNotifier
	 */
	public Notifier<BinningResult> getMeansChangeNotifier() {
		return meansChangeNotifier;
	}

	/**
	 * @return the modelFunctionSeriesNum
	 */
	public int getModelFunctionSeriesNum() {
		return modelFunctionSeriesNum;
	}

	/**
	 * @return the model
	 */
	public ContinuousModelFunction getModelFunction() {
		return modelFunction;
	}

	// Helper methods.

	/**
	 * Determine which series will initially be the source of the mean series.
	 * Note that this may be changed subsequently. Visual bands have the highest
	 * priority. If not found, the Unspecified series is looked at, otherwise
	 * the first band encountered other than fainter-than, excluded, or
	 * discrepant will be chosen.
	 * 
	 * @return The series number on which to base the mean series.
	 */
	public int determineMeanSeriesSource() {
		int seriesNum = NO_SERIES;

		// TODO:
		// - use keySet().contains() below!
		// - out of V and Visual, choose the series with the most observations

		// Look for Visual, then V.
		for (SeriesType series : srcTypeToSeriesNumMap.keySet()) {
			if (series == SeriesType.Visual) {
				// Visual band
				seriesNum = srcTypeToSeriesNumMap.get(series);
				break;
			}
		}

		if (seriesNum == NO_SERIES) {
			for (SeriesType series : srcTypeToSeriesNumMap.keySet()) {
				if (series == SeriesType.Johnson_V) {
					// Johnson V band
					seriesNum = srcTypeToSeriesNumMap.get(series);
					break;
				}
			}
		}

		// No visual bands present. Try 'Unspecified'.
		if (seriesNum == NO_SERIES) {
			for (SeriesType series : srcTypeToSeriesNumMap.keySet()) {
				if (series == SeriesType.Unspecified) {
					// Unspecified
					seriesNum = srcTypeToSeriesNumMap.get(series);
					break;
				}
			}
		}

		// No match: choose a non-empty series other than fainter-than,
		// discrepant, or excluded. More specifically, choose the series with
		// the greatest number of observations.
		int maxObsSeriesNum = NO_SERIES;
		int maxObs = Integer.MIN_VALUE;
		if (seriesNum == NO_SERIES) {
			for (SeriesType series : srcTypeToSeriesNumMap.keySet()) {
				if (series != SeriesType.FAINTER_THAN
						&& series != SeriesType.DISCREPANT
						&& series != SeriesType.Excluded
						&& !series.isSynthetic()
						&& !seriesNumToObSrcListMap.get(
								srcTypeToSeriesNumMap.get(series)).isEmpty()) {
					//
					seriesNum = srcTypeToSeriesNumMap.get(series);
					int numObs = seriesNumToObSrcListMap.get(seriesNum).size();
					if (numObs > maxObs) {
						maxObsSeriesNum = seriesNum;
						maxObs = numObs;
					}

				}
			}
			seriesNum = maxObsSeriesNum;
		}

		// Still nothing? Okay, now we just choose the first non-empty series
		// we come to, including fainter-thans or discrepants.
		//
		// For this to happen should be very rare, but it could happen
		// For example, CM Cru as of Sep 2010 contains one data value
		// since around 1949 and that is a Fainter-than. Note that currently,
		// determineMeanSeriesSource() will ignore Fainter-thans, discrepants,
		// and excluded observations with respect to mean curves. We might want
		// to revise this. Of course, fainter-thans can later be selected as the
		// means-source.
		//
		// Note: This code is showing its age; "very rare" may have been true before
		//       observation source plug-ins with non-standard filters. Also, there's
		//       much cruft in general, e.g. see setMeanSeries() (dbenn)
		if (seriesNum == NO_SERIES) {
			for (SeriesType series : srcTypeToSeriesNumMap.keySet()) {
				seriesNum = srcTypeToSeriesNumMap.get(series);
				if (seriesNumToObSrcListMap.get(seriesNum).size() != 0) {
				    break;
				}
			}
		}

		assert seriesNum != -1;

		return seriesNum;
	}

	/**
	 * Inform the views that the dataset has changed.
	 */
	public void update() {
		this.fireDatasetChanged();
	}
	
	/**
	 * Listen for discrepant observation change notification and add/remove it
	 * from the relevant collections. Since a discrepant observation is ignored
	 * for statistical analysis purposes (see DescStats class), we need to
	 * re-calculate the means series if the discrepant observation's series type
	 * is the same as the mean source series type.
	 */
	protected Listener<DiscrepantObservationMessage> createDiscrepantChangeListener() {

		final ObservationAndMeanPlotModel model = this;

		return new Listener<DiscrepantObservationMessage>() {
			public void update(DiscrepantObservationMessage info) {
				ValidObservation ob = info.getObservation();

				// Did we go to or from being discrepant?
				if (ob.isDiscrepant()) {
					// Now marked as discrepant so move observation from
					// its designated band series to the discrepant series.
					removeObservationFromSeries(ob, ob.getBand());
					addObservationToSeries(ob, SeriesType.DISCREPANT);
				} else {
					// Was marked as discrepant, now is not, so move
					// observation from the discrepant series to its
					// designated band series.
					removeObservationFromSeries(ob, SeriesType.DISCREPANT);
					addObservationToSeries(ob, ob.getBand());
				}

				fireDatasetChanged();

				// If the discrepant observation's band is the source of the
				// means series, re-compute the means series.
				if (info.getObservation().getBand() == seriesNumToSrcTypeMap
						.get(meanSourceSeriesNum)) {
					model.setMeanSeries(false);
				}
			}

			/**
			 * @see org.aavso.tools.vstar.util.notification.Listener#canBeRemoved()
			 */
			public boolean canBeRemoved() {
				return true;
			}
		};
	}

	/**
	 * Listen for excluded observation change notification and add/remove it
	 * from the relevant collections. We need to re-calculate the means series
	 * if any of the excluded observations' series type is the same as the mean
	 * source series type.
	 */
	protected Listener<ExcludedObservationMessage> createExcludedChangeListener() {

		final ObservationAndMeanPlotModel model = this;

		return new Listener<ExcludedObservationMessage>() {

			@Override
			public void update(ExcludedObservationMessage info) {
				List<ValidObservation> obs = info.getObservations();
				boolean excluded = obs.get(0).isExcluded();

				// Did we go to or from being excluded?
				if (excluded) {
					for (ValidObservation ob : info.getObservations()) {
						// Now marked as excluded so move observation from
						// its designated band to the excluded series.
						removeObservationFromSeries(ob, ob.getBand());
					}
					// All are going to the same series, so we can do this
					// en-masse. Note that we cannot do the reverse en-masse!
					addObservationsToSeries(obs, SeriesType.Excluded);
				} else {
					// Was previously marked as excluded, now is not, so move
					// observation from the excluded series to its
					// designated series. Reversing observation exclusion is
					// less efficient than the initial exclusion.
					for (ValidObservation ob : info.getObservations()) {
						removeObservationFromSeries(ob, SeriesType.Excluded);
						addObservationToSeries(ob, ob.getBand());
					}
				}

				fireDatasetChanged();

				// If any of the excluded observations bands is the source of
				// the means series, re-compute the means series.
				for (ValidObservation ob : info.getObservations()) {
					if (ob.getBand() == seriesNumToSrcTypeMap
							.get(meanSourceSeriesNum)) {
						model.setMeanSeries(false);
						break;
					}
				}
			}

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

	/**
	 * Update the model's fit and residual observation collections.
	 */
	public void updateModelSeries(List<ValidObservation> modelObs,
			List<ValidObservation> residualObs, IModel model) {

		// Add or replace model pointing to continuous function.
		// if (this.seriesExists(SeriesType.ModelFunction)) {
		// // Replace it.
		// this.modelFunction = model.getModelFunction();
		// } else {
		// // Add it.
		// // TODO: create up front like we do with some other series, e.g. in
		// // obs retriever?
		// modelFunctionSeriesNum = getNextSeriesNum();
		//
		// this.srcTypeToSeriesNumMap.put(SeriesType.ModelFunction,
		// modelFunctionSeriesNum);
		//
		// this.seriesNumToSrcTypeMap.put(modelFunctionSeriesNum,
		// SeriesType.ModelFunction);
		//
		// this.seriesVisibilityMap.put(SeriesType.ModelFunction,
		// isSeriesVisibleByDefault(SeriesType.ModelFunction));
		//
		// this.modelFunction = model.getModelFunction();
		// }

		// Add or replace a series for the model and make sure
		// the series is visible.
		if (this.seriesExists(SeriesType.Model)) {
			modelSeriesNum = replaceObservationSeries(SeriesType.Model,
					modelObs);
		} else {
			modelSeriesNum = addObservationSeries(SeriesType.Model, modelObs);
		}

		// Make the model series visible either because this
		// is its first appearance or because it may have been made
		// invisible via the change series dialog.
		this.changeSeriesVisibility(modelSeriesNum, true);

		// Make the model function series visible either because this
		// is its first appearance or because it may have been made
		// invisible via the change series dialog.
		// this.changeSeriesVisibility(modelFunctionSeriesNum, true);

		// TODO: do we really need this? if not, revert means join
		// handling code
		// this.addSeriesToBeJoinedVisually(modelSeriesNum);

		// Add or replace a series for the residuals.
		if (this.seriesExists(SeriesType.Residuals)) {
			this.replaceObservationSeries(SeriesType.Residuals, residualObs);
		} else {
			residualsSeriesNum = addObservationSeries(SeriesType.Residuals,
					residualObs);
		}

		// Hide the residuals series initially. We toggle the series
		// visibility to achieve this since the default is false. That
		// shouldn't be necessary; investigate.
		// this.changeSeriesVisibility(residualsSeriesNum, true);
		changeSeriesVisibility(residualsSeriesNum, false);
	}

	/**
	 * @see org.aavso.tools.vstar.ui.model.plot.ObservationPlotModel#createModelSelectionListener()
	 */
	@Override
	protected Listener<ModelSelectionMessage> createModelSelectionListener() {

		final ObservationAndMeanPlotModel model = this;

		return new Listener<ModelSelectionMessage>() {
			@Override
			public void update(ModelSelectionMessage info) {
				updateModelSeries(info.getModel().getFit(), info.getModel()
						.getResiduals(), info.getModel());

				// If the means sources series is model or residuals (from
				// previous modelling operation), re-compute the means series.
				if (seriesNumToSrcTypeMap.get(meanSourceSeriesNum) == SeriesType.Model
						|| seriesNumToSrcTypeMap.get(meanSourceSeriesNum) == SeriesType.Residuals) {
					model.setMeanSeries(false);
				}
			}

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

	protected boolean handleNoFilter(FilteredObservationMessage info) {
		boolean result = false;

		if (info == FilteredObservationMessage.NO_FILTER) {
			// No filter, so make the filtered series invisible.
			if (this.seriesExists(SeriesType.Filtered)) {
				int num = this.getSrcTypeToSeriesNumMap().get(
						SeriesType.Filtered);
				changeSeriesVisibility(num, false);
			}
			result = true;
		}

		return result;
	}

	public void updateFilteredSeries(List<ValidObservation> obs) {
		if (this.seriesExists(SeriesType.Filtered)) {
			filterSeriesNum = replaceObservationSeries(SeriesType.Filtered, obs);
		} else {
			filterSeriesNum = addObservationSeries(SeriesType.Filtered, obs);
		}

		// Make the filter series visible either because this is
		// its first appearance or because it may have been made
		// invisible via a previous NO_FILTER message.
		changeSeriesVisibility(filterSeriesNum, true);
	}

	@Override
	public boolean removeAllObservationFromSeries(SeriesType type) {
		boolean removed = super.removeAllObservationFromSeries(type);

		if (removed) {
			// If the specified series is the source of the means series,
			// re-compute the means series.
			if (type == seriesNumToSrcTypeMap.get(meanSourceSeriesNum)) {
				setMeanSeries(false);
			}
		}

		return removed;
	}

	/**
	 * @see org.aavso.tools.vstar.ui.model.plot.ObservationPlotModel#createFilteredObservationListener()
	 */
	@Override
	protected Listener<FilteredObservationMessage> createFilteredObservationListener() {

		final ObservationAndMeanPlotModel model = this;

		return new Listener<FilteredObservationMessage>() {

			@Override
			public void update(FilteredObservationMessage info) {
				if (!handleNoFilter(info)) {
					// Convert set of filtered observations to list then add
					// or replace the filter series.
					List<ValidObservation> obs = new ArrayList<ValidObservation>(
							info.getFilteredObs());

					updateFilteredSeries(obs);

					// If the means sources series is filtered (from
					// previous filtering operation), re-compute the means
					// series.
					if (seriesNumToSrcTypeMap.get(meanSourceSeriesNum) == SeriesType.Filtered) {
						model.setMeanSeries(false);
					}
				}
			}

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