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