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