SeriesType.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.data;

import java.awt.Color;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.prefs.Preferences;

import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.util.notification.Notifier;

// TODO:
// - Eventually change this class so that it starts out with a default set
//   of static series values but when network is available, it "refreshes" that
//   Set<SeriesType> dynamically.

/**
 * A type for bands and other series types, e.g. fainter-thans, means.
 */
public class SeriesType implements Comparable<SeriesType> {

    // Static members

    private final static int NO_INDEX = -1;

    // Plot point size.
    public final static int DEFAULT_SIZE = 4;

    private final static String COLOR_PREFS_PREFIX = "SERIES_COLOR_";
    private final static String SIZE_PREFS_PREFIX = "SERIES_SIZE_";

    private static Map<Integer, SeriesType> index2SeriesMap = new HashMap<Integer, SeriesType>();
    private static Map<String, SeriesType> shortName2SeriesMap = new HashMap<String, SeriesType>();
    private static Map<String, SeriesType> description2SeriesMap = new HashMap<String, SeriesType>();
    private static Map<SeriesType, Color> series2ColorMap = new HashMap<SeriesType, Color>();
    private static Map<SeriesType, Integer> series2SizeMap = new HashMap<SeriesType, Integer>();

    private static Notifier<Map<SeriesType, Color>> seriesColorChangeNotifier = new Notifier<Map<SeriesType, Color>>();
    private static Notifier<Map<SeriesType, Integer>> seriesSizeChangeNotifier = new Notifier<Map<SeriesType, Integer>>();

    private static Set<SeriesType> values = new TreeSet<SeriesType>();

    private static Preferences prefs;

    static {
        // Create preferences node for series colors.
        try {
            prefs = Preferences.userNodeForPackage(SeriesType.class);
        } catch (Throwable t) {
            // We need VStar to function in the absence of prefs.
        }
    }

    public static void initClass() {
    }

    // ** Auto-generated bands from aid.bands start here **

    public static final SeriesType Visual = new SeriesType(0, LocaleProps.get("VISUAL_SERIES"), "Vis.",
            new Color(0, 0, 0));

    public static final SeriesType Unknown = new SeriesType(1, LocaleProps.get("UNKNOWN_SERIES"), "N/A",
            new Color(255, 255, 0));

    public static final SeriesType Johnson_U = new SeriesType(7, "Johnson U", "U", new Color(0, 255, 255));

    public static final SeriesType Johnson_B = new SeriesType(3, "Johnson B", "B", new Color(0, 0, 255));

    public static final SeriesType Johnson_V = new SeriesType(2, "Johnson V", "V", new Color(0, 255, 0));

    public static final SeriesType Johnson_R = new SeriesType(10, "Johnson R", "RJ", new Color(192, 0, 64));

    public static final SeriesType Johnson_I = new SeriesType(11, "Johnson I", "IJ", new Color(192, 64, 128));

    public static final SeriesType Halpha = new SeriesType(13, "Halpha", "HA", new Color(192, 32, 0));

    public static final SeriesType Halpha_continuum = new SeriesType(14, "Halpha-continuum", "HAC",
            new Color(160, 32, 32));

    public static final SeriesType Blue = new SeriesType(21, LocaleProps.get("BLUE_SERIES"),
            LocaleProps.get("BLUE_SERIES") + "-Vis.", new Color(0, 0, 75));

    public static final SeriesType Green = new SeriesType(22, LocaleProps.get("GREEN_SERIES"),
            LocaleProps.get("GREEN_SERIES") + "-Vis.", new Color(0, 75, 0));

    public static final SeriesType Red = new SeriesType(23, LocaleProps.get("RED_SERIES"),
            LocaleProps.get("RED_SERIES") + "-Vis.", new Color(75, 0, 0));

    public static final SeriesType Yellow = new SeriesType(24, LocaleProps.get("YELLOW_SERIES"),
            LocaleProps.get("YELLOW_SERIES") + "-Vis.", new Color(255, 255, 128));

