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