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