    public static final SeriesType K_NIR_2pt2micron = new SeriesType(26, "K NIR 2.2micron", "K",
            new Color(255, 128, 255));

    public static final SeriesType H_NIR_1pt6micron = new SeriesType(27, "H NIR 1.6micron", "H",
            new Color(128, 128, 128));

    public static final SeriesType J_NIR_1pt2micron = new SeriesType(28, "J NIR 1.2micron", "J",
            new Color(255, 0, 255));

    public static final SeriesType Sloan_z = new SeriesType(29, "Sloan z", "SZ", new Color(255, 192, 0));

    public static final SeriesType Stromgren_u = new SeriesType(30, "Stromgren u", "STU", new Color(0, 192, 255));

    public static final SeriesType Stromgren_v = new SeriesType(31, "Stromgren v", "STV", new Color(0, 255, 192));

    public static final SeriesType Stromgren_b = new SeriesType(32, "Stromgren b", "STB", new Color(0, 0, 192));

    public static final SeriesType Stromgren_y = new SeriesType(33, "Stromgren y", "STY", new Color(192, 255, 0));

    public static final SeriesType Stromgren_Hbw = new SeriesType(34, "Stromgren Hbw", "STHBW", new Color(0, 128, 255));

    public static final SeriesType Stromgren_Hbn = new SeriesType(35, "Stromgren Hbn", "STHBN", new Color(0, 128, 192));

    public static final SeriesType Cousins_R = new SeriesType(4, "Cousins R", "R", new Color(255, 0, 0));

    public static final SeriesType Sloan_u = new SeriesType(40, "Sloan u", "SU", new Color(192, 192, 0));

    public static final SeriesType Sloan_g = new SeriesType(41, "Sloan g", "SG", new Color(0, 64, 64));

    public static final SeriesType Sloan_r = new SeriesType(42, "Sloan r", "SR", new Color(128, 64, 0));

    public static final SeriesType Sloan_i = new SeriesType(43, "Sloan i", "SI", new Color(192, 64, 0));

    public static final SeriesType PanSTARRS_Z_short = new SeriesType(44, "PanSTARRS Z-short", "ZS",
            new Color(255, 64, 32));

    public static final SeriesType PanSTARRS_Y = new SeriesType(45, "PanSTARRS Y", "Y", new Color(96, 0, 0));

    public static final SeriesType Cousins_I = new SeriesType(5, "Cousins I", "I", new Color(255, 64, 0));

    public static final SeriesType Tri_Color_Blue = new SeriesType(50, "Tri-Color Blue", "TB", new Color(0, 0, 128));

    public static final SeriesType Tri_Color_Green = new SeriesType(51, "Tri-Color Green", "TG", new Color(0, 128, 0));

    public static final SeriesType Tri_Color_Red = new SeriesType(52, "Tri-Color Red", "TR", new Color(128, 0, 0));

    public static final SeriesType Optec_Wing_A = new SeriesType(55, "Optec Wing A", "MA", new Color(128, 64, 255));

    public static final SeriesType Optec_Wing_B = new SeriesType(56, "Optec Wing B", "MB", new Color(128, 64, 128));

    public static final SeriesType Optec_Wing_C = new SeriesType(57, "Optec Wing C", "MI", new Color(128, 0, 192));

    public static final SeriesType Orange_Liller = new SeriesType(6, LocaleProps.get("ORANGE_SERIES") + " (Liller)",
            LocaleProps.get("ORANGE_SERIES"), new Color(255, 128, 0));

    public static final SeriesType Clear_Blue_Blocking = new SeriesType(60, "Clear Blue Blocking", "CBB",
            new Color(255, 220, 32));

    public static final SeriesType Unfiltered_with_V_Zeropoint = new SeriesType(8, "Unfiltered with V Zeropoint", "CV",
            new Color(0, 192, 0));

    public static final SeriesType Unfiltered_with_R_Zeropoint = new SeriesType(9, "Unfiltered with R Zeropoint", "CR",
            new Color(192, 0, 0));

