001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028package edu.wisc.ssec.mcidasv.servermanager;
029
030import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.safeGetText;
031
032import java.awt.Component;
033import java.awt.Container;
034import java.awt.event.ActionEvent;
035
036import java.io.File;
037
038import java.util.Collections;
039import java.util.Set;
040
041import javax.swing.DefaultComboBoxModel;
042import javax.swing.JButton;
043import javax.swing.JComboBox;
044import javax.swing.JDialog;
045import javax.swing.JFileChooser;
046import javax.swing.JLabel;
047import javax.swing.JList;
048import javax.swing.JOptionPane;
049import javax.swing.JTextField;
050import javax.swing.SwingUtilities;
051import javax.swing.WindowConstants;
052import javax.swing.plaf.basic.BasicComboBoxRenderer;
053
054import net.miginfocom.swing.MigLayout;
055
056import ucar.unidata.xml.XmlObjectStore;
057
058import edu.wisc.ssec.mcidasv.McIDASV;
059import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
060import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EditorAction;
061import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
062import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
063import edu.wisc.ssec.mcidasv.util.McVTextField;
064
065/**
066 * A dialog that allows the user to define or modify
067 * {@link LocalAddeEntry LocalAddeEntries}.
068 */
069@SuppressWarnings("serial")
070public class LocalEntryEditor extends JDialog {
071
072//    private static final Logger logger = LoggerFactory.getLogger(LocalEntryEditor.class);
073
074    /** Property ID for the last directory selected. */
075    private static final String PROP_LAST_PATH = "mcv.localdata.lastpath";
076
077    /** The valid local ADDE formats. */
078    private static final DefaultComboBoxModel<AddeFormat> formats =
079        new DefaultComboBoxModel<>(new AddeFormat[] {
080            // note: if you are looking to add a new value you may need to make
081            // changes to LocalAddeEntry's ServerName and AddeFormat enums,
082            // the format combo box in LocalEntryShortcut, and the _formats
083            // dictionary in mcvadde.py.
084            AddeFormat.MCIDAS_AREA,
085            AddeFormat.AMSRE_L1B,
086            AddeFormat.AMSRE_L2A,
087            AddeFormat.AMSRE_RAIN_PRODUCT,
088            AddeFormat.GINI,
089            AddeFormat.GOES16_ABI,
090            AddeFormat.HIMAWARI8,
091            AddeFormat.INSAT3D_IMAGER,
092            AddeFormat.INSAT3D_SOUNDER,
093            AddeFormat.LRIT_GOES9,
094            AddeFormat.LRIT_GOES10,
095            AddeFormat.LRIT_GOES11,
096            AddeFormat.LRIT_GOES12,
097            AddeFormat.LRIT_MET5,
098            AddeFormat.LRIT_MET7,
099            AddeFormat.LRIT_MTSAT1R,
100            AddeFormat.METEOSAT_OPENMTP,
101            AddeFormat.METOP_AVHRR_L1B,
102            AddeFormat.MODIS_L1B_MOD02,
103            AddeFormat.MODIS_L2_MOD06,
104            AddeFormat.MODIS_L2_MOD07,
105            AddeFormat.MODIS_L2_MOD35,
106            AddeFormat.MODIS_L2_MOD04,
107            AddeFormat.MODIS_L2_MOD28,
108            AddeFormat.MODIS_L2_MODR,
109            AddeFormat.MSG_HRIT_FD,
110            AddeFormat.MSG_HRIT_HRV,
111            AddeFormat.MTSAT_HRIT,
112            AddeFormat.NOAA_AVHRR_L1B,
113            AddeFormat.SSMI,
114            AddeFormat.TRMM           
115            // AddeFormat.MCIDAS_MD
116        });
117
118    /** The server manager GUI. Be aware that this can be {@code null}. */
119    private final TabbedAddeManager managerController;
120
121    /** Reference back to the server manager. */
122    private final EntryStore entryStore;
123
124    private final LocalAddeEntry currentEntry;
125
126    /** Either the path to an ADDE directory as selected by the user or an empty {@link String}. */
127    private String selectedPath = "";
128
129    /** The last dialog action performed by the user. */
130    private EditorAction editorAction = EditorAction.INVALID;
131
132    private final String datasetText;
133
134    /**
135     * Creates a modal local ADDE data editor. It's pretty useful when adding
136     * from a chooser.
137     * 
138     * @param entryStore The server manager. Should not be {@code null}.
139     * @param group Name of the group/dataset containing the desired data. Be aware that {@code null} is okay.
140     */
141    public LocalEntryEditor(final EntryStore entryStore, final String group) {
142        super((JDialog)null, true);
143        this.managerController = null;
144        this.entryStore = entryStore;
145        this.datasetText = group;
146        this.currentEntry = null;
147        SwingUtilities.invokeLater(() -> initComponents(LocalAddeEntry.INVALID_ENTRY));
148    }
149
150    // TODO(jon): hold back on javadocs, this is likely to change
151    public LocalEntryEditor(java.awt.Frame parent, boolean modal,
152                            final TabbedAddeManager manager,
153                            final EntryStore store)
154    {
155        super(manager, modal);
156        this.managerController = manager;
157        this.entryStore = store;
158        this.datasetText = null;
159        this.currentEntry = null;
160        SwingUtilities.invokeLater(() -> initComponents(LocalAddeEntry.INVALID_ENTRY));
161    }
162
163    // TODO(jon): hold back on javadocs, this is likely to change
164    public LocalEntryEditor(java.awt.Frame parent, boolean modal,
165                            final TabbedAddeManager manager,
166                            final EntryStore store,
167                            final LocalAddeEntry entry)
168    {
169        super(manager, modal);
170        this.managerController = manager;
171        this.entryStore = store;
172        this.datasetText = null;
173        this.currentEntry = entry;
174        SwingUtilities.invokeLater(() -> initComponents(entry));
175    }
176
177    /**
178     * Creates the editor dialog and initializes the various GUI components.
179     * 
180     * @param initEntry Use {@link LocalAddeEntry#INVALID_ENTRY} to specify 
181     * that the user is creating a new entry; otherwise provide the actual
182     * entry that the user is editing.
183     */
184    private void initComponents(final LocalAddeEntry initEntry) {
185        JLabel datasetLabel = new JLabel("Dataset (e.g. MYDATA):");
186        datasetField =
187            McVGuiUtils.makeTextFieldDeny("", 8, true, McVTextField.mcidasDeny);
188        datasetLabel.setLabelFor(datasetField);
189        datasetField.setColumns(20);
190        if (datasetText != null) {
191            datasetField.setText(datasetText);
192        }
193
194        JLabel typeLabel = new JLabel("Image Type (e.g. JAN 07 GOES):");
195        typeField = new JTextField();
196        typeLabel.setLabelFor(typeField);
197        typeField.setColumns(20);
198
199        JLabel formatLabel = new JLabel("Format:");
200        formatComboBox = new JComboBox<>();
201        formatComboBox.setRenderer(new TooltipComboBoxRenderer());
202
203        // TJJ Apr 2016
204        // certain local servers are not available on Windows, remove them from the list
205        if (McIDASV.isWindows()) {
206            formats.removeElement(AddeFormat.GOES16_ABI);
207            formats.removeElement(AddeFormat.HIMAWARI8);
208            formats.removeElement(AddeFormat.INSAT3D_IMAGER);
209            formats.removeElement(AddeFormat.INSAT3D_SOUNDER);
210        }
211
212        formatComboBox.setModel(formats);
213        formatComboBox.setSelectedIndex(0);
214        formatLabel.setLabelFor(formatComboBox);
215
216        JLabel directoryLabel = new JLabel("Directory:");
217        directoryField = new JTextField();
218        directoryLabel.setLabelFor(directoryField);
219        directoryField.setColumns(20);
220
221        JButton browseButton = new JButton("Browse...");
222        browseButton.addActionListener(this::browseButtonActionPerformed);
223
224        JButton saveButton = new JButton("Add Dataset");
225        saveButton.addActionListener(evt -> {
226            if (initEntry == LocalAddeEntry.INVALID_ENTRY) {
227                saveButtonActionPerformed(evt);
228            } else {
229                editButtonActionPerformed(evt);
230            }
231        });
232
233        JButton cancelButton = new JButton("Cancel");
234        cancelButton.addActionListener(this::cancelButtonActionPerformed);
235
236        if (initEntry == LocalAddeEntry.INVALID_ENTRY) {
237            setTitle("Add Local Dataset");
238        } else {
239            setTitle("Edit Local Dataset");
240            saveButton.setText("Save Changes");
241            datasetField.setText(initEntry.getGroup());
242            typeField.setText(initEntry.getName());
243            directoryField.setText(EntryTransforms.demungeFileMask(initEntry.getFileMask()));
244            formatComboBox.setSelectedItem(initEntry.getFormat());
245        }
246
247        setResizable(false);
248        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
249        Container c = getContentPane();
250        c.setLayout(new MigLayout(
251            "",                    // general layout constraints; currently
252                                   // none are specified.
253            "[align right][fill]", // column constraints; defined two columns
254                                   // leftmost aligns the components right;
255                                   // rightmost simply fills the remaining space
256            "[][][][][][]"));      // row constraints; possibly not needed in
257                                   // this particular example?
258
259        // done via WindowBuilder + Eclipse
260//        c.add(datasetLabel,   "cell 0 0"); // row: 0; col: 0
261//        c.add(datasetField,   "cell 1 0"); // row: 0; col: 1
262//        c.add(typeLabel,      "cell 0 1"); // row: 1; col: 0
263//        c.add(typeField,      "cell 1 1"); // row: 1; col: 1
264//        c.add(formatLabel,    "cell 0 2"); // row: 2; col: 0
265//        c.add(formatComboBox, "cell 1 2"); // row: 2; col: 1
266//        c.add(directoryLabel, "cell 0 3"); // row: 3; col: 0 ... etc!
267//        c.add(directoryField, "flowx,cell 1 3");
268//        c.add(browseButton,   "cell 1 3,alignx right");
269//        c.add(saveButton,     "flowx,cell 1 5,alignx right,aligny top");
270//        c.add(cancelButton,   "cell 1 5,alignx right,aligny top");
271
272        // another way to accomplish the above layout.
273        c.add(datasetLabel);
274        c.add(datasetField,   "wrap"); // think "newline" or "new row"
275        c.add(typeLabel);
276        c.add(typeField,      "wrap"); // think "newline" or "new row"
277        c.add(formatLabel);
278        c.add(formatComboBox, "wrap"); // think "newline" or "new row"
279        c.add(directoryLabel);
280        c.add(directoryField, "flowx, split 2"); // split this current cell 
281                                                 // into two "subcells"; this
282                                                 // will cause browseButton to
283                                                 // be grouped into the current
284                                                 // cell.
285        c.add(browseButton,   "alignx right, wrap");
286
287        // skips "cell 0 5" causing this row to start in "cell 1 5"; splits 
288        // the cell so that saveButton and cancelButton both occupy cell 1 5.
289        c.add(saveButton,     "flowx, split 2, skip 1, alignx right, aligny top");
290        c.add(cancelButton,   "alignx right, aligny top");
291        pack();
292    }// </editor-fold>
293
294    /**
295     * Triggered when the {@literal "add"} button is clicked.
296     *
297     * @param evt Ignored.
298     */
299    private void saveButtonActionPerformed(final ActionEvent evt) {
300        addEntry();
301    }
302
303    /**
304     * Triggered when the {@literal "edit"} button is clicked.
305     *
306     * @param evt Ignored.
307     */
308    private void editButtonActionPerformed(final ActionEvent evt) {
309        editEntry();
310    }
311
312    /**
313     * Triggered when the {@literal "file picker"} button is clicked.
314     *
315     * @param evt Ignored.
316     */
317    private void browseButtonActionPerformed(final ActionEvent evt) {
318        String lastPath = getLastPath();
319        selectedPath = getDataDirectory(lastPath);
320        // yes, the "!=" is intentional! getDataDirectory(String) will return
321        // the exact String it is given if the user cancelled the file picker
322        if (selectedPath != lastPath) {
323            directoryField.setText(selectedPath);
324            setLastPath(selectedPath);
325        }
326    }
327
328    /**
329     * Returns the value of the {@link #PROP_LAST_PATH} McIDAS-V property.
330     * 
331     * @return Either the {@code String} representation of the last path 
332     * selected by the user, or an empty {@code String}.
333     */
334    private String getLastPath() {
335        McIDASV mcv = McIDASV.getStaticMcv();
336        String path = "";
337        if (mcv != null) {
338            path = mcv.getObjectStore().get(PROP_LAST_PATH, "");
339        }
340        return path;
341    }
342
343    /**
344     * Sets the value of the {@link #PROP_LAST_PATH} McIDAS-V property to be
345     * the contents of {@code path}.
346     * 
347     * @param path New value for {@link #PROP_LAST_PATH}. {@code null} will be
348     * converted to an empty {@code String}.
349     */
350    public void setLastPath(final String path) {
351        String okayPath = (path != null) ? path : "";
352        McIDASV mcv = McIDASV.getStaticMcv();
353        if (mcv != null) {
354            XmlObjectStore store = mcv.getObjectStore();
355            store.put(PROP_LAST_PATH, okayPath);
356            store.saveIfNeeded();
357        }
358    }
359
360    /**
361     * Calls {@link #dispose} if the dialog is visible.
362     *
363     * @param evt Ignored.
364     */
365    private void cancelButtonActionPerformed(ActionEvent evt) {
366        if (isDisplayable()) {
367            dispose();
368        }
369    }
370
371    /**
372     * Poll the various UI components and attempt to construct valid ADDE
373     * entries based upon the information provided by the user.
374     * 
375     * @param newEntry a boolean, {@code true} if we are adding a new entry.
376     *
377     * @return {@link Set} of entries that represent the user's input, or an
378     * empty {@code Set} if the input was somehow invalid.
379     */
380    private Set<LocalAddeEntry> pollWidgets(boolean newEntry) {
381        String group = safeGetText(datasetField);
382        String name = safeGetText(typeField);
383        String mask = getLastPath();
384        
385        // consider the UI in error if any field is blank
386        if (group.isEmpty() || name.isEmpty() || mask.isEmpty()) {
387            JOptionPane.showMessageDialog(this.getContentPane(),
388                "Group, Name, or Mask field is empty, please correct this.");
389            return Collections.emptySet();
390        }
391
392        // if there is something in the directoryField, that's the value we
393        // should be using.
394        if (!safeGetText(directoryField).isEmpty()) {
395            mask = safeGetText(directoryField);
396            setLastPath(mask);
397        }
398        
399        AddeFormat format = (AddeFormat)formatComboBox.getSelectedItem();
400        LocalAddeEntry entry = new LocalAddeEntry.Builder(name, group, mask, format).status(EntryStatus.ENABLED).build();
401        
402        // if adding a new entry, make sure dataset is not a duplicate
403        if (newEntry) {
404            String newGroup = entry.getGroup();
405            for (AddeEntry storeEntry : entryStore.getEntrySet()) {
406                String storeGroup = storeEntry.getGroup();
407                if (newGroup.equals(storeGroup)) {
408                    // only apply this restriction to MSG HRIT data
409                    if ((format == AddeFormat.MSG_HRIT_FD) || (format == AddeFormat.MSG_HRIT_HRV)) {
410                        JOptionPane.showMessageDialog(this.getContentPane(),
411                            "Dataset specified is a duplicate, not supported with MSG HRIT format.");
412                        return Collections.emptySet();
413                    }
414                }
415            }
416        }
417        return Collections.singleton(entry);
418    }
419
420    /**
421     * Creates new {@link LocalAddeEntry}s based upon the contents of the dialog
422     * and adds {@literal "them"} to the managed servers. If the dialog is
423     * displayed, we call {@link #dispose()} and attempt to refresh the
424     * server manager GUI if it is available.
425     */
426    private void addEntry() {
427        Set<LocalAddeEntry> addedEntries = pollWidgets(true);
428        entryStore.addEntries(addedEntries);
429        if (isDisplayable()) {
430            dispose();
431        }
432        if (managerController != null) {
433            managerController.refreshDisplay();
434        }
435    }
436
437    private void editEntry() {
438        Set<LocalAddeEntry> newEntries = pollWidgets(false);
439        Set<LocalAddeEntry> currentEntries = Collections.singleton(currentEntry);
440        entryStore.replaceEntries(currentEntries, newEntries);
441        if (isDisplayable()) {
442            dispose();
443        }
444        if (managerController != null) {
445            managerController.refreshDisplay();
446        }
447    }
448
449    /**
450     * Ask the user for a data directory from which to create a MASK=
451     * 
452     * @param startDir If this is a valid path, then the file picker will 
453     * (presumably) use that as its initial location. Should not be 
454     * {@code null}?
455     * 
456     * @return Either a path to a data directory or {@code startDir}.
457     */
458    private String getDataDirectory(final String startDir) {
459        JFileChooser fileChooser = new JFileChooser();
460        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
461        fileChooser.setSelectedFile(new File(startDir));
462        switch (fileChooser.showOpenDialog(this)) {
463            case JFileChooser.APPROVE_OPTION:
464                return fileChooser.getSelectedFile().getAbsolutePath();
465            case JFileChooser.CANCEL_OPTION:
466                return startDir;
467            default:
468                return startDir;
469        }
470    }
471
472    /**
473     * Returns the last {@link EditorAction} that was performed.
474     *
475     * @return Last editor action performed.
476     *
477     * @see #editorAction
478     */
479    public EditorAction getEditorAction() {
480        return editorAction;
481    }
482
483    /**
484     * Dave's nice combobox tooltip renderer!
485     */
486    private static class TooltipComboBoxRenderer extends BasicComboBoxRenderer {
487        @Override public Component getListCellRendererComponent(JList list, 
488            Object value, int index, boolean isSelected, boolean cellHasFocus) 
489        {
490            if (isSelected) {
491                setBackground(list.getSelectionBackground());
492                setForeground(list.getSelectionForeground());
493                if (value instanceof AddeFormat) {
494                    list.setToolTipText(((AddeFormat)value).getTooltip());
495                }
496            } else {
497                setBackground(list.getBackground());
498                setForeground(list.getForeground());
499            }
500            setFont(list.getFont());
501            setText((value == null) ? "" : value.toString());
502            return this;
503        }
504    }
505
506    // Variables declaration - do not modify
507    private JTextField datasetField;
508    private JTextField directoryField;
509    private JComboBox<AddeFormat> formatComboBox;
510    private JTextField typeField;
511    // End of variables declaration
512}