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