/* * 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. */ protected 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 (textField.getText().isEmpty()) { 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 (textField.getText().isEmpty()) { 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 (field.getText().isEmpty()) { 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 (field.getText().isEmpty()) { 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 (content.isEmpty()) { 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); } }); } }