AAVSOPhotometryURLObservationSourceBase.java

/**
 * VStar: a statistical analysis tool for variable star data.
 * Copyright (C) 2010  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.plugin.ob.src.impl;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

import org.aavso.tools.vstar.data.DateInfo;
import org.aavso.tools.vstar.data.Magnitude;
import org.aavso.tools.vstar.data.SeriesType;
import org.aavso.tools.vstar.data.ValidObservation;
import org.aavso.tools.vstar.exception.CancellationException;
import org.aavso.tools.vstar.exception.ObservationReadError;
import org.aavso.tools.vstar.input.AbstractObservationRetriever;
import org.aavso.tools.vstar.plugin.InputType;
import org.aavso.tools.vstar.plugin.ObservationSourcePluginBase;
import org.aavso.tools.vstar.plugin.PluginComponentFactory;
import org.aavso.tools.vstar.ui.dialog.AbstractOkCancelDialog;
import org.aavso.tools.vstar.ui.dialog.DoubleField;
import org.aavso.tools.vstar.ui.dialog.MessageBox;
import org.aavso.tools.vstar.ui.dialog.TextArea;
import org.aavso.tools.vstar.ui.dialog.TextDialog;
import org.aavso.tools.vstar.ui.dialog.TextField;
import org.aavso.tools.vstar.ui.mediator.Mediator;
//12/02/2018 C. Kotnik added name to observations so they can be
//saved and reloaded from a file.
import org.aavso.tools.vstar.util.Pair;
import org.aavso.tools.vstar.util.help.Help;

/**
 * The base class for URL based AAVSO photometry observation source plugins.
 */
