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