    public static final SeriesType GAIA_G = new SeriesType(58, "GAIA G", "GG",
            new Color(255, 165, 0));
    
    // ** Auto-generated bands from aid.bands end here **

    public static final SeriesType FAINTER_THAN = new SeriesType(SeriesType.NO_INDEX,
            LocaleProps.get("FAINTER_THAN_SERIES"), "FainterThan", Color.YELLOW);

    public static final SeriesType MEANS = new SeriesType(SeriesType.NO_INDEX, LocaleProps.get("MEANS_SERIES"),
            LocaleProps.get("MEANS_SERIES"), Color.BLUE, true, false);

    // Aaron's suggestion was to make Discrepant points light gray.
    public static final SeriesType DISCREPANT = new SeriesType(SeriesType.NO_INDEX,
            LocaleProps.get("DISCREPANT_SERIES"), LocaleProps.get("DISCREPANT_SERIES"), Color.LIGHT_GRAY);

    public static final SeriesType Unspecified = new SeriesType(SeriesType.NO_INDEX,
            LocaleProps.get("UNSPECIFIED_SERIES"), LocaleProps.get("UNSPECIFIED_SERIES"), Color.ORANGE);

    public static final SeriesType Filtered = new SeriesType(SeriesType.NO_INDEX, LocaleProps.get("FILTERED_SERIES"),
            LocaleProps.get("FILTERED_SERIES"), new Color(0, 153, 204), true, false);

    // Model series.
    public static final SeriesType Model = new SeriesType(SeriesType.NO_INDEX, LocaleProps.get("MODEL_SERIES"),
            LocaleProps.get("MODEL_SERIES"), Color.RED, true, false);

    // Model function series.
    public static final SeriesType ModelFunction = new SeriesType(SeriesType.NO_INDEX, LocaleProps.get("MODEL_SERIES"),
            LocaleProps.get("MODEL_SERIES"), Color.RED, true, false);

    // Residuals series.
    public static final SeriesType Residuals = new SeriesType(SeriesType.NO_INDEX, LocaleProps.get("RESIDUALS_SERIES"),
            LocaleProps.get("RESIDUALS_SERIES"), Color.CYAN, true, false);

    // This series can be used to mark an observation as being excluded for some
    // other reason than it being discrepant and all that classification
    // entails. The key thing is that this provides a category under which an
    // observation can be grouped in order to remove it from consideration in
    // analysis.
    public static final SeriesType Excluded = new SeriesType(SeriesType.NO_INDEX, LocaleProps.get("EXCLUDED_SERIES"),
            LocaleProps.get("EXCLUDED_SERIES"), Color.DARK_GRAY);

    /**
     * @return The series color change notifier.
     */
    public static Notifier<Map<SeriesType, Color>> getSeriesColorChangeNotifier() {
        return seriesColorChangeNotifier;
    }

    /**
     * @return The series size change notifier.
     */
    public static Notifier<Map<SeriesType, Integer>> getSeriesSizeChangeNotifier() {
        return seriesSizeChangeNotifier;
    }

    /**
     * Adds a series type instance to the appropriate collections if it does not
     * already exist.
     * 
     * @param type The series type to be added.
     */
    private static void updateStaticCollections(SeriesType type) {
        if (!values.contains(type)) {
            values.add(type);
            index2SeriesMap.put(type.getIndex(), type);
            shortName2SeriesMap.put(type.getShortName(), type);
            description2SeriesMap.put(type.getDescription(), type);

            Color colorPref = getColorPref(type);
            series2ColorMap.put(type, colorPref == null ? type.getColor() : colorPref);

            series2SizeMap.put(type, getSizePref(type));
        }
    }

    // Instance members per SeriesType value.
    private int index;
    private String description;
    private String shortName;
    private Color color;
    private int size;
    private boolean synthetic;
    private boolean userDefined;

