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;
035import java.io.File;
036import java.util.Collections;
037import java.util.Objects;
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.JTextField;
048import javax.swing.SwingUtilities;
049import javax.swing.WindowConstants;
050import javax.swing.plaf.basic.BasicComboBoxRenderer;
051
052import net.miginfocom.swing.MigLayout;
053
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057import ucar.unidata.xml.XmlObjectStore;
058
059import edu.wisc.ssec.mcidasv.McIDASV;
060import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
061import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EditorAction;
062import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
063import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
064import edu.wisc.ssec.mcidasv.util.McVTextField;
065
066/**
067 * A dialog that allows the user to define or modify {@link LocalAddeEntry}s.
068 * 
069 * Temporary solution for adding entries via the adde choosers.
070 */
071@SuppressWarnings("serial")
072public class LocalEntryShortcut extends JDialog {
073
074    private static final Logger logger =
075        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.GOES16_ABI,
092            AddeFormat.GINI,
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 LocalEntryShortcut(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 LocalEntryShortcut(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 LocalEntryShortcut(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 (Objects.equals(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 (Objects.equals(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    }// </editor-fold>
296
297    /**
298     * Triggered when the {@literal "add"} button is clicked.
299     *
300     * @param evt Ignored.
301     */
302    private void saveButtonActionPerformed(final ActionEvent evt) {
303        addEntry();
304    }
305
306    /**
307     * Triggered when the {@literal "edit"} button is clicked.
308     *
309     * @param evt Ignored.
310     */
311    private void editButtonActionPerformed(final ActionEvent evt) {
312        editEntry();
313    }
314
315    /**
316     * Triggered when the {@literal "file picker"} button is clicked.
317     *
318     * @param evt Ignored.
319     */
320    private void browseButtonActionPerformed(final ActionEvent evt) {
321        String lastPath = getLastPath();
322        selectedPath = getDataDirectory(lastPath);
323        // yes, the "!=" is intentional! getDataDirectory(String) will return
324        // the exact String it is given if the user cancelled the file picker
325        if (selectedPath != lastPath) {
326            directoryField.setText(selectedPath);
327            setLastPath(selectedPath);
328        }
329    }
330
331    /**
332     * Returns the value of the {@link #PROP_LAST_PATH} McIDAS-V property.
333     * 
334     * @return Either the {@code String} representation of the last path 
335     * selected by the user, or an empty {@code String}.
336     */
337    private String getLastPath() {
338        McIDASV mcv = McIDASV.getStaticMcv();
339        String path = "";
340        if (mcv != null) {
341            return mcv.getObjectStore().get(PROP_LAST_PATH, "");
342        }
343        return path;
344    }
345
346    /**
347     * Sets the value of the {@link #PROP_LAST_PATH} McIDAS-V property to be
348     * the contents of {@code path}.
349     * 
350     * @param path New value for {@link #PROP_LAST_PATH}. {@code null} will be
351     * converted to an empty {@code String}.
352     */
353    public void setLastPath(final String path) {
354        String okayPath = (path != null) ? path : "";
355        McIDASV mcv = McIDASV.getStaticMcv();
356        if (mcv != null) {
357            XmlObjectStore store = mcv.getObjectStore();
358            store.put(PROP_LAST_PATH, okayPath);
359            store.saveIfNeeded();
360        }
361    }
362
363    /**
364     * Calls {@link #dispose} if the dialog is visible.
365     *
366     * @param evt Ignored.
367     */
368    private void cancelButtonActionPerformed(ActionEvent evt) {
369        if (isDisplayable()) {
370            dispose();
371        }
372    }
373
374    /**
375     * Poll the various UI components and attempt to construct valid ADDE
376     * entries based upon the information provided by the user.
377     * 
378     * @return {@link Set} of entries that represent the user's input, or an
379     * empty {@code Set} if the input was somehow invalid.
380     */
381    private Set<LocalAddeEntry> pollWidgets() {
382        String group = safeGetText(datasetField);
383        String name = safeGetText(typeField);
384        String mask = getLastPath();
385        if (mask.isEmpty() && !safeGetText(directoryField).isEmpty()) {
386            mask = safeGetText(directoryField);
387            setLastPath(mask);
388        }
389        AddeFormat format = (AddeFormat)formatComboBox.getSelectedItem();
390        LocalAddeEntry entry = new LocalAddeEntry.Builder(name, group, mask, format).status(EntryStatus.ENABLED).build();
391        return Collections.singleton(entry);
392    }
393
394    /**
395     * Creates new {@link LocalAddeEntry}s based upon the contents of the dialog
396     * and adds {@literal "them"} to the managed servers. If the dialog is
397     * displayed, we call {@link #dispose()} and attempt to refresh the
398     * server manager GUI if it is available.
399     */
400    private void addEntry() {
401        Set<LocalAddeEntry> addedEntries = pollWidgets();
402        entryStore.addEntries(addedEntries);
403        if (isDisplayable()) {
404            dispose();
405        }
406        if (managerController != null) {
407            managerController.refreshDisplay();
408        }
409    }
410
411    private void editEntry() {
412        Set<LocalAddeEntry> newEntries = pollWidgets();
413        Set<LocalAddeEntry> currentEntries = Collections.singleton(currentEntry);
414        entryStore.replaceEntries(currentEntries, newEntries);
415        if (isDisplayable()) {
416            dispose();
417        }
418        if (managerController != null) {
419            managerController.refreshDisplay();
420        }
421    }
422
423    /**
424     * Ask the user for a data directory from which to create a MASK=
425     * 
426     * @param startDir If this is a valid path, then the file picker will 
427     * (presumably) use that as its initial location. Should not be 
428     * {@code null}?
429     * 
430     * @return Either a path to a data directory or {@code startDir}.
431     */
432    private String getDataDirectory(final String startDir) {
433        JFileChooser fileChooser = new JFileChooser();
434        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
435        fileChooser.setSelectedFile(new File(startDir));
436        switch (fileChooser.showOpenDialog(this)) {
437            case JFileChooser.APPROVE_OPTION:
438                return fileChooser.getSelectedFile().getAbsolutePath();
439            case JFileChooser.CANCEL_OPTION:
440                return startDir;
441            default:
442                return startDir;
443        }
444    }
445
446    /**
447     * Returns the last {@link EditorAction} that was performed.
448     *
449     * @return Last editor action performed.
450     *
451     * @see #editorAction
452     */
453    public EditorAction getEditorAction() {
454        return editorAction;
455    }
456
457    /**
458     * Dave's nice combobox tooltip renderer!
459     */
460    private class TooltipComboBoxRenderer extends BasicComboBoxRenderer {
461        @Override public Component getListCellRendererComponent(JList list, 
462            Object value, int index, boolean isSelected, boolean cellHasFocus) 
463        {
464            if (isSelected) {
465                setBackground(list.getSelectionBackground());
466                setForeground(list.getSelectionForeground());
467                if (value instanceof AddeFormat) {
468                    list.setToolTipText(((AddeFormat)value).getTooltip());
469                }
470            } else {
471                setBackground(list.getBackground());
472                setForeground(list.getForeground());
473            }
474            setFont(list.getFont());
475            setText((value == null) ? "" : value.toString());
476            return this;
477        }
478    }
479
480    // Variables declaration - do not modify
481    private JTextField datasetField;
482    private JTextField directoryField;
483    private JComboBox<AddeFormat> formatComboBox;
484    private JTextField typeField;
485    // End of variables declaration
486}