VStarScriptingAPI.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.scripting;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.aavso.tools.vstar.data.SeriesType;
import org.aavso.tools.vstar.data.ValidObservation;
import org.aavso.tools.vstar.exception.AlgorithmError;
import org.aavso.tools.vstar.exception.ObservationReadError;
import org.aavso.tools.vstar.input.database.VSXWebServiceStarInfoSource;
import org.aavso.tools.vstar.plugin.InputType;
import org.aavso.tools.vstar.plugin.ModelCreatorPluginBase;
import org.aavso.tools.vstar.plugin.ObservationSinkPluginBase;
import org.aavso.tools.vstar.plugin.ObservationSourcePluginBase;
import org.aavso.tools.vstar.plugin.model.impl.ApacheCommonsPolynomialFitCreatorPlugin;
import org.aavso.tools.vstar.plugin.ob.src.impl.AIDWebServiceObservationSourcePluginBase;
import org.aavso.tools.vstar.ui.MenuBar;
import org.aavso.tools.vstar.ui.dialog.MessageBox;
import org.aavso.tools.vstar.ui.mediator.AnalysisType;
import org.aavso.tools.vstar.ui.mediator.Mediator;
import org.aavso.tools.vstar.ui.mediator.StarInfo;
import org.aavso.tools.vstar.ui.mediator.ViewModeType;
import org.aavso.tools.vstar.ui.mediator.message.AnalysisTypeChangeMessage;
import org.aavso.tools.vstar.ui.model.plot.ObservationAndMeanPlotModel;
import org.aavso.tools.vstar.ui.resources.PluginLoader;
import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.util.model.IModel;
import org.aavso.tools.vstar.util.notification.Listener;
import org.aavso.tools.vstar.util.period.PeriodAnalysisCoordinateType;
import org.aavso.tools.vstar.util.period.dcdft.DcDftAnalysisType;
import org.aavso.tools.vstar.util.period.dcdft.TSDcDft;
import org.aavso.tools.vstar.util.period.wwz.WWZCoordinateType;
import org.aavso.tools.vstar.util.period.wwz.WWZStatistic;
import org.aavso.tools.vstar.util.period.wwz.WeightedWaveletZTransform;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

/**
 * This is VStar's scripting Application Programming Interface. An instance of
 * this class will be passed to scripts.
 * 
 * All methods are synchronised to ensure that only one API method is being
 * called at a time.
 */
public class VStarScriptingAPI {

    private Mediator mediator;

    private static VStarScriptingAPI instance;

    private AnalysisTypeChangeMessage analysisTypeMsg;

    /**
     * Constructor
     */
    private VStarScriptingAPI() {
	mediator = Mediator.getInstance();
	analysisTypeMsg = null;
	mediator.getAnalysisTypeChangeNotifier().addListener(createAnalysisTypeChangeListener());
    }

    /**
     * Return Singleton.
     */
    public static VStarScriptingAPI getInstance() {
	if (instance == null) {
	    instance = new VStarScriptingAPI();
	}

	return instance;
    }

