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