    /**
     * Create a new series type or return an existing one.
     * 
     * @param description The series type's description.
     * @param shortName   The series type's short name (AID.bands).
     * @param color       The series type's color.
     * @param synthetic   Is this series synthetic (i.e. not associated with data
     *                    but derived from data)?
     * @param userDefined Is this series user-defined?
     * @return the new or pre-existing SeriesType instance.
     */
    public static SeriesType create(String description, String shortName, Color color, boolean synthetic,
            boolean userDefined) {
        // Create the series type of interest.
        SeriesType newSeries = new SeriesType(NO_INDEX, description, shortName, color, synthetic, userDefined);

        // Find which ever one now exists in the values set. That may be the
        // new instance or a previously created instance.
        for (SeriesType series : values()) {
            // One series type is equal to another if their descriptions are the
            // same. We can't have 2 series with the same name!
            if (series.equals(newSeries)) {
                newSeries = series;
                break;
            }
        }

        return newSeries;
    }

    /**
     * Delete the specified series type.
     * 
     * @param type The series type to delete.
     */
    public static void delete(SeriesType type) {
        // We don't want to delete in-built series!
        assert type.isUserDefined();

        if (values.contains(type)) {
            values.remove(type);

            index2SeriesMap.remove(type.getIndex());
            shortName2SeriesMap.remove(type.getShortName());
            description2SeriesMap.remove(type.getDescription());

            series2ColorMap.remove(type);

            series2SizeMap.remove(type);
        }
    }

    /**
     * Constructor
     * 
     * @param index       The series type's index (AID.Code).
     * @param description The series type's description.
     * @param shortName   The series type's short name (AID.bands).
     * @param color       The series type's color.
     * @param synthetic   Is this series synthetic (i.e. not associated with data
     *                    but derived from data)?
     * @param userDefined Is this series user-defined?
     */
    private SeriesType(int index, String description, String shortName, Color color, boolean synthetic,
            boolean userDefined) {
        this.index = index;
        this.description = description;
        this.shortName = shortName;
        this.color = color;
        this.size = DEFAULT_SIZE;
        this.synthetic = synthetic;
        this.userDefined = userDefined;
        updateStaticCollections(this);
    }

    /**
     * Constructor
     * 
     * Non-synthetic series type.
     * 
     * @param index       The series type's index (AID.Code).
     * @param description The series type's description.
     * @param shortName   The series type's short name (AID.bands).
     * @param color       The series type's color.
     */
    private SeriesType(int index, String description, String shortName, Color color) {
        this(index, description, shortName, color, false, false);
    }

    /**
     * Constructor
     * 
     * Non-synthetic series type with no index.
     * 
     * @param description The series type's description.
     * @param shortName   The series type's short name (AID.bands).
     * @param color       The series type's color.
     */
    private SeriesType(String description, String shortName, Color color) {
        this(SeriesType.NO_INDEX, description, shortName, color, false, false);
    }

