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 }