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 }