001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.chooser.adde;
030
031import static javax.swing.GroupLayout.DEFAULT_SIZE;
032import static javax.swing.GroupLayout.PREFERRED_SIZE;
033import static javax.swing.GroupLayout.Alignment.BASELINE;
034import static javax.swing.GroupLayout.Alignment.LEADING;
035import static javax.swing.KeyStroke.getKeyStroke;
036import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
037
038import java.awt.BorderLayout;
039import java.awt.Color;
040import java.awt.FlowLayout;
041import java.awt.IllegalComponentStateException;
042import java.awt.Point;
043import java.awt.event.ActionEvent;
044import java.awt.event.ActionListener;
045import java.awt.event.KeyEvent;
046import java.awt.event.KeyListener;
047import java.text.SimpleDateFormat;
048import java.util.ArrayList;
049import java.util.Arrays;
050import java.util.Collections;
051import java.util.Date;
052import java.util.Enumeration;
053import java.util.Hashtable;
054import java.util.List;
055import java.util.SortedSet;
056import java.util.TreeSet;
057
058import javax.swing.GroupLayout;
059import javax.swing.JButton;
060import javax.swing.JComboBox;
061import javax.swing.JComponent;
062import javax.swing.JDialog;
063import javax.swing.JLabel;
064import javax.swing.JOptionPane;
065import javax.swing.JPanel;
066import javax.swing.KeyStroke;
067import javax.swing.ListSelectionModel;
068
069import org.joda.time.LocalDate;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072import org.w3c.dom.Element;
073
074import edu.wisc.ssec.mcidas.McIDASUtil;
075import edu.wisc.ssec.mcidas.adde.AddePointDataReader;
076import edu.wisc.ssec.mcidas.adde.DataSetInfo;
077import edu.wisc.ssec.mcidasv.ui.JCalendarDateEditor;
078import edu.wisc.ssec.mcidasv.ui.JCalendarPicker;
079import edu.wisc.ssec.mcidasv.ui.JTimeRangePicker;
080import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
081import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Width;
082
083import ucar.unidata.data.DataSelection;
084import ucar.unidata.data.AddeUtil;
085import ucar.unidata.data.point.AddePointDataSource;
086import ucar.unidata.idv.chooser.IdvChooserManager;
087import ucar.unidata.idv.chooser.adde.AddeServer;
088import ucar.unidata.ui.symbol.StationModel;
089import ucar.unidata.ui.symbol.StationModelManager;
090import ucar.unidata.util.GuiUtils;
091import ucar.unidata.util.Misc;
092import ucar.unidata.util.TwoFacedObject;
093import ucar.visad.UtcDate;
094
095import visad.DateTime;
096import visad.VisADException;
097
098/**
099 * Selection widget for ADDE point data
100 * 
101 * @version $Revision$ $Date$
102 */
103
104public class AddePointDataChooser extends AddeChooser {
105
106    private static final long serialVersionUID = 1L;
107
108    /** Logging object. Use it! */
109    private static final Logger logger = LoggerFactory.getLogger(AddePointDataChooser.class);
110
111    /**
112     * Property for the dataset name key.
113     * @see edu.wisc.ssec.mcidasv.chooser.adde.AddeChooser#getDataSetName()
114     */
115    public static String DATASET_NAME_KEY = "name";
116
117    /** Property for the data type. */
118    public static String DATA_TYPE = "ADDE.POINT.V";
119
120    /** Are we currently reading times */
121    private Object readTimesTask;
122    
123    /** box and label for the relative time */
124    protected JLabel relTimeIncLabel;
125    protected JComboBox relTimeIncBox;
126
127    /** the relative time increment */
128    private float relativeTimeIncrement = 1.0f;
129
130    /** Date will default to current */
131    DateTime dt = null;
132
133    /** archive day button and label */
134    protected JLabel archiveDayLabel;
135    protected JButton archiveDayBtn;
136
137    /** archive date formatter */
138    private SimpleDateFormat archiveDayFormatter;
139
140    /** station model manager */
141    private StationModelManager stationModelManager;
142    
143    /** allowed descriptor prefix */
144    protected String descriptorsAllowPrefix = "";
145        
146    protected boolean firstTime = true;
147    protected boolean retry = true;
148    
149    /** we reset the retry flag any time remote server changes */
150    protected String previousServer = "";
151
152    /** Possibly ask for times a second time if the first sampling doesn't get any */
153    private boolean gotObs = false;
154    protected boolean tryWithoutSampling = false;
155
156    /** Julian Date formatter */
157    private static SimpleDateFormat jdFormat = new SimpleDateFormat("yyyyDDD");
158        
159    /**
160     * Create a chooser for ADDE POINT data
161     *
162     * @param mgr The chooser manager
163     * @param root The chooser.xml node
164     */
165
166    public AddePointDataChooser(IdvChooserManager mgr, Element root) {
167        super(mgr, root);
168                                
169        this.stationModelManager = getIdv().getStationModelManager();
170
171        relTimeIncLabel = new JLabel(" Interval:");
172        relTimeIncBox = new JComboBox();
173        relTimeIncBox.setToolTipText("Set the increment between relative times");
174        relTimeIncBox.addActionListener(new ActionListener() {
175            @Override public void actionPerformed(ActionEvent ae) {
176                JComboBox box = (JComboBox) ae.getSource();
177                if (GuiUtils.anySelected(box)) {
178                    setRelativeTimeIncrement(getRelBoxValue());
179                }
180            }
181        });
182        McVGuiUtils.setComponentWidth(relTimeIncBox, Width.ONEHALF);
183        
184        descriptorsAllowPrefix = "";
185        
186        try {
187            dt = new DateTime();
188        } catch (VisADException vade) {
189            vade.printStackTrace();
190        }
191
192        archiveDayBtn = new JButton("Set Day");
193        archiveDayBtn.addActionListener(e -> getArchiveDay());
194        archiveDayBtn.setToolTipText("Select a specific day");
195
196        // Initialize time range to full day
197        archiveBegTime = "00:00:00";
198        archiveEndTime = "23:59:59";
199
200        archiveDayLabel = new JLabel("Select day:");
201        archiveDayFormatter = new SimpleDateFormat(UtcDate.IYD_FORMAT);
202    }
203    
204    /**
205     * Do server connection stuff... override this with type-specific methods
206     */
207    @Override protected void readFromServer() {
208        if (archiveDayLabel != null) {
209            archiveDayLabel.setText("Select day:");
210        }
211        super.readFromServer();
212    }
213
214    /**
215     *  Generate a list of image descriptors for the descriptor list.
216     */
217    @Override protected void readDescriptors() {
218        try {
219
220            String currentServer = this.getServer();
221            logger.info("Current server: " + currentServer);
222            logger.info("Previous server: " + previousServer);
223            if (! currentServer.equals("previousServer")) {
224                // Reset retry flag to force archive servers to throw up date picker
225                logger.info("Resetting RETRY flag");
226                retry = true;
227            }
228            previousServer = currentServer;
229
230            StringBuffer buff = getGroupUrl(REQ_DATASETINFO, getGroup());
231            buff.append("&type=").append(getDataType());
232            DataSetInfo dsinfo = new DataSetInfo(buff.toString());
233            descriptorTable = dsinfo.getDescriptionTable();
234            
235            // Only show descriptorsAllowPrefix if set
236            for (Enumeration e = descriptorTable.keys(); e.hasMoreElements();) {
237                Object key = e.nextElement();
238                String str = (String)descriptorTable.get(key);
239                if (!descriptorsAllowPrefix.isEmpty() && str.indexOf(descriptorsAllowPrefix) != 0) {
240                    descriptorTable.remove(key);
241                }
242            }
243            
244            String[] names = new String[descriptorTable.size()];
245            Enumeration enumeration = descriptorTable.keys();
246            for (int i = 0; enumeration.hasMoreElements(); i++) {
247                Object thisElement = enumeration.nextElement();
248                if (!isLocalServer()) {
249                    names[i] = descriptorTable.get(thisElement).toString() + nameSeparator + thisElement.toString();
250                } else {
251                    names[i] = thisElement.toString();
252                }
253            }
254            Arrays.sort(names);
255            setDescriptors(names);
256            setState(STATE_CONNECTED);
257        } catch (Exception e) {
258            handleConnectionError(e);
259        }
260    }
261
262    /**
263     * Load in an ADDE point data set based on the {@code PropertyChangeEvent}.
264     */
265    @Override public void doLoadInThread() {
266        showWaitCursor();
267        try {
268            StationModel selectedStationModel = getSelectedStationModel();
269            String source = getRequestUrl();
270
271            // make properties Hashtable to hand the station name
272            // to the AddeProfilerDataSource
273            Hashtable ht = new Hashtable();
274            getDataSourceProperties(ht);
275            ht.put(DataSelection.PROP_CHOOSERTIMEMATCHING, getDoTimeDrivers());
276            ht.put(AddePointDataSource.PROP_STATIONMODELNAME,
277                   selectedStationModel.getName());
278            ht.put(DATASET_NAME_KEY, getDescriptor());
279            ht.put(DATA_NAME_KEY, getDataName());
280            if (source.contains(AddeUtil.RELATIVE_TIME)) {
281                ht.put(AddeUtil.NUM_RELATIVE_TIMES, getRelativeTimeIndices());
282                ht.put(AddeUtil.RELATIVE_TIME_INCREMENT, getRelativeTimeIncrement());
283            }
284
285            if (getDoAbsoluteTimes()) {
286              ht.put(AddeUtil.ABSOLUTE_TIMES, getSelectedAbsoluteTimes());
287            }
288
289            makeDataSource(source, DATA_TYPE, ht);
290            saveServerState();
291        } catch (Exception excp) {
292            logException("Unable to open ADDE point dataset", excp);
293        }
294        showNormalCursor();
295        // uncheck the check box every time click the add source button
296        drivercbx.setSelected(false);
297        enableTimeWidgets();
298        setDoTimeDrivers(false);
299    }
300        
301    /**
302     * Show the archive dialog. This method is not meant to be called but is
303     * public by reason of implementation (or insanity).
304     */
305    public void getArchiveDay() {
306
307        final JDialog dialog = GuiUtils.createDialog("Set Day and Time Range", true);
308        final JCalendarPicker picker = new JCalendarPicker(false);
309        final JTimeRangePicker trp = new JTimeRangePicker();
310
311        if (archiveDay != null) {
312            if (archiveDayFormatter == null) {
313                archiveDayFormatter = new SimpleDateFormat(UtcDate.YMD_FORMAT);
314            }
315            Date d = null;
316            try {
317                d = archiveDayFormatter.parse(archiveDay);
318                picker.setDate(d);
319            } catch (Exception e) {
320                logException("parsing archive day " + archiveDay, e);
321            }
322        }
323
324        ActionListener listener = new ActionListener() {
325            public void actionPerformed(ActionEvent ae) {
326                String cmd = ae.getActionCommand();
327                if (cmd.equals(GuiUtils.CMD_OK)) {
328
329                    // bad time range, throw up error window
330                    if (! trp.timeRangeOk()) {
331                        String msg = "Time range is invalid.\n" +
332                                     "Please provide valid hours, minutes and\n" +
333                                     "seconds, with End Time > Start Time.";
334                        Object[] params = { msg };
335                        JOptionPane.showMessageDialog(null, params, "Invalid Time Range", JOptionPane.OK_OPTION);
336                        return;
337                    } else {
338                        archiveBegTime = trp.getBegTimeStr();
339                        archiveEndTime = trp.getEndTimeStr();
340                    }
341                    try {
342                        String pickerDate = picker.getUserSelectedDay();
343                        LocalDate ld = LocalDate.parse(pickerDate);
344                        dt = new DateTime(ld.toDate());
345                        archiveDay = jdFormat.format(ld.toDate());
346                        archiveDayBtn.setText(pickerDate);
347                    } catch (Exception e) {
348                    }
349
350                    setDoAbsoluteTimes(true);
351                    clearTimesList();
352                    descriptorChanged();
353                }
354                dialog.dispose();
355            }
356        };
357
358        final JCalendarDateEditor dateEditor =
359            (JCalendarDateEditor)picker.getDateChooser().getDateEditor();
360        dateEditor.getUiComponent().addKeyListener(new KeyListener() {
361            @Override public void keyTyped(KeyEvent e) { }
362
363            @Override public void keyPressed(KeyEvent e) { }
364
365            @Override public void keyReleased(KeyEvent e) {
366                if (!Color.RED.equals(dateEditor.getForeground())) {
367                    KeyStroke stroke =
368                        getKeyStroke(e.getKeyCode(), e.getModifiers());
369                    if (stroke.getKeyCode() == KeyEvent.VK_ENTER) {
370                        try {
371                            String pickerDate = picker.getUserSelectedDay();
372                            LocalDate ld = LocalDate.parse(pickerDate);
373                            dt = new DateTime(ld.toDate());
374                            archiveDay = jdFormat.format(ld.toDate());
375                            archiveDayBtn.setText(pickerDate);
376                        } catch (Exception ex) {
377                            // nothing to do
378                        }
379                        setDoAbsoluteTimes(true);
380                        descriptorChanged();
381                        dialog.dispose();
382                    }
383                }
384            }
385        });
386
387        JPanel buttons = GuiUtils.makeButtons(listener, new String[] {
388                GuiUtils.CMD_OK, GuiUtils.CMD_CANCEL });
389
390        JPanel dateTimePanel = new JPanel(new FlowLayout());
391        dateTimePanel.add(picker);
392        dateTimePanel.add(trp);
393
394        JComponent contents = GuiUtils.topCenterBottom(GuiUtils.inset(GuiUtils
395                .lLabel("Please select a day and optional time range for this dataset:"), 10), GuiUtils
396                .inset(dateTimePanel, 10), buttons);
397        Point p = new Point(200, 200);
398        if (archiveDayBtn != null) {
399            try {
400                p = archiveDayBtn.getLocationOnScreen();
401            } catch (IllegalComponentStateException ice) {
402            }
403        }
404        dialog.setLocation(p);
405        dialog.getContentPane().add(contents);
406        dialog.pack();
407        dialog.setVisible(true);
408    }
409
410    /**
411     * Get the selected station model.
412     *
413     * @return StationModel to use: defined by defaultModels list in ctor
414     */
415    public StationModel getSelectedStationModel() {
416        StationModel returnModel = null;
417        if (isUpperAir()) {
418            returnModel = this.stationModelManager.getStationModel("Observations>Upper Air");
419        } else if (isSynoptic()) {
420            returnModel = this.stationModelManager.getStationModel("Observations>SYNOP");
421        } else {
422            returnModel = this.stationModelManager.getStationModel("Observations>METAR");
423        }
424        return returnModel;
425    }
426    
427    /**
428     * Add the interval selector to the component.
429     * @return superclass component with extra stuff
430     */
431    @Override protected JPanel makeTimesPanel() {
432        JComponent extra1 = getExtraTimeComponentRelative();
433        GuiUtils.enableTree(extra1, false);
434        JComponent extra2 = getExtraTimeComponentAbsolute();
435        JPanel timesPanel = super.makeTimesPanel(extra1, extra2);
436        JPanel buttonPanel = new JPanel(new FlowLayout());
437        buttonPanel.add(archiveDayBtn);
438        underTimelistPanel.add(BorderLayout.CENTER, buttonPanel);
439        return timesPanel;
440    }
441    
442    /**
443     * Get the extra time widget, but built in a different way.
444     * Designed to be put into a GroupLayout
445     *
446     * @return Extra time widget
447     */
448    protected JComponent getExtraTimeComponentRelative() {
449        TwoFacedObject[] intervals = { 
450            new TwoFacedObject("30 minute", .5f),
451            new TwoFacedObject("Hourly", 1f),
452            new TwoFacedObject("Three hourly", 3f),
453            new TwoFacedObject("Six hourly", 6f),
454            new TwoFacedObject("12 hourly", 12f),
455            new TwoFacedObject("24 hourly", 24f)
456        };
457
458        GuiUtils.setListData(relTimeIncBox, intervals);
459        if (relTimeIncBox.getItemCount()>=2) relTimeIncBox.setSelectedIndex(1);
460        
461        return McVGuiUtils.makeLabeledComponent(relTimeIncLabel, relTimeIncBox, McVGuiUtils.Position.LEFT);
462    }
463
464    /**
465     * Overridden in McIDAS-V to get a nicer set of interval combo box options.
466     *
467     * @return {@code JPanel} containing a label and the interval combo box.
468     */
469    @Override protected JComponent getExtraRelativeTimeComponent() {
470        return getExtraTimeComponentRelative();
471    }
472
473    /**
474     * Get the time popup widget
475     * 
476     * @return a widget for selecing the day
477     */
478    protected JComponent getExtraTimeComponentAbsolute() {
479        return null;
480//      return McVGuiUtils.makeLabeledComponent(archiveDayLabel, archiveDayBtn);
481    }
482        
483    /**
484     * Get the value from the relative increment box
485     *
486     * @return the selected value or a default
487     */
488    protected float getRelBoxValue() {
489        float value = relativeTimeIncrement;
490        if (relTimeIncBox != null) {
491            Object o = relTimeIncBox.getSelectedItem();
492            if (o != null) {
493                String val = TwoFacedObject.getIdString(o);
494                value = (float) Misc.parseNumber(val);
495            }
496        }
497        return value;
498    }
499    
500    /**
501     * Get the string from the relative increment box
502     *
503     * @return the selected string or a default
504     */
505    public String getRelBoxString() {
506        String value = "";
507        if (relTimeIncBox != null) {
508            Object o = relTimeIncBox.getSelectedItem();
509            if (o != null) {
510                value = TwoFacedObject.getIdString(o);
511            }
512        }
513        return value;
514    }
515
516    /**
517     * Get the request URL
518     *
519     * @return the request URL
520     */
521    public String getRequestUrl() {
522        StringBuffer request = getGroupUrl(REQ_POINTDATA, getGroup());
523        appendKeyValue(request, PROP_USER, getLastAddedUser());
524        appendKeyValue(request, PROP_PROJ, getLastAddedProj());
525        appendKeyValue(request, PROP_DESCR, getDescriptor());
526        appendRequestSelectClause(request);
527        appendKeyValue(request, PROP_NUM, "ALL");
528        // TJJ Dec 2020 - looks like POS = 0 limits requests to current day, so
529        // we should always use ALL, and let the rel/abs filters dictate what is
530        // delivered?
531        // appendKeyValue(request, PROP_POS, getDoRelativeTimes() ? "ALL" : "0");
532        appendKeyValue(request, PROP_POS, "ALL");
533        logger.info("Request URL: " + request.toString());
534        return request.toString();
535    }
536        
537    /**
538     * Get the select clause for the ADDE request specific to this
539     * type of data.
540     *
541     * @param buf The buffer to append to
542     */
543
544    protected void appendRequestSelectClause(StringBuffer buf) {
545        StringBuilder selectValue = new StringBuilder(1024);
546        selectValue.append('\'');
547        selectValue.append(getDayTimeSelectString());
548        // TODO: why is SFCHOURLY explicit here?  better way to do it?
549        if ("SFCHOURLY".equalsIgnoreCase(getDescriptor())) {
550            selectValue.append(";TYPE 0");
551        }
552        selectValue.append(';');
553
554        if (isUpperAir()){
555            selectValue.append(AddeUtil.LEVEL);
556            selectValue.append(';');
557        }
558        // TJJ - This must be a placeholder macro for the URL
559        selectValue.append(AddeUtil.LATLON_BOX);
560        selectValue.append('\'');
561        appendKeyValue(buf, PROP_SELECT, selectValue.toString());
562    }
563    
564    /**
565     * Check if we are ready to read times
566     *
567     * @return  true if times can be read
568     */
569    protected boolean canReadTimes() {
570        return haveDescriptorSelected();
571    }
572        
573    /**
574     * Enable or disable the GUI widgets based on what has been
575     * selected.
576     */
577    @Override protected void enableWidgets() {
578        boolean descriptorState = ((getState() == STATE_CONNECTED)
579                                   && canReadTimes());
580
581        for (int i = 0; i < compsThatNeedDescriptor.size(); i++) {
582            JComponent comp = (JComponent) compsThatNeedDescriptor.get(i);
583            GuiUtils.enableTree(comp, descriptorState);
584        }
585
586        boolean timesOk = timesOk();
587        
588        // Require times to be selected
589        GuiUtils.enableTree(loadButton, descriptorState && timesOk);
590
591        checkTimesLists();
592        
593        enableAbsoluteTimesList(getDoAbsoluteTimes() && descriptorState);
594
595        getRelativeTimesChooser().setEnabled( !getDoAbsoluteTimes()
596                && descriptorState);
597
598        if (drivercbx != null) {
599//            logger.trace("set drivercbx={}", anyTimeDrivers() && descriptorState);
600            drivercbx.setEnabled(anyTimeDrivers() && descriptorState);
601        }
602
603        revalidate();
604    }
605    
606    /**
607     * Do we have times selected. Either we are doing absolute
608     * times and there are some selected in the list. Or we
609     * are doing relative times and we have done a connect to the
610     * server
611     *
612     * @return Do we have times
613     */
614    public boolean timesOk() {
615        if (getDoAbsoluteTimes() && !haveTimeSelected()) {
616            return false;
617        }
618        return true;
619    }
620    
621    /**
622     * Return {@code true} if selected descriptor is for SYNOPTIC data.
623     *
624     * @return {@code true} iff {@link edu.wisc.ssec.mcidasv.chooser.adde.AddePointDataChooser#getDescriptor()}
625     * is {@literal "SYNOP"}.
626     */
627    protected boolean isSynoptic() {
628        return "SYNOP".equals(getDescriptor());
629    }
630    
631    /**
632     * Return {@code true} if selected descriptor is for upper air.
633     *
634     * @return {@code true} iff {@link edu.wisc.ssec.mcidasv.chooser.adde.AddePointDataChooser#getDescriptor()}
635     * is {@literal "UPPERMAND"}.
636     */
637    protected boolean isUpperAir() {
638        return "UPPERMAND".equals(getDescriptor());
639
640    }
641    
642    /**
643     * Return {@code true} if selected descriptor is for profiler.
644     *
645     * @return {@code true} iff {@link edu.wisc.ssec.mcidasv.chooser.adde.AddePointDataChooser#getDescriptor()}
646     * is {@literal "PROF"}.
647     */
648    protected boolean isProfiler() {
649        return "PROF".equals(getDescriptor());
650    }
651        
652    /**
653     * Update the widget with the latest data.
654     *
655     * @throws Exception On badness
656     */
657    @Override public void handleUpdate() throws Exception {
658        if (getState() != STATE_CONNECTED) {
659            //If not connected then update the server list
660            updateServerList();
661        } else {
662            //If we are already connected then update the rest of the chooser
663            descriptorChanged();
664        }
665        updateStatus();
666    }
667        
668    /**
669     * Get the request string for times particular to this chooser
670     *
671     * @return request string
672     */
673    protected String getTimesRequest() {
674        StringBuffer buf = getGroupUrl(REQ_POINTDATA, getGroup());
675        appendKeyValue(buf, PROP_USER, getLastAddedUser());
676        appendKeyValue(buf, PROP_PROJ, getLastAddedProj());
677        appendKeyValue(buf, PROP_DESCR, getDescriptor());
678        // TJJ - I don't know what is up with these hardcoded CONUS-like
679        // bounds, but finding it's best if I just leave them in
680        if (! isUpperAir() && ! tryWithoutSampling) {
681            appendKeyValue(buf, PROP_POS, "ALL");
682            appendKeyValue(buf, PROP_SELECT, "'DAY " + getJulianDay() +
683                    ";TIME " + archiveBegTime + " " + archiveEndTime +
684                    ";LAT 38 42;LON 70 75'");
685        }
686        else {
687            appendKeyValue(buf, PROP_SELECT, "'DAY " + getJulianDay() +
688                    ";TIME " + archiveBegTime + " " + archiveEndTime +
689                    ";LAT 38 42;LON 70 75'");
690            appendKeyValue(buf, PROP_POS, "ALL");
691        }
692        if (getDoAbsoluteTimes()) {
693            appendKeyValue(buf, PROP_NUM, "ALL");
694        }
695
696        appendKeyValue(buf, PROP_PARAM, "DAY TIME");
697        return buf.toString();
698    }
699    
700    /**
701     * Get the current, or archive, if selected, Julian day as a String
702     *
703     * @return the julian day as a string (yyyyDDD)
704     */
705
706    private String getJulianDay() {
707        return UtcDate.formatUtcDate(dt, "yyyyDDD");
708    }
709
710    /**
711     * This allows derived classes to provide their own name for labeling, etc.
712     *
713     * @return  the dataset name
714     */
715    @Override public String getDataName() {
716        return "Point Data";
717    }
718    
719    /**
720     * _more_
721     */
722    @Override public void doCancel() {
723        readTimesTask = null;
724        setState(STATE_UNCONNECTED);
725        super.doCancel();
726    }
727
728    /** locking mutex */
729    private final Object MUTEX = new Object();
730    
731    /**
732     *  Read the set of image times available for the current server/group/type
733     *  This method is a wrapper, setting the wait cursor and wrapping the
734     *  call to {@link #readTimesInner()}; in a try/catch block
735     */
736    @Override public void readTimes() {
737        clearTimesList();
738        if (!canReadTimes()) {
739            return;
740        }
741        Misc.run(new Runnable() {
742            @Override public void run() {
743                updateStatus();
744                showWaitCursor();
745                try {
746                    gotObs = false;
747                    tryWithoutSampling = false;
748                    readTimesInner();
749                    // Try again, this time not sampling by LAT/LON
750                    if (haveDescriptorSelected() && !gotObs) {
751                        tryWithoutSampling = true;
752                        readTimesInner();
753                    }
754                } catch (Exception e) {
755                    handleConnectionError(e);
756                }
757                showNormalCursor();
758                updateStatus();
759            }
760        });
761    }
762
763    /**
764     * Set the list of dates/times based on the image selection
765     */
766    protected void readTimesInner() {
767        SortedSet<DateTime> uniqueTimes = Collections.synchronizedSortedSet(new TreeSet<DateTime>());
768
769        readTimesTask = startTask();
770        updateStatus();
771        Object task = readTimesTask;
772        try {
773            AddePointDataReader apr = new AddePointDataReader(getTimesRequest());
774            //Make sure no other loads are  occurred
775            boolean ok = stopTaskAndIsOk(task);
776            if (!Misc.equals(readTimesTask, task) || !ok) {
777                return;
778            }
779            readTimesTask = null;
780
781            synchronized (MUTEX) {
782                int[][] data = apr.getData();
783                String[] units = apr.getUnits();
784                if ( !"CYD".equals(units[0]) || !"HMS".equals(units[1])) {
785                    throw new Exception("can't handle date/time units");
786                }
787                int numObs = data[0].length;
788                //System.out.println("found " + numObs + " obs");
789                // loop through and find all the unique times
790                try {
791                    for (int i = 0; i < numObs; i++) {
792                        DateTime dt =
793                            new DateTime(McIDASUtil.mcDayTimeToSecs(data[0][i],
794                                data[1][i]));
795                        uniqueTimes.add(dt);
796                    }
797                } catch (Exception e) {
798                    logger.error("problem building list of unique times", e);
799                }
800                //System.out.println(
801                //      "found " + uniqueTimes.size() + " unique times");
802                if (getDoAbsoluteTimes()) {
803                    if (!uniqueTimes.isEmpty()) {
804                        setAbsoluteTimes(new ArrayList<DateTime>(uniqueTimes));
805                        getTimesList().setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
806                    }
807
808                    //   Select the last n hours 
809                    int selectedIndex = getAbsoluteTimes().size() - 1;
810                    int firstIndex = Math.max(0, selectedIndex
811                            - getDefaultRelativeTimeIndex());
812                    if (selectedIndex >= 0)
813                        setSelectedAbsoluteTime(selectedIndex, firstIndex);
814                }
815                if (numObs>0) {
816                    gotObs = true;
817                }
818            }
819            setState(STATE_CONNECTED);
820        } catch (Exception excp) {
821            stopTask(task);
822            readTimesTask = null;
823            handleConnectionError(excp);
824            if (! retry) {
825                return;
826            }
827            try {
828                handleUpdate();
829            } catch (Exception e) {
830                logger.error("problem handling update", e);
831            }
832        }
833    }
834
835    /**
836     * Show the given error to the user. If it was an Adde exception
837     * that was a bad server error then print out a nice message.
838     *
839     * @param e The exception
840     */
841
842    @Override protected void handleConnectionError(Exception e) {
843        if (retry) {
844            // initialize Archive Date to PREVIOUS DAY (archive server)
845            LocalDate ld = LocalDate.now();
846            ld = ld.minusDays(1);
847            archiveDay = jdFormat.format(ld.toDate());
848            logger.info("setting archiveDay to: " + archiveDay);
849            getArchiveDay();
850            retry = false;
851            return;
852        }
853        super.handleConnectionError(e);
854    }
855
856    /**
857     * Are there any times selected.
858     *
859     * @return Any times selected.
860     */
861    @Override protected boolean haveTimeSelected() {
862        return !getDoAbsoluteTimes() || getHaveAbsoluteTimesSelected();
863    }
864
865    /**
866     * Create the date time selection string for the "select" clause
867     * of the ADDE URL.
868     *
869     * @return the select day and time strings
870     */
871    protected String getDayTimeSelectString() {
872        StringBuilder buf = new StringBuilder(1024);
873        if (getDoAbsoluteTimes()) {
874            List times = getSelectedAbsoluteTimes();
875
876            // no time selection is permitted as a valid choice -
877            // will then use all times today by default.
878            if (times.isEmpty()) {
879                return "";
880            }
881
882            //check for the "no times available" message
883            if (times.get(0) instanceof String) {
884                return "";
885            }
886
887            if (archiveDay != null) {
888                logger.trace("archiveDay: {}", archiveDay);
889                try {
890                    buf.append("DAY ").append(archiveDay).append(';');
891                } catch (Exception e) {
892                    logger.error("archiveDay parse error", e);
893                }
894            }
895            else {
896                // TJJ - I don't think this will happen any more, but if I'm wrong,
897                // set the value to current day and pass that in the SELECT clause
898                logger.trace("archiveDay is null!");
899                LocalDate ld = LocalDate.now();
900                try {
901                    dt = new DateTime(ld.toDate());
902                } catch (VisADException e) {
903                    e.printStackTrace();
904                }
905                archiveDay = jdFormat.format(ld.toDate());
906                logger.trace("Setting archiveDay to: " + archiveDay);
907                try {
908                    buf.append("DAY ").append(archiveDay).append(';');
909                } catch (Exception e) {
910                    logger.error("archiveDay parse error", e);
911                }
912            }
913
914            buf.append("TIME ");
915            for (int i = 0; i < times.size(); i++) {
916                DateTime dt = (DateTime) times.get(i);
917                buf.append(UtcDate.getHMS(dt));
918                if (i != times.size() - 1) {
919                    buf.append(',');
920                }
921            }
922        } else {
923            buf.append(AddeUtil.RELATIVE_TIME);
924        }
925        return buf.toString();
926    }
927
928    /**
929     * Get the data type for this chooser
930     *
931     * @return  the type
932     */
933    @Override public String getDataType() {
934        return "POINT";
935    }
936
937    /**
938     * Get the increment between times for relative time requests
939     *
940     * @return time increment (hours)
941     */
942    @Override public float getRelativeTimeIncrement() {
943        return relativeTimeIncrement;
944    }
945
946    /**
947     * Set the increment between times for relative time requests
948     *
949     * @param increment time increment (hours)
950     */
951    public void setRelativeTimeIncrement(float increment) {
952        relativeTimeIncrement = increment;
953        if (relTimeIncBox != null) {
954            relTimeIncBox.setSelectedItem(relativeTimeIncrement);
955        }
956    }
957
958    /**
959     * Update labels, enable widgets, etc.
960     */
961    @Override protected void updateStatus() {
962        super.updateStatus();
963        if (readTimesTask != null) {
964            if (taskOk(readTimesTask)) {
965                setStatus("Reading available times from server");
966            }
967        } else if (getDoAbsoluteTimes() && !haveTimeSelected()) {
968            setStatus(MSG_TIMES);
969        }
970        enableWidgets();
971    }
972
973                
974    /**
975     * Get the descriptor widget label.
976     *
977     * @return  label for the descriptor widget
978     */
979    @Override public String getDescriptorLabel() {
980        return "Point Type"; 
981    }
982    
983    /**
984     * get the ADDE server group type to use
985     *
986     * @return group type
987     */
988    @Override protected String getGroupType() {
989        return AddeServer.TYPE_POINT;
990    }
991    
992    /**
993     * Make the UI for this selector.
994     * 
995     * @return The gui
996     */   
997    @Override public JComponent doMakeContents() {
998        JPanel myPanel = new JPanel();
999        
1000        McVGuiUtils.setComponentWidth(descriptorComboBox, Width.DOUBLEDOUBLE);
1001                
1002        JLabel stationLabel = McVGuiUtils.makeLabelRight("Station:");
1003        addServerComp(stationLabel);
1004        
1005        JLabel timesLabel = McVGuiUtils.makeLabelRight("Times:");
1006        addDescComp(timesLabel);
1007        
1008        JPanel timesPanel = makeTimesPanel();
1009        timesPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
1010        addDescComp(timesPanel);
1011                
1012        GroupLayout layout = new GroupLayout(myPanel);
1013        myPanel.setLayout(layout);
1014        layout.setHorizontalGroup(
1015            layout.createParallelGroup(LEADING)
1016            .addGroup(layout.createSequentialGroup()
1017                .addGroup(layout.createParallelGroup(LEADING)
1018                    .addGroup(layout.createSequentialGroup()
1019                        .addComponent(descriptorLabel)
1020                        .addGap(GAP_RELATED)
1021                        .addComponent(descriptorComboBox))
1022                    .addGroup(layout.createSequentialGroup()
1023                        .addComponent(timesLabel)
1024                        .addGap(GAP_RELATED)
1025                        .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))))
1026        );
1027        layout.setVerticalGroup(
1028            layout.createParallelGroup(LEADING)
1029            .addGroup(layout.createSequentialGroup()
1030                .addGroup(layout.createParallelGroup(BASELINE)
1031                    .addComponent(descriptorLabel)
1032                    .addComponent(descriptorComboBox))
1033                .addPreferredGap(RELATED)
1034                .addGroup(layout.createParallelGroup(LEADING)
1035                    .addComponent(timesLabel)
1036                    .addComponent(timesPanel, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)))
1037        );
1038        
1039        setInnerPanel(myPanel);
1040        return super.doMakeContents();
1041    }
1042
1043    public JComponent doMakeContents(boolean doesOverride) {
1044        if (doesOverride) {
1045            return super.doMakeContents();
1046        } else {
1047            return doMakeContents();
1048        }
1049    }
1050}