/*
* This file is a part of EpnTAPClient.
* This program aims to provide EPN-TAP support for software clients, like CASSIS spectrum analyzer.
* See draft specifications: https://voparis-confluence.obspm.fr/pages/viewpage.action?pageId=559861
* Copyright (C) 2016 Institut de Recherche en Astrophysique et Planétologie.
*
* This program is free software: you can
* redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. You should have received a copy of
* the GNU General Public License along with this program. If not, see
* .
*/
package eu.omp.irap.vespa.epntapclient.view;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import eu.omp.irap.vespa.epntapclient.votable.controller.VOTableConnection;
import eu.omp.irap.vespa.epntapclient.votable.controller.VOTableException.CantSendQueryException;
/**
* A field used to set a service parameter to build the query (in the parameter panel). ParamField
* is an abstract method and all type of parameter field should extend it. See
* https://voparis-confluence.obspm.fr/display/VES/4+-+EPN-TAP+queries to get all parameters
* details.
*
* @author N. Jourdane
*/
public abstract class ParamField extends JPanel {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The logger for the class ParamField. */
static final Logger logger = Logger.getLogger(ParamField.class.getName());
/** The minimum width of the field. */
private static final int MIN_FIELD_WIDTH = 30;
/** The preferred field height. */
private static final int FIELD_HEIGHT = 20;
/** The maximum width of the field. */
private static final int MAX_FIELD_WIDTH = 400;
/** The preferred label width. */
private static final int LABEL_WIDTH = 140;
/** The URL of the resolver used for the `target name` field. */
private static final String RESOLVER_URL = "http://voparis-registry.obspm.fr/ssodnet/1/autocomplete";
/** The date format used in the DateRange field */
private static final String DATE_FORMAT = "dd/MM/yyyy";
/** The regex used to validate the Date fields */
private static final String DATE_REGEX = "(^(((0[1-9]|1[0-9]|2[0-8])[\\/](0[1-9]|1[012]))|((29|30|31)[\\/](0[13578]|1[02]))|((29|30)[\\/](0[4,6,9]|11)))[\\/](19|[2-9][0-9])\\d\\d$)|(^29[\\/]02[\\/](19|[2-9][0-9])(00|04|08|12|16|20|24|28|32|36|40|44|48|52|56|60|64|68|72|76|80|84|88|92|96)$)";
/** The suffix used in REG-TAP parameters names, indicating that it's a beginning of a range. */
private static final String MIN_SUFFIX = "min";
/** The suffix used in REG-TAP parameters names, indicating that it is a end of a range. */
private static final String MAX_SUFFIX = "max";
/** The main view of the application. */
protected EpnTapMainView mainView;
/** The parameter name of the field */
protected String paramName;
/**
* Method constructor for the parameter field abstract class, which do all common action for all
* type of field, such as displaying the name of the parameter.
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public ParamField(EpnTapMainView mainView, String paramName) {
super();
this.mainView = mainView;
this.paramName = paramName;
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setMaximumSize(new Dimension(ParamField.MAX_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
String strLabel = paramName.replaceAll("_", " ").trim();
JLabel label = new JLabel(strLabel.substring(0, 1).toUpperCase() + strLabel.substring(1));
label.setPreferredSize(new Dimension(ParamField.LABEL_WIDTH, ParamField.FIELD_HEIGHT));
this.add(label);
// TODO: Add tooltip text based on rr.table_column.column_description
}
/**
* The string field is used for parameter with a `String` class. It is a simple JTextField with
* no verification. The parameter is sent to the controller each time it is modified.
*
* @author N. Jourdane
*/
public static class StringField extends ParamField implements TextFieldListener {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The JTextField used to put the parameter value. */
JTextField field;
/**
* Method constructor for the string field.
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public StringField(EpnTapMainView mainView, String paramName) {
super(mainView, paramName);
field = new JTextField();
ParamField.addChangeListener(this, field);
this.add(field);
}
/**
* This method is called each time the field is modified.
*/
@Override
public void update(JTextField textField) {
if ("".equals(textField.getText())) {
mainView.event(Event.PARAMETER_CHANGED, paramName, null);
} else {
mainView.event(Event.PARAMETER_CHANGED, paramName, textField.getText());
}
}
}
/**
* The float field is used for parameter with a `Float` class. It is a JTextField which checks
* if the content is a valid float. If the parameter is valid or if it is empty, then the float
* value is sent to the controller.
*
* @author N. Jourdane
*/
public static class FloatField extends ParamField implements TextFieldListener {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The JTextField used to put the parameter value. */
JTextField field;
/**
* Method constructor
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public FloatField(EpnTapMainView mainView, String paramName) {
super(mainView, paramName);
field = new JTextField();
ParamField.addChangeListener(this, field);
this.add(field);
}
/**
* This method is called each time the field is modified.
*/
@Override
public void update(JTextField textField) {
if ("".equals(textField.getText())) {
textField.setBackground(Color.WHITE);
mainView.event(Event.PARAMETER_REMOVED, paramName);
} else {
try {
float value = Float.parseFloat(textField.getText());
mainView.event(Event.PARAMETER_CHANGED, paramName, value);
textField.setBackground(Color.WHITE);
} catch (@SuppressWarnings("unused") NumberFormatException e) {
textField.setBackground(Color.PINK);
}
}
}
}
/**
* The date range field is used for couples of parameter with both a `Date` type (actually only
* `time_min` and `time_max` parameters is concerned for now). These are JTextFields which check
* if the content is a valid date, according to DATE_FORMAT. If the parameter is valid or if it
* is empty, then the dates value are sent to the controller, in Julian Day format.
*
* @author N. Jourdane
*/
public static class DateRangeField extends ParamField implements TextFieldListener {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The JTextField used to put the parameter minimum value of the range. */
JTextField fieldMin;
/** The JTextField used to put the parameter maximum value of the range. */
JTextField fieldMax;
/**
* Method constructor
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public DateRangeField(EpnTapMainView mainView, String paramName) {
super(mainView, paramName);
this.add(new JLabel("min "));
fieldMin = new JTextField();
fieldMin.setName(ParamField.MIN_SUFFIX);
fieldMin.setPreferredSize(
new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
ParamField.addChangeListener(this, fieldMin);
this.add(fieldMin);
this.add(new JLabel("max "));
fieldMax = new JTextField();
fieldMax.setName(ParamField.MAX_SUFFIX);
fieldMax.setPreferredSize(
new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
ParamField.addChangeListener(this, fieldMin);
this.add(fieldMax);
}
/**
* This method is called each time the field is modified.
*/
@Override
public void update(JTextField field) {
DateFormat df = new SimpleDateFormat(ParamField.DATE_FORMAT, Locale.ENGLISH);
if ("".equals(field.getText())) {
field.setBackground(Color.WHITE);
mainView.event(Event.PARAMETER_REMOVED, paramName + field.getName());
} else if (field.getText().matches(ParamField.DATE_REGEX)) {
try {
long date = df.parse(field.getText()).getTime();
date = Math.round(date / 86400000.0 + 2440587.5); // to JD
mainView.event(Event.PARAMETER_CHANGED, paramName + field.getName(), date);
field.setBackground(Color.WHITE);
} catch (@SuppressWarnings("unused") ParseException e) {
field.setBackground(Color.PINK);
}
// TODO: check if date min < date max
} else {
field.setBackground(Color.PINK);
}
}
}
/**
* The float range field is used for couples of parameter with both a `Float` class. These are
* JTextFields which check if the content is a valid float. If the parameter is valid or if it
* is empty, then the float value are sent to the controller.
*
* @author N. Jourdane
*/
public static class FloatRangeField extends ParamField implements TextFieldListener {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The JTextField used to put the parameter minimum value of the range. */
JTextField fieldMin;
/** The JTextField used to put the parameter maximum value of the range. */
JTextField fieldMax;
/**
* Method constructor
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public FloatRangeField(EpnTapMainView mainView, String paramName) {
super(mainView, paramName);
fieldMin = new JTextField();
fieldMin.setName(ParamField.MIN_SUFFIX);
ParamField.addChangeListener(this, fieldMin);
this.add(fieldMin);
fieldMax = new JTextField();
fieldMax.setName(ParamField.MAX_SUFFIX);
ParamField.addChangeListener(this, fieldMax);
this.add(fieldMax);
}
/**
* This method is called each time the field is modified.
*/
@Override
public void update(JTextField field) {
if ("".equals(field.getText())) {
field.setBackground(Color.WHITE);
mainView.event(Event.PARAMETER_REMOVED, paramName + field.getName());
} else {
try {
mainView.event(Event.PARAMETER_CHANGED, paramName + field.getName(),
Float.parseFloat(field.getText()));
field.setBackground(Color.WHITE);
} catch (@SuppressWarnings("unused") NumberFormatException e) {
field.setBackground(Color.PINK);
}
}
}
}
/**
* The target name field is used only for the `target_name` parameter. It is a ComboBox which is
* automatically filled with actual target names which begins by the entered characters, by
* asking to an online resolver (RESOLVER_URL). The parameter is sent to the controller each
* time it is updated, so it is possible to enter a parameter that the resolver do not know.
*
* @author N. Jourdane
*/
public static class TargetNameField extends ParamField implements TextFieldListener {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The comboBox to enter the target_name and display target name propositions. */
JComboBox comboBox;
/** The JTextField related to the ComboBox, allowing to listen for text content update. */
JTextField field;
/**
* The content of the last entered value. It is used to avoid recursions, because each time
* an update event is detected, the resolver is called and the ComboBox is filled with new
* values, which trigger a new event.
*/
String lastContent;
/**
* This method is called each time the field is modified. A Runnable is used it is
* impossible to modify the comboBox from a DocumentEvent.
*/
Runnable updateComboBox = new Runnable() {
@Override
public void run() {
String content = field.getText();
if (!content.equals(lastContent)) {
if (content.length() >= 2) {
lastContent = content;
comboBox.removeAllItems();
try {
for (String s : TargetNameField.getItems(content)) {
comboBox.addItem(s);
}
} catch (CantSendQueryException e) {
ParamField.logger.log(Level.WARNING,
"Can't get table names for the resolver", e);
}
comboBox.getEditor().setItem(content);
}
if ("".equals(content)) {
mainView.event(Event.PARAMETER_REMOVED, paramName);
} else {
mainView.event(Event.PARAMETER_CHANGED, paramName, content);
}
}
}
};
/**
* Method constructor
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public TargetNameField(EpnTapMainView mainView, String paramName) {
super(mainView, paramName);
comboBox = new JComboBox<>();
comboBox.setPreferredSize(
new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
comboBox.setEditable(true);
field = (JTextField) comboBox.getEditor().getEditorComponent();
ParamField.addChangeListener(this, field);
this.add(comboBox);
}
/**
* The method used to get target names propositions by asking to the resolver.
*
* @param begining The beginning of the target_name.
* @return An array of Strings corresponding to the target names got.
* @throws CantSendQueryException If the resolver do not work.
*/
static String[] getItems(String begining) throws CantSendQueryException {
StringBuilder resolverResult;
resolverResult = VOTableConnection.sendGet(ParamField.RESOLVER_URL,
"q=\"" + begining + "\"");
JsonObject root = new JsonParser().parse(resolverResult.toString()).getAsJsonObject();
int count = Integer.parseInt(root.get("count").toString());
String[] targetNames = new String[count];
JsonArray hits = root.getAsJsonArray("hits");
for (int i = 0; i < count; i++) {
JsonObject elmt = hits.get(i).getAsJsonObject();
targetNames[i] = elmt.get("name").toString().replace("\"", "");
// TODO: Display "[name] ([type])" on the JComboBox, but only "[name]" on the query.
}
return targetNames;
}
/**
* This method is called each time the field is modified.
*/
@Override
public void update(JTextField textField) {
SwingUtilities.invokeLater(updateComboBox);
}
}
/**
* The data product type field is used only for the `dataproduct_type` parameter. It is a
* ComboBox filled with a list of static data product types (see
* https://voparis-confluence.obspm.fr/display/VES/4+-+EPN-TAP+queries#id-4-EPN-TAPqueries-
* __RefHeading__35_312326667_Toc3037660444.2.4DataProductType). The parameter is sent to the
* controller each time it is updated.
*
* @author N. Jourdane
*/
public static class DataProductTypeField extends ParamField {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The comboBox used to select the data product type. */
JComboBox comboBox;
/**
* An enumeration of all available data product types. Each values comes with an id because
* EPN-TAP table can possibly be filled with the id instead of the name, so the query have
* to ask for both of them.
*
* @author N. Jourdane
*/
@SuppressWarnings("javadoc")
enum DataProductType {
// @noformat
ALL("All", "all"), IM("Image", "im"), SP("Spectrum", "sp"),
DS("Dynamic spectrum", "ds"), SC("Spectral cube", "sc"), PR("Profile", "pr"),
VO("Volume", "vo"), MO("Movie", "mo"), CU("Cube", "cu"),
TS("Time series", "ts"), CA("Catalog", "ca"), SV("Spatial vector", "sv");
// @format
/** The full name of the data product type, such as `Dynamic spectrum`. */
private String name = "";
/** The id of the data product type, such as `ds`. */
private String id = "";
/**
* Method constructor for the enumeration.
*
* @param name The full name of the data product type, such as `Dynamic spectrum`.
* @param id The id of the data product type, such as `ds`.
*/
DataProductType(String name, String id) {
this.name = name;
this.id = id;
}
/**
* @return A list of two strings, containing the name (formated for the query) and the
* id, used in the query. A list is used instead of a array because the getQuery
* function ( @see Queries) needs to know the parameter type.
*/
public List query() {
List item = new ArrayList<>();
item.add(name.replace(" ", "-").toLowerCase());
item.add(id);
return item;
}
@Override
public String toString() {
return name;
}
}
/**
* Method constructor
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public DataProductTypeField(EpnTapMainView mainView, String paramName) {
super(mainView, paramName);
comboBox = new JComboBox<>(DataProductType.values());
comboBox.setSelectedItem(DataProductType.ALL);
comboBox.setPreferredSize(
new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
comboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
update();
}
});
this.add(comboBox);
}
/**
* This method is called each time the field is modified.
*/
void update() {
DataProductType item = (DataProductType) comboBox.getSelectedItem();
if (DataProductType.ALL.equals(item)) {
mainView.event(Event.PARAMETER_REMOVED, paramName);
} else {
mainView.event(Event.PARAMETER_CHANGED, paramName, item.query());
}
}
}
/**
* The target class field is used only for the `target_class` parameter. It is a ComboBox filled
* with a list of static target classes (see
* https://voparis-confluence.obspm.fr/display/VES/4+-+EPN-TAP+queries#id-4-EPN-TAPqueries-
* __RefHeading__39_312326667_Toc3037660464.2.6TargetClass). The parameter is sent to the
* controller each time it is updated.
*
* @author N. Jourdane
*/
public static class TargetClassField extends ParamField {
/** The serial version UID. */
private static final long serialVersionUID = 1L;
/** The comboBox used to select the target class. */
JComboBox comboBox;
/**
* An enumeration of all available target classes.
*
* @author N. Jourdane
*/
@SuppressWarnings("javadoc")
enum TargetClass {
// @noformat
ALL("All"), COMET("Comet"), EXOPLANET("Exoplanet"), RING("Ring"),
SAMPLE("Sample"), SKY("Sky"), SPACECRAFT("Spacecraft"), SPACEJUNK("Spacejunk"),
STAR("Star"), INTERPLANETARY_MEDIUM("Interplanetary medium");
// @format
/** The name of the target class. */
String name;
/**
* Method constructor for the enumeration.
*
* @param name The name of the target class.
*/
TargetClass(String name) {
this.name = name;
}
/**
* @return The name formated for the query.
*/
String query() {
return name.replace(" ", "_").toLowerCase();
}
}
/**
* Method constructor
*
* @param mainView The main view of the application.
* @param paramName The name of the parameter.
*/
public TargetClassField(EpnTapMainView mainView, String paramName) {
super(mainView, paramName);
comboBox = new JComboBox<>(TargetClass.values());
comboBox.setPreferredSize(
new Dimension(ParamField.MIN_FIELD_WIDTH, ParamField.FIELD_HEIGHT));
comboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
update();
}
});
this.add(comboBox);
}
/**
* This method is called each time the field is modified.
*/
void update() {
TargetClass value = (TargetClass) comboBox.getSelectedItem();
if (TargetClass.ALL.equals(value)) {
mainView.event(Event.PARAMETER_REMOVED, paramName);
} else {
mainView.event(Event.PARAMETER_CHANGED, paramName, value.query());
}
}
}
/**
* The listener of text field, it aims to provide a simple way to listen a text field without to
* override removeUpdate(DocumentEvent de), insertUpdate(DocumentEvent de) and
* changedUpdate(DocumentEvent de) on each field to listen.
*
* @author N. Jourdane
*/
interface TextFieldListener {
/**
* When the content of the JTextField is updated.
*
* @param field The JTextField. Is useful for classes containing several text fields, such
* as DateRangeField, to know which one triggered the event.
*/
void update(JTextField field);
}
/**
* To add the listener. @see TextFieldListener
*
* @param changeListener The listener of text fields.
* @param field The field to listen.
*/
static void addChangeListener(final TextFieldListener changeListener, final JTextField field) {
field.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void removeUpdate(DocumentEvent de) {
changeListener.update(field);
}
@Override
public void insertUpdate(DocumentEvent de) {
changeListener.update(field);
}
@Override
public void changedUpdate(DocumentEvent de) {
changeListener.update(field);
}
});
}
}