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