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;
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.GroupLayout.Alignment.TRAILING;
036import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
037import static javax.swing.LayoutStyle.ComponentPlacement.UNRELATED;
038
039import java.awt.Component;
040import java.awt.Dimension;
041import java.awt.event.ActionListener;
042import java.awt.event.ItemEvent;
043import java.awt.event.ItemListener;
044import java.io.IOException;
045import java.net.URI;
046import java.util.ArrayList;
047import java.util.Date;
048import java.util.Hashtable;
049import java.util.List;
050import java.util.Vector;
051import java.util.regex.Pattern;
052
053import javax.swing.*;
054import javax.swing.event.DocumentEvent;
055import javax.swing.event.DocumentListener;
056import javax.swing.text.BadLocationException;
057
058import edu.wisc.ssec.mcidasv.McIDASV;
059import edu.wisc.ssec.mcidasv.chooser.adde.AddeChooser;
060import edu.wisc.ssec.mcidasv.util.McVTextField;
061import org.jdom2.Document;
062import org.jdom2.JDOMException;
063import org.jdom2.Namespace;
064import org.jdom2.input.SAXBuilder;
065import org.slf4j.Logger;
066import org.slf4j.LoggerFactory;
067import org.w3c.dom.Element;
068
069import thredds.catalog.XMLEntityResolver;
070import ucar.unidata.data.DataSelection;
071import ucar.unidata.data.radar.TDSRadarDatasetCollection;
072import ucar.nc2.units.DateUnit;
073import ucar.unidata.data.radar.RadarQuery;
074import ucar.unidata.geoloc.StationImpl;
075import ucar.unidata.idv.chooser.IdvChooserManager;
076import ucar.unidata.idv.chooser.TimesChooser;
077import ucar.unidata.metdata.NamedStation;
078import ucar.unidata.metdata.NamedStationImpl;
079import ucar.unidata.util.DateSelection;
080import ucar.unidata.util.DateUtil;
081import ucar.unidata.util.DatedThing;
082import ucar.unidata.util.GuiUtils;
083import ucar.unidata.util.LogUtil;
084import ucar.unidata.util.Misc;
085import ucar.unidata.util.PreferenceList;
086import ucar.unidata.util.Product;
087import ucar.unidata.util.TwoFacedObject;
088
089import visad.CommonUnit;
090import visad.DateTime;
091
092import edu.wisc.ssec.mcidasv.Constants;
093import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
094import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Position;
095import edu.wisc.ssec.mcidasv.util.McVGuiUtils.TextColor;
096import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Width;
097
098/**
099 * This class is responsible for the {@literal "Remote Level II Radar"} 
100 * chooser in McIDAS-V.
101 */
102public class TDSRadarChooser extends TimesChooser implements Constants {
103    
104    /** Loggging object. */
105    private static final Logger logger = 
106        LoggerFactory.getLogger(TDSRadarChooser.class);
107        
108    /** The collection */
109    private TDSRadarDatasetCollection collection;
110    
111    /** The currently selected station */
112    private NamedStation selectedStation;
113
114    /** The currently selected level3 product */
115    private String selectedProduct;
116
117    /** Those urls we connect to */
118    //"http://motherlode.ucar.edu:8080/thredds/radarServer/catalog.xml";
119    private String serverUrl;
120
121    ///** Each dataset collection URL */
122    //"http://motherlode.ucar.edu:8080/thredds/radarServer/level2/idd/dataset.xml";
123    //private String collectionUrl;
124
125    /** Component to hold collections */
126    private JComboBox collectionSelector;
127
128    /** Component to hold product list */
129    private JComboBox productComboBox;
130    
131    /** Level 3 panel that can be hidden */
132    private JPanel productPanel;
133    
134    /** components that need a server for activation */
135    private List compsThatNeedServer = new ArrayList();
136
137    /** components that need a server for activation */
138    private List level3CompsThatNeedServer = new ArrayList();
139
140    /** persistent holder for catalog URLS */
141    private PreferenceList urlListHandler;
142
143    /** catalog URL holder */
144    private JComboBox urlBox;
145
146    /** ok flag */
147    private boolean okToDoUrlListEvents = true;
148
149    /** dataset list */
150    private List datasetList;
151
152    /** Command for connecting */
153    protected static final String CMD_CONNECT = "cmd.connect";
154    
155    /** _more_          */
156    private boolean isLevel3;
157    
158    /** _more_          */
159    public static final String[] level3_ExName = { "NVW", "DPA" };
160    
161    /** Number of relative time steps to load */
162    private int relativeTimes = 5;
163    
164    /**
165     * Create the RadarChooser.
166     *
167     * @param mgr {@code IdvChooserManager}
168     * @param root XML root that defines this chooser.
169     *
170     */
171    public TDSRadarChooser(IdvChooserManager mgr, Element root) {
172        super(mgr, root);
173        
174        loadButton = McVGuiUtils.makeImageTextButton(ICON_ACCEPT_SMALL, getLoadCommandName());
175        loadButton.setActionCommand(getLoadCommandName());
176        loadButton.addActionListener(this);
177        
178        cancelButton = McVGuiUtils.makeImageButton(ICON_CANCEL, "Cancel");
179        cancelButton.setActionCommand(GuiUtils.CMD_CANCEL);
180        cancelButton.addActionListener(this);
181        cancelButton.setEnabled(false);
182        
183    }
184
185
186
187    /**
188     * Handle the update event. Just pass it through to the imageChooser
189     */
190    public void doUpdate() {
191        if ((serverUrl == null) || (datasetList == null)
192                || (datasetList.size() == 0) || (selectedProduct == null)) {
193            if (urlBox != null) {
194                setServer((String) urlBox.getSelectedItem());
195            }
196            return;
197        }
198        Misc.run(this, "stationOrProductChanged");
199    }
200
201
202
203    /**
204     * Update the status of the gui
205     */
206    protected void updateStatus() {
207        super.updateStatus();
208        if (serverUrl == null) {
209            setHaveData(false);
210            setStatus("Please connect to the server");
211        }
212        else if (selectedStation == null) {
213            setHaveData(false);
214            setStatus("Please select a station", "stations");
215        }
216        else if (isLevel3 && (selectedProduct == null)) {
217            setHaveData(false);
218            setStatus("Please select a level 3 product", "products");
219        }
220        else {
221            boolean haveTimesSelected;
222            if (getDoAbsoluteTimes()) {
223                haveTimesSelected = getSelectedAbsoluteTimes().size() > 0;
224            } else {
225                haveTimesSelected = true;
226            }
227            setHaveData(haveTimesSelected);
228            if (haveTimesSelected) {
229                setStatus("Press \"" + CMD_LOAD + "\" to load the selected radar data", "buttons");
230            } else {
231                setStatus("Please select times", "timepanel");
232            }
233        }
234        GuiUtils.enableTree(loadButton, getHaveData());
235        if (drivercbx != null) {
236            drivercbx.setEnabled(anyTimeDrivers() && getHaveData());
237        }
238    }
239
240
241
242    /**
243     * Handle when there are newly selected stations
244     *
245     * @param stations list of newly selected stations
246     */
247    protected void newSelectedStations(List stations) {
248        super.newSelectedStations(stations);
249        if ((stations == null) || (stations.size() == 0)) {
250            selectedStation = null;
251        } else {
252            NamedStation newStation = (NamedStation) stations.get(0);
253            if (Misc.equals(newStation, selectedStation)) {
254                return;
255            }
256            selectedStation = newStation;
257        }
258        Misc.run(TDSRadarChooser.this, "stationOrProductChanged");
259    }
260
261
262    /** A widget for the list of dataset descriptors */
263
264
265    /** Flag to keep from infinite looping */
266    private boolean ignoreProductChange = false;
267
268    /** Selection label text */
269    protected static final String LABEL_SELECT = " -- Select -- ";
270
271    /**
272     * _more_
273     */
274    protected void productChanged() {
275        stationOrProductChanged();
276        // updateStatus();
277    }
278
279    /**
280     * Reset the descriptor stuff
281     */
282    private void resetProductBox() {
283        ignoreProductChange = true;
284        productComboBox.setSelectedItem(LABEL_SELECT);
285        ignoreProductChange = false;
286    }
287
288    /**
289     * Should we update on first display
290     *
291     * @return true
292     */
293    protected boolean shouldDoUpdateOnFirstDisplay() {
294        return false;
295    }
296
297    /**
298     * Set the server
299     *
300     * @param s the server URL
301     */
302    private void setServer(String s) {
303        datasetList = new ArrayList();
304        serverUrl = s;
305        try {
306            datasetList = getRadarCollections(serverUrl);
307            GuiUtils.setListData(collectionSelector, datasetList);
308            for (int i = 0; i < datasetList.size(); i++) {
309                TwoFacedObject obj = (TwoFacedObject) datasetList.get(i);
310                String tmpStr = (String) (obj.getLabel());
311                if ((tmpStr).contains("NEXRAD Level II Radar from IDD")) {
312                    collectionSelector.setSelectedIndex(i);
313                }
314            }
315        } catch (Exception e) {
316            GuiUtils.setListData(collectionSelector, new ArrayList());
317        }
318    }
319
320    /**
321     * Set the active collection
322     *
323     * @param s collection URL
324     */
325    private void setCollection(String s) {
326        isLevel3 = false;
327        GuiUtils.enableComponents(level3CompsThatNeedServer, false);
328        productPanel.setVisible(false);
329        GuiUtils.enableComponents(compsThatNeedServer, true);
330        if (drivercbx != null) {
331            drivercbx.setEnabled(anyTimeDrivers() && getHaveData());
332        }
333        setAbsoluteTimes(new ArrayList());
334        selectedProduct = null;
335        selectedStation = null;
336        Misc.run(this, "initializeCollection", s);
337    }
338
339    /**
340     * _more_
341     *
342     * @param s _more_
343     */
344    private void setLevel3Collection(String s) {
345        isLevel3 = true;
346        GuiUtils.enableComponents(level3CompsThatNeedServer, true);
347        productPanel.setVisible(true);
348        GuiUtils.enableComponents(compsThatNeedServer, true);
349        if (drivercbx != null) {
350            drivercbx.setEnabled(anyTimeDrivers() && getHaveData());
351        }
352        setAbsoluteTimes(new ArrayList());
353        selectedProduct = null;
354        selectedStation = null;
355        Misc.run(this, "initializeLevel3Collection", s);
356    }
357
358    /**
359     * Add a component that needs to have a valid server
360     *
361     * @param comp  the component
362     *
363     * @return  the component
364     */
365    protected JComponent addServerComp(JComponent comp) {
366        compsThatNeedServer.add(comp);
367        return comp;
368    }
369
370    /**
371     * Add a component that needs to have a valid server
372     *
373     * @param comp  the component
374     *
375     * @return  the component
376     */
377    protected JComponent addLevel3ServerComp(JComponent comp) {
378        level3CompsThatNeedServer.add(comp);
379        return comp;
380    }
381
382    /**
383     * Get  the radar collections for  the given server URL
384     *
385     * @param radarServerURL  server URL
386     *
387     * @return  a map of the collection names to URL
388     */
389    private List getRadarCollections(String radarServerURL) {
390        SAXBuilder        builder;
391        Document          doc  = null;
392        XMLEntityResolver jaxp = new XMLEntityResolver(true);
393        builder = jaxp.getSAXBuilder();
394        List<TwoFacedObject> collections = new ArrayList<>();
395
396        try {
397            doc = builder.build(radarServerURL);
398        } catch (JDOMException e) {
399            if (canShowErrorWindows()) {
400                userMessage("Invalid catalog");
401            }
402            logger.warn("Invalid catalog: "+radarServerURL, e);
403        } catch (IOException e) {
404            if (canShowErrorWindows()) {
405                userMessage("Unable to open catalog");
406            }
407            logger.warn("Unable to open catalog: "+radarServerURL, e);
408        }
409
410        org.jdom2.Element rootElem    = doc.getRootElement();
411        org.jdom2.Element serviceElem = readElements(rootElem, "service");
412        String           uriBase     = serviceElem.getAttributeValue("base");
413        org.jdom2.Element dsElem      = readElements(rootElem, "dataset");
414        String           naming      = "catalogRef";
415        Namespace        nss         = rootElem.getNamespace("xlink");
416        List             children    = dsElem.getChildren();
417        for (int j = 0; j < children.size(); j++) {
418            org.jdom2.Element child     = (org.jdom2.Element) children.get(j);
419            String           childName = child.getName();
420            if (childName.equals(naming)) {
421                //String id   = child.getAttributeValue("ID");
422                String desc    = child.getAttributeValue("title", nss);
423                String urlpath = child.getAttributeValue("href", nss);
424                String[] c = radarServerURL.split(uriBase);  //.replaceFirst("catalog.xml", "");
425                String         ul     = c[0] + uriBase + urlpath;
426                TwoFacedObject twoObj = new TwoFacedObject(desc, ul);
427                collections.add(twoObj);
428                //collections.put(desc, ul);
429            }
430
431        }
432
433        return collections;
434    }
435
436    /**
437     * Read the elements
438     *
439     * @param elem  element
440     * @param eleName element name
441     *
442     * @return an element
443     */
444    public org.jdom2.Element readElements(org.jdom2.Element elem,
445                                         String eleName) {
446        List children = elem.getChildren();
447        for (int j = 0; j < children.size(); j++) {
448            org.jdom2.Element child     = (org.jdom2.Element) children.get(j);
449            String           childName = child.getName();
450            if (childName.equals(eleName)) {
451                return child;
452            }
453        }
454        return null;
455    }
456
457    /**
458     * Make the collection.  If there is an error, pop up a user message.
459     *
460     * @param url   URL for the collection
461     */
462    public synchronized void initializeCollection(String url) {
463
464        List<NamedStationImpl> stations = new ArrayList<NamedStationImpl>();
465        try {
466            StringBuffer errlog = new StringBuffer();
467            try {
468                logger.debug("Initializing collection from URL: " + url);
469                collection = TDSRadarDatasetCollection.factory("test", url, errlog);
470            } catch (Exception exc) {
471                if (canShowErrorWindows()) {
472                    userMessage("Invalid catalog");
473                }
474                logger.warn("Invalid catalog: "+url, exc);
475                return;
476            }
477            logger.debug("Iterating over collection...");
478            List tdsStations = collection.getRadarStations();
479            for (int i = 0; i < tdsStations.size(); i++) {
480                StationImpl stn = (StationImpl) tdsStations.get(i);
481                // thredds.catalog.query.Location loc = stn.getLocation();
482                //TODO: need better station  need to switch lat lon
483                NamedStationImpl station =
484                    new NamedStationImpl(stn.getName(), stn.getName(),
485                                         stn.getLatitude(),
486                                         stn.getLongitude(),
487                                         stn.getAltitude(), CommonUnit.meter);
488                stations.add(station);
489
490            }
491
492            getStationMap().setStations(stations);
493        } catch (Exception exc) {
494            if (canShowErrorWindows()) {
495                userMessage("Unable to load stations");
496            }
497            logger.warn("Unable to load stations from URL: "+url, exc);
498            return;
499        }
500
501        SwingUtilities.invokeLater(() -> urlListHandler.saveState(urlBox));
502    }
503
504    /**
505     * Utility method to check to see if McIDAS-V is ready to show error dialogs.
506     *
507     * @return {@code true} if McIDAS-V has finished starting and not in background mode.
508     * {@code false} otherwise.
509     */
510    private static boolean canShowErrorWindows() {
511        // TODO(jon): maybe this should be integrated into the userMessage method instead?
512        McIDASV mcv = McIDASV.getStaticMcv();
513        boolean result = false;
514        if (mcv != null) {
515            result = mcv.getHaveInitialized() && mcv.okToShowWindows();
516        }
517        return result;
518    }
519
520    /**
521     * _more_
522     *
523     * @param url _more_
524     */
525    public void initializeLevel3Collection(String url) {
526
527        List<NamedStationImpl> stations = new ArrayList<NamedStationImpl>();
528        List<Product>          products;
529        List<String>           exProducts = new ArrayList<String>();
530
531        for(String ename: level3_ExName){
532            exProducts.add(ename);
533        }
534
535        try {
536            StringBuffer errlog = new StringBuffer();
537            try {
538                collection = TDSRadarDatasetCollection.factory("test", url,
539                        errlog);
540            } catch (Exception exc) {
541                if (canShowErrorWindows()) {
542                    userMessage("Invalid catalog");
543                }
544                logger.warn("Invalid catalog: "+url, exc);
545                return;
546            }
547            products = collection.getRadarProducts();
548            List tdsStations = collection.getRadarStations();
549            for (int i = 0; i < tdsStations.size(); i++) {
550                StationImpl stn = (StationImpl) tdsStations.get(i);
551                // thredds.catalog.query.Location loc = stn.getLocation();
552                //TODO: need better station  need to switch lat lon
553                NamedStationImpl station =
554                    new NamedStationImpl(stn.getName(), stn.getName(),
555                                         stn.getLatitude(),
556                                         stn.getLongitude(),
557                                         stn.getAltitude(), CommonUnit.meter);
558                stations.add(station);
559
560            }
561            List<TwoFacedObject> productNames = new ArrayList();
562            for (Product product : products) {
563               // if ( !product.getID().contains("DPA")
564                 //       && !product.getID().contains("NVW")) {
565                if ( !exProducts.contains(product.getID())) {
566                    String lable = product.getName() + " (" + product.getID()
567                                   + ")";
568                    TwoFacedObject twoObj = new TwoFacedObject(lable,
569                                                product.getID());
570                    productNames.add(twoObj);
571                }
572            }
573            GuiUtils.setListData(productComboBox, productNames);
574
575            // GuiUtils.setListData(dataTypeComboBox, dataTypes);
576            getStationMap().setStations(stations);
577        } catch (Exception exc) {
578            if (canShowErrorWindows()) {
579                userMessage("Unable to load stations");
580            }
581            logger.warn("Unable to load stations from URL: "+url, exc);
582            return;
583        }
584        urlListHandler.saveState(urlBox);
585    }
586
587
588    /**
589     * Handle when the user has selected a new station
590     */
591    public void stationOrProductChanged() {
592        Vector times = new Vector();
593        setHaveData(false);
594        if ((!isLevel3 && selectedStation != null) ||
595                (isLevel3 && selectedStation != null && selectedProduct != null)) {
596            List timeSpan = collection.getRadarTimeSpan();
597            Date fromDate =  DateUnit.getStandardOrISO((String) timeSpan.get(0));
598            //Date toDate = DateUnit.getStandardOrISO((String) timeSpan.get(1));
599            Date toDate = new Date(System.currentTimeMillis()
600                                   + DateUtil.daysToMillis(1));
601            //Go back 10 years (or so)
602            //Date fromDate = new Date(System.currentTimeMillis()
603            //                         - DateUtil.daysToMillis(365 * 10));
604            try {
605                showWaitCursor();
606                setAbsoluteTimes(new ArrayList());
607                setStatus("Reading times for station: " + selectedStation,
608                          "");
609                //                LogUtil.message("Reading times for station: "
610                //                                + selectedStation);
611                String pid = null;
612                if(isLevel3)
613                    pid = TwoFacedObject.getIdString(
614                                 productComboBox.getSelectedItem());
615                List allTimes =
616                    collection.getRadarStationTimes(selectedStation.getID(),
617                        pid, fromDate, toDate);
618
619             //   if(allTimes.size() == 0) {
620             //       toDate = new Date(System.currentTimeMillis()
621             //                + DateUtil.daysToMillis(1));
622             //       allTimes =
623             //       collection.getRadarStationTimes(selectedStation.getID(),
624             //           pid, fromDate, toDate);
625             //   }
626
627                for (int timeIdx = 0; timeIdx < allTimes.size(); timeIdx++) {
628                    Object timeObj = allTimes.get(timeIdx);
629                    Date   date;
630                    if (timeObj instanceof Date) {
631                        date = (Date) timeObj;
632                    } else {
633                        date = DateUnit.getStandardOrISO(timeObj.toString());
634                    }
635                    times.add(new DateTime(date));
636                }
637                //                LogUtil.message("");
638                showNormalCursor();
639            } catch (Exception exc) {
640                if (canShowErrorWindows()) {
641                    userMessage("Error reading times for station: "
642                            + selectedStation);
643                }
644                //logException("Getting times for station: " + selectedStation,
645                //             exc);
646                setStatus("Select a different collection", "collections");
647                showNormalCursor();
648                return;
649            }
650        }
651        setAbsoluteTimes(times);
652        updateStatus();
653    }
654
655
656
657
658
659    /**
660     * Load the data
661     */
662    public void doLoadInThread() {
663        // to the CDMRadarDataSource
664        Hashtable ht = new Hashtable();
665        if (selectedStation != null) {
666            ht.put(ucar.unidata.data.radar.RadarDataSource.STATION_LOCATION,
667                   selectedStation.getNamedLocation());
668            ht.put(DataSelection.PROP_CHOOSERTIMEMATCHING, getDoTimeDrivers());
669        } else {
670            LogUtil.userMessage("No Station selected");
671        }
672
673        if (isLevel3 && (selectedProduct == null)) {
674
675            LogUtil.userMessage("No Product selected");
676        }
677
678        try {
679            DateSelection dateSelection = new DateSelection();
680            String collectionUrl = TwoFacedObject.getIdString(
681                                       collectionSelector.getSelectedItem());
682            String     pid = null;
683            RadarQuery radarQuery;
684            if (isLevel3) {
685                pid = TwoFacedObject.getIdString(
686                    productComboBox.getSelectedItem());
687                radarQuery = new RadarQuery(collectionUrl,
688                                            selectedStation.getID(), pid,
689                                            dateSelection);
690            } else {
691                radarQuery = new RadarQuery(collectionUrl,
692                                            selectedStation.getID(),
693                                            dateSelection);
694            }
695
696            List urls = new ArrayList();
697
698            if (getDoAbsoluteTimes()) {
699                List times    = new ArrayList();
700                List selected = makeDatedObjects(getSelectedAbsoluteTimes());
701                for (int i = 0; i < selected.size(); i++) {
702                    DatedThing datedThing = (DatedThing) selected.get(i);
703                    Date       date       = datedThing.getDate();
704                    times.add(date);
705                    URI uri = null;
706                    try {
707                        uri = collection.getRadarDatasetURI(
708                            selectedStation.getID(), pid, date);
709                    } catch (Exception excp) {
710                        LogUtil.userMessage("incorrect times selected");
711                        return;
712                    }
713                    urls.add(uri.toString());
714                }
715                if (urls.size() == 0) {
716                    LogUtil.userMessage("No times selected");
717                    return;
718                }
719                dateSelection.setTimes(times);
720            } else {
721//                int count = getRelativeTimesList().getSelectedIndex() + 1;
722                if (relativeTimes == 0) {
723                    LogUtil.userMessage("No relative times selected");
724                    return;
725                }
726                Date toDate = new Date(System.currentTimeMillis()
727                                       + DateUtil.daysToMillis(365 * 100));
728                //Go back 10 years (or so)
729                Date fromDate = new Date(System.currentTimeMillis()
730                                         - DateUtil.daysToMillis(365 * 10));
731
732                dateSelection.setStartFixedTime(fromDate);
733                dateSelection.setEndFixedTime(toDate);
734                dateSelection.setCount(relativeTimes);
735            }
736            makeDataSource(radarQuery, "FILE.RADAR", ht);
737        } catch (Exception exc) {
738            logException("Loading radar data", exc);
739        }
740        // uncheck the check box every time click the add source button
741        drivercbx.setSelected(false);
742        enableTimeWidgets();
743        setDoTimeDrivers(false);
744    }
745    
746    protected int getNumTimesToSelect() {
747        return 5;
748    }
749    
750    /**
751     * Get the default selected index for the relative times list.
752     *
753     * @return default index
754     */
755    protected int getDefaultRelativeTimeIndex() {
756        return 4;
757    }
758    
759    /**
760     * Check the times lists
761     */
762    protected void checkTimesLists() {
763        super.checkTimesLists();
764        if (timesCardPanelExtra == null) {
765            return;
766        }
767        if (getDoAbsoluteTimes()) {
768            timesCardPanelExtra.show("absolute");
769        } else {
770            timesCardPanelExtra.show("relative");
771        }
772    }
773        
774    /** Card panel to hold extra relative and absolute time components */
775    private GuiUtils.CardLayoutPanel timesCardPanelExtra;
776    
777    /**
778     * Add the interval selector to the component.
779     * @return superclass component with extra stuff
780     */
781    protected JPanel makeTimesPanel() {
782        JComponent extra = getExtraTimeComponent();
783        GuiUtils.enableTree(extra, false);
784        JPanel timesPanel = makeTimesPanel(extra, null);
785        return timesPanel;
786    }
787    
788    /**
789     * Set the relative and absolute extra components
790     */
791    protected JPanel makeTimesPanel(JComponent relativeCard, JComponent absoluteCard) {
792        JPanel timesPanel = super.makeTimesPanel(false, true, getIdv().getUseTimeDriver());
793
794        // Make a new timesPanel that has extra components tacked on the bottom, inside the tabs
795        Component[] comps = timesPanel.getComponents();
796
797        if ((comps.length == 2) && (comps[0] instanceof JTabbedPane) && (comps[1] instanceof JLabel)) {
798            timesCardPanelExtra = new GuiUtils.CardLayoutPanel();
799            if (relativeCard == null) {
800                relativeCard = new JPanel();
801            }
802            if (absoluteCard == null) {
803                absoluteCard = new JPanel();
804            }
805            absoluteCard = GuiUtils.hbox(comps[1], GuiUtils.right(absoluteCard));
806            timesCardPanelExtra.add(relativeCard, "relative");
807            timesCardPanelExtra.add(absoluteCard, "absolute");
808            timesPanel = GuiUtils.centerBottom(comps[0], timesCardPanelExtra);
809        }
810        return timesPanel;
811    }
812    
813    /**
814     * Get the time popup widget
815     *
816     * @return  a widget for selecting the day
817     */
818    protected JComponent getExtraTimeComponent() {
819        JPanel filler = new JPanel();
820        McVGuiUtils.setComponentHeight(filler, new JComboBox());
821        return filler;
822    }
823 
824    /**
825     * Make the contents
826     *
827     * @return  the contents
828     */
829    protected JPanel doMakeInnerPanel() {
830        JPanel myPanel = new JPanel();
831
832        JLabel collectionLabel = McVGuiUtils.makeLabelRight("Collection:");
833
834        collectionSelector = new JComboBox();
835        collectionSelector.addItemListener(new ItemListener() {
836            public void itemStateChanged(ItemEvent e) {
837                newSelectedStations(new ArrayList());
838                if (collectionSelector.getSelectedItem() == null) {
839                    return;
840                }
841                String collectionUrl =
842                    TwoFacedObject.getIdString(
843                        collectionSelector.getSelectedItem());
844
845                if (collectionUrl.contains("level3")) {
846                    setLevel3Collection(collectionUrl);
847                } else {
848                    setCollection(collectionUrl);
849                }
850            }
851
852        });
853        addServerComp(collectionLabel);
854        addServerComp(collectionSelector);
855                
856        productComboBox = new JComboBox();
857        productComboBox.addItemListener(new ItemListener() {
858            public void itemStateChanged(ItemEvent e) {
859                if (productComboBox.getSelectedItem() == null) {
860                    return;
861                }
862                selectedProduct =
863                    productComboBox.getSelectedItem().toString();
864                resetProductBox();
865                productChanged();
866            }
867
868        });
869        addLevel3ServerComp(productComboBox);
870                
871        productPanel = McVGuiUtils.makeLabeledComponent("Product:", productComboBox);
872        
873        JLabel stationLabel = McVGuiUtils.makeLabelRight("Station:");
874        addServerComp(stationLabel);
875
876        JComponent stationPanel = getStationMap();
877        registerStatusComp("stations", stationPanel);
878        addServerComp(stationPanel);
879        
880        JLabel timesLabel = McVGuiUtils.makeLabelRight("Times:");
881        addServerComp(timesLabel);
882        
883        JPanel timesPanel = makeTimesPanel();
884        timesPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
885        addServerComp(timesPanel);
886
887        GuiUtils.enableComponents(compsThatNeedServer, false);
888        GuiUtils.enableComponents(level3CompsThatNeedServer, false);
889        productPanel.setVisible(false);
890
891        GroupLayout layout = new GroupLayout(myPanel);
892        myPanel.setLayout(layout);
893        layout.setHorizontalGroup(
894            layout.createParallelGroup(LEADING)
895            .addGroup(layout.createSequentialGroup()
896                .addGroup(layout.createParallelGroup(LEADING)
897                    .addGroup(layout.createSequentialGroup()
898                        .addComponent(collectionLabel)
899                        .addGap(GAP_RELATED)
900                        .addComponent(collectionSelector, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
901                        .addComponent(productPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
902                    .addGroup(layout.createSequentialGroup()
903                        .addComponent(stationLabel)
904                        .addGap(GAP_RELATED)
905                        .addComponent(stationPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
906                    .addGroup(layout.createSequentialGroup()
907                        .addComponent(timesLabel)
908                        .addGap(GAP_RELATED)
909                        .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))))
910        );
911        layout.setVerticalGroup(
912            layout.createParallelGroup(LEADING)
913            .addGroup(layout.createSequentialGroup()
914                .addGroup(layout.createParallelGroup(BASELINE)
915                    .addComponent(collectionLabel)
916                    .addComponent(collectionSelector)
917                    .addComponent(productPanel))
918                .addPreferredGap(RELATED)
919                .addGroup(layout.createParallelGroup(LEADING)
920                    .addComponent(stationLabel)
921                    .addComponent(stationPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
922                .addPreferredGap(RELATED)
923                .addGroup(layout.createParallelGroup(LEADING)
924                    .addComponent(timesLabel)
925                    .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
926                .addPreferredGap(RELATED))
927        );
928        
929        return myPanel;
930    }
931
932    private JPanel innerPanel = doMakeInnerPanel();
933
934    private JLabel statusLabel = new JLabel("Status");
935
936    @Override public void setStatus(String statusString, String foo) {
937        if (statusString == null) {
938            statusString = "";
939        }
940        statusLabel.setText(statusString);
941    }
942    
943    protected void setInnerPanel(JPanel newInnerPanel) {
944        innerPanel = newInnerPanel;
945    }
946    
947    /**
948     * Create the widget responsible for handling relative time selection.
949     *
950     * @return GUI widget.
951     */
952    @Override public JComponent getRelativeTimesChooser() {
953        McVTextField relativeTimesField =
954            McVGuiUtils.makeTextFieldAllow(String.valueOf(relativeTimes),
955                                           4,
956                                           false,
957                                           '0', '1', '2', '3', '4', '5', '6',
958                                           '7','8','9');
959                                           
960        // need to keep *both* the ActionListener and DocumentListener around.
961        // removing the ActionListener results in strange behavior when you've
962        // accidentally cleared out the text field.
963        relativeTimesField.setAllow(Pattern.compile("^[1-9][0-9]*$"), true);
964//        relativeTimesField.setDeny(Pattern.compile("^0$"), true);
965        relativeTimesField.setColumns(4);
966        relativeTimesField.addActionListener(e -> {
967            String text = ((JTextField)e.getSource()).getText();
968            validateRelativeTimeInput(text);
969        });
970        relativeTimesField.getDocument().addDocumentListener(new DocumentListener() {
971            @Override public void insertUpdate(DocumentEvent e) {
972                handleRelativeTimeChange(e);
973            }
974            
975            @Override public void removeUpdate(DocumentEvent e) {
976                handleRelativeTimeChange(e);
977            }
978            
979            @Override public void changedUpdate(DocumentEvent e) {
980                handleRelativeTimeChange(e);
981            }
982        });
983        relativeTimesField.setToolTipText(AddeChooser.RELATIVE_TIMES_TOOLTIP);
984        
985        JPanel panel =
986            GuiUtils.topLeft(GuiUtils.label(AddeChooser.RELATIVE_TIMES_LABEL,
987                relativeTimesField));
988        JScrollPane scrollPane = new JScrollPane(panel);
989        scrollPane.setPreferredSize(new Dimension(150, 100));
990        return scrollPane;
991    }
992    
993    /**
994     * Validate the contents of the relative times text field.
995     *
996     * <p>This method overwrites {@link #relativeTimes} if {@code text} is an 
997     * integer greater than zero.</p>
998     *
999     * @param text Contents of the text field.
1000     */
1001    private void validateRelativeTimeInput(String text) {
1002        try {
1003            int value = Integer.valueOf(text);
1004            if (value > 0) {
1005                relativeTimes = value;
1006                setHaveData(true);
1007                updateStatus();
1008            }
1009        } catch (NumberFormatException e) {
1010            setHaveData(false);
1011            setStatus("Please provide an integer value greater than zero.");
1012        }
1013    }
1014    
1015    /**
1016     * Handle {@link DocumentListener} events for the {@link JTextField}
1017     * created by {@link #getRelativeTimesChooser()}.
1018     *
1019     * @param event Event to handle. Cannot be {@code null}.
1020     */
1021    private void handleRelativeTimeChange(DocumentEvent event) {
1022        int len = event.getDocument().getLength();
1023        try {
1024            String text = event.getDocument().getText(0, len);
1025            validateRelativeTimeInput(text);
1026        } catch (BadLocationException ex) {
1027            logger.warn("Could not get contents of text field!", ex);
1028        }
1029    }
1030    
1031    /**
1032     * Get the relative time indices
1033     *
1034     * @return an array of indices
1035     */
1036    @Override public int[] getRelativeTimeIndices() {
1037        int[] indices = new int[relativeTimes];
1038        for (int i = 0; i < indices.length; i++) {
1039            indices[i] = i;
1040        }
1041        return indices;
1042    }
1043    
1044    /**
1045     * Make the UI for this selector.
1046     *
1047     * Thank you NetBeans for helping with the layout!
1048     *
1049     * @return The GUI.
1050     */
1051    public JComponent doMakeContents() {
1052        JPanel outerPanel = new JPanel();
1053        
1054        JLabel serverLabel = McVGuiUtils.makeLabelRight("Catalog:");                
1055        
1056        // Get the list of catalogs but remove the old catalog.xml entry
1057        urlListHandler = getPreferenceList(PREF_TDSRADARSERVER);
1058
1059        ActionListener catListListener = ae -> {
1060            if (okToDoUrlListEvents) {
1061                setServer((String) urlBox.getSelectedItem());
1062            }
1063        };
1064
1065        urlBox = urlListHandler.createComboBox(GuiUtils.CMD_UPDATE, catListListener, true);
1066        
1067        // TJJ Nov 2019
1068        // Since URLs are stored in user prefs, and THREDDS wants HTTPS now for radar catalog,
1069        // need to convert on-the-fly where they occur
1070        // See: https://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=2857
1071
1072        String selectedURL = (String) urlBox.getSelectedItem();
1073        // At first init, URL may be null. If so, set to default
1074        if (selectedURL != null) {
1075            selectedURL = selectedURL.replace("http:", "https:");
1076            urlBox.setSelectedItem(selectedURL);
1077        } else {
1078            urlBox.setSelectedItem(getProperty(PREF_TDSRADARSERVER, null));
1079        }
1080
1081        McVGuiUtils.setComponentWidth(urlBox, Width.DOUBLEDOUBLE);
1082        
1083        // productComboBox gets created a little too tall--set to same height as urlBox
1084        if (productComboBox != null) {
1085            McVGuiUtils.setComponentHeight(productComboBox, urlBox);
1086        }
1087        JButton connectButton = McVGuiUtils.makeImageTextButton(ICON_CONNECT_SMALL, "Connect");
1088        McVGuiUtils.setComponentWidth(connectButton, Width.DOUBLE);
1089        connectButton.setActionCommand(GuiUtils.CMD_UPDATE);
1090        connectButton.addActionListener(this);
1091        
1092        JLabel statusLabelLabel = McVGuiUtils.makeLabelRight("");
1093        
1094        statusLabel.setText("Status");
1095        McVGuiUtils.setLabelPosition(statusLabel, Position.RIGHT);
1096        McVGuiUtils.setComponentColor(statusLabel, TextColor.STATUS);
1097        
1098        JButton helpButton = McVGuiUtils.makeImageButton(ICON_HELP, "Show help");
1099        helpButton.setActionCommand(GuiUtils.CMD_HELP);
1100        helpButton.addActionListener(this);
1101        
1102        JButton refreshButton = McVGuiUtils.makeImageButton(ICON_REFRESH, "Refresh");
1103        refreshButton.setActionCommand(GuiUtils.CMD_UPDATE);
1104        refreshButton.addActionListener(this);
1105        
1106        McVGuiUtils.setComponentWidth(loadButton, Width.DOUBLE);
1107        
1108        GroupLayout layout = new GroupLayout(outerPanel);
1109        outerPanel.setLayout(layout);
1110        layout.setHorizontalGroup(
1111            layout.createParallelGroup(LEADING)
1112            .addGroup(TRAILING, layout.createSequentialGroup()
1113                .addGroup(layout.createParallelGroup(TRAILING)
1114                    .addGroup(layout.createSequentialGroup()
1115                        .addContainerGap()
1116                        .addComponent(helpButton)
1117                        .addGap(GAP_RELATED)
1118                        .addComponent(refreshButton)
1119                        .addGap(GAP_RELATED)
1120                        .addComponent(cancelButton)
1121                        .addPreferredGap(RELATED)
1122                        .addComponent(loadButton))
1123                        .addGroup(LEADING, layout.createSequentialGroup()
1124                        .addContainerGap()
1125                        .addGroup(layout.createParallelGroup(LEADING)
1126                            .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
1127                            .addGroup(layout.createSequentialGroup()
1128                                .addComponent(serverLabel)
1129                                .addGap(GAP_RELATED)
1130                                .addComponent(urlBox, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
1131                                .addGap(GAP_UNRELATED)
1132                                .addComponent(connectButton))
1133                            .addGroup(layout.createSequentialGroup()
1134                                .addComponent(statusLabelLabel)
1135                                .addGap(GAP_RELATED)
1136                                .addComponent(statusLabel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)))))
1137                .addContainerGap())
1138        );
1139        layout.setVerticalGroup(
1140            layout.createParallelGroup(LEADING)
1141            .addGroup(layout.createSequentialGroup()
1142                .addContainerGap()
1143                .addGroup(layout.createParallelGroup(BASELINE)
1144                    .addComponent(serverLabel)
1145                    .addComponent(urlBox)
1146                    .addComponent(connectButton))
1147                .addPreferredGap(UNRELATED)
1148                .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
1149                .addPreferredGap(UNRELATED)
1150                .addGroup(layout.createParallelGroup(BASELINE)
1151                    .addComponent(statusLabelLabel)
1152                    .addComponent(statusLabel))
1153                .addPreferredGap(UNRELATED)
1154                .addGroup(layout.createParallelGroup(BASELINE)
1155                    .addComponent(loadButton)
1156                    .addComponent(cancelButton)
1157                    .addComponent(refreshButton)
1158                    .addComponent(helpButton))
1159                .addContainerGap())
1160        );
1161        return outerPanel;
1162    }
1163}