public class AAVSOPhotometryURLObservationSourceBase extends
		ObservationSourcePluginBase {

	private final String kind;

	protected final String baseURL;
	protected final Map<String, SeriesType> seriesNameToTypeMap;

	protected Locale locale;

	// Current parameter values.
	protected double raDegs;
	protected double decDegs;
	protected double radiusDegs;
	protected Set<String> seriesNames;
	protected boolean isHeliocentricObsSrc;

	// Ordered list of series.
	protected List<SeriesType> seriesList;

	// Create static VeLa filter field here since cannot create it in
	// inner dialog class.
	private static Pair<TextArea, JPanel> velaFilterFieldPanelPair;

	static {
		velaFilterFieldPanelPair = PluginComponentFactory
				.createVeLaFilterPane();
	}
	
	/**
	 * Constructor
	 * 
	 * @param kind
	 *            The kind of photometry observation source this is.
	 * @param baseURL
	 *            The base URL to which query parameters can be added.
	 * @param user
	 *            The user name to pass to the authenticator.
	 * @param password
	 *            The password to pass to the authenticator.
	 * @param isHeliocentricObsSrc
	 *            Will the observations for this plug-in have heliocentric JD
	 *            values?
	 */
	public AAVSOPhotometryURLObservationSourceBase(String kind, String baseURL,
			String user, String password, boolean isHeliocentricObsSrc) {
		super(user, password);
		this.kind = kind;
		this.baseURL = baseURL;
		this.seriesNameToTypeMap = new LinkedHashMap<String, SeriesType>();
		this.seriesNames = new HashSet<String>();
		this.isHeliocentricObsSrc = isHeliocentricObsSrc;

		locale = Locale.getDefault();
	}

	@Override
	public InputType getInputType() {
		return InputType.URL;
	}

	@Override
	public AbstractObservationRetriever getObservationRetriever() {
		return new AAVSOPhotometryURLObservationRetriever();
	}

	@Override
	public String getDescription() {
		String str = kind + " epoch photometry observation source plug-in.";

		if (locale.equals("es")) {
			// TODO
		}

		return str;
	}

	@Override
	public String getDisplayName() {
		String str = "New Star from " + kind + " epoch photometry database...";

		if (locale.equals("es")) {
			// TODO
		}

		return str;
	}

	@Override
	public List<URL> getURLs() throws Exception {
		List<URL> urls = new ArrayList<URL>();
		seriesList = new ArrayList<SeriesType>();

		AAVSOPhotometryURLSearchParameterDialog paramDialog = new AAVSOPhotometryURLSearchParameterDialog();

		if (!paramDialog.isCancelled()) {
			raDegs = paramDialog.getRADeg();
			decDegs = paramDialog.getDecDeg();
			radiusDegs = paramDialog.getRadiusDeg();
			setAdditive(paramDialog.isLoadAdditive());

			// We want numbers in the the URL to correspond to the server's
			// locale!
			String params = String
					.format(Locale.ENGLISH, "radeg=%f&decdeg=%f&raddeg=%f",
							raDegs, decDegs, radiusDegs);

			for (String seriesName : seriesNameToTypeMap.keySet()) {
				try {
					if (seriesNames.contains(seriesName)) {
						URL url = new URL(baseURL + params + "&filter="
								+ seriesName);
						urls.add(url);
						seriesList.add(seriesNameToTypeMap.get(seriesName));
					}
				} catch (MalformedURLException e) {
					throw new ObservationReadError("Cannot construct " + kind
							+ " URL (reason: " + e.getLocalizedMessage() + ")");
				}
			}

			setVelaFilterStr(paramDialog.getVelaFilterStr());

		} else {
			throw new CancellationException();
		}

		return urls;
	}

	@Override
	public String getInputName() {
		String desc = String.format(": RA=%f, Dec=%f, radius=%f, filter=",
				raDegs, decDegs, radiusDegs);
		for (String seriesName : seriesNameToTypeMap.keySet()) {
			if (seriesNames.contains(seriesName)) {
				desc += seriesName + ", ";
			}
		}
		desc = desc.substring(0, desc.lastIndexOf(", "));
		return kind + desc;
	}

	@Override
	public Set<SeriesType> getVisibleSeriesTypes() {
		return new LinkedHashSet<SeriesType>(seriesList);
	}

	class AAVSOPhotometryURLObservationRetriever extends
			AbstractObservationRetriever {

		public AAVSOPhotometryURLObservationRetriever() {
			super(getVelaFilterStr());
			setHeliocentric(isHeliocentricObsSrc);
		}

		@Override
		public void retrieveObservations() throws ObservationReadError,
				InterruptedException {
			try {
				List<InputStream> streams = getInputStreams();

				if (streams == null || seriesList == null
						|| streams.size() != seriesList.size()) {
					// If getURLs() has completed correctly, we should not get
					// to this point.
					throw new ObservationReadError(kind
							+ " input stream configuration error.");
				}

				int i = 0;
				for (InputStream stream : streams) {
					retrieveAAVSOPhotometryURLObs(stream, seriesList.get(i));
					i++;
				}
			} catch (IOException e) {
				throw new ObservationReadError(e.getLocalizedMessage());
			}
		}

		/**
		 * Retrieve a set of observations for a particular filter.
		 * 
		 * @param stream
		 *            The observation input stream.
		 * @param series
		 *            The series type to be used for the observations' band.
		 * @throws IOException
		 *             if a HTTP I/O error occurs.
		 * @throws ObservationReadError
		 *             if an error occurs during observation.
		 */
		private void retrieveAAVSOPhotometryURLObs(InputStream stream,
				SeriesType series) throws IOException, ObservationReadError {
			LineNumberReader reader = new LineNumberReader(
					new InputStreamReader(stream));

			String line = reader.readLine();

			while (line != null) {
				if (line.contains("An error occured while trying to connect to database")) {
					throw new ObservationReadError("Cannot access " + kind
							+ " database.");
				} else if (line.contains("No rows were returned by query")) {
					break;
				} else if (line.startsWith("#")) {
					if (inputName == null) {
						setInputInfo(null, line.substring(2).trim());
					}
				} else {
					String[] fields = line.split(",");

					double jd = Double.parseDouble(fields[0]);
					double mag = Double.parseDouble(fields[1]);
					double error = Double.parseDouble(fields[2]);

					ValidObservation ob = new ValidObservation();
					ob.setName(getInputName());
					ob.setDateInfo(new DateInfo(jd));
					ob.setMagnitude(new Magnitude(mag, error));
					ob.setBand(series);
					ob.setRecordNumber(reader.getLineNumber());

					if (fields.length == 4) {
						// Source field.
						int id = 0;
						try {
							id = Integer.parseInt(fields[3]);
						} catch (NumberFormatException e) {
							// Nothing to do.
						}
						String source = AAVSOPhotometrySource.fromID(id)
								.toString();
						ob.addDetail("SOURCE", source, "Source");
					}

					collectObservation(ob);
				}

				line = reader.readLine();
			}
		}

		@Override
		public String getSourceName() {
			return getInputName();
		}

		@Override
		public String getSourceType() {
			String str = kind + " epoch photometry database";

			if (locale.equals("es")) {
				// TODO
			}

			return str;
		}
	}

	@SuppressWarnings("serial")
	class AAVSOPhotometryURLSearchParameterDialog extends
			AbstractOkCancelDialog {

		private DoubleField raDegField;
		private DoubleField decDegField;
		private JComboBox radiusDegSelector;
		private List<JCheckBox> checkBoxes;
		private JCheckBox additiveLoadCheckbox;
		private TextField urlField;
		
		/**
		 * Constructor
		 */
		public AAVSOPhotometryURLSearchParameterDialog() {
			super(kind + " Search Parameters");

			Container contentPane = this.getContentPane();

			JPanel topPane = new JPanel();
			topPane.setLayout(new BoxLayout(topPane, BoxLayout.PAGE_AXIS));
			topPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

			topPane.add(createParameterPane());

			topPane.add(createAdditiveLoadCheckboxPane());

			// OK, Cancel
			//topPane.add(createButtonPane());
			
			// OK, Cancel, Help
			topPane.add(createButtonPane2());

			contentPane.add(topPane);

			this.pack();
			setLocationRelativeTo(Mediator.getUI().getContentPane());
			this.setVisible(true);
		}

		private JPanel createParameterPane() {
			JPanel panel = new JPanel();
			panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));

			raDegField = new DoubleField("RA (degrees)", 0.0, 360.0, raDegs);
			panel.add(raDegField.getUIComponent());
			panel.add(Box.createRigidArea(new Dimension(75, 10)));

			decDegField = new DoubleField("Dec (degrees)", -90.0, 90.0, decDegs);
			panel.add(decDegField.getUIComponent());
			panel.add(Box.createRigidArea(new Dimension(75, 10)));

			String[] radii = new String[] { "0.001 deg = 3.6 arcsec",
					"0.002 deg = 7.2 arcsec", "0.005 deg = 18.0 arcsec" };
			radiusDegSelector = new JComboBox(radii);
			radiusDegSelector.setBorder(BorderFactory
					.createTitledBorder("Radius (degrees"));
			panel.add(radiusDegSelector);
			panel.add(Box.createRigidArea(new Dimension(75, 10)));

			panel.add(createDataSeriesCheckboxes());
			panel.add(Box.createRigidArea(new Dimension(75, 10)));

			panel.add(createUrlPane());
			
			panel.add(velaFilterFieldPanelPair.second);
			
			panel.add(Box.createRigidArea(new Dimension(75, 10)));

			return panel;
		}

		private JPanel createAdditiveLoadCheckboxPane() {
			JPanel panel = new JPanel();
			panel.setBorder(BorderFactory.createTitledBorder("Additive Load"));

			additiveLoadCheckbox = new JCheckBox("Add to current?");
			panel.add(additiveLoadCheckbox);

			return panel;
		}

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

			checkBoxes = new ArrayList<JCheckBox>();

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

			for (String seriesName : seriesNameToTypeMap.keySet()) {
				JCheckBox checkBox = new JCheckBox(seriesName);

				checkBox.addActionListener(createSeriesVisibilityCheckBoxListener());

				checkBox.setSelected(seriesNames.contains(seriesName));

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

				checkBoxes.add(checkBox);
			}

			return panel;
		}

		// Return a listener for the series visibility checkboxes.
		private ActionListener createSeriesVisibilityCheckBoxListener() {
			return new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					JCheckBox checkBox = (JCheckBox) e.getSource();
					String seriesName = checkBox.getText();
					if (checkBox.isSelected()
							&& !seriesNames.contains(seriesName)) {
						seriesNames.add(seriesName);
					} else if (!checkBox.isSelected()
							&& seriesNames.contains(seriesName)) {
						seriesNames.remove(seriesName);
					}
				}
			};
		}

		/**
		 * This component provides a URL request button and corresponding
		 * action.
		 */
		private JPanel createUrlPane() {
			final Pattern raParamPattern = Pattern
					.compile("^.+radeg=(\\d+(\\.\\d+)?).+$");

			final Pattern decParamPattern = Pattern
					.compile("^.+decdeg=((\\-|\\+)?\\d+(\\.\\d+)?).+$");

			final Pattern radiusParamPattern = Pattern
					.compile("^.+raddeg=(\\d+\\.\\d+).*$");

			JPanel pane = new JPanel();

			JButton urlRequestButton = new JButton(
					"Populate Parameters From VSX URL");

			urlRequestButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					urlField = new TextField("URL");
					TextDialog urlDialog = new TextDialog("Enter URL", urlField);
					if (!urlDialog.isCancelled()
							&& !urlField.getValue().matches("^\\s*$")) {

						boolean raGiven = false;
						boolean decGiven = false;
						boolean radiusGiven = false;

						// Set RA, Dec, radius from URL and select all
						// checkboxes since the VSX URL contains no filter
						// information, returning all available.
						Matcher raMatcher = raParamPattern.matcher(urlField
								.getStringValue());

						if (raMatcher.matches()) {
							raGiven = true;
							raDegField.setValue(Double.parseDouble(raMatcher
									.group(1)));
						}

						Matcher decMatcher = decParamPattern.matcher(urlField
								.getStringValue());

						if (decMatcher.matches()) {
							decGiven = true;
							decDegField.setValue(Double.parseDouble(decMatcher
									.group(1)));
						}

						Matcher radiusMatcher = radiusParamPattern
								.matcher(urlField.getStringValue());

						if (radiusMatcher.matches()) {
							String radiusStr = radiusMatcher.group(1);
							switch (radiusStr) {
							case "0.001":
								radiusGiven = true;
								radiusDegSelector.setSelectedIndex(0);
								break;
							case "0.002":
								radiusGiven = true;
								radiusDegSelector.setSelectedIndex(1);
								break;
							case "0.005":
								radiusGiven = true;
								radiusDegSelector.setSelectedIndex(2);
								break;
							}
						}

						if (raGiven && decGiven && radiusGiven) {
							for (JCheckBox checkbox : checkBoxes) {
								checkbox.setSelected(true);
							}
						} else {
							MessageBox.showWarningDialog("Parameter Error",
									"Invalid URL");
						}
					}
				}
			});

			pane.add(urlRequestButton);

			return pane;
		}

		public double getRADeg() {
			return raDegField.getValue();
		}

		public double getDecDeg() {
			return decDegField.getValue();
		}

		public double getRadiusDeg() {
			// We'll just parse the first part of the string, which contains the
			// degrees value.
			String[] fields = ((String) radiusDegSelector.getSelectedItem())
					.split("\\s+");
			return Double.parseDouble(fields[0]);
		}

		/**
		 * @return The URL string.
		 */
		public String getUrlString() {
			return urlField.getValue().trim();
		}

		/**
		 * Return whether or not the load is additive.
		 * 
		 * @return Whether or not the load is additive.
		 */
		public boolean isLoadAdditive() {
			return additiveLoadCheckbox.isSelected();
		}

		/**
		 * @return The VeLa filter string.
		 */
		public String getVelaFilterStr() {
			return velaFilterFieldPanelPair.first.getValue().trim();
		}

		/**
		 * @see org.aavso.tools.vstar.ui.dialog.AbstractOkCancelDialog#helpAction()
		 */
		@Override
		protected void helpAction() {
			Help.openPluginHelp(getDocName());
		}

		/**
		 * @see org.aavso.tools.vstar.ui.dialog.AbstractOkCancelDialog#cancelAction()
		 */
		@Override
		protected void cancelAction() {
			// Nothing to do.
		}

		/**
		 * @see org.aavso.tools.vstar.ui.dialog.AbstractOkCancelDialog#okAction()
		 */
		@Override
		protected void okAction() {
			boolean ok = true;

			if (raDegField.getValue() == null || decDegField.getValue() == null) {
				ok = false;
			}

			if (ok) {
				cancelled = false;
				setVisible(false);
				dispose();
			}
		}
	}

}