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     * 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}