ObservationPlotModel.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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

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.NewStarType;
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.ui.mediator.message.NewStarMessage;
import org.aavso.tools.vstar.ui.mediator.message.SeriesCreationMessage;
import org.aavso.tools.vstar.ui.mediator.message.SeriesVisibilityChangeMessage;
import org.aavso.tools.vstar.util.notification.Listener;
import org.jfree.data.DomainOrder;
import org.jfree.data.xy.AbstractIntervalXYDataset;

/**
 * This is the abstract base class for models that represent a series of valid
 * variable star observations, e.g. for different bands (or from different
 * sources). In practice, this class is only intended to be used as a the base
 * class for ObservationAndMeanPlot. The two could be merged with the latter
 * assuming the current class's name.
 */
@SuppressWarnings("serial")
abstract public class ObservationPlotModel extends AbstractIntervalXYDataset
		implements ISeriesInfoProvider {

	protected static final int NO_SERIES = -1;

	/**
	 * Coordinate and error source.
	 */
	protected ICoordSource coordSrc;

	/**
	 * An observation comparator (e.g. to provide an ordering over time: JD or
	 * phase).
	 */
	protected Comparator<ValidObservation> obComparator;

	/**
	 * A unique next series number for this model.
	 */
	protected int seriesNum;

	/**
	 * A mapping from series number to a list of observations where each such
	 * list is a data series.
	 */
	protected Map<Integer, List<ValidObservation>> seriesNumToObSrcListMap;

	/**
	 * A mapping from series number to source type.
	 */
	protected Map<Integer, SeriesType> seriesNumToSrcTypeMap;

	/**
	 * A mapping from source type to series number.
	 */
	protected Map<SeriesType, Integer> srcTypeToSeriesNumMap;

	/**
	 * A mapping from series numbers to whether or not they visible.
	 */
	protected Map<SeriesType, Boolean> seriesVisibilityMap;

	/**
	 * Is at least one visual band observation present?
	 */
	protected boolean atLeastOneVisualBandPresent;

	/**
	 * A collection of series to be joined visually.
	 */
	protected Set<Integer> seriesToBeJoinedVisually;

	/**
	 * Discrepant observation listener.
	 */
	protected Listener<DiscrepantObservationMessage> discrepantListener;

	/**
	 * Excluded observation listener.
	 */
	protected Listener<ExcludedObservationMessage> excludedListener;

	/**
	 * What was the most recently singly selected series (e.g. via a dialog).
	 */
	protected SeriesType lastSinglySelectedSeries;

	// Particular series numbers to be used by listener code.
	protected int modelSeriesNum = NO_SERIES;
	protected int modelFunctionSeriesNum = NO_SERIES;
	protected int residualsSeriesNum = NO_SERIES;
	protected int filterSeriesNum = NO_SERIES;

	/**
	 * Common constructor.
	 * 
	 * @param coordSrc
	 *            A coordinate and error source.
	 */
	private ObservationPlotModel(ICoordSource coordSrc) {
		super();
		this.coordSrc = coordSrc;
		this.seriesNum = 0;
		this.seriesNumToSrcTypeMap = new HashMap<Integer, SeriesType>();
		this.srcTypeToSeriesNumMap = new TreeMap<SeriesType, Integer>();
		this.seriesVisibilityMap = new HashMap<SeriesType, Boolean>();
		this.seriesNumToObSrcListMap = new HashMap<Integer, List<ValidObservation>>();
		this.atLeastOneVisualBandPresent = false;
		this.seriesToBeJoinedVisually = new HashSet<Integer>();
		this.lastSinglySelectedSeries = null;

		Mediator.getInstance().getSeriesCreationNotifier()
				.addListener(createSeriesCreationListener());
	}

	/**
	 * Constructor (for light curve plots).
	 * 
	 * We add named observation source lists to unique series numbers.
	 * 
	 * @param obsSourceListMap
	 *            A mapping from source series to lists of observation sources.
	 * @param coordSrc
	 *            A coordinate and error source.
	 * @param obComparator
	 *            A valid observation comparator (e.g. by JD or phase).
	 */
	private ObservationPlotModel(
			Map<SeriesType, List<ValidObservation>> obsSourceListMap,
			ICoordSource coordSrc, Comparator<ValidObservation> obComparator) {

		this(coordSrc);

		this.obComparator = obComparator;

		for (SeriesType type : obsSourceListMap.keySet()) {
			addObservationSeries(type, obsSourceListMap.get(type));
		}

		// We should only make "unspecified" band-based observations visible
		// by default only if one of the visual bands is *not* present.
		// See
		// https://sourceforge.net/tracker/?func=detail&aid=2837957&group_id=263306&atid=1152052
		//
		// What if there are no visual bands or "Unspecified" observations? Then
		// we make all series from the dataset visible.
		// See
		// https://sourceforge.net/tracker/?func=detail&aid=3188139&group_id=263306&atid=1152052
		if (atLeastOneVisualBandPresent) {
			if (srcTypeToSeriesNumMap.containsKey(SeriesType.Unspecified)) {
				if (seriesVisibilityMap.get(SeriesType.Unspecified) == true) {
					seriesVisibilityMap.put(SeriesType.Unspecified, false);
				}
			}
		} else {
			// Make all series visible.
			for (SeriesType type : obsSourceListMap.keySet()) {
				seriesVisibilityMap.put(type, true);
			}
		}

		// If any series is empty initially (e.g. discrepant or excluded), make
		// the series not visible.
		for (SeriesType type : obsSourceListMap.keySet()) {
			int num = srcTypeToSeriesNumMap.get(type);
			if (seriesNumToObSrcListMap.get(num).isEmpty()) {
				seriesVisibilityMap.put(type, false);
			}
		}

		fireDatasetChanged();
	}

	/**
	 * Constructor (for phase plots).
	 * 
	 * We add named observation source lists to unique series numbers, and if
	 * the visibility map is non-null, potentially change the set of visible
	 * series.
	 * 
	 * @param obsSourceListMap
	 *            A mapping from source series to lists of observation sources.
	 * @param coordSrc
	 *            A coordinate and error source.
	 * @param obComparator
	 *            A valid observation comparator (e.g. by JD or phase).
	 * @param seriesVisibilityMap
	 *            A mapping from series number to visibility status.
	 */
	protected ObservationPlotModel(
			Map<SeriesType, List<ValidObservation>> obsSourceListMap,
			ICoordSource coordSrc, Comparator<ValidObservation> obComparator,
			Map<SeriesType, Boolean> seriesVisibilityMap) {

		this(obsSourceListMap, coordSrc, obComparator);

		if (seriesVisibilityMap != null) {
			for (SeriesType seriesType : seriesVisibilityMap.keySet()) {
				Integer seriesNum = srcTypeToSeriesNumMap.get(seriesType);
				// A series number may not be available yet, specifically for
				// means series; can't handle that yet.
				if (seriesNum != null) {
					changeSeriesVisibility(seriesNum,
							seriesVisibilityMap.get(seriesType));
				}
			}
		}
	}

	/**
	 * Add an observation series.
	 * 
	 * @param type
	 *            The series type to be associated with the series.
	 * @param obs
	 *            A series (list) of observations, in particular, magnitude and
	 *            Julian Day.
	 * @return The number of the series added.
	 * @precondition The series has not yet been added to the plot.
	 * @postcondition Both seriesNumToObSrcListMap and seriesNumToSrcTypeMap
	 *                must be the same length.
	 */
	public int addObservationSeries(SeriesType type, List<ValidObservation> obs) {

		assert this.srcTypeToSeriesNumMap.get(type) == null;

		int seriesNum = this.getNextSeriesNum();

		this.srcTypeToSeriesNumMap.put(type, seriesNum);
		this.seriesNumToObSrcListMap.put(seriesNum, obs);
		this.seriesNumToSrcTypeMap.put(seriesNum, type);
		this.seriesVisibilityMap.put(type, isSeriesVisibleByDefault(type));

		assert (this.seriesNumToObSrcListMap.size() == this.seriesNumToSrcTypeMap
				.size());

		return seriesNum;
	}

	/**
	 * Add a single observation to a series list, creating the series first if
	 * necessary.
	 * 
	 * @param ob
	 *            A valid observation.
	 * @param series
	 *            A series.
	 */
	public void addObservationToSeries(ValidObservation ob, SeriesType series) {
		Integer seriesNum = this.srcTypeToSeriesNumMap.get(series);

		if (seriesNum != null) {
			List<ValidObservation> obList = this.seriesNumToObSrcListMap
					.get(seriesNum);
			obList.add(ob);
			// TODO: this is an expensive operation for the addition of a
			// single observation! Perhaps we should mandate these lists as
			// having to be SortedSet. Traversal is no more expensive and
			// insertion is Log2(n).
			// SortedSet<E> l = null;
			Collections.sort(obList, obComparator);
		} else {
			// The series does not yet exist, so create it with
			// a single datapoint.
			List<ValidObservation> obsList = new ArrayList<ValidObservation>();
			obsList.add(ob);
			addObservationSeries(series, obsList);
		}
	}

	/**
	 * Add a list of observations to a series list, creating the series first if
	 * necessary.
	 * 
	 * @param obs
	 *            The list of observations to be added.
	 * @param series
	 *            The series to which to add the list.
	 */
	public void addObservationsToSeries(List<ValidObservation> obs,
			SeriesType series) {
		Integer seriesNum = this.srcTypeToSeriesNumMap.get(series);

		if (seriesNum != null) {
			List<ValidObservation> obList = this.seriesNumToObSrcListMap
					.get(seriesNum);
			obList.addAll(obs);
			// TODO: use sorted set instead, as above.
			Collections.sort(obList, obComparator);
		} else {
			// The series does not yet exist, so create it with the observation
			// list.
			addObservationSeries(series, obs);
		}
	}

	/**
	 * Replace an existing series
	 * 
	 * @param type
	 *            The series type to be associated with the series.
	 * @param obs
	 *            A series (list) of observations, in particular, magnitude and
	 *            Julian Day.
	 * @return The number of the series replaced.
	 * @precondition The series has already been added to the plot.
	 */
	public int replaceObservationSeries(SeriesType type,
			List<ValidObservation> obs) {
		Integer seriesNum = this.srcTypeToSeriesNumMap.get(type);
		assert seriesNum != null;
		this.seriesNumToObSrcListMap.put(seriesNum, obs);
		this.fireDatasetChanged();
		return seriesNum;
	}

	/**
	 * Remove a single observation from a series list.
	 * 
	 * @param ob
	 *            A valid observation.
	 * @param series
	 *            A series.
	 * @return Whether or not the observation was removed.
	 */
	public boolean removeObservationFromSeries(ValidObservation ob,
			SeriesType series) {
		boolean removed = false;

		Integer seriesNum = this.srcTypeToSeriesNumMap.get(series);

		if (seriesNum != null) {
			removed = this.seriesNumToObSrcListMap.get(seriesNum).remove(ob);
		}

		return removed;
	}

	/**
	 * Remove a single observation from a series list.
	 * 
	 * @param obs
	 *            The list of valid observations to be removed.
	 * @param series
	 *            The series from which the list is to be removed.
	 * @return Whether or not the observations were removed.
	 */
	public boolean removeObservationsFromSeries(List<ValidObservation> obs,
			SeriesType series) {
		boolean removed = false;

		Integer seriesNum = this.srcTypeToSeriesNumMap.get(series);

		if (seriesNum != null) {
			removed = this.seriesNumToObSrcListMap.get(seriesNum)
					.removeAll(obs);
		}

		return removed;
	}

	/**
	 * Remove all observations from the specified series, but not the series
	 * itself.
	 * 
	 * @param type
	 *            The series type.
	 * @return Whether or not the series observations were removed.
	 */
	public boolean removeAllObservationFromSeries(SeriesType type) {
		boolean removed = false;

		Integer seriesNum = this.srcTypeToSeriesNumMap.get(type);

		if (seriesNum != null) {
			List<ValidObservation> obs = this.seriesNumToObSrcListMap
					.get(seriesNum);
			removed = removeObservationsFromSeries(obs, type);
		}

		if (removed) {
			Mediator.getInstance().getValidObservationCategoryMap().get(type)
					.clear();

			 srcTypeToSeriesNumMap.remove(type);
			 seriesNumToSrcTypeMap.remove(seriesNum);
			 seriesNumToObSrcListMap.remove(seriesNum);
			 seriesVisibilityMap.remove(type);
//
//			fireDatasetChanged();
		}

		return removed;
	}

	/**
	 * Attempt to change the specified series' visibility.
	 * 
	 * @param seriesNum
	 *            The series number of interest.
	 * @param visibility
	 *            Whether this series should be visible.
	 * @return Whether or not the visibility of the object changed.
	 */
	public boolean changeSeriesVisibility(int seriesNum, boolean visibility) {
		SeriesType seriesType = seriesNumToSrcTypeMap.get(seriesNum);
		Boolean currVis = this.seriesVisibilityMap.get(seriesType);

		boolean changed = currVis != null && currVis != visibility;

		if (changed) {
			// Update the map and views.
			this.seriesVisibilityMap.put(seriesType, visibility);
			this.fireDatasetChanged();

			// Generate a series visibility message.
			SeriesVisibilityChangeMessage message = new SeriesVisibilityChangeMessage(
					this, getVisibleSeries());
			Mediator.getInstance().getSeriesVisibilityChangeNotifier()
					.notifyListeners(message);
		}

		return changed;
	}

	/**
	 * @see org.aavso.tools.vstar.ui.model.plot.ISeriesInfoProvider#getVisibleSeries
	 *      ()
	 */
	public Set<SeriesType> getVisibleSeries() {
		Set<SeriesType> visibleSeries = new HashSet<SeriesType>();

		for (SeriesType series : seriesVisibilityMap.keySet()) {
			boolean visible = seriesVisibilityMap.get(series);
			if (visible) {
				visibleSeries.add(series);
			}
		}

		return visibleSeries;
	}

	/**
	 * @see org.aavso.tools.vstar.ui.model.plot.ISeriesInfoProvider#getSeriesCount()
	 */
	public int getSeriesCount() {
		return this.seriesNumToObSrcListMap.size();
	}

	/**
	 * @see org.jfree.data.general.AbstractSeriesDataset#getSeriesKey(int)
	 */
	public Comparable getSeriesKey(int series) {
		if (!seriesNumToObSrcListMap.containsKey(series)) {
			throw new IllegalArgumentException("Series number '" + series
					+ "' out of range.");
		}

		return this.seriesNumToSrcTypeMap.get(series);
	}

	/**
	 * @see org.aavso.tools.vstar.ui.model.plot.ISeriesInfoProvider#getSeriesKeys()
	 */
	public Set<SeriesType> getSeriesKeys() {
		return srcTypeToSeriesNumMap.keySet();
	}

	/**
	 * @see org.aavso.tools.vstar.ui.model.plot.ISeriesInfoProvider#seriesExists(org.aavso.tools.vstar.data.SeriesType)
	 */
	public boolean seriesExists(SeriesType type) {
		return this.srcTypeToSeriesNumMap.containsKey(type);
	}

	@Override
	public List<ValidObservation> getObservations(SeriesType type) {
		int num = getSrcTypeToSeriesNumMap().get(type);
		return getSeriesNumToObSrcListMap().get(num);
	}

	/**
	 * @see org.jfree.data.xy.XYDataset#getItemCount(int)
	 * @return The number of observations (items) in the requested series.
	 */
	public int getItemCount(int series) {
		return coordSrc.getItemCount(series, seriesNumToObSrcListMap);
	}

	/**
	 * @see org.jfree.data.xy.XYDataset#getX(int, int)
	 */
	public Number getX(int series, int item) {
		return coordSrc.getXCoord(series, item, this.seriesNumToObSrcListMap);
	}

	/**
	 * @see org.jfree.data.xy.XYDataset#getY(int, int)
	 */
	public Number getY(int series, int item) {
		return getMagAsYCoord(series, coordSrc.getActualYItemNum(series, item,
				seriesNumToObSrcListMap));
	}

	/**
	 * @see org.jfree.data.xy.AbstractXYDataset#getDomainOrder()
	 */
	public DomainOrder getDomainOrder() {
		return DomainOrder.ASCENDING;
	}

	// TODO: in future, I think we want to get rid of this approach and just
	// leave all series join logic in the view classes.
	// In addition, it ought to be possible for *any* series to joined, so we
	// need to unify this at the series change dialog level (for example).

	/**
	 * 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() {
		return seriesToBeJoinedVisually;
	}

	// AbstractIntervalXYDataSet methods.
	// To be used for error bar handling.

	public Number getStartX(int series, int item) {
		return coordSrc.getXCoord(series, item, this.seriesNumToObSrcListMap);
	}

	public Number getEndX(int series, int item) {
		return coordSrc.getXCoord(series, item, this.seriesNumToObSrcListMap);
	}

	public Number getStartY(int series, int item) {
		int actualItem = coordSrc.getActualYItemNum(series, item,
				seriesNumToObSrcListMap);
		return getMagAsYCoord(series, actualItem)
				- getMagError(series, actualItem);
	}

	public Number getEndY(int series, int item) {
		int actualItem = coordSrc.getActualYItemNum(series, item,
				seriesNumToObSrcListMap);
		return getMagAsYCoord(series, actualItem)
				+ getMagError(series, actualItem);
	}

	// Helpers

	protected int getNextSeriesNum() {
		return seriesNum++;
	}

	/**
	 * Return the magnitude as the Y coordinate.
	 * 
	 * @param series
	 *            The series number.
	 * @param item
	 *            The item number within the series.
	 * @return The magnitude value.
	 */
	public double getMagAsYCoord(int series, int item) {
		return this.seriesNumToObSrcListMap.get(series).get(item)
				.getMagnitude().getMagValue();
	}

	/**
	 * 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) {
		double error = 0;

		// If the HQ uncertainty field is non-null, use that, otherwise
		// use the uncertainty value, which may be zero, in which case
		// the error will be zero.

		Double hqUncertainty = this.seriesNumToObSrcListMap.get(series)
				.get(item).getHqUncertainty();

		if (hqUncertainty != null && hqUncertainty != 0) {
			error = hqUncertainty;
		} else {
			error = this.seriesNumToObSrcListMap.get(series).get(item)
					.getMagnitude().getUncertainty();
		}

		return error;
	}

	/**
	 * Given a series and item number, return the corresponding observation.
	 * 
	 * @param series
	 *            The series number.
	 * @param item
	 *            The item within the series.
	 * @return The valid observation.
	 * @throws IllegalArgumentException
	 *             if series or item are out of range.
	 */
	public ValidObservation getValidObservation(int series, int item) {
		return coordSrc.getValidObservation(series, item,
				seriesNumToObSrcListMap);
	}

	/**
	 * @return the seriesNumToObSrcListMap
	 */
	public Map<Integer, List<ValidObservation>> getSeriesNumToObSrcListMap() {
		return seriesNumToObSrcListMap;
	}

	/**
	 * @return the srcTypeToSeriesNumMap
	 */
	public Map<SeriesType, Integer> getSrcTypeToSeriesNumMap() {
		return srcTypeToSeriesNumMap;
	}

	/**
	 * @return the seriesNumToSrcTypeMap
	 */
	public Map<Integer, SeriesType> getSeriesNumToSrcTypeMap() {
		return seriesNumToSrcTypeMap;
	}

	/**
	 * @return the seriesVisibilityMap
	 */
	public Map<SeriesType, Boolean> getSeriesVisibilityMap() {
		return seriesVisibilityMap;
	}

	/**
	 * @param lastSinglySelectedSeries
	 *            the lastSinglySelectedSeries to set
	 */
	public void setLastSinglySelectedSeries(SeriesType series) {
		this.lastSinglySelectedSeries = series;
	}

	/**
	 * @return the lastSinglySelectedSeries
	 */
	public SeriesType getLastSinglySelectedSeries() {
		return lastSinglySelectedSeries;
	}

	// Helper methods.

	/**
	 * Should the specified series be visible by default?
	 * 
	 * @param series
	 *            The series name.
	 * @return Whether or not the series should be visible by default.
	 */
	public boolean isSeriesVisibleByDefault(SeriesType series) {
		boolean visible = false;

		// TODO: We could delegate "is visible" to SeriesType which
		// could also be used by means plot model code.

		if (series != SeriesType.Excluded) {
			// Visual bands.
			visible |= series == SeriesType.Visual;
			visible |= series == SeriesType.Johnson_V;

			if (!atLeastOneVisualBandPresent && visible) {
				atLeastOneVisualBandPresent = true;
			}

			// We also allow for unspecified series type, e.g. since the
			// source could be from a simple observation file where no band
			// is specified.
			visible |= series == SeriesType.Unspecified;

			// Finally, if observations come from an external source (via a
			// plugin), make a series visible by default.
			NewStarMessage msg = Mediator.getInstance()
					.getLatestNewStarMessage();
			visible |= msg.getNewStarType() == NewStarType.NEW_STAR_FROM_ARBITRARY_SOURCE;
		}

		return visible;
	}
	
	// Returns a series creation listener, adding a new observation series to
	// the plot, and making it visible.
	protected Listener<SeriesCreationMessage> createSeriesCreationListener() {
		return new Listener<SeriesCreationMessage>() {

			@Override
			public void update(SeriesCreationMessage info) {
				int seriesNum = NO_SERIES;

				if (seriesExists(info.getType())) {
					seriesNum = replaceObservationSeries(info.getType(),
							info.getObs());
				} else {
					seriesNum = addObservationSeries(info.getType(),
							info.getObs());
				}

				// Make sure the new series is initially not visible, before
				// making it visible.
				seriesVisibilityMap.put(info.getType(), false);
				changeSeriesVisibility(seriesNum, true);
			}

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

	/**
	 * Listen for a model series selection and add/remove its fit and residual
	 * observations from the relevant collections. We need to re-calculate the
	 * means series if any of the model observations' series type is the same as
	 * the mean source series type.
	 */
	abstract protected Listener<ModelSelectionMessage> createModelSelectionListener();

	/**
	 * Listen for a filtered observation and add/remove its observations from
	 * the relevant collections. We need to re-calculate the means series if any
	 * of the model observations' series type is the same as the mean source
	 * series type.
	 */
	abstract protected Listener<FilteredObservationMessage> createFilteredObservationListener();

}