001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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     *
269     * @return  list of station IDs
270     */
271    private List readStations() {
272        ArrayList stations = new ArrayList();
273        try {
274            if ((descriptorNames == null) || (descriptorNames.length == 0)) {
275                return stations;
276            }
277            StringBuffer buff        = getGroupUrl(REQ_IMAGEDIR, getGroup());
278            String       descrForIds = descriptorNames[0];
279            // try to use base reflectivity if it's available.
280            for (int i = 0; i < descriptorNames.length; i++) {
281                if ((descriptorNames[i] != null)
282                        && descriptorNames[i].toLowerCase().startsWith(
283                            "base")) {
284                    descrForIds = descriptorNames[i];
285                    break;
286                }
287            }
288            appendKeyValue(buff, PROP_DESCR,
289                           getDescriptorFromSelection(descrForIds));
290            appendKeyValue(buff, PROP_ID, VALUE_LIST);
291            Hashtable         seen    = new Hashtable();
292            AreaDirectoryList dirList =
293                new AreaDirectoryList(buff.toString());
294            for (Iterator it = dirList.getDirs().iterator(); it.hasNext(); ) {
295                AreaDirectory ad = (AreaDirectory) it.next();
296                String stationId =
297                    McIDASUtil.intBitsToString(ad.getValue(20)).trim();
298                //Check for uniqueness
299                if (seen.get(stationId) != null) {
300                    continue;
301                }
302                seen.put(stationId, stationId);
303                //System.err.println ("id:" + stationId);
304                Object station = findStation(stationId);
305                if (station != null) {
306                    stations.add(station);
307                }
308            }
309        } catch (AreaFileException e) {
310            String msg = e.getMessage();
311            if (msg.toLowerCase().indexOf(
312                    "no images meet the selection criteria") >= 0) {
313                LogUtil.userErrorMessage(
314                    "No stations could be found on the server");
315            } else {
316                handleConnectionError(e);
317            }
318            stations = new ArrayList();
319            setState(STATE_UNCONNECTED);
320        }
321        return stations;
322    }
323
324    /**
325     * Find the station for the given ID
326     *
327     * @param stationId  the station ID
328     *
329     * @return  the station or null if not found
330     */
331    private Object findStation(String stationId) {
332        for (int i = 0; i < nexradStations.size(); i++) {
333            NamedStationTable table =
334                (NamedStationTable) nexradStations.get(i);
335            Object station = table.get(stationId);
336            if (station != null) {
337                return station;
338            }
339        }
340        return null;
341    }
342
343    public void doCancel() {
344        readStationTask = null;
345        super.doCancel();
346    }
347
348    /**
349     * Get the list of properties for the base URL
350     * @return list of properties
351     */
352    protected String[] getBaseUrlProps() {
353        return new String[] { PROP_DESCR, PROP_ID, PROP_UNIT, PROP_SPAC,
354                              PROP_BAND, PROP_USER, PROP_PROJ, };
355    }
356
357    /**
358     * Overwrite the base class method to return the default property value
359     * for PROP_ID.
360     *
361     * @param prop The property
362     * @param ad The area directory
363     * @param forDisplay Is this to show the end user in the gui.
364     *
365     * @return The value of the property
366     */
367    protected String getDefaultPropValue(String prop, AreaDirectory ad,
368                                         boolean forDisplay) {
369        if (prop.equals(PROP_ID)) {
370            return getSelectedStation();
371        }
372        if (prop.equals(PROP_SPAC)) {
373            // Don't want this to default to "1" or it will break
374            // Hydrometeor Classification product...see inquiry 1518
375            return "4";
376        }
377        return super.getDefaultPropValue(prop, ad, forDisplay);
378    }
379
380    /**
381     * Get a description of the properties
382     *
383     * @return  a description
384     */
385    protected String getPropertiesDescription() {
386        StringBuilder buf = new StringBuilder();
387        if (unitComboBox != null) {
388            buf.append(getAdvancedLabels()[0]);
389            buf.append(' ');
390            buf.append(unitComboBox.getSelectedItem());
391        }
392        return buf.toString();
393    }
394
395    /**
396     * get properties
397     *
398     * @param ht properties
399     */
400    protected void getDataSourceProperties(Hashtable ht) {
401        unitComboBox.setSelectedItem(ALLUNITS);
402        super.getDataSourceProperties(ht);
403        ht.put(ImageDataSource.PROP_IMAGETYPE, ImageDataSource.TYPE_RADAR);
404    }
405    
406    /**
407     * Get the time popup widget
408     *
409     * @return  a widget for selecing the day
410     */
411    protected JComponent getExtraTimeComponent() {
412        JPanel filler = new JPanel();
413        McVGuiUtils.setComponentHeight(filler, new JComboBox());
414        return filler;
415    }
416    
417    /**
418     * Make the UI for this selector.
419     *
420     * @return The gui
421     */
422    public JComponent doMakeContents() {      
423        JPanel myPanel = new JPanel();
424                
425        JLabel stationLabel = McVGuiUtils.makeLabelRight("Station:");
426        addServerComp(stationLabel);
427
428        JComponent stationPanel = getStationMap();
429        registerStatusComp("stations", stationPanel);
430        addServerComp(stationPanel);
431        
432        JLabel timesLabel = McVGuiUtils.makeLabelRight("Times:");
433        addDescComp(timesLabel);
434        
435        JPanel timesPanel = makeTimesPanel();
436        timesPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
437        addDescComp(timesPanel);
438        
439        // We need to create this but never show it... AddeImageChooser requires it to be instantiated
440        unitComboBox = new JComboBox();
441        
442        enableWidgets();
443
444        GroupLayout layout = new GroupLayout(myPanel);
445        myPanel.setLayout(layout);
446        layout.setHorizontalGroup(
447            layout.createParallelGroup(LEADING)
448            .addGroup(layout.createSequentialGroup()
449                .addGroup(layout.createParallelGroup(LEADING)
450                    .addGroup(layout.createSequentialGroup()
451                        .addComponent(descriptorLabel)
452                        .addGap(GAP_RELATED)
453                        .addComponent(descriptorComboBox))
454                    .addGroup(layout.createSequentialGroup()
455                        .addComponent(stationLabel)
456                        .addGap(GAP_RELATED)
457                        .addComponent(stationPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
458                    .addGroup(layout.createSequentialGroup()
459                        .addComponent(timesLabel)
460                        .addGap(GAP_RELATED)
461                        .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))))
462        );
463        layout.setVerticalGroup(
464            layout.createParallelGroup(LEADING)
465            .addGroup(layout.createSequentialGroup()
466                .addGroup(layout.createParallelGroup(BASELINE)
467                    .addComponent(descriptorLabel)
468                    .addComponent(descriptorComboBox))
469                .addPreferredGap(RELATED)
470                .addGroup(layout.createParallelGroup(LEADING)
471                    .addComponent(stationLabel)
472                    .addComponent(stationPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
473                .addPreferredGap(RELATED)
474                .addGroup(layout.createParallelGroup(LEADING)
475                    .addComponent(timesLabel)
476                    .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)))
477        );
478        
479        setInnerPanel(myPanel);
480        return super.doMakeContents(true);
481    }
482    
483    /**
484     * Get the default value for a key
485     * 
486     * @return null for SIZE, else super
487     */
488    protected String getDefault(String property, String dflt) {
489        if (PROP_SIZE.equals(property)) {
490            return dflt;
491        }
492        return super.getDefault(property, dflt);
493    }
494    
495    /**
496     * Make an AddeImageInfo from a URL and an AreaDirectory
497     * 
498     * @param dir
499     *            AreaDirectory
500     * @param isRelative
501     *            true if is relative
502     * @param num
503     *            number (for relative images)
504     * 
505     * @return corresponding AddeImageInfo
506     */
507    protected AddeImageInfo makeImageInfo(AreaDirectory dir,
508            boolean isRelative, int num) {
509        AddeImageInfo info = new AddeImageInfo(getAddeServer().getName(),
510                AddeImageInfo.REQ_IMAGEDATA, getGroup(), getDescriptor());
511        if (isRelative) {
512            info.setDatasetPosition((num == 0) ? 0 : -num);
513        } else {
514            info.setStartDate(dir.getNominalTime());
515        }
516        setImageInfoProps(info, getMiscKeyProps(), dir);
517        setImageInfoProps(info, getBaseUrlProps(), dir);
518
519        info.setLocateKey(PROP_LINELE);
520        info.setLocateValue("0 0 F");
521        info.setPlaceValue("ULEFT");
522        
523        String magKey = getPropValue(PROP_MAG, dir);
524        int lmag = 1;
525        int emag = 1;
526        StringTokenizer tok = new StringTokenizer(magKey);
527        lmag = (int) Misc.parseNumber((String) tok.nextElement());
528        if (tok.hasMoreTokens()) {
529            emag = (int) Misc.parseNumber((String) tok.nextElement());
530        } else {
531            emag = lmag;
532        }
533        info.setLineMag(lmag);
534        info.setElementMag(emag);
535
536        int lines = dir.getLines();
537        int elems = dir.getElements();
538        String sizeKey = getPropValue(PROP_SIZE, dir);
539        tok = new StringTokenizer(sizeKey);
540        String size = (String) tok.nextElement();
541        if (!size.equalsIgnoreCase("all")) {
542            lines = (int) Misc.parseNumber(size);
543            if (tok.hasMoreTokens()) {
544                elems = (int) Misc.parseNumber((String) tok.nextElement());
545            } else {
546                elems = lines;
547            }
548        }
549        info.setLines(lines);
550        info.setElements(elems);
551        /*
552         * System.out.println("url = " + info.getURLString().toLowerCase() +
553         * "\n");
554         */
555        return info;
556    }
557
558    /**
559     * Set the relative and absolute extra components.
560     */
561//    @Override protected JPanel makeTimesPanel() {
562//        // show the time driver if the rest of the choosers are doing so.
563//        JPanel timesPanel =
564//            super.makeTimesPanel(false, true, getIdv().getUseTimeDriver());
565//
566//        // Make a new timesPanel that has extra components tacked on the
567//        // bottom, inside the tabs
568//        Component[] comps = timesPanel.getComponents();
569//
570//        if ((comps.length == 1) && (comps[0] instanceof JTabbedPane)) {
571//            timesCardPanelExtra = new GuiUtils.CardLayoutPanel();
572//            timesCardPanelExtra.add(new JPanel(), "relative");
573//            timesCardPanelExtra.add(getExtraTimeComponent(), "absolute");
574//            timesPanel = GuiUtils.centerBottom(comps[0], timesCardPanelExtra);
575//        }
576//        return timesPanel;
577//    }
578
579    //The previous makeTimesPanel method was an override of makeTimesPanel() from AddeImageChooser.java
580    //Not sure if the if block in the previous method was working/required. Hence it has been left commented out
581    //The makeTimesPanel method below is an override of the makeTimesPanel() from TimesChooser.java
582    // and is introduced to include the Num Images text field [2793] -PM
583    @Override
584    protected JPanel makeTimesPanel() {
585        JPanel timesPanel = super.makeTimesPanel(false, true, getIdv().getUseTimeDriver());
586        JPanel buttonPanel = new JPanel(new FlowLayout());
587        buttonPanel.add(archiveDayBtn);
588        buttonPanel.add(new JLabel("Num Images: "));
589        buttonPanel.add(imageCountTextField);
590        underTimelistPanel.add(BorderLayout.CENTER, buttonPanel);
591        return timesPanel;
592    }
593
594    /**
595     * Number of absolute times to list in the chooser.
596     * Must be a positive integer, or the word "ALL".
597     * Will throw up a dialog for invalid entries.
598     *
599     * @return 0 for valid entries, -1 for invalid
600     */
601    private int parseImageCount() {
602        String countStr = imageCountTextField.getText();
603        try {
604            int newCount = Integer.parseInt(countStr);
605            // Make sure it's reasonable
606            if (newCount > 0) {
607                int addeParam = 0 - newCount + 1;
608                numTimes = "" + addeParam;
609            } else {
610                throw new NumberFormatException();
611            }
612        } catch (NumberFormatException nfe) {
613            // Still ok if they entered "ALL"
614            if (imageCountTextField.getText().isEmpty()) {
615                JOptionPane.showMessageDialog(this,
616                        "Empty field, please enter a valid positive integer");
617                return -1;
618            }
619            if (! imageCountTextField.getText().equalsIgnoreCase("all")) {
620                JOptionPane.showMessageDialog(this,
621                        "Invalid entry: " + imageCountTextField.getText());
622                return -1;
623            }
624            numTimes = imageCountTextField.getText();
625        }
626        XmlObjectStore imgStore = getIdv().getStore();
627        imgStore.put(PREF_NUM_IMAGE_PRESET_RADARCHOOSER, countStr);
628        imgStore.save();
629        return 0;
630    }
631
632    /**
633     * Read the set of image times available for the current server/group/type
634     * This method is a wrapper, setting the wait cursor and wrapping the call
635     * to {@link #readTimesInner(boolean)}; in a try/catch block
636     */
637    @Override public void readTimes() {
638        readTimes(false);
639    }
640
641    public void readTimes(boolean forceAll) {
642
643        // Make sure there is a valid entry in the image count text field
644        if (parseImageCount() < 0) return;
645
646        clearTimesList();
647        if (!canReadTimes()) {
648            return;
649        }
650        Misc.run(new Runnable() {
651            public void run() {
652                updateStatus();
653                showWaitCursor();
654                try {
655                    readTimesInner(forceAll);
656                    checkSetNav();
657                } catch (Exception e) {
658                    handleConnectionError(e);
659                }
660                showNormalCursor();
661                updateStatus();
662            }
663        });
664    }
665}