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