    /**
     * Return an analysis type change listener.
     */
    private Listener<AnalysisTypeChangeMessage> createAnalysisTypeChangeListener() {
	return new Listener<AnalysisTypeChangeMessage>() {
	    @Override
	    public void update(AnalysisTypeChangeMessage info) {
		analysisTypeMsg = info;
	    }

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

    // ***************************************
    // ** VStar scripting API methods start **
    // ***************************************

    /**
     * Load a dataset from the specified path. This is equivalent to "File -> New
     * Star from File..."
     * 
     * @param path The path to the file.
     */
    public synchronized void loadFromFile(final String path) {
	commonLoadFromFile(path, false);
    }

    /**
     * Load a dataset from the specified path, adding it to the existing dataset.
     * 
     * @param path The path to the file.
     */
    public synchronized void additiveLoadFromFile(final String path) {
	commonLoadFromFile(path, true);
    }

    /**
     * Load a dataset from the specified URL.
     * 
     * @param url The URL of the file.
     */
    public synchronized void loadFromURL(final String path) {
	commonLoadFromURL(path, false);
    }

    /**
     * Load a dataset from the specified path, adding it to the existing dataset.
     * 
     * @param url The URL of the file.
     */
    public synchronized void additiveLoadFromURL(final String path) {
	commonLoadFromURL(path, true);
    }

    /**
     * Load a dataset from the specified path using the (possibly partial) plug-in
     * name to identify the observation source plug-in to use. This is equivalent to
     * plugin display name.
     * 
     * @param pluginName The sub-string with which to match the plug-in name.
     * @param path       The path to the file.
     */
    public synchronized void loadFromFile(final String pluginName, final String path) {
	commonLoadFromFileOrURLViaPlugin(pluginName, InputType.FILE, path, false);
    }

    /**
     * Load a dataset from the specified path, adding it to the existing dataset
     * using the (possibly partial) plug-in name to identify the observation source
     * plug-in to use. his is equivalent to plugin display name with the additive
     * checkbox selected.
     * 
     * @param pluginName The sub-string with which to match the plug-in name.
     * @param path       The path to the file.
     */
    public synchronized void additiveLoadFromFile(final String pluginName, final String path) {
	commonLoadFromFileOrURLViaPlugin(pluginName, InputType.FILE, path, true);
    }

    /**
     * Load a dataset from the specified URL using the (possibly partial) plug-in
     * name to identify the observation source plug-in to use.
     * 
     * @param pluginName The sub-string with which to match the plug-in name.
     * @param url        The URL.
     */
    public synchronized void loadFromURL(final String pluginName, final String url) {
	commonLoadFromFileOrURLViaPlugin(pluginName, InputType.URL, url, false);
    }

    /**
     * Load a dataset from the specified URL, adding it to the existing dataset,
     * using the (possibly partial) plug-in name to identify the observation source
     * plug-in to use.
     * 
     * @param pluginName The sub-string with which to match the plug-in name.
     * @param url        The URL.
     */
    public synchronized void additiveLoadFromURL(final String pluginName, final String url) {
	commonLoadFromFileOrURLViaPlugin(pluginName, InputType.URL, url, true);
    }

    /**
     * Load a dataset from the AAVSO international database.
     * 
     * @param name  The name (not AUID) of the object.
     * @param minJD The minimum JD of the range to be loaded.
     * @param maxJD The maximum JD of the range to be loaded.
     */
    public synchronized void loadFromAID(final String name, double minJD, double maxJD) {

	commonLoadFromAID(name, minJD, maxJD, null, null, false, false);
    }

    /**
     * Load a dataset from the AAVSO international database.
     * 
     * @param name  The name (not AUID) of the object.
     * @param minJD The minimum JD of the range to be loaded.
     * @param maxJD The maximum JD of the range to be loaded.
     * @param bands A short band name.
     * @oaram obscodes A comma-delimited list of observer codes or null for all
     *        available observer codes.
     * @param loadMinimalFields Load a minimal field subset?
     */
    public synchronized void loadFromAID(final String name, double minJD, double maxJD, String bands, String obscodes,
	    boolean loadMinimalFields) {

	commonLoadFromAID(name, minJD, maxJD, bands, obscodes, loadMinimalFields, false);
    }

    /**
     * Load a dataset from the AAVSO international database, adding it to the
     * existing dataset.
     * 
     * @param name  The name (not AUID) of the object.
     * @param minJD The minimum JD of the range to be loaded.
     * @param maxJD The maximum JD of the range to be loaded.
     */
    public synchronized void additiveLoadFromAID(final String name, double minJD, double maxJD) {

	commonLoadFromAID(name, minJD, maxJD, null, null, false, true);
    }

    /**
     * Load a dataset from the AAVSO international database, adding it to the
     * existing dataset.
     * 
     * @param name  The name (not AUID) of the object.
     * @param minJD The minimum JD of the range to be loaded.
     * @param maxJD The maximum JD of the range to be loaded.
     * @param bands A short band name.
     * @oaram obscodes A comma-delimited list of observer codes or null for all
     *        available observer codes.
     * @param loadMinimalFields Load a minimal field subset?
     */
    public synchronized void additiveLoadFromAID(final String name, double minJD, double maxJD, String bands,
	    String obscodes, boolean loadMinimalFields) {

	commonLoadFromAID(name, minJD, maxJD, bands, obscodes, loadMinimalFields, true);
    }

    // TODO: add loadFromAID(name) => all

    /**
     * Return a StarInfo object for named object.
     * 
     * @param name The name (not AUID) of the object.
     * @return The StarInfo object.
     */
    public synchronized StarInfo getStarInfo(String name) {
	init();
	VSXWebServiceStarInfoSource infoSrc = new VSXWebServiceStarInfoSource();
	return infoSrc.getStarByName(name);
    }

    /**
     * Save the raw or phase plot dataset (according to current mode) to a file of
     * rows of values separated by the specified delimiter.
     * 
     * @param path      The path to the file to save to (as a string).
     * @param delimiter The delimiter between data items.
     */
    public synchronized void saveObsList(final String path, String delimiter) {
	init();

	ObservationSinkPluginBase obSinkPlugin = getObsSinkPlugin(LocaleProps.get("TEXT_FORMAT_FILE"));

	if (obSinkPlugin != null) {
	    mediator.saveObsListToFile(Mediator.getUI().getComponent(), obSinkPlugin, new File(path), delimiter);
	} else {
	    MessageBox.showErrorDialog("Observation Save",
		    String.format("%s observation sink plug-in not found", LocaleProps.get("TEXT_FORMAT_FILE")));
	}

	mediator.waitForJobCompletion();
    }

    /**
     * Save the raw or phase plot mean list (according to current mode) to a file of
     * rows of values separated by the specified delimiter.
     * 
     * @param path      The path to the file to save to (as a string).
     * @param delimiter The delimiter between data items.
     */
    public synchronized void saveMeanList(final String path, String delimiter) {
	init();

	ObservationSinkPluginBase obSinkPlugin = getObsSinkPlugin(LocaleProps.get("TEXT_FORMAT_FILE"));

	if (obSinkPlugin != null) {
	    mediator.saveSyntheticObsListToFile(Mediator.getUI().getComponent(), obSinkPlugin,
		    ViewModeType.LIST_MEANS_MODE, new File(path), delimiter);
	} else {
	    MessageBox.showErrorDialog("Observation Save",
		    String.format("%s observation sink plug-in not found", LocaleProps.get("TEXT_FORMAT_FILE")));
	}

	mediator.waitForJobCompletion();
    }

    /**
     * Save the raw or phase plot model list (according to current mode) to a file
     * of rows of values separated by the specified delimiter.
     * 
     * @param path      The path to the file to save to (as a string).
     * @param delimiter The delimiter between data items.
     */
    public synchronized void saveModelList(final String path, String delimiter) {
	init();

	ObservationSinkPluginBase obSinkPlugin = getObsSinkPlugin(LocaleProps.get("TEXT_FORMAT_FILE"));

	if (obSinkPlugin != null) {
	    mediator.saveSyntheticObsListToFile(Mediator.getUI().getComponent(), obSinkPlugin, ViewModeType.MODEL_MODE,
		    new File(path), delimiter);
	} else {
	    MessageBox.showErrorDialog("Observation Save",
		    String.format("%s observation sink plug-in not found", LocaleProps.get("TEXT_FORMAT_FILE")));
	}

	mediator.waitForJobCompletion();
    }

    /**
     * Save the raw or phase plot residual list (according to current mode) to a
     * file of rows of values separated by the specified delimiter.
     * 
     * @param path      The path to the file to save to (as a string).
     * @param delimiter The delimiter between data items.
     */
    public synchronized void saveResidualList(final String path, String delimiter) {
	init();

	ObservationSinkPluginBase obSinkPlugin = getObsSinkPlugin(LocaleProps.get("TEXT_FORMAT_FILE"));

	if (obSinkPlugin != null) {
	    mediator.saveSyntheticObsListToFile(Mediator.getUI().getComponent(), obSinkPlugin,
		    ViewModeType.RESIDUALS_MODE, new File(path), delimiter);
	} else {
	    MessageBox.showErrorDialog("Observation Save",
		    String.format("%s observation sink plug-in not found", LocaleProps.get("TEXT_FORMAT_FILE")));
	}

	mediator.waitForJobCompletion();
    }

    /**
     * Save the light curve for the current view mode (raw or phase plot) to a PNG
     * image file.
     * 
     * @param path   The path to the file to save to (as a string).
     * @param width  The desired width of the image.
     * @param height The desired height of the image.
     */
    public synchronized void saveLightCurve(final String path, int width, int height) {
	init();
	mediator.saveCurrentPlotToFile(new File(path), width, height);
    }

    /**
     * Switch to phase plot mode. If no phase plot has been created yet, this will
     * open the phase parameter dialog.
     */
    public synchronized void phasePlotMode() {
	init();
	mediator.changeAnalysisType(AnalysisType.PHASE_PLOT);
	mediator.waitForJobCompletion();
    }

    /**
     * Switch to phase raw (light curve) mode.
     */
    public synchronized void lightCurveMode() {
	init();
	mediator.changeAnalysisType(AnalysisType.RAW_DATA);
	mediator.waitForJobCompletion();
    }

    /**
     * Create a phase plot given period and epoch.
     * 
     * @param period The period on which to fold the data.
     * @param epoch  The epoch (first Julian Date) for the phase plot.
     */
    public synchronized void phasePlot(double period, double epoch) {
	init();
	mediator.createPhasePlot(period, epoch);
	mediator.waitForJobCompletion();
    }

    /**
     * Create a polynomial fit model given the specified series and polynomial
     * degree.
     * 
     * @param seriesName Name of series to which polynomial fit should be applied.
     * 
     * @param degree     The required polynomial degree.
     */
    public synchronized void polyfit(String seriesName, double degree) {
	modelCreatorCommon(null, ApacheCommonsPolynomialFitCreatorPlugin.class, seriesName, new Double[] { degree });
    }

    /**
     * Create a model given the specified partial plugin name, series and
     * parameters.
     * 
     * @param seriesName Name of series to which the model creation operation should
     *                   be applied.
     * @param params     Array of parameters; could be of any type.
     */
    public synchronized void createModel(String pluginName, String seriesName, Object[] params) {
	modelCreatorCommon(pluginName, null, seriesName, params);
    }

    /**
     * Perform DCDFT period analysis with period range.
     * 
     * @param seriesName The short or long form of the series name, e.g. V or
     *                   Johnson V.
     * @param lowPeriod  The low value of the period range to search in.
     * @param highPeriod The high value of the period range to search in.
     * @param resolution The resolution of the search over the range.
     * @return An array of top-hits periods.
     */
    public synchronized Double[] dcdftPeriod(String seriesName, double lowPeriod, double highPeriod,
	    double resolution) {

	return dcdftCommon(seriesName, DcDftAnalysisType.PERIOD_RANGE, lowPeriod, highPeriod, resolution);
    }

    /**
     * Perform DCDFT period analysis with frequency range.
     * 
     * @param seriesName   The short or long form of the series name, e.g. V or
     *                     Johnson V.
     * @param lowFrequency The low value of the frequency range to search in.
     * @param highPeriod   The high value of the frequency range to search in.
     * @param resolution   The resolution of the search over the range.
     * @return An array of top-hits frequencies.
     */
    public synchronized Double[] dcdftFrequency(String seriesName, double lowFrequency, double highFrequency,
	    double resolution) {

	return dcdftCommon(seriesName, DcDftAnalysisType.FREQUENCY_RANGE, lowFrequency, highFrequency, resolution);
    }

    /**
     * Perform DCDFT period analysis with standard scan.
     * 
     * @param seriesName The short or long form of the series name, e.g. V or
     *                   Johnson V.
     * @return An array of top-hits frequencies.
     */
    public synchronized Double[] dcdftStandardScan(String seriesName) {

	return dcdftCommon(seriesName, DcDftAnalysisType.STANDARD_SCAN, 0, 0, 0);
    }

    /**
     * Perform WWZ time-frequency analysis with period range.
     * 
     * @param seriesName    The short or long form of the series name, e.g. V or
     *                      Johnson V.
     * @param minPeriod     The low value of the period range to search in.
     * @param maxPeriod     The high value of the period range to search in.
     * @param periodStep    The resolution of the search over the range.
     * @param decay         The wavelet decay constant to use.
     * @param timeDivisions The number of time divisions to use.
     * @return An array of top-hits periods; may be empty.
     */
    public synchronized Double[][] wwzPeriod(String seriesName, double minPeriod, double maxPeriod, double periodStep,
	    double decay, double timeDivisions) {

	List<ValidObservation> obs = getObsForSeries(seriesName);

	Double[][] results = {};

	if (obs.size() > 0) {
	    WeightedWaveletZTransform wwz = new WeightedWaveletZTransform(obs, decay, timeDivisions);

	    wwz.make_freqs_from_period_range(Math.min(minPeriod, maxPeriod), Math.max(minPeriod, maxPeriod),
		    periodStep);

	    results = wwzCommon(wwz, WWZCoordinateType.PERIOD);
	}

	return results;
    }

    /**
     * Perform WWZ time-frequency analysis with period range.
     * 
     * @param seriesName    The short or long form of the series name, e.g. V or
     *                      Johnson V.
     * @param minFreq       The low value of the frequency range to search in.
     * @param maxFreq       The high value of the frequency range to search in.
     * @param freqStep      The resolution of the search over the range.
     * @param decay         The wavelet decay constant to use.
     * @param timeDivisions The number of time divisions to use.
     * @return An array of top-hits frequencies; may be empty.
     */
    public synchronized Double[][] wwzFrequency(String seriesName, double minFreq, double maxFreq, double freqStep,
	    double decay, double timeDivisions) {

	List<ValidObservation> obs = getObsForSeries(seriesName);

	Double[][] results = {};

	if (obs.size() > 0) {
	    WeightedWaveletZTransform wwz = new WeightedWaveletZTransform(obs, decay, timeDivisions);

	    wwz.make_freqs_from_freq_range(Math.min(minFreq, maxFreq), Math.max(minFreq, maxFreq), freqStep);

	    results = wwzCommon(wwz, WWZCoordinateType.FREQUENCY);
	}

	return results;
    }

    /**
     * Return the last error.
     * 
     * @return The error string; may be null.
     */
    public synchronized String getError() {
	return ScriptRunner.getInstance().getError();
    }

    /**
     * Return the last warning.
     * 
     * @return The warning string; may be null.
     */
    public synchronized String getWarning() {
	return ScriptRunner.getInstance().getWarning();
    }

    /**
     * Returns a comma-separated string of series names for the current dataset,
     * including bands and synthetic series such as means, model, residuals.
     * 
     * @return a comma-separated series names
     */
    public synchronized String getSeries() {
	init();
	ObservationAndMeanPlotModel model = analysisTypeMsg.getObsAndMeanChartPane().getObsModel();

	String nameStr = "";

	for (SeriesType type : model.getSeriesKeys()) {
	    nameStr += type.getShortName() + ",";
	}

	if (nameStr.lastIndexOf(",") != -1) {
	    nameStr = nameStr.substring(0, nameStr.lastIndexOf(","));
	}

	return nameStr;
    }

    /**
     * Returns the time values (e.g. JD, HJD) for the specified series.
     * 
     * @param seriesName The short or long series name.
     * @return An array of time values.
     */
    public double[] getTimes(String seriesName) {
	List<ValidObservation> obs = getObsForSeries(seriesName);

	List<Double> timeList = obs.stream().map(ob -> ob.getJD()).collect(Collectors.toList());

	double[] times = new double[timeList.size()];

	int i = 0;
	for (Double time : timeList) {
	    times[i++] = time;
	}

	return times;
    }

    /**
     * Returns the (standard) phase values for the specified series.
     * 
     * @param seriesName The short or long series name.
     * @return An array of phase values.
     */
    public double[] getPhases(String seriesName) {
	List<ValidObservation> obs = getObsForSeries(seriesName);

	List<Double> phaseList = obs.stream().map(ob -> ob.getStandardPhase()).collect(Collectors.toList());

	double[] phases = new double[phaseList.size()];

	int i = 0;
	for (Double phase : phaseList) {
	    phases[i++] = phase;
	}

	return phases;
    }

    /**
     * Returns the magnitude values for the specified series.
     * 
     * @param seriesName The short or long series name.
     * @return An array of magnitude values.
     */
    public double[] getMags(String seriesName) {
	List<ValidObservation> obs = getObsForSeries(seriesName);

	List<Double> magList = obs.stream().map(ob -> ob.getMag()).collect(Collectors.toList());

	double[] mags = new double[magList.size()];

	int i = 0;
	for (Double mag : magList) {
	    mags[i++] = mag;
	}

	return mags;
    }

    /**
     * Makes the specified series in the current dataset visible. Calling this
     * method more than one consecutive time with the same visibility value has no
     * effect on the current visibility status of a series.
     * 
     * @param seriesName The long name (e.g. "Johnson V" not "V") of the series to
     *                   make visible.
     */
    public synchronized void makeVisible(final String seriesName) {
	init();
	ObservationAndMeanPlotModel obsPlotModel = analysisTypeMsg.getObsAndMeanChartPane().getObsModel();

	if (SeriesType.exists(seriesName)) {
	    SeriesType series = SeriesType.getSeriesFromDescription(seriesName);

	    if (obsPlotModel.getSeriesKeys().contains(series)) {
		int seriesNum = obsPlotModel.getSrcTypeToSeriesNumMap().get(series);
		obsPlotModel.changeSeriesVisibility(seriesNum, true);
	    } else {
		ScriptRunner.getInstance().setError("Series does not exist in loaded dataset: " + seriesName);
	    }
	} else {
	    ScriptRunner.getInstance().setError("Unknown series type: " + seriesName);
	}

	mediator.waitForJobCompletion();
    }

    // TODO:
    // - makeInvisible()
    // - allow AoV, other period search plugins: see
    // commonLoadFromFileOrURLViaPlugin() re: pattern;
    // API may need to change to accommodate this, e.g.
    // a generic way to get results as a collection

    /**
     * Create a scatter plot in the named tab.
     * 
     * @param name   The name of the dataset to create or add to.
     * @param xTitle The x axis title.
     * @param yTitle The y axis title.
     * @param xs     The x values to plot.
     * @param ys     The y values to plot.
     */
    public synchronized void scatter(String name, String xTitle, String yTitle, double[] xs, double[] ys) {
	// TODO: in a future revision, store/get dataset in/from DocumentManager and
	// fire dataset change if exists when adding new series; also need plot if e.g.
	// scatter then lines(); also setting renderer for dataset
	XYSeriesCollection dataset = new XYSeriesCollection();

	XYSeries series = new XYSeries(name);
	for (int i = 0; i < xs.length; i++) {
	    series.add(xs[i], ys[i]);
	}
	dataset.addSeries(series);

	JFreeChart chart = ChartFactory.createScatterPlot(name, xTitle, yTitle, dataset, PlotOrientation.VERTICAL, true,
		true, true);

	XYPlot plot = (XYPlot) chart.getPlot();

	plot.setBackgroundPaint(new Color(255, 255, 255));

	ChartPanel panel = new ChartPanel(chart);

	Mediator.getUI().addTab(name, ViewModeType.PLOT_OBS_MODE, panel, false);
    }

    /**
     * Pause for the specified number of milliseconds.
     * 
     * @param millis The time to pause for.
     */
    public synchronized void pause(long millis) {
	try {
	    Thread.sleep(millis);
	} catch (InterruptedException e) {
	}
    }

    /**
     * Exit VStar
     */
    public synchronized void exit() {
	mediator.quit();
    }

    // *************************************
    // ** VStar scripting API methods end **
    // *************************************

    // Common methods

    /**
     * Common dataset file load method.
     * 
     * @param path       The path to the file or URL.
     * @param isAdditive Is this load additive?
     */
    private void commonLoadFromFile(final String path, boolean isAdditive) {
	init();

	commonLoadFromFileOrURLViaPlugin(MenuBar.NEW_STAR_FROM_FILE, InputType.FILE, path, isAdditive);
    }

    /**
     * Common dataset URL load method.
     * 
     * @param url        The URL of the file.
     * @param isAdditive Is this load additive?
     */
    private void commonLoadFromURL(final String url, boolean isAdditive) {
	init();

	commonLoadFromFileOrURLViaPlugin(MenuBar.NEW_STAR_FROM_FILE, InputType.URL, url, isAdditive);
    }

    /**
     * Common dataset plug-in load method.
     * 
     * @param pluginName The sub-string with which to match the plug-in name.
     * @param inputType  The input type (e.g. file, URL).
     * @param location   The path or URL to the file.
     * @param isAdditive Is this load additive?
     */
    private void commonLoadFromFileOrURLViaPlugin(final String pluginName, InputType inputType, final String location,
	    boolean isAdditive) {
	init();

	ObservationSourcePluginBase obSourcePlugin = null;

	for (ObservationSourcePluginBase plugin : PluginLoader.getObservationSourcePlugins()) {
	    if (plugin.getDisplayName().contains(pluginName)
		    && (plugin.getInputType() == inputType || plugin.getInputType() == InputType.FILE_OR_URL)) {
		obSourcePlugin = plugin;
		break;
	    }
	}

	if (obSourcePlugin != null) {
	    try {
		if (inputType == InputType.FILE) {
		    mediator.createObservationArtefactsFromObSourcePlugin(obSourcePlugin, new File(location),
			    isAdditive);
		} else if (inputType == InputType.URL) {
		    mediator.createObservationArtefactsFromObSourcePlugin(obSourcePlugin, new URL(location),
			    isAdditive);

		}
	    } catch (IOException e) {
		MessageBox.showErrorDialog("Load File", "Cannot load file: " + location);
	    } catch (ObservationReadError e) {
		MessageBox.showErrorDialog("Load File", "Error reading observations from file: " + location
			+ " (reason: " + e.getLocalizedMessage() + ")");
	    }
	} else {
	    MessageBox.showErrorDialog("Load File", "No matching observation plugin found '" + pluginName + "'");
	}

	mediator.waitForJobCompletion();
    }

    /**
     * Common AID dataset load method.
     * 
     * @param name  The target name (not AUID).
     * @param minJD The minimum JD of the range to be loaded.
     * @param maxJD The maximum JD of the range to be loaded.
     * @param band  A comma-delimited list of short band names or null for all
     *              available bands.
     * @oaram obscodes A comma-delimited list of observer codes or null for all
     *        available observer codes.
     * @param loadMinimalFields Load a minimal field subset?
     * @param isAdditive        Is this load additive?
     */
    private void commonLoadFromAID(final String name, double minJD, double maxJD, String bands, String obscodes,
	    boolean loadMinimalFields, boolean isAdditive) {
	init();

	ObservationSourcePluginBase obSourcePlugin = null;

	for (ObservationSourcePluginBase plugin : PluginLoader.getObservationSourcePlugins()) {
	    if (plugin.getDisplayName().contains(MenuBar.NEW_STAR_FROM_DATABASE)) {
		obSourcePlugin = plugin;
		break;
	    }
	}

	if (obSourcePlugin != null) {
	    try {
		VSXWebServiceStarInfoSource infoSrc = new VSXWebServiceStarInfoSource();
		StarInfo info = infoSrc.getStarByName(name);

		String url = "";

		AIDWebServiceObservationSourcePluginBase aidPlugin = (AIDWebServiceObservationSourcePluginBase) obSourcePlugin;

		if (bands == null) {
		    url = aidPlugin.createAIDUrlForAUID(info.getAuid(), minJD, maxJD);
		} else {
		    // TODO: change null and false to use parameters passed in
		    url = aidPlugin.createAIDUrlForAUID(info.getAuid(), minJD, maxJD, bands, null, false);
		}

		aidPlugin.setUrl(url);
		aidPlugin.setInfo(info);

		mediator.createObservationArtefactsFromObSourcePlugin(aidPlugin, (URL) null, isAdditive);
	    } catch (IOException e) {
		MessageBox.showErrorDialog("Load from AID", "Cannot load from AID:  " + name);
	    } catch (ObservationReadError e) {
		MessageBox.showErrorDialog("Load from AID",
			"Error reading observations from AID: " + name + " (reason: " + e.getLocalizedMessage() + ")");
	    }
	} else {
	    MessageBox.showErrorDialog("Load from AID", "Error initialising load from AID plug-in");
	}

	mediator.waitForJobCompletion();
    }

    /**
     * 
     * @param pluginName
     * @return
     */
    private ObservationSinkPluginBase getObsSinkPlugin(String pluginName) {
	ObservationSinkPluginBase obSinkPlugin = null;

	for (ObservationSinkPluginBase plugin : PluginLoader.getObservationSinkPlugins()) {
	    if (pluginName.contains(plugin.getDisplayName())) {
		obSinkPlugin = plugin;
		break;
	    }
	}

	return obSinkPlugin;
    }

    /**
     * Perform DCDFT period analysis.
     * 
     * @param seriesName   The short or long form of the series name, e.g. V or
     *                     Johnson V.
     * @param analysisType Period range, frequency range, standard scan?
     * @param lowPeriod    The low value of the period range to search in.
     * @param highPeriod   The high value of the period range to search in.
     * @param resolution   The resolution of the search over the range.
     * @return An array of top-hits periods or frequencies.
     */
    private synchronized Double[] dcdftCommon(String seriesName, DcDftAnalysisType analysisType, double low,
	    double high, double resolution) {

	init();

	Double[] topHitPeriods = null;

	List<ValidObservation> obs = getObsForSeries(seriesName);

	if (obs.size() > 0) {
	    TSDcDft dcdft = null;

	    switch (analysisType) {
	    case PERIOD_RANGE:
		dcdft = new TSDcDft(obs, DcDftAnalysisType.PERIOD_RANGE);
		dcdft.setLoPeriodValue(low);
		dcdft.setHiPeriodValue(high);
		dcdft.setResolutionValue(resolution);
		break;

	    case FREQUENCY_RANGE:
		dcdft = new TSDcDft(obs, low, high, resolution);
		break;

	    case STANDARD_SCAN:
		dcdft = new TSDcDft(obs);
		break;
	    }

	    try {
		dcdft.execute();

		Map<PeriodAnalysisCoordinateType, List<Double>> topHits = dcdft.getTopHits();

		PeriodAnalysisCoordinateType coordType = null;

		if (analysisType == DcDftAnalysisType.PERIOD_RANGE) {
		    coordType = PeriodAnalysisCoordinateType.PERIOD;
		} else {
		    coordType = PeriodAnalysisCoordinateType.FREQUENCY;
		}

		// Get array of top-hit periods or frequencies.
		topHitPeriods = topHits.get(coordType).toArray(new Double[0]);

	    } catch (AlgorithmError e) {
		ScriptRunner.getInstance().setError(e.getMessage());
	    }
	} else {
	    ScriptRunner.getInstance().setError("No observations in series " + seriesName);
	}

	return topHitPeriods;
    }

    /**
     * Perform WWZ period analysis.
     * 
     * @param wwz       The initialised (WWZ transform object.
     * @param coordType The coordinate type: period or frequency.
     * @return An array of top-hits times and periods or frequencies.
     */
    private synchronized Double[][] wwzCommon(WeightedWaveletZTransform wwz, WWZCoordinateType coordType) {

	init();

	Double[][] maximalStats = null;

	try {
	    wwz.execute();

	    List<WWZStatistic> maximalStatsList = wwz.getMaximalStats();

	    maximalStats = new Double[maximalStatsList.size()][2];

	    int i = 0;
	    for (WWZStatistic stat : maximalStatsList) {
		Double[] pair = new Double[2];
		pair[0] = stat.getTau();

		switch (coordType) {
		case FREQUENCY:
		    pair[1] = stat.getFrequency();
		    break;
		case PERIOD:
		    pair[1] = stat.getPeriod();
		    break;
		default:
		    throw new IllegalArgumentException("WWZ: only period or frequency allowed in output");
		}

		maximalStats[i++] = pair;
	    }
	} catch (AlgorithmError e) {
	    ScriptRunner.getInstance().setError(e.getMessage());
	}

	return maximalStats;
    }

    /**
     * Common model creator plugin method.
     * 
     * @param seriesName Partial plugin name; may be null.
     * @param clazz      Plugin class; may be null.
     * @param series     Name of series to which the model creation operation should
     *                   be applied.
     * @param params     Array of parameters; could be of any type.
     */
    private void modelCreatorCommon(String pluginName, Class<?> clazz, String seriesName, Object[] params) {
	// It's not okay for them both to be null!
	assert pluginName != null || clazz != null;

	for (ModelCreatorPluginBase plugin : PluginLoader.getModelCreatorPlugins()) {
	    if ((pluginName != null && plugin.getDisplayName().contains(pluginName))
		    || (clazz != null && plugin.getClass() == clazz)) {
		plugin.setParams(params);
		List<ValidObservation> obs = getObsForSeries(seriesName);
		IModel model = plugin.getModel(obs);
		Mediator.getInstance().performModellingOperation(model);
		mediator.waitForJobCompletion();
		break;
	    }
	}
    }

    /**
     * Given a series name, return a list of observations for the series.
     * 
     * @param seriesName The short or long form of the series name, e.g. V or
     *                   Johnson V.
     * @return A list of valid observations for the series; may be empty.
     */
    private List<ValidObservation> getObsForSeries(String seriesName) {

	List<ValidObservation> obs = Collections.emptyList();

	// Find the requested series...
	SeriesType series = SeriesType.getSeriesFromShortName(seriesName);

	if (series == SeriesType.getDefault()) {
	    series = SeriesType.getSeriesFromDescription(seriesName);
	}

	// ...if the user wasn't really asking for the default series name but
	// we got it anyway, we treat this as an error.
	if (series == SeriesType.getDefault()
		&& !SeriesType.getDefault().getDescription().equals(seriesName.toLowerCase())) {
	    ScriptRunner.getInstance().setError("Unknown series " + seriesName);
	} else {
	    // ...otherwise, get the observations for the requested series.
	    obs = mediator.getObservationPlotModel(mediator.getAnalysisType()).getObservations(series);

	    if (obs.size() == 0) {
		ScriptRunner.getInstance().setError("No observations in series " + seriesName);
	    }
	}

	return obs;
    }

    // Helpers

    private void init() {
	clearError();
    }

    private void clearError() {
	ScriptRunner.getInstance().setError(null);
    }
}