    /**
     * @return the index
     */
    public int getIndex() {
        return index;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @return the shortName
     */
    public String getShortName() {
        return shortName;
    }

    /**
     * @return the default color
     */
    public Color getColor() {
        return color;
    }

    /**
     * @return the default size
     */
    public int getSize() {
        return size;
    }

    /**
     * @return the synthetic
     */
    public boolean isSynthetic() {
        return synthetic;
    }

    /**
     * @return the userDefined
     */
    public boolean isUserDefined() {
        return userDefined;
    }

    /**
     * Map from AID band index to series type.
     * 
     * @param index The integer band index
     * @return The band, Unspecified if not found.
     */
    public static SeriesType getSeriesFromIndex(int index) {
        SeriesType type = index2SeriesMap.get(index);

        if (type == null) {
            type = getDefault();
        }

        return type;
    }

    /**
     * Map from short band description to series type.
     * 
     * @param shortName The short description of the band.
     * @return The band, Unspecified if not found.
     */
    public static SeriesType getSeriesFromShortName(String shortName) {
        SeriesType type = shortName2SeriesMap.get(shortName);

        if (type == null) {
            // TODO: We can remove this block when we have changed or
            // downloaded new (or just deleted) files to replace existing
            // ones in the case where band short-names have changed!
            // Actually, we still see such names as V and B in formats like
            // AAVSO extended upload file format.
            if (shortName.equals("Unknown")) {
                type = Unknown;
            } else if (shortName.equals("Visual")) {
                type = Visual;
            } else if (shortName.equals("U")) {
                type = Johnson_U;
            } else if (shortName.equals("B")) {
                type = Johnson_B;
            } else if (shortName.equals("V")) {
                type = Johnson_V;
            } else if (shortName.equals("R")) {
                type = Cousins_R;
            } else if (shortName.equals("I")) {
                type = Cousins_I;
            }
        }

        if (type == null) {
            type = getDefault();
        }

        return type;
    }

    /**
     * Map from band descriptive name to series type.
     * 
     * @param description The descriptive description of the band.
     * @return The band, Unspecified if not found.
     */
    public static SeriesType getSeriesFromDescription(String description) {
        SeriesType type = description2SeriesMap.get(description);

        if (type == null) {
            type = getDefault();
        }

        return type;
    }

    /**
     * Does the series, specified by description, exist?
     * 
     * @param description The description, as passed to create().
     * @return Whether or not the series already exists.
     */
    public static boolean exists(String description) {
        return description2SeriesMap.keySet().contains(description);
    }

    private static Color getColorPref(SeriesType series) {
        Color color = null;

        if (series != null) {
            String colorPrefName = COLOR_PREFS_PREFIX + series.getDescription();
            try {
                String colorPrefValue = prefs.get(colorPrefName, null);
                if (colorPrefValue != null) {
                    // We expect this to be an integer RGB color value
                    // but we need a way to distinguish between there
                    // being no preference for the value and a valid
                    // color RGB value which there is no way of doing
                    // with a primitive integer.
                    color = new Color(Integer.parseInt(colorPrefValue));
                }
            } catch (Throwable t) {
                // We need VStar to function in the absence of prefs.
            }
        }

        return color;
    }

    private static void setColorPref(SeriesType series, Color color) {
        if (series != null && color != null) {
            String colorPrefName = COLOR_PREFS_PREFIX + series.getDescription();
            try {
                prefs.put(colorPrefName, color.getRGB() + "");
            } catch (Throwable t) {
                // We need VStar to function in the absence of prefs.
            }
        }
    }

    private static Integer getSizePref(SeriesType series) {
        int size = DEFAULT_SIZE;

        if (series != null) {
            String sizePrefName = SIZE_PREFS_PREFIX + series.getDescription();
            try {
                size = prefs.getInt(sizePrefName, DEFAULT_SIZE);
            } catch (Throwable t) {
                // We need VStar to function in the absence of prefs.
            }
        }

        return size;
    }

    private static void setSizePref(SeriesType series, int size) {
        if (series != null) {
            String sizePrefName = SIZE_PREFS_PREFIX + series.getDescription();
            try {
                prefs.putInt(sizePrefName, size);
            } catch (Throwable t) {
                // We need VStar to function in the absence of prefs.
            }
        }
    }

    /**
     * Given a series, retrieve its color.
     * 
     * @param series The series in question.
     * @return The corresponding color.
     */
    public static Color getColorFromSeries(SeriesType series) {
        Color color = series2ColorMap.get(series);

        if (color == null) {
            color = getDefault().getColor();
        }

        return color;
    }

    /**
     * Given a series, retrieve its size.
     * 
     * @param series The series in question.
     * @return The corresponding size.
     */
    public static int getSizeFromSeries(SeriesType series) {
        Integer size = series2SizeMap.get(series);

        if (size == null) {
            size = getDefault().getSize();
        }

        return size;
    }

    /**
     * Updates the series to color mapping according to the pairs in the supplied
     * map. Note that this may be a subset of all series-color pairs, so it may not
     * completely replace the existing map, just overwrite some pairs. It also
     * updates the series color preference and notifies listeners of the change.
     * 
     * @param newSeries2ColorMap The map with which to update the series-color map.
     */
    public static void updateSeriesColorMap(Map<SeriesType, Color> newSeries2ColorMap) {

        if (!newSeries2ColorMap.isEmpty()) {
            for (SeriesType series : newSeries2ColorMap.keySet()) {
                Color color = newSeries2ColorMap.get(series);
                series2ColorMap.put(series, color);
                setColorPref(series, color);
            }

            try {
                prefs.flush();
            } catch (Throwable t) {
                // We need VStar to function in the absence of prefs.
            }

            seriesColorChangeNotifier.notifyListeners(newSeries2ColorMap);
        }
    }

    /**
     * Updates the series to size mapping according to the pairs in the supplied
     * map. Note that this may be a subset of all series-size pairs, so it may not
     * completely replace the existing map, just overwrite some pairs. It also
     * updates the series size preference and notifies listeners of the change.
     * 
     * @param newSeries2SizeMap The map with which to update the series-size map.
     */
    public static void updateSeriesSizeMap(Map<SeriesType, Integer> newSeries2SizeMap) {

        if (!newSeries2SizeMap.isEmpty()) {
            for (SeriesType series : newSeries2SizeMap.keySet()) {
                int size = newSeries2SizeMap.get(series);
                series2SizeMap.put(series, size);
                setSizePref(series, size);
            }

            try {
                prefs.flush();
            } catch (Throwable t) {
                // We need VStar to function in the absence of prefs.
            }

            seriesSizeChangeNotifier.notifyListeners(newSeries2SizeMap);
        }
    }

    /**
     * Restore the default series colors and notifies listeners.
     */
    public static void setDefaultSeriesColors() {
        series2ColorMap.clear();

        try {
            prefs.clear();
        } catch (Throwable t) {
            // We need VStar to function in the absence of prefs.
        }

        for (SeriesType type : values()) {
            Color color = type.getColor();
            series2ColorMap.put(type, color);
            setColorPref(type, color);
        }

        try {
            prefs.flush();
        } catch (Throwable t) {
            // We need VStar to function in the absence of prefs.
        }

        seriesColorChangeNotifier.notifyListeners(series2ColorMap);
    }

    /**
     * Restore the default series sizes and notifies listeners.
     */
    public static void setDefaultSeriesSizes() {
        series2SizeMap.clear();

        try {
            prefs.clear();
        } catch (Throwable t) {
            // We need VStar to function in the absence of prefs.
        }

        for (SeriesType type : values()) {
            int size = type.getSize();
            series2SizeMap.put(type, size);
            setSizePref(type, size);
        }

        try {
            prefs.flush();
        } catch (Throwable t) {
            // We need VStar to function in the absence of prefs.
        }

        seriesSizeChangeNotifier.notifyListeners(series2SizeMap);
    }

    /**
     * Returns the default series type. This is like the equivalent of null for this
     * type.
     * 
     * @return The default series type.
     */
    public static SeriesType getDefault() {
        return Unspecified;
    }

    /**
     * We override toString() to return description rather than enum name.
     */
    public String toString() {
        return this.getDescription();
    }

    @Override
    public int compareTo(SeriesType other) {
        int result = 0;

        // Full field equality otherwise relational value defined in terms of
        // description.
        if (index == other.index && description.equals(other.description) && shortName.equals(other.shortName)
                && color.equals(other.color) && synthetic == other.synthetic) {
            result = 0;
        } else {
            result = description.compareTo(other.description);
        }

        return result;
    }

    /**
     * @return the set of all series type values.
     */
    public static Set<SeriesType> values() {
        return values;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((description == null) ? 0 : description.hashCode());
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof SeriesType)) {
            return false;
        }
        SeriesType other = (SeriesType) obj;
        if (description == null) {
            if (other.description != null) {
                return false;
            }
        } else if (!description.equals(other.description)) {
            return false;
        }
        return true;
    }
}