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 */ 028 029package edu.wisc.ssec.mcidasv.chooser.adde; 030 031import static javax.swing.GroupLayout.DEFAULT_SIZE; 032import static javax.swing.GroupLayout.PREFERRED_SIZE; 033import static javax.swing.GroupLayout.Alignment.BASELINE; 034import static javax.swing.GroupLayout.Alignment.LEADING; 035import static javax.swing.LayoutStyle.ComponentPlacement.RELATED; 036 037import java.awt.FlowLayout; 038import java.awt.BorderLayout; 039import java.util.ArrayList; 040import java.util.Hashtable; 041import java.util.Iterator; 042import java.util.List; 043import java.util.StringTokenizer; 044 045import javax.swing.GroupLayout; 046import javax.swing.JComboBox; 047import javax.swing.JComponent; 048import javax.swing.JLabel; 049import javax.swing.JPanel; 050import javax.swing.JTextField; 051import javax.swing.JOptionPane; 052 053import org.w3c.dom.Element; 054 055import edu.wisc.ssec.mcidas.AreaDirectory; 056import edu.wisc.ssec.mcidas.AreaDirectoryList; 057import edu.wisc.ssec.mcidas.AreaFileException; 058import edu.wisc.ssec.mcidas.McIDASUtil; 059 060import ucar.unidata.data.imagery.AddeImageInfo; 061import ucar.unidata.data.imagery.ImageDataSource; 062import ucar.unidata.idv.chooser.IdvChooserManager; 063import ucar.unidata.idv.chooser.adde.AddeServer; 064import ucar.unidata.metdata.NamedStationTable; 065import ucar.unidata.util.GuiUtils; 066import ucar.unidata.util.LogUtil; 067import ucar.unidata.util.Misc; 068 069 070import edu.wisc.ssec.mcidasv.util.McVGuiUtils; 071import ucar.unidata.xml.XmlObjectStore; 072 073/** 074 * Widget to select NEXRAD radar images from a remote ADDE server 075 * Displays a list of the descriptors (names) of the radar datasets 076 * available for a particular ADDE group on the remote server. 077 * 078 * @author Don Murray 079 */ 080public class AddeRadarChooser extends AddeImageChooser { 081 082 /** Use to list the stations */ 083 protected static final String VALUE_LIST = "list"; 084 085 /** This is the list of properties that are used in the advanced gui */ 086 private static final String[] RADAR_PROPS = { PROP_UNIT }; 087 088 /** This is the list of labels used for the advanced gui */ 089 private static final String[] RADAR_LABELS = { "Data Type:" }; 090 091 /** Am I currently reading the stations */ 092 private boolean readingStations = false; 093 094 /** handle on the station update task */ 095 private Object readStationTask; 096 097 /** station table */ 098 private List nexradStations; 099 100 private static final String DEFAULT_ARCHIVE_IMAGE_COUNT = "100"; 101 102 103 104 /** 105 * Construct an Adde image selection widget displaying information 106 * for the specified dataset located on the specified server. 107 * 108 * 109 * 110 * @param mgr The chooser manager 111 * @param root The chooser.xml node 112 */ 113 public AddeRadarChooser(IdvChooserManager mgr, Element root) { 114 super(mgr, root); 115 this.nexradStations = 116 getIdv().getResourceManager().findLocationsByType("radar"); 117 String numImage = getIdv().getStore().get(PREF_NUM_IMAGE_PRESET_RADARCHOOSER, AddeRadarChooser.DEFAULT_ARCHIVE_IMAGE_COUNT); 118 imageCountTextField = new JTextField(numImage, 4); 119 imageCountTextField.addActionListener(e -> readTimes(false)); 120 imageCountTextField.setToolTipText( 121 "<html>Enter a numerical value or the word ALL and press Enter<br/><br/>" + 122 "By default, up to the 100 most recent times are listed.<br/><br/>" + 123 "You may set this field to any positive integer, or the value ALL.<br/>" + 124 "Using ALL may take awhile for datasets with many times.</html>" 125 ); 126 } 127 128 /** 129 * get the adde server grup type to use 130 * 131 * @return group type 132 */ 133 protected String getGroupType() { 134 return AddeServer.TYPE_RADAR; 135 } 136 137 /** 138 * Overwrite base class method to return the correct name 139 * (used for labeling, etc.) 140 * 141 * @return data name specific to this selector 142 */ 143 public String getDataName() { 144 return "Radar Data"; 145 } 146 147 @Override public String getDataType() { 148 return "RADAR"; 149 } 150 151 /** 152 * _more_ 153 * 154 * @return _more_ 155 */ 156 public String getDescriptorLabel() { 157 return "Product"; 158 } 159 160 /** 161 * Get the size of the image list 162 * 163 * @return the image list size 164 */ 165 protected int getImageListSize() { 166 return 6; 167 } 168 169 /** 170 * Get a description of the currently selected dataset 171 * 172 * @return the data set description. 173 */ 174 public String getDatasetName() { 175 return getSelectedStation() + " (" + super.getDatasetName() + ")"; 176 } 177 178 /** 179 * Method to call if the server changed. 180 */ 181 protected void connectToServer() { 182 clearStations(); 183 super.connectToServer(); 184 setAvailableStations(); 185 } 186 187 /** 188 * Check if we are ready to read times 189 * 190 * @return true if times can be read 191 */ 192 protected boolean canReadTimes() { 193 return super.canReadTimes() && (getSelectedStation() != null); 194 } 195 196 /** 197 * Get the advanced property names 198 * 199 * @return array of advanced properties 200 */ 201 protected String[] getAdvancedProps() { 202 return RADAR_PROPS; 203 } 204 205 /** 206 * Get the labels for the advanced properties 207 * 208 * @return array of labels 209 */ 210 protected String[] getAdvancedLabels() { 211 return RADAR_LABELS; 212 } 213 214 /** 215 * Update labels, etc. 216 */ 217 protected void updateStatus() { 218 super.updateStatus(); 219 if (getState() != STATE_CONNECTED) { 220 clearStations(); 221 } 222 if (readStationTask!=null) { 223 if(taskOk(readStationTask)) { 224 setStatus("Reading available stations from server"); 225 } else { 226 readStationTask = null; 227 setState(STATE_UNCONNECTED); 228 } 229 } 230 } 231 232 /** 233 * A new station was selected. Update the gui. 234 * 235 * @param stations List of selected stations 236 */ 237 protected void newSelectedStations(List stations) { 238 super.newSelectedStations(stations); 239 descriptorChanged(); 240 } 241 242 /** 243 * Generate a list of radar ids for the id list. 244 */ 245 private void setAvailableStations() { 246 readStationTask = startTask(); 247 clearSelectedStations(); 248 updateStatus(); 249 List stations = readStations(); 250 if(stopTaskAndIsOk(readStationTask)) { 251 readStationTask = null; 252 if (stations != null) { 253 getStationMap().setStations(stations); 254 } else { 255 clearStations(); 256 } 257 updateStatus(); 258 revalidate(); 259 } else { 260 //User pressed cancel 261 setState(STATE_UNCONNECTED); 262 return; 263 } 264 } 265 266 /** 267 * Generate a list of radar ids for the id list. 268 * McIDAS Inquiry #2794-3141 269 * Replaced previous readStations with one from IDV 270 * so that the stations are not plotted 271 * sporadically 272 * 273 * @return list of station IDs 274 */ 275 private List readStations() { 276 ArrayList stations = new ArrayList(); 277 try { 278 if ((descriptorNames == null) || (descriptorNames.length == 0)) { 279 return stations; 280 } 281 StringBuffer buff = getGroupUrl(REQ_IMAGEDIR, getGroup()); 282 String descrForIds = descriptorNames[0]; 283 Hashtable dtable = getDescriptorTable(); 284 Iterator iter = dtable.keySet().iterator(); 285 String group = getGroup().toLowerCase(); 286 while (iter.hasNext()) { 287 String name = (String) iter.next(); 288 String descriptor = ((String) dtable.get(name)).toLowerCase(); 289 if (group.indexOf("tdw") >= 0 && descriptor.equals("tr0")) { 290 descrForIds = ((String) dtable.get(name)); 291 break; 292 } else if (descriptor.equals("daa") 293 || descriptor.equals("eet") 294 || descriptor.startsWith("bref")) { 295 descrForIds = ((String) dtable.get(name)); 296 break; 297 } 298 } 299 appendKeyValue(buff, PROP_DESCR, 300 descrForIds); 301 appendKeyValue(buff, PROP_ID, VALUE_LIST); 302 if (archiveDay != null) { 303 appendKeyValue(buff, PROP_DAY, archiveDay); 304 } 305 Hashtable seen = new Hashtable(); 306 AreaDirectoryList dirList = 307 new AreaDirectoryList(buff.toString()); 308 for (Iterator it = dirList.getDirs().iterator(); it.hasNext(); ) { 309 AreaDirectory ad = (AreaDirectory) it.next(); 310 String stationId = 311 McIDASUtil.intBitsToString(ad.getValue(20)).trim(); 312 //Check for uniqueness 313 if (seen.get(stationId) != null) { 314 continue; 315 } 316 seen.put(stationId, stationId); 317 //System.err.println ("id:" + stationId); 318 Object station = findStation(stationId); 319 if (station != null) { 320 stations.add(station); 321 } 322 } 323 } catch (AreaFileException e) { 324 String msg = e.getMessage(); 325 if (msg.toLowerCase().indexOf( 326 "no images meet the selection criteria") >= 0) { 327 LogUtil.userErrorMessage( 328 "No stations could be found on the server"); 329 stations = new ArrayList(); 330 setState(STATE_UNCONNECTED); 331 } else { 332 handleConnectionError(e); 333 } 334 } 335 return stations; 336 } 337 338 /** 339 * Find the station for the given ID 340 * 341 * @param stationId the station ID 342 * 343 * @return the station or null if not found 344 */ 345 private Object findStation(String stationId) { 346 for (int i = 0; i < nexradStations.size(); i++) { 347 NamedStationTable table = 348 (NamedStationTable) nexradStations.get(i); 349 Object station = table.get(stationId); 350 if (station != null) { 351 return station; 352 } 353 } 354 return null; 355 } 356 357 public void doCancel() { 358 readStationTask = null; 359 super.doCancel(); 360 } 361 362 /** 363 * Get the list of properties for the base URL 364 * @return list of properties 365 */ 366 protected String[] getBaseUrlProps() { 367 return new String[] { PROP_DESCR, PROP_ID, PROP_UNIT, PROP_SPAC, 368 PROP_BAND, PROP_USER, PROP_PROJ, }; 369 } 370 371 /** 372 * Overwrite the base class method to return the default property value 373 * for PROP_ID. 374 * 375 * @param prop The property 376 * @param ad The area directory 377 * @param forDisplay Is this to show the end user in the gui. 378 * 379 * @return The value of the property 380 */ 381 protected String getDefaultPropValue(String prop, AreaDirectory ad, 382 boolean forDisplay) { 383 if (prop.equals(PROP_ID)) { 384 return getSelectedStation(); 385 } 386 if (prop.equals(PROP_SPAC)) { 387 // Don't want this to default to "1" or it will break 388 // Hydrometeor Classification product...see inquiry 1518 389 return "4"; 390 } 391 return super.getDefaultPropValue(prop, ad, forDisplay); 392 } 393 394 /** 395 * Get a description of the properties 396 * 397 * @return a description 398 */ 399 protected String getPropertiesDescription() { 400 StringBuilder buf = new StringBuilder(); 401 if (unitComboBox != null) { 402 buf.append(getAdvancedLabels()[0]); 403 buf.append(' '); 404 buf.append(unitComboBox.getSelectedItem()); 405 } 406 return buf.toString(); 407 } 408 409 /** 410 * get properties 411 * 412 * @param ht properties 413 */ 414 protected void getDataSourceProperties(Hashtable ht) { 415 unitComboBox.setSelectedItem(ALLUNITS); 416 super.getDataSourceProperties(ht); 417 ht.put(ImageDataSource.PROP_IMAGETYPE, ImageDataSource.TYPE_RADAR); 418 } 419 420 /** 421 * Get the time popup widget 422 * 423 * @return a widget for selecing the day 424 */ 425 protected JComponent getExtraTimeComponent() { 426 JPanel filler = new JPanel(); 427 McVGuiUtils.setComponentHeight(filler, new JComboBox()); 428 return filler; 429 } 430 431 /** 432 * Make the UI for this selector. 433 * 434 * @return The gui 435 */ 436 public JComponent doMakeContents() { 437 JPanel myPanel = new JPanel(); 438 439 JLabel stationLabel = McVGuiUtils.makeLabelRight("Station:"); 440 addServerComp(stationLabel); 441 442 JComponent stationPanel = getStationMap(); 443 registerStatusComp("stations", stationPanel); 444 addServerComp(stationPanel); 445 446 JLabel timesLabel = McVGuiUtils.makeLabelRight("Times:"); 447 addDescComp(timesLabel); 448 449 JPanel timesPanel = makeTimesPanel(); 450 timesPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder()); 451 addDescComp(timesPanel); 452 453 // We need to create this but never show it... AddeImageChooser requires it to be instantiated 454 unitComboBox = new JComboBox(); 455 456 enableWidgets(); 457 458 GroupLayout layout = new GroupLayout(myPanel); 459 myPanel.setLayout(layout); 460 layout.setHorizontalGroup( 461 layout.createParallelGroup(LEADING) 462 .addGroup(layout.createSequentialGroup() 463 .addGroup(layout.createParallelGroup(LEADING) 464 .addGroup(layout.createSequentialGroup() 465 .addComponent(descriptorLabel) 466 .addGap(GAP_RELATED) 467 .addComponent(descriptorComboBox)) 468 .addGroup(layout.createSequentialGroup() 469 .addComponent(stationLabel) 470 .addGap(GAP_RELATED) 471 .addComponent(stationPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)) 472 .addGroup(layout.createSequentialGroup() 473 .addComponent(timesLabel) 474 .addGap(GAP_RELATED) 475 .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)))) 476 ); 477 layout.setVerticalGroup( 478 layout.createParallelGroup(LEADING) 479 .addGroup(layout.createSequentialGroup() 480 .addGroup(layout.createParallelGroup(BASELINE) 481 .addComponent(descriptorLabel) 482 .addComponent(descriptorComboBox)) 483 .addPreferredGap(RELATED) 484 .addGroup(layout.createParallelGroup(LEADING) 485 .addComponent(stationLabel) 486 .addComponent(stationPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)) 487 .addPreferredGap(RELATED) 488 .addGroup(layout.createParallelGroup(LEADING) 489 .addComponent(timesLabel) 490 .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))) 491 ); 492 493 setInnerPanel(myPanel); 494 return super.doMakeContents(true); 495 } 496 497 /** 498 * Get the default value for a key 499 * 500 * @return null for SIZE, else super 501 */ 502 protected String getDefault(String property, String dflt) { 503 if (PROP_SIZE.equals(property)) { 504 return dflt; 505 } 506 return super.getDefault(property, dflt); 507 } 508 509 /** 510 * Make an AddeImageInfo from a URL and an AreaDirectory 511 * 512 * @param dir 513 * AreaDirectory 514 * @param isRelative 515 * true if is relative 516 * @param num 517 * number (for relative images) 518 * 519 * @return corresponding AddeImageInfo 520 */ 521 protected AddeImageInfo makeImageInfo(AreaDirectory dir, 522 boolean isRelative, int num) { 523 AddeImageInfo info = new AddeImageInfo(getAddeServer().getName(), 524 AddeImageInfo.REQ_IMAGEDATA, getGroup(), getDescriptor()); 525 if (isRelative) { 526 info.setDatasetPosition((num == 0) ? 0 : -num); 527 } else { 528 info.setStartDate(dir.getNominalTime()); 529 } 530 setImageInfoProps(info, getMiscKeyProps(), dir); 531 setImageInfoProps(info, getBaseUrlProps(), dir); 532 533 info.setLocateKey(PROP_LINELE); 534 info.setLocateValue("0 0 F"); 535 info.setPlaceValue("ULEFT"); 536 537 String magKey = getPropValue(PROP_MAG, dir); 538 int lmag = 1; 539 int emag = 1; 540 StringTokenizer tok = new StringTokenizer(magKey); 541 lmag = (int) Misc.parseNumber((String) tok.nextElement()); 542 if (tok.hasMoreTokens()) { 543 emag = (int) Misc.parseNumber((String) tok.nextElement()); 544 } else { 545 emag = lmag; 546 } 547 info.setLineMag(lmag); 548 info.setElementMag(emag); 549 550 int lines = dir.getLines(); 551 int elems = dir.getElements(); 552 String sizeKey = getPropValue(PROP_SIZE, dir); 553 tok = new StringTokenizer(sizeKey); 554 String size = (String) tok.nextElement(); 555 if (!size.equalsIgnoreCase("all")) { 556 lines = (int) Misc.parseNumber(size); 557 if (tok.hasMoreTokens()) { 558 elems = (int) Misc.parseNumber((String) tok.nextElement()); 559 } else { 560 elems = lines; 561 } 562 } 563 info.setLines(lines); 564 info.setElements(elems); 565 /* 566 * System.out.println("url = " + info.getURLString().toLowerCase() + 567 * "\n"); 568 */ 569 return info; 570 } 571 572 /** 573 * Set the relative and absolute extra components. 574 */ 575// @Override protected JPanel makeTimesPanel() { 576// // show the time driver if the rest of the choosers are doing so. 577// JPanel timesPanel = 578// super.makeTimesPanel(false, true, getIdv().getUseTimeDriver()); 579// 580// // Make a new timesPanel that has extra components tacked on the 581// // bottom, inside the tabs 582// Component[] comps = timesPanel.getComponents(); 583// 584// if ((comps.length == 1) && (comps[0] instanceof JTabbedPane)) { 585// timesCardPanelExtra = new GuiUtils.CardLayoutPanel(); 586// timesCardPanelExtra.add(new JPanel(), "relative"); 587// timesCardPanelExtra.add(getExtraTimeComponent(), "absolute"); 588// timesPanel = GuiUtils.centerBottom(comps[0], timesCardPanelExtra); 589// } 590// return timesPanel; 591// } 592 593 //The previous makeTimesPanel method was an override of makeTimesPanel() from AddeImageChooser.java 594 //Not sure if the if block in the previous method was working/required. Hence it has been left commented out 595 //The makeTimesPanel method below is an override of the makeTimesPanel() from TimesChooser.java 596 // and is introduced to include the Num Images text field [2793] -PM 597 @Override 598 protected JPanel makeTimesPanel() { 599 JPanel timesPanel = super.makeTimesPanel(false, true, getIdv().getUseTimeDriver()); 600 JPanel buttonPanel = new JPanel(new FlowLayout()); 601 buttonPanel.add(archiveDayBtn); 602 buttonPanel.add(new JLabel("Num Images: ")); 603 buttonPanel.add(imageCountTextField); 604 underTimelistPanel.add(BorderLayout.CENTER, buttonPanel); 605 return timesPanel; 606 } 607 608 /** 609 * Number of absolute times to list in the chooser. 610 * Must be a positive integer, or the word "ALL". 611 * Will throw up a dialog for invalid entries. 612 * 613 * @return 0 for valid entries, -1 for invalid 614 */ 615 private int parseImageCount() { 616 String countStr = imageCountTextField.getText(); 617 try { 618 int newCount = Integer.parseInt(countStr); 619 // Make sure it's reasonable 620 if (newCount > 0) { 621 int addeParam = 0 - newCount + 1; 622 numTimes = "" + addeParam; 623 } else { 624 throw new NumberFormatException(); 625 } 626 } catch (NumberFormatException nfe) { 627 // Still ok if they entered "ALL" 628 if (imageCountTextField.getText().isEmpty()) { 629 JOptionPane.showMessageDialog(this, 630 "Empty field, please enter a valid positive integer"); 631 return -1; 632 } 633 if (! imageCountTextField.getText().equalsIgnoreCase("all")) { 634 JOptionPane.showMessageDialog(this, 635 "Invalid entry: " + imageCountTextField.getText()); 636 return -1; 637 } 638 numTimes = imageCountTextField.getText(); 639 } 640 XmlObjectStore imgStore = getIdv().getStore(); 641 imgStore.put(PREF_NUM_IMAGE_PRESET_RADARCHOOSER, countStr); 642 imgStore.save(); 643 return 0; 644 } 645 646 /** 647 * Read the set of image times available for the current server/group/type 648 * This method is a wrapper, setting the wait cursor and wrapping the call 649 * to {@link #readTimesInner(boolean)}; in a try/catch block 650 */ 651 @Override public void readTimes() { 652 readTimes(false); 653 } 654 655 public void readTimes(boolean forceAll) { 656 657 // Make sure there is a valid entry in the image count text field 658 if (parseImageCount() < 0) return; 659 660 clearTimesList(); 661 if (!canReadTimes()) { 662 return; 663 } 664 Misc.run(new Runnable() { 665 public void run() { 666 updateStatus(); 667 showWaitCursor(); 668 try { 669 readTimesInner(forceAll); 670 checkSetNav(); 671 } catch (Exception e) { 672 handleConnectionError(e); 673 } 674 showNormalCursor(); 675 updateStatus(); 676 } 677 }); 678 } 679}