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.control;
030
031import static java.lang.Math.asin;
032import static java.lang.Math.atan2;
033import static java.lang.Math.cos;
034import static java.lang.Math.sin;
035import static java.lang.Math.sqrt;
036import static ucar.unidata.util.LayoutUtil.centerBottom;
037import static ucar.unidata.util.LayoutUtil.inset;
038import static visad.RealTupleType.LatitudeLongitudeTuple;
039import edu.wisc.ssec.mcidasv.Constants;
040import edu.wisc.ssec.mcidasv.McIdasPreferenceManager;
041import edu.wisc.ssec.mcidasv.data.GroundStation;
042import edu.wisc.ssec.mcidasv.data.GroundStations;
043import edu.wisc.ssec.mcidasv.data.PolarOrbitTrackDataSource;
044import edu.wisc.ssec.mcidasv.data.TimeRangeSelection;
045import edu.wisc.ssec.mcidasv.data.hydra.CurveDrawer;
046import edu.wisc.ssec.mcidasv.ui.ColorSwatchComponent;
047import edu.wisc.ssec.mcidasv.util.XmlUtil;
048
049import java.awt.Color;
050import java.awt.Container;
051import java.awt.Dimension;
052import java.awt.FlowLayout;
053import java.awt.Font;
054import java.awt.Window;
055import java.awt.event.ActionEvent;
056import java.awt.event.ActionListener;
057import java.awt.event.ItemEvent;
058import java.rmi.RemoteException;
059import java.util.ArrayList;
060import java.util.HashMap;
061import java.util.Hashtable;
062import java.util.List;
063import java.util.Map;
064import java.util.Objects;
065import java.util.regex.Pattern;
066
067import javax.swing.BorderFactory;
068import javax.swing.Box;
069import javax.swing.BoxLayout;
070import javax.swing.DefaultComboBoxModel;
071import javax.swing.JButton;
072import javax.swing.JCheckBox;
073import javax.swing.JComboBox;
074import javax.swing.JComponent;
075import javax.swing.JDialog;
076import javax.swing.JLabel;
077import javax.swing.JOptionPane;
078import javax.swing.JPanel;
079import javax.swing.JSpinner;
080import javax.swing.JTabbedPane;
081import javax.swing.JTextField;
082import javax.swing.SpinnerNumberModel;
083import javax.swing.SwingUtilities;
084import javax.swing.ToolTipManager;
085
086import name.gano.astro.AstroConst;
087import net.miginfocom.swing.MigLayout;
088
089import org.slf4j.Logger;
090import org.slf4j.LoggerFactory;
091import org.w3c.dom.Element;
092import org.w3c.dom.NodeList;
093
094import ucar.unidata.data.DataChoice;
095import ucar.unidata.data.DataSourceImpl;
096import ucar.unidata.idv.ControlContext;
097import ucar.unidata.idv.control.DisplayControlBase;
098import ucar.unidata.idv.control.DisplayControlImpl;
099import ucar.unidata.ui.FontSelector;
100import ucar.unidata.util.GuiUtils;
101import ucar.unidata.util.IOUtil;
102import ucar.unidata.util.Msg;
103import ucar.unidata.view.geoloc.NavigatedDisplay;
104import ucar.visad.UtcDate;
105import ucar.visad.Util;
106import ucar.visad.display.CompositeDisplayable;
107import ucar.visad.display.TextDisplayable;
108import visad.Data;
109import visad.DisplayRealType;
110import visad.Gridded2DSet;
111import visad.MathType;
112import visad.RealTuple;
113import visad.RealTupleType;
114import visad.SampledSet;
115import visad.Text;
116import visad.TextControl;
117import visad.TextType;
118import visad.Tuple;
119import visad.TupleType;
120import visad.UnionSet;
121import visad.VisADException;
122import visad.georef.EarthLocation;
123import visad.georef.EarthLocationTuple;
124import visad.georef.LatLonTuple;
125
126/**
127 * {@link DisplayControlImpl} with some McIDAS-V specific extensions.
128 *
129 * <p>Namely parameter sets and support for inverted parameter defaults.</p>
130 */
131
132public class PolarOrbitTrackControl extends DisplayControlImpl {
133    
134    private static final Logger logger =
135        LoggerFactory.getLogger(PolarOrbitTrackControl.class);
136    
137    private static final String ERR_DIALOG_TITLE = "Time Range Selection Error";
138
139    private static final Pattern REGEX = Pattern.compile(" ");
140    
141    private JLabel satelliteName = new JLabel("");
142    private static final JLabel kmLabel = new JLabel("km");
143    private JTextField swathWidthFld = null;
144    private JPanel swathWidthPanel;
145    
146    private double latitude;
147    private double longitude;
148    private JPanel fontSizePanel;
149    private JPanel colorPanel;
150    private JPanel antColorPanel;
151    private JPanel locationPanel;
152    private JPanel latLonAltPanel;
153    
154    /** Property name to get the list or URLs */
155    public final String PREF_GROUNDSTATIONS = "mcv.groundstations";
156    
157    private JComboBox<GroundStation> locationComboBox;
158    private JComboBox<GroundStation> jcbStationsPlotted;
159    
160    private final List<GroundStation> stations = new ArrayList<>();
161    
162    private JComboBox<String> jcbTrackLineStyle =
163        new JComboBox<>(Constants.lineStyles);
164    
165    private JComboBox<String> jcbEdgeLineStyle =
166        new JComboBox<>(Constants.lineStyles);
167    
168    private JComboBox<String> jcbStationLineStyle =
169        new JComboBox<>(Constants.lineStyles);
170    
171    private JCheckBox jcbLabels;
172    private JCheckBox jcbSwathEdges;
173    private boolean swathEdgesOn = false;
174    private String [] lineWidths = {"1", "2", "3", "4"};
175    
176    private JComboBox<String> jcbStationLineWidth =
177        new JComboBox<>(lineWidths);
178    
179    private JComboBox<String> jcbSwathCenterLineWidth =
180        new JComboBox<>(lineWidths);
181    
182    // names to distinguish checkbox event sources
183    private static final String CHECKBOX_LABELS = "CHECKBOX_LABELS";
184    private static final String CHECKBOX_SWATH_EDGES = "CHECKBOX_SWATH_EDGES";
185    
186    private String station = "";
187    
188    private static final int SWATH_WIDTH_MIN = 0;
189    // swath width not applicable, e.g. GEO sensor
190    private static final String SWATH_NA = "N/A";
191    // TJJ Feb 2014 - need to determine max of any sensor. VIIRS is over 3000 km
192    private static final int SWATH_WIDTH_MAX = 4000;
193    
194    private static final int MAX_ANTENNA_ANGLE = 90;
195    private int curAngle = GroundStation.DEFAULT_ANTENNA_ANGLE;
196    private int curElevation = 0;
197    private static final double LABEL_DISTANCE_THRESHOLD = 2.5d;
198    
199    // Valid range for custom ground station elevation
200    private static final int MIN_ELEVATION = -500;
201    private static final int MAX_ELEVATION = 8850;
202
203    private DataChoice dataChoice;
204    
205    private JLabel latLabel;
206    private JLabel lonLabel;
207    private JLabel altLabel;
208    private JTextField antennaAngle = new JTextField("" +
209        GroundStation.DEFAULT_ANTENNA_ANGLE, GroundStation.DEFAULT_ANTENNA_ANGLE);
210    
211    // custom ground station UI components
212    JTextField customLat = null;
213    JTextField customLon = null;
214    JTextField customLab = null;
215    JTextField customAlt = null;
216    
217    /** the font selectors, Orbit Track (ot) and Ground Station (gs) */
218    private FontSelector otFontSelector;
219    private Font otCurFont = null;
220    private int otCurFontSize = -1;
221    private FontSelector gsFontSelector;
222    
223    // line width combo boxes, Station: Ground Station, SC: Swath Center, SE: Swath Edge
224    private JComboBox<String> jcbSwathEdgeLineWidth =
225        new JComboBox<>(lineWidths);
226    
227    private JSpinner js = null;
228    
229    private CompositeDisplayable trackDsp;
230    private CompositeDisplayable timeLabelDsp;
231    private CompositeDisplayable swathEdgeDsp;
232    
233    // time label variables
234    private static final int DEFAULT_LABEL_INTERVAL = 5;
235    private int curLabelInterval = DEFAULT_LABEL_INTERVAL;
236    private int prvLabelInterval = DEFAULT_LABEL_INTERVAL;
237    
238    private ColorSwatchComponent colorSwatch;
239    
240    private static final Color DEFAULT_COLOR = Color.GREEN;
241    private Color curSwathColor = null;
242    private Color prvSwathColor = DEFAULT_COLOR;
243    
244    private ColorSwatchComponent antColorSwatch;
245    private Color antColor;
246    private Color defaultAntColor = Color.MAGENTA;
247    private PolarOrbitTrackDataSource dataSource;
248    
249    private double trackZ = 0.0d;
250    private double gsZ = 0.0d;
251    private NavigatedDisplay navDsp = null;
252    private TextType otTextType = null;
253    private static long ttCounter = 0;
254    private double curWidth = 0.0d;
255    private double prvWidth = 0.0d;
256    
257    private int prvTrackLineStyle = 0;
258    private int prvEdgeLineStyle = 1;
259    private int curTrackLineStyle = 0;
260    private int curEdgeLineStyle = 1;
261    private static final float FONT_SCALE_FACTOR = 12.0f;
262    
263    // line width for drawing track center and swath edges
264    private int prvSwathCenterWidth = 2;
265    private int curSwathCenterWidth = 2;
266    private int prvSwathEdgeWidth = 1;
267    private int curSwathEdgeWidth = 1;
268    
269    /** Path to the McV swathwidths.xml */
270    private static final String SWATH_WIDTHS = "/edu/wisc/ssec/mcidasv/resources/swathwidths.xml";
271    private static final String TAG_SATELLITE = "satellite";
272    private static final String ATTR_NAME = "name";
273    private static final String ATTR_WIDTH = "width";
274    
275    private static final String SWATH_MODS = "OrbitTrack";
276    private static final String STATION_MODS = "GroundStation";
277    private static final String STATION_ADD = "AddStation";
278    private static final String STATION_REM = "RemStation";
279    private static final String CUSTOM_ADD = "AddCustom";
280    private static final String ACTIVE_STATION = "ActiveStation";
281    
282    // Constants for the various UI tool-tips
283    private static final String TOOLTIP_ADD_CUSTOM =
284       "Station will be plotted with Color, Font, and Line Width/Style options currently selected";
285    private static final String TOOLTIP_ADD_SELECTED =
286       "Station will be plotted with Color, Font, and Line Width/Style options currently selected";
287    private static final String TOOLTIP_ANTENNA_ANGLE =
288       "Antenna elevation angle, valid range 5 to 90 degrees";
289    private static final String TOOLTIP_CUSTOM_ALT =
290       "Antenna altitude valid range: " + MIN_ELEVATION + " m to: " + MAX_ELEVATION + " m";
291    private static final String TOOLTIP_CUSTOM_LABEL =
292       "Choose a label, e.g. \"Mesa, AZ\"";
293    private static final String TOOLTIP_CUSTOM_LAT =
294       "Latitude of your custom groundstation";
295    private static final String TOOLTIP_CUSTOM_LON =
296       "Longitude of your custom groundstation";
297    private static final String TOOLTIP_SWATH_WIDTH =
298       "Valid range: > " + SWATH_WIDTH_MIN + " km to: " + SWATH_WIDTH_MAX + " km";
299    private static final String TOOLTIP_LABEL_INTERVAL =
300       "Interval in minutes between orbit track time labels";
301
302    private final Map<GroundStation, TextDisplayable> stationToText =
303        new HashMap<>();
304    
305    private final Map<GroundStation, CurveDrawer> stationToCurve =
306        new HashMap<>();
307    
308    // Used in showProperties override to help decide if we need to redraw
309    Hashtable<String, Object> oldProps = null;
310    
311    private Element root = null;
312    
313    private boolean showingLabels;
314    
315    // initial scale for labeling 
316    float scale = 1.0f;
317    
318    public PolarOrbitTrackControl() {
319        logger.trace("created new PolarOrbitTrackControl...");
320        setHelpUrl("idv.control.orbittrackcontrol");
321        try {
322            final String xml =
323                IOUtil.readContents(SWATH_WIDTHS, McIdasPreferenceManager.class);
324            root = XmlUtil.getRoot(xml);
325            if (curSwathColor == null) curSwathColor = DEFAULT_COLOR;
326        } catch (Exception e) {
327            logger.error("problem reading swathwidths.xml", e);
328        }
329    }
330
331    /**
332     * Get the DisplayListTemplate property. This method is a fair bit different from its parent,
333     * in order to allow overrides to utilize the default display list template method.
334     * TJJ Jun 2023 - See https://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=2772
335     *
336     * @return The DisplayListTemplate
337     */
338    public String getDisplayListTemplate() {
339        if (displayListTemplate == null) {
340            String pref = PREF_DISPLAYLIST_TEMPLATE + '.' + displayId;
341            boolean haveData = (getShortParamName() != null);
342            pref = pref + (haveData ? ".data" : ".nodata");
343            displayListTemplate = getStore().get(pref, getDefaultDisplayListTemplate());
344        }
345        return displayListTemplate;
346    }
347
348    /**
349     * Override because the base class template results in a very long layer label.
350     * TJJ Jun 2023 - See https://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=2772
351     *
352     * @return The DefaultDisplayListTemplate
353     */
354    @Override protected String getDefaultDisplayListTemplate() {
355        return (getShortParamName() != null)  // haveData
356                ? MACRO_DISPLAYNAME + " - " + MACRO_TIMESTAMP : MACRO_DISPLAYNAME;
357    }
358
359    /**
360     * Deal with action events
361     *
362     * @param  ae the ActionEvent fired when the user applies changes
363     */
364    
365    public void actionPerformed(ActionEvent ae) {
366        
367        // user trying to add a custom ground station
368        if (CUSTOM_ADD.equals(ae.getActionCommand())) {
369
370            logger.debug("Custom Ground Station...");
371            String labStr = customLab.getText();
372            if ((labStr == null) || (labStr.isEmpty())) {
373                JOptionPane.showMessageDialog(null,
374                    "Please provide a label for the custom ground station.");
375                return;
376            }
377            float fLat;
378            float fLon;
379            try {
380                fLat = Float.parseFloat(customLat.getText());
381                fLon = Float.parseFloat(customLon.getText());
382            } catch (NumberFormatException nfe) {
383                JOptionPane.showMessageDialog(null,
384                    "Latitude and Longitude must be floating point numbers, please correct.");
385                return;
386            }
387            if ((fLat < -90) || (fLat > 90)) {
388                JOptionPane.showMessageDialog(null,
389                    "Latitude is out of valid range: " + fLat);
390                return;
391            }
392            if ((fLon < -180) || (fLon > 180)) {
393                JOptionPane.showMessageDialog(null,
394                    "Longitude is out of valid range: " + fLon);
395                return;
396            }
397
398            // Validate the elevation
399            String s = customAlt.getText();
400            try {
401                int newElevation = Integer.parseInt(s);
402                if (newElevation != curElevation) {
403                    // Always need to do range check too
404                    if ((newElevation < MIN_ELEVATION) ||
405                        (newElevation > MAX_ELEVATION)) {
406                        throw new NumberFormatException();
407                    }
408                    curElevation = newElevation;
409                }
410            } catch (NumberFormatException nfe) {
411                JOptionPane.showMessageDialog(null,
412                    TOOLTIP_CUSTOM_ALT);
413                return;
414            }
415
416            // Validate the antenna angle
417            s = antennaAngle.getText();
418            try {
419                int newAngle = Integer.parseInt(s);
420                if (newAngle != curAngle) {
421                    // Always need to do range check too
422                    if ((newAngle < GroundStation.DEFAULT_ANTENNA_ANGLE) ||
423                        (newAngle > MAX_ANTENNA_ANGLE)) {
424                        throw new NumberFormatException();
425                    }
426                    curAngle = newAngle;
427                }
428            } catch (NumberFormatException nfe) {
429                JOptionPane.showMessageDialog(null,
430                    "Invalid antenna angle: " + s);
431                return;
432            }
433
434            // last check, is this label already used?
435            int numPlotted = jcbStationsPlotted.getItemCount();
436            for (int i = 0; i < numPlotted; i++) {
437                GroundStation gs = jcbStationsPlotted.getItemAt(i);
438                if ((gs.getName() != null) && gs.getName().equals(station)) {
439                    JOptionPane.showMessageDialog(null,
440                        "A station with this label has already been plotted: " + station);
441                    return;
442                }
443            }
444            
445            // if we made it this far, fields are valid, we can create a custom ground station
446            // create new earth location, add it to stations plotted, set index, 
447
448            // update scale in case user changed zoom level
449            scale = getViewManager().getMaster().getDisplayScale();
450            
451            // make an Earth location
452            EarthLocationTuple elt = null;
453            try {
454                elt = new EarthLocationTuple(fLat, fLon, curElevation);
455            } catch (VisADException | RemoteException e) {
456                logger.error("Problem creating EarthLocationTuple", e);
457            }
458            
459            double satelliteAltitude = dataSource.getNearestAltToGroundStation(latitude, longitude) / 1000.0;
460            GroundStation gs = new GroundStation(labStr, elt, curAngle, satelliteAltitude);
461            addGroundStation(gs, true);
462            jcbStationsPlotted.addItem(gs);
463            jcbStationsPlotted.setSelectedItem(gs);
464            updateDisplayList();
465            return;
466        }
467        
468        // user trying to add a new ground station to those plotted on display
469        if (STATION_ADD.equals(ae.getActionCommand())) {
470            logger.debug("Add Station...");
471            GroundStation addedStation =
472                (GroundStation) locationComboBox.getSelectedItem();
473            
474            boolean alreadyPlotted = false;
475            int numPlotted = jcbStationsPlotted.getItemCount();
476            for (int i = 0; i < numPlotted; i++) {
477                GroundStation gs = jcbStationsPlotted.getItemAt(i);
478                if (Objects.equals(gs.getName(), addedStation.getName())) {
479                    alreadyPlotted = true;
480                    break;
481                }
482            }
483            if (alreadyPlotted) {
484                JOptionPane.showMessageDialog(null,
485                    "Station already plotted on display: " + addedStation);
486                return;
487            } else {
488                // Validate the antenna angle - only piece that can have errors for "stock" stations
489                String s = antennaAngle.getText();
490                try {
491                    int newAngle = Integer.parseInt(s);
492                    if (newAngle != curAngle) {
493                        // Always need to do range check too
494                        if ((newAngle < GroundStation.DEFAULT_ANTENNA_ANGLE) ||
495                            (newAngle > MAX_ANTENNA_ANGLE)) {
496                            throw new NumberFormatException();
497                        }
498                        curAngle = newAngle;
499                        addedStation.setAntennaAngle(curAngle);
500                    }
501                } catch (NumberFormatException nfe) {
502                    JOptionPane.showMessageDialog(null,
503                        "Invalid antenna angle: " + s);
504                    return;
505                }
506                addGroundStation(addedStation, false);
507                jcbStationsPlotted.addItem(addedStation);
508                jcbStationsPlotted.setSelectedItem(addedStation);
509                
510            }
511            updateDisplayList();
512            return;
513        }
514        
515        // Active station changed (selection from plotted stations)
516        if (ACTIVE_STATION.equals(ae.getActionCommand())) {
517            logger.debug("Active Station changed...");
518            GroundStation gs = (GroundStation) jcbStationsPlotted.getSelectedItem();
519            if (gs == null) {
520                JOptionPane.showMessageDialog(null,
521                    "No Active Stations");
522            } else {
523                // Update UI with settings for this station
524                updateGroundStationWidgets(gs);
525            }
526            return;
527        }
528        
529        // user removing a ground station from the display
530        if (STATION_REM.equals(ae.getActionCommand())) {
531            logger.debug("Rem Station...");
532            GroundStation gs = (GroundStation) jcbStationsPlotted.getSelectedItem();
533            if (gs == null) {
534                JOptionPane.showMessageDialog(null,
535                    "Nothing to remove");
536            } else {
537                try {
538                    removeDisplayable(stationToCurve.get(gs));
539                    removeDisplayable(stationToText.get(gs));
540                } catch (RemoteException | VisADException e) {
541                    logger.error("Problem removing displayables", e);
542                }
543                
544                jcbStationsPlotted.removeItem(gs);
545                // Did we remove the last active station?
546                if (jcbStationsPlotted.getItemCount() == 0) {
547                    latLabel.setText(" - ");
548                    lonLabel.setText(" - ");
549                    altLabel.setText(" - ");
550                }
551                
552            }
553            updateDisplayList();
554            return;
555        }
556        
557        // swath-related changes
558        if (SWATH_MODS.equals(ae.getActionCommand())) {
559            logger.debug("Apply Swath Mods...");
560            
561            boolean fontChanged = false;
562            boolean swathChanged = false;
563            scale = getViewManager().getMaster().getDisplayScale();
564            
565            curSwathCenterWidth = jcbSwathCenterLineWidth.getSelectedIndex() + 1;
566            if (curSwathCenterWidth != prvSwathCenterWidth) {
567                prvSwathCenterWidth = curSwathCenterWidth;
568                swathChanged = true;
569            }
570            
571            curSwathEdgeWidth = jcbSwathEdgeLineWidth.getSelectedIndex() + 1;
572            if (curSwathEdgeWidth != prvSwathEdgeWidth) {
573                prvSwathEdgeWidth = curSwathEdgeWidth;
574                swathChanged = true;
575            }
576            
577            curTrackLineStyle = jcbTrackLineStyle.getSelectedIndex();
578            if (curTrackLineStyle != prvTrackLineStyle) {
579                prvTrackLineStyle = curTrackLineStyle;
580                swathChanged = true;
581            }
582            
583            curEdgeLineStyle = jcbEdgeLineStyle.getSelectedIndex();
584            if (curEdgeLineStyle != prvEdgeLineStyle) {
585                prvEdgeLineStyle = curEdgeLineStyle;
586                swathChanged = true;
587            }
588            
589            curSwathColor = colorSwatch.getColor();
590            if (! curSwathColor.equals(prvSwathColor)) {
591                prvSwathColor = curSwathColor;
592                swathChanged = true;
593            }
594            
595            float newSwathWidth = validateSwathWidthField();
596            if (newSwathWidth > 0) {
597                curWidth = newSwathWidth;
598                if (Double.compare(curWidth, prvWidth) != 0) {
599                    prvWidth = curWidth;
600                    swathChanged = true;
601                }
602            } else {
603                // Don't apply anything if there are "errors on the form"
604                if (newSwathWidth == -1) return;
605            }
606            
607            // update font attributes if necessary
608            Font f = otFontSelector.getFont();
609            if (! f.equals(otCurFont)) {
610                otCurFont = f;
611                fontChanged = true;
612            }
613            if (f.getSize() != otCurFontSize) {
614                otCurFontSize = f.getSize();
615                fontChanged = true;
616            }
617            
618            // see if label interval has changed
619            SpinnerNumberModel snm = (SpinnerNumberModel) (js.getModel());
620            prvLabelInterval = ((Integer) snm.getValue()).intValue();
621            if ((prvLabelInterval != curLabelInterval) || fontChanged) {
622                curLabelInterval = prvLabelInterval;
623                swathChanged = true;
624            }
625            
626            // check swath width field, update if necessary
627            if (swathChanged || fontChanged) redrawAll();
628            updateDisplayList();
629            return;
630        }
631        
632        // Ground station mods
633        if (STATION_MODS.equals(ae.getActionCommand())) {
634            
635            GroundStation gs = (GroundStation) jcbStationsPlotted.getSelectedItem();
636            if (gs == null) {
637                // No stations plotted, nothing to do
638                JOptionPane.showMessageDialog(null,
639                    "No stations plotted, nothing to apply.");
640                return;
641            }
642            
643            logger.debug("Apply Station mods for: {}, cur font name: {}, cur font size: {}, cur color: {}",
644                gs.getName(),
645                gs.getFont().getFontName(),
646                gs.getFont().getSize(),
647                gs.getColor());
648            
649            // flag indicates user changed some parameter
650            boolean somethingChanged = false;
651            
652            // Check each parameter of the ground station selected with UI settings
653            
654            // Color
655            if (stationToText.get(gs).getColor() != antColorSwatch.getColor()) {
656                logger.debug("GroundStation color change...");
657                try {
658                    updateStationColor(gs, antColorSwatch.getColor());
659                } catch (RemoteException | VisADException e) {
660                    logger.error("Problem changing ground station color", e);
661                }
662                somethingChanged = true;
663            }
664            
665            // Font
666            if (stationToText.get(gs).getFont() != gsFontSelector.getFont()) {
667                logger.debug("GroundStation font change...");
668                try {
669                    updateStationFont(gs, gsFontSelector.getFont());
670                } catch (RemoteException | VisADException e) {
671                    logger.error("Problem changing ground station font", e);
672                }
673                somethingChanged = true;
674            }
675            
676            // Antenna angle
677            // If this changes, need to create a new CurveDrawer object
678            
679            String s = antennaAngle.getText();
680            curAngle = gs.getAntennaAngle();
681            try {
682                int newAngle = Integer.parseInt(s);
683                if (newAngle != curAngle) {
684                    // TJJ Jun 2015 range check
685                    if ((newAngle < GroundStation.DEFAULT_ANTENNA_ANGLE) ||
686                        (newAngle > MAX_ANTENNA_ANGLE)) {
687                        throw new NumberFormatException();
688                    } else {
689                        logger.debug("GroundStation antenna angle change...");
690                        try {
691                            removeDisplayable(stationToCurve.get(gs));
692                            removeDisplayable(stationToText.get(gs));
693                        } catch (RemoteException | VisADException e) {
694                            logger.error("Problem removing displayable", e);
695                        }
696                        gs.setAntennaAngle(newAngle);
697                        gs.setColor(antColorSwatch.getColor());
698                        CurveDrawer cdNew = makeCoverageCircle(gs);
699                        addDisplayable(cdNew);
700                        addDisplayable(stationToText.get(gs));
701                        stationToCurve.put(gs, cdNew);
702                        curAngle = newAngle;
703                        somethingChanged = true;
704                    }
705                }
706            } catch (NumberFormatException nfe) {
707                JOptionPane.showMessageDialog(latLonAltPanel,
708                    "Antenna angle valid range is " + GroundStation.DEFAULT_ANTENNA_ANGLE +
709                        " to " + MAX_ANTENNA_ANGLE + " degrees");
710                return;
711            }
712            
713            // Line style and width
714            
715            CurveDrawer cd = stationToCurve.get(gs);
716            int cdWidth = (int) cd.getLineWidth();
717            int curStyle = cd.getLineStyle();
718            
719            if (cdWidth != (jcbStationLineWidth.getSelectedIndex() + 1)) {
720                try {
721                    logger.debug("GroundStation line width change...");
722                    gs.setColor(antColorSwatch.getColor());
723                    replaceCurve(gs);
724                } catch (RemoteException | VisADException e) {
725                    logger.error("Problem changing ground station line width", e);
726                }
727                somethingChanged = true;
728            }
729            
730            if (curStyle != (jcbStationLineStyle.getSelectedIndex())) {
731                try {
732                    logger.debug("GroundStation line style change...");
733                    gs.setColor(antColorSwatch.getColor());
734                    replaceCurve(gs);
735                } catch (RemoteException | VisADException e) {
736                    logger.error("Problem changing ground station line style", e);
737                }
738                somethingChanged = true;
739            }
740            
741            if (somethingChanged) {
742                updateDisplayList();
743            }
744            return;
745        }
746    }
747    
748    /**
749     * Apply the map (height) position to the displays
750     */
751    
752    private void applyDisplayableLevels() {
753        try {
754            DisplayRealType dispType = navDsp.getDisplayAltitudeType();
755            trackDsp.setConstantPosition(trackZ, dispType);
756            timeLabelDsp.setConstantPosition(trackZ, dispType);
757            swathEdgeDsp.setConstantPosition(trackZ, dispType);
758            applyProperties();
759        } catch (Exception e) {
760            logger.error("Problem applying displayable levels", e);
761        }
762    }
763    
764    // No explicit dimension changes, but for times we need to redraw
765    // everything due to combinations of zooming and visibility toggling
766    private void redrawAll() {
767        logger.debug("redrawAll() in...");
768        try {
769            removeDisplayable(swathEdgeDsp);
770            removeDisplayable(trackDsp);
771            removeDisplayable(timeLabelDsp);
772            swathEdgeDsp = null;
773            trackDsp = null;
774            timeLabelDsp = null;
775            Data data = getData(getDataInstance());
776            swathEdgeDsp = new CompositeDisplayable();
777            trackDsp = new CompositeDisplayable();
778            timeLabelDsp = new CompositeDisplayable();
779            // turn visibility off for those elements which have checkboxes for this
780            if (! jcbSwathEdges.isSelected()) swathEdgeDsp.setVisible(false);
781            if (! jcbLabels.isSelected()) timeLabelDsp.setVisible(false);
782            createTrackDisplay(data, true);
783            applyDisplayableLevels();
784        } catch (Exception e) {
785            logger.error("Problem redrawing", e);
786        }
787    }
788    
789    private void createTrackDisplay(Data data, boolean doTrack) {
790        
791        logger.debug("createTrackDisplay() in...");
792        // Always check for View scale change (user zoomed in or out)
793        scale = getViewManager().getMaster().getDisplayScale();
794        try {
795            List<String> dts = new ArrayList<>();
796            if (data instanceof Tuple) {
797                Data[] dataArr = ((Tuple) data).getComponents();
798                
799                int npts = dataArr.length;
800                float[][] latlon = new float[2][npts];
801                double distance = 0.0d;
802                LatLonTuple prvPoint = null;
803                
804                for (int i = 0; i < npts; i++) {
805                    Tuple t = (Tuple) dataArr[i];
806                    Data[] tupleComps = t.getComponents();
807                    
808                    LatLonTuple llt = (LatLonTuple) tupleComps[1];
809                    double dlat = llt.getLatitude().getValue();
810                    double dlon = llt.getLongitude().getValue();
811                    
812                    if (doTrack) {
813                        if ((i % curLabelInterval) == 0) {
814                            
815                            if (prvPoint != null) {
816                                distance = Util.distance(prvPoint, llt);
817                                if (distance < LABEL_DISTANCE_THRESHOLD) {
818                                    latlon[0][i] = (float) dlat;
819                                    latlon[1][i] = (float) dlon;
820                                    continue;
821                                }
822                            }
823                            
824                            String str = ((Text) tupleComps[0]).getValue();
825                            dts.add(str);
826                            int indx = str.indexOf(' ') + 1;
827                            String subStr = "- " + str.substring(indx, indx + 5);
828                            TextDisplayable time = new TextDisplayable(SWATH_MODS, otTextType);
829                            time.setJustification(TextControl.Justification.LEFT);
830                            time.setVerticalJustification(TextControl.Justification.CENTER);
831                            time.setColor(curSwathColor);
832                            time.setTextSize((float) scale * otFontSelector.getFontSize() / FONT_SCALE_FACTOR);
833                            time.setFont(otFontSelector.getFont());
834                            time.setSphere(inGlobeDisplay());
835                            time.setUseFastRendering(false);
836                            
837                            RealTuple lonLat =
838                                new RealTuple(RealTupleType.SpatialEarth2DTuple,
839                                    new double[] { dlon, dlat });
840                            Tuple tup = new Tuple(makeTupleType(otTextType),
841                                new Data[] { lonLat, new Text(otTextType, subStr)});
842                            time.setData(tup);
843                            if (jcbLabels.isSelected()) timeLabelDsp.addDisplayable(time);
844                            
845                            prvPoint = llt;
846                        }
847                    }
848                    latlon[0][i] = (float) dlat;
849                    latlon[1][i] = (float) dlon;
850                }
851                
852                if (doTrack) {
853                    drawSwathLine(latlon,
854                                  npts,
855                                  jcbTrackLineStyle.getSelectedIndex(),
856                                  trackDsp,
857                                  curSwathColor,
858                                  curSwathCenterWidth);
859                    
860                    addDisplayable(trackDsp);
861                    addDisplayable(timeLabelDsp);
862                }
863                
864                // We initialize swath edge objects whenever possible, we just show or
865                // hide them based on checkbox state
866                if (curWidth > 0) {
867                    float[][][] crv = getSwath(latlon);
868                    int npt = crv[0][0].length;
869                    float[][] leftC = new float[2][npt];
870                    float[][] rightC = new float[2][npt];
871                    for (int i = 0; i < npt; i++) {
872                        leftC[0][i] = crv[0][0][i];
873                        leftC[1][i] = crv[0][1][i];
874                        rightC[0][i] = crv[1][0][i];
875                        rightC[1][i] = crv[1][1][i];
876                    }
877                    
878                    drawSwathLine(leftC,
879                                  npt,
880                                  jcbEdgeLineStyle.getSelectedIndex(),
881                                  swathEdgeDsp,
882                                  curSwathColor,
883                                  curSwathEdgeWidth);
884                    
885                    drawSwathLine(rightC,
886                                  npt,
887                                  jcbEdgeLineStyle.getSelectedIndex(),
888                                  swathEdgeDsp,
889                                  curSwathColor,
890                                  curSwathEdgeWidth);
891                    
892                    if (! jcbSwathEdges.isSelected()) {
893                        swathEdgeDsp.setVisible(false);
894                    }
895                    addDisplayable(swathEdgeDsp);
896                }
897            }
898        } catch (Exception e) {
899            logger.error("Problem creating track display", e);
900        }
901    }
902    
903    private static void drawSwathLine(float[][] points,
904                                      int pointCount,
905                                      int lineStyle,
906                                      CompositeDisplayable displayable,
907                                      Color color,
908                                      int width)
909        throws VisADException, RemoteException
910    {
911        Gridded2DSet g2dset = new Gridded2DSet(LatitudeLongitudeTuple,
912                                               points,
913                                               pointCount);
914        SampledSet[] sampledSets = new SampledSet[] { g2dset };
915        UnionSet unionSet = new UnionSet(sampledSets);
916        CurveDrawer lines = new CurveDrawer(unionSet);
917        lines.setLineStyle(lineStyle);
918        lines.setData(unionSet);
919        lines.setDrawingEnabled(false);
920        displayable.addDisplayable(lines);
921        displayable.setColor(color);
922        displayable.setLineWidth(width);
923    }
924    
925    /* (non-Javadoc)
926     * @see ucar.unidata.idv.control.DisplayControlImpl#displayableToFront()
927     */
928    @Override
929    public void displayableToFront() {
930        redrawAll();
931    }
932    
933    /**
934     * Called by doMakeWindow in DisplayControlImpl, which then calls its
935     * doMakeMainButtonPanel(), which makes more buttons.
936     *
937     * @return container of contents
938     */
939    
940    public Container doMakeContents() {
941        
942        fontSizePanel = new JPanel();
943        fontSizePanel.setLayout(new BoxLayout(fontSizePanel, BoxLayout.Y_AXIS));
944        JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
945        labelPanel.add(jcbLabels);
946        
947        // same row, add label interval spinner
948        Integer defaultInterval = 5;
949        Integer minInterval = 1;
950        Integer maxInterval = 120;
951        Integer intervalStep = 1;
952        SpinnerNumberModel snm =
953            new SpinnerNumberModel(defaultInterval, minInterval, maxInterval, intervalStep);
954        JLabel intervalLabel = new JLabel("Label Interval:");
955        JLabel intervalUnits = new JLabel("minutes");
956        js = new JSpinner(snm);
957        js.setToolTipText(TOOLTIP_LABEL_INTERVAL);
958        labelPanel.add(Box.createHorizontalStrut(5));
959        labelPanel.add(intervalLabel);
960        labelPanel.add(js);
961        labelPanel.add(intervalUnits);
962        
963        // line style for drawing swath track and width edges
964        jcbTrackLineStyle.addActionListener(this);
965        // will init to default of solid
966        jcbTrackLineStyle.setSelectedIndex(curTrackLineStyle);
967        
968        // Swath center line width
969        jcbSwathCenterLineWidth.addActionListener(this);
970        jcbSwathCenterLineWidth.setSelectedIndex(curSwathCenterWidth - 1);
971        
972        // Swath edge line width
973        jcbSwathEdgeLineWidth.addActionListener(this);
974        jcbSwathEdgeLineWidth.setSelectedIndex(curSwathEdgeWidth - 1);
975        
976        jcbEdgeLineStyle.addActionListener(this);
977        // will init to default of dashed
978        jcbEdgeLineStyle.setSelectedIndex(curEdgeLineStyle);
979        
980        fontSizePanel.add(labelPanel);
981        JPanel botPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
982        botPanel.add(new JLabel("Font: "));
983        botPanel.add(otFontSelector.getComponent());
984        fontSizePanel.add(botPanel);
985        
986        colorSwatch = new ColorSwatchComponent(getStore(), curSwathColor, "Color");
987        colorSwatch.setPreferredSize(Constants.DEFAULT_COLOR_PICKER_SIZE);
988        
989        colorPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
990        colorPanel.add(new JLabel("Color: "));
991        colorPanel.add(colorSwatch);
992        
993        colorPanel.add(Box.createHorizontalStrut(5));
994        colorPanel.add(new JLabel("Track Width: "));
995        colorPanel.add(jcbSwathCenterLineWidth);
996        
997        colorPanel.add(Box.createHorizontalStrut(4));
998        colorPanel.add(new JLabel("Track Style: "));
999        colorPanel.add(jcbTrackLineStyle);
1000        
1001        colorPanel.add(Box.createHorizontalStrut(5));
1002        colorPanel.add(new JLabel("Edge Width: "));
1003        colorPanel.add(jcbSwathEdgeLineWidth);
1004        
1005        colorPanel.add(Box.createHorizontalStrut(4));
1006        colorPanel.add(new JLabel("Edge Style: "));
1007        colorPanel.add(jcbEdgeLineStyle);
1008        
1009        JPanel groundStationPanel = makeGroundStationPanel();
1010        
1011        swathWidthPanel = makeSwathWidthPanel();
1012        
1013        JPanel outerPanel = new JPanel(new MigLayout());
1014        
1015        JPanel mainPanel = new JPanel(new MigLayout());
1016        mainPanel.setBorder(BorderFactory.createTitledBorder(" Swath Controls "));
1017        mainPanel.add(swathWidthPanel, "wrap");
1018        mainPanel.add(fontSizePanel, "wrap");
1019        mainPanel.add(colorPanel, "wrap");
1020        mainPanel.add(makeBottomRow(SWATH_MODS));
1021        outerPanel.add(mainPanel, "wrap");
1022        outerPanel.add(groundStationPanel, "wrap");
1023        return outerPanel;
1024    }
1025    
1026    private CurveDrawer makeCoverageCircle(GroundStation gs) {
1027        double lat = Math.toRadians(gs.getElt().getLatitude().getValue());
1028        double lon = Math.toRadians(gs.getElt().getLongitude().getValue());
1029        double satelliteAltitude = gs.getAltitude();
1030        double elevation = gs.getElt().getAltitude().getValue();
1031        
1032        /* mean Earth radius in km */
1033        double earthRadius = AstroConst.R_Earth_mean / 1000.0;
1034        // total radius to satellite
1035        satelliteAltitude += earthRadius;
1036        double SAC = (Math.PI / 2.0) + Math.toRadians(gs.getAntennaAngle());
1037        // now accounts for station elevation - don't forget to convert from meters to km
1038        double sinASC = (earthRadius * sin(SAC)) / (satelliteAltitude - (elevation / 1000.0d));
1039        double dist = earthRadius * (Math.PI - SAC - asin(sinASC));
1040        double rat = dist / earthRadius;
1041        
1042        // 360 degrees +1 points so we connect final segment, last point to first
1043        int npts = 361;
1044        float[][] latlon = new float[2][npts];
1045        double cosDist = cos(rat);
1046        double sinDist = sin(rat);
1047        double sinLat = sin(lat);
1048        double cosLat = cos(lat);
1049        double sinLon = -sin(lon);
1050        double cosLon = cos(lon);
1051        for (int i = 0; i < npts; i++) {
1052            double azimuth = Math.toRadians((double) i);
1053            double cosBear = cos(azimuth);
1054            double sinBear = sin(azimuth);
1055            
1056            double z = (cosDist * sinLat) + (sinDist * cosLat * cosBear);
1057            
1058            double y = (cosLat * cosLon * cosDist) +
1059                (sinDist * ((sinLon * sinBear) - (sinLat * cosLon * cosBear)));
1060            
1061            double x = (cosLat * sinLon * cosDist) -
1062                (sinDist * ((cosLon * sinBear) + (sinLat * sinLon * cosBear)));
1063            
1064            double r = sqrt((x * x) + (y * y));
1065            
1066            double latRad = atan2(z, r);
1067            double lonRad = 0.0;
1068            if (r > 0.0) lonRad = -atan2(x, y);
1069            latlon[0][i] = (float) Math.toDegrees(latRad);
1070            latlon[1][i] = (float) Math.toDegrees(lonRad);
1071        }
1072        
1073        CurveDrawer coverageCircle = null;
1074        try {
1075            Gridded2DSet circle = new Gridded2DSet(LatitudeLongitudeTuple,
1076                latlon, npts);
1077            SampledSet[] set = new SampledSet[1];
1078            set[0] = circle;
1079            UnionSet uset = new UnionSet(set);
1080            coverageCircle = new CurveDrawer(uset);
1081            coverageCircle.setLineWidth(gs.getLineWidth());
1082            coverageCircle.setLineStyle(gs.getLineStyle());
1083            coverageCircle.setColor(gs.getColor());
1084            coverageCircle.setData(uset);
1085            coverageCircle.setDrawingEnabled(false);
1086            if (! inGlobeDisplay()) {
1087                coverageCircle.setConstantPosition(gsZ, navDsp.getDisplayAltitudeType());
1088            }
1089        } catch (Exception e) {
1090            logger.error("Problem creating coverage circle", e);
1091        }
1092        stationToCurve.put(gs, coverageCircle);
1093        return coverageCircle;
1094    }
1095    
1096    public Color getAntColor() {
1097        if (antColor == null) antColor = defaultAntColor;
1098        return antColor;
1099    }
1100    
1101    /**
1102     * @return the curEdgeLineStyle
1103     */
1104    public int getCurEdgeLineStyle() {
1105        return curEdgeLineStyle;
1106    }
1107    
1108    /**
1109     * @param curEdgeLineStyle the curEdgeLineStyle to set
1110     */
1111    public void setCurEdgeLineStyle(int curEdgeLineStyle) {
1112        this.curEdgeLineStyle = curEdgeLineStyle;
1113    }
1114    
1115    /**
1116     * @return the curLabelInterval
1117     */
1118    public int getCurLabelInterval() {
1119        return curLabelInterval;
1120    }
1121    
1122    /**
1123     * @param curLabelInterval the curLabelInterval to set
1124     */
1125    public void setCurLabelInterval(int curLabelInterval) {
1126        this.curLabelInterval = curLabelInterval;
1127    }
1128    
1129    /**
1130     * @return the curSwathCenterWidth
1131     */
1132    public int getCurSwathCenterWidth() {
1133        return curSwathCenterWidth;
1134    }
1135    
1136    /**
1137     * @param curSwathCenterWidth the curSwathCenterWidth to set
1138     */
1139    public void setCurSwathCenterWidth(int curSwathCenterWidth) {
1140        this.curSwathCenterWidth = curSwathCenterWidth;
1141    }
1142    
1143    /**
1144     * @return the curSwathEdgeWidth
1145     */
1146    public int getCurSwathEdgeWidth() {
1147        return curSwathEdgeWidth;
1148    }
1149    
1150    /**
1151     * @param curSwathEdgeWidth the curSwathEdgeWidth to set
1152     */
1153    public void setCurSwathEdgeWidth(int curSwathEdgeWidth) {
1154        this.curSwathEdgeWidth = curSwathEdgeWidth;
1155    }
1156    
1157    /**
1158     * @return the curSwathColor
1159     */
1160    public Color getCurSwathColor() {
1161        return curSwathColor;
1162    }
1163    
1164    /**
1165     * @param curSwathColor the curSwathColor to set
1166     */
1167    public void setCurSwathColor(Color curSwathColor) {
1168        this.curSwathColor = curSwathColor;
1169    }
1170    
1171    /**
1172     * @return the curTrackLineStyle
1173     */
1174    public int getCurTrackLineStyle() {
1175        return curTrackLineStyle;
1176    }
1177    
1178    /**
1179     * @param curTrackLineStyle the curTrackLineStyle to set
1180     */
1181    public void setCurTrackLineStyle(int curTrackLineStyle) {
1182        this.curTrackLineStyle = curTrackLineStyle;
1183    }
1184    
1185    /**
1186     * @return the curWidth
1187     */
1188    public double getCurWidth() {
1189        return curWidth;
1190    }
1191    
1192    /**
1193     * @param curWidth the curWidth to set
1194     */
1195    public void setCurWidth(double curWidth) {
1196        this.curWidth = curWidth;
1197    }
1198    
1199    public PolarOrbitTrackDataSource getDataSource() {
1200        DataSourceImpl ds = null;
1201        List dataSources = getDataSources();
1202        boolean gotit = false;
1203        if (! dataSources.isEmpty()) {
1204            int nsrc = dataSources.size();
1205            for (int i = 0; i < nsrc; i++) {
1206                ds = (DataSourceImpl) dataSources.get(nsrc - i - 1);
1207                if (ds instanceof PolarOrbitTrackDataSource) {
1208                    gotit = true;
1209                    break;
1210                }
1211            }
1212        }
1213        if (! gotit) return null;
1214        return (PolarOrbitTrackDataSource) ds;
1215    }
1216    
1217    /* (non-Javadoc)
1218     * @see ucar.unidata.idv.control.DisplayControlImpl#getDisplayListData()
1219     */
1220    @Override
1221    protected Data getDisplayListData() {
1222        
1223        // get time range that was specified in the Field Selector
1224        String startTime = (String) getDataInstance().getDataSelection().getProperties().get(TimeRangeSelection.PROP_BEGTIME);
1225        String endTime = (String) getDataInstance().getDataSelection().getProperties().get(TimeRangeSelection.PROP_ENDTIME);
1226        
1227        // get the template used for the Display Properties Layer Label
1228        String labelTemplate = getDisplayListTemplate();
1229        
1230        // see if time macro is enabled
1231        boolean hasTimeMacro = UtcDate.containsTimeMacro(labelTemplate);
1232        
1233        // fetch the label superclass would normally generate
1234        Data data = super.getDisplayListData();
1235
1236        // if so, modify label with time range for this selection
1237        if (hasTimeMacro) {
1238            try {
1239                TextType tt = TextType.getTextType(DISPLAY_LIST_NAME);
1240                data  = new Text(tt, data.toString() + startTime + " - " + endTime);
1241            } catch (VisADException vade) {
1242                logger.error("Problem creating text", vade);
1243            }
1244        }
1245        
1246        // return either original or modified data object
1247        return data;
1248    }
1249    
1250    public double getLatitude() {
1251        return latitude;
1252    }
1253    
1254    public double getLongitude() {
1255        return longitude;
1256    }
1257    
1258    /**
1259     * @return the otCurFont
1260     */
1261    public Font getOtCurFont() {
1262        return otCurFont;
1263    }
1264    
1265    /**
1266     * @param otCurFont the otCurFont to set
1267     */
1268    public void setOtCurFont(Font otCurFont) {
1269        this.otCurFont = otCurFont;
1270    }
1271    
1272    public String getStation() {
1273        return station;
1274    }
1275    
1276    /**
1277     * @return the swathEdgesOn
1278     */
1279    public boolean isSwathEdgesOn() {
1280        return swathEdgesOn;
1281    }
1282    
1283    /**
1284     * @param swathEdgesOn the swathEdgesOn to set
1285     */
1286    public void setSwathEdgesOn(boolean swathEdgesOn) {
1287        this.swathEdgesOn = swathEdgesOn;
1288    }
1289    
1290    private float[][][] getSwath(float[][] track) {
1291        double earthRadius = AstroConst.R_Earth_mean / 1000.0;
1292        int npt = track[0].length - 1;
1293        float[][][] ret = new float[2][2][npt - 1];
1294        try {
1295            int indx = 0;
1296            for (int i = 1; i < npt; i++) {
1297                double latA = Math.toRadians(track[0][i - 1]);
1298                double lonA = Math.toRadians(track[1][i - 1]);
1299                
1300                double latB = Math.toRadians(track[0][i]);
1301                double lonB = Math.toRadians(track[1][i]);
1302                
1303                double diffLon = lonB - lonA;
1304                double bX = cos(latB) * cos(diffLon);
1305                double bY = cos(latB) * sin(diffLon);
1306                double xFac = cos(latA) + bX;
1307                double latC = atan2(sin(latA) + sin(latB), sqrt(xFac * xFac + bY * bY));
1308                double lonC = lonA + atan2(bY, xFac);
1309                
1310                double bearing = atan2(sin(diffLon) * cos(latB),
1311                    cos(latA) * sin(latB) - sin(latA) * cos(latB) * cos(diffLon))
1312                    + (Math.PI / 2.0);
1313                double dist = curWidth / 2.0;
1314                dist /= earthRadius;
1315                double lat = asin((sin(latC) * cos(dist)) +
1316                    (cos(latC) * sin(dist) * cos(bearing)));
1317                double lon = lonC + atan2(sin(bearing) * sin(dist) * cos(latC),
1318                    cos(dist) - (sin(latC) * sin(lat)));
1319                float latD = (float) Math.toDegrees(lat);
1320                float lonD = (float) Math.toDegrees(lon);
1321                
1322                bearing += Math.PI;
1323                lat = asin((sin(latC) * cos(dist)) +
1324                    (cos(latC) * sin(dist) * cos(bearing)));
1325                lon = lonC + atan2(sin(bearing) * sin(dist) * cos(latC),
1326                    cos(dist) - (sin(latC) * sin(lat)));
1327                float latE = (float) Math.toDegrees(lat);
1328                float lonE = (float) Math.toDegrees(lon);
1329                
1330                ret[0][0][indx] = latD;
1331                ret[0][1][indx] = lonD;
1332                
1333                ret[1][0][indx] = latE;
1334                ret[1][1][indx] = lonE;
1335                ++indx;
1336            }
1337        } catch (Exception e) {
1338            logger.error("Problem getting swath", e);
1339            return null;
1340        }
1341        return ret;
1342    }
1343    
1344    /**
1345     * Overridden by McIDAS-V so that we can force the {@code display name} to
1346     * {@literal "Satellite Orbit Track"} when loading from a bundle.
1347     *
1348     * <p>This is done because {@link #init(DataChoice)} will call 
1349     * {@link #setDisplayName(String)} essentially like this: 
1350     * {@code setDisplayName(getLongParamName() + " " + getDisplayName()}. 
1351     * This results in the display name for a bundled orbit track control
1352     * being something like 
1353     * {@literal "SUOMI NPP SUOMI NPP Satellite Orbit Track"}.</p>
1354     *
1355     * @param vc Context in which this control exists.
1356     * @param properties Properties that may hold things.
1357     * @param preSelectedDataChoices Set of preselected data choices.
1358     */
1359
1360    @Override public void initAfterUnPersistence(ControlContext vc,
1361                                                 Hashtable properties,
1362                                                 List preSelectedDataChoices)
1363    {
1364        setDisplayName("Satellite Orbit Track");
1365        super.initAfterUnPersistence(vc, properties, preSelectedDataChoices);
1366        jcbTrackLineStyle.setSelectedIndex(curTrackLineStyle);
1367        jcbEdgeLineStyle.setSelectedIndex(curEdgeLineStyle);
1368        jcbSwathCenterLineWidth.setSelectedIndex(curSwathCenterWidth - 1);
1369        jcbSwathEdgeLineWidth.setSelectedIndex(curSwathEdgeWidth - 1);
1370        
1371        // no idea if these invokeLater calls should be grouped into a single
1372        // call or not :(
1373        
1374        SwingUtilities.invokeLater(() -> {
1375            DefaultComboBoxModel<GroundStation> cbm =
1376                new DefaultComboBoxModel<>();
1377            
1378            for (GroundStation s : stations) {
1379                logger.trace("adding ground station {}", s);
1380                cbm.addElement(s);
1381                CurveDrawer cd = makeCoverageCircle(s);
1382                TextDisplayable td = labelGroundStation(s);
1383                stationToCurve.put(s, cd);
1384                stationToText.put(s, td);
1385                addDisplayable(cd);
1386                addDisplayable(td);
1387                // We don't know how many to expect, so update "current station" labels
1388                // with last one processed when restoring bundles.
1389                latLabel.setText(s.getElt().getLatitude().toString());
1390                lonLabel.setText(s.getElt().getLongitude().toString());
1391                altLabel.setText(s.getElt().getAltitude().toString());
1392            }
1393            jcbStationsPlotted.setModel(cbm);
1394        });
1395        
1396        SwingUtilities.invokeLater(() -> {
1397            js.getModel().setValue(curLabelInterval);
1398            jcbLabels.getModel().setSelected(showingLabels);
1399        });
1400    }
1401
1402    @Override public boolean init(DataChoice dataChoice)
1403        throws VisADException, RemoteException
1404    {
1405        logger.debug("init() in...");
1406        
1407        PolarOrbitTrackDataSource potdc = getDataSource();
1408        
1409        // Show tool tips immediately
1410        ToolTipManager.sharedInstance().setInitialDelay(0);
1411
1412        // if we're not coming back from a bundle, then we need to handle
1413        // otCurFont being null (was previously done in constructor)
1414        if (otCurFont == null) {
1415            otCurFont = FontSelector.DEFAULT_FONT;
1416        }
1417        otCurFontSize = otCurFont.getSize();
1418        
1419        if (potdc.getTrs() != null) {
1420            // validate time range before going ahead with control initialization
1421            if (! potdc.getTrs().begTimeOk()) {
1422                JOptionPane.showMessageDialog(null,
1423                    "Invalid start time, must follow format HH:MM:SS",
1424                    ERR_DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
1425                return false;
1426            }
1427            
1428            if (! potdc.getTrs().endTimeOk()) {
1429                JOptionPane.showMessageDialog(null,
1430                    "Invalid end time, must follow format HH:MM:SS",
1431                    ERR_DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
1432                return false;
1433            }
1434            
1435            if (! potdc.getTrs().timeRangeOk()) {
1436                JOptionPane.showMessageDialog(null,
1437                    "Invalid time range selection, please correct",
1438                    ERR_DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
1439                return false;
1440            }
1441            
1442            // allow at most two full days of orbit tracks - more than this will
1443            // at best clutter the display and at worst grind McV indefinitely
1444            long timeDiff = potdc.getTrs().getTimeRangeInSeconds();
1445            if (timeDiff >= (60 * 60 * 24 * 2)) {
1446                JOptionPane.showMessageDialog(null,
1447                    "Time range greater than two full days is not allowed, please correct",
1448                    ERR_DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
1449                return false;
1450            }
1451        }
1452        
1453        // instantiate components we need to exist at initialization
1454        latLabel = new JLabel();
1455        lonLabel = new JLabel();
1456        altLabel = new JLabel();
1457        
1458        // create time label checkbox toggle, start out enabled
1459        jcbLabels = new JCheckBox("Labels On/Off");
1460        jcbLabels.setSelected(true);
1461        jcbLabels.setName(CHECKBOX_LABELS);
1462        jcbLabels.addItemListener(this);
1463        
1464        // create swath edges toggle, start out disabled
1465        jcbSwathEdges = new JCheckBox("Swath Edges On/Off");
1466        jcbSwathEdges.setSelected(swathEdgesOn);
1467        jcbSwathEdges.setName(CHECKBOX_SWATH_EDGES);
1468        jcbSwathEdges.addItemListener(this);
1469        
1470        // initialize the various swath and groundstation params
1471        jcbSwathCenterLineWidth.setSelectedIndex(curSwathCenterWidth - 1);
1472        jcbSwathEdgeLineWidth.setSelectedIndex(curSwathEdgeWidth - 1);
1473        jcbEdgeLineStyle.setSelectedIndex(curEdgeLineStyle);
1474        jcbTrackLineStyle.setSelectedIndex(curTrackLineStyle);
1475        
1476        otFontSelector = new FontSelector(FontSelector.COMBOBOX_UI, false, false);
1477        otFontSelector.setFont(otCurFont);
1478        gsFontSelector = new FontSelector(FontSelector.COMBOBOX_UI, false, false);
1479        gsFontSelector.setFont(FontSelector.DEFAULT_FONT);
1480        
1481        // Bump default font size down just a bit...
1482        gsFontSelector.setFontSize(9);
1483        otCurFont = otFontSelector.getFont();
1484        otCurFontSize = otCurFont.getSize();
1485        
1486        this.dataChoice = dataChoice;
1487        String choiceName = dataChoice.getName();
1488        NodeList nodeList = root.getElementsByTagName(TAG_SATELLITE);
1489        int num = nodeList.getLength();
1490        if (num > 0) {
1491            for (int i = 0; i < num; i++) {
1492                Element n = (Element) (nodeList.item(i));
1493                String satName = n.getAttribute(ATTR_NAME);
1494                if (satName.equals(choiceName)) {
1495                    String strWidth = n.getAttribute(ATTR_WIDTH);
1496                    if (strWidth.isEmpty()) strWidth = "0";
1497                    Double dWidth = new Double(strWidth);
1498                    curWidth = dWidth.doubleValue();
1499                    break;
1500                }
1501            }
1502        }
1503        try {
1504            trackDsp = new CompositeDisplayable();
1505            timeLabelDsp = new CompositeDisplayable();
1506            swathEdgeDsp = new CompositeDisplayable();
1507        } catch (Exception e) {
1508            logger.error("Problem creating composite displayable", e);
1509            return false;
1510        }
1511        boolean result = super.init((DataChoice) this.getDataChoices().get(0));
1512        
1513        String dispName = getDisplayName();
1514        setDisplayName(getLongParamName() + ' ' + dispName);
1515        logger.debug("Setting display name: {}", getDisplayName());
1516        try {
1517            String longName = 
1518                REGEX.matcher(getLongParamName()).replaceAll("");
1519            otTextType = new TextType(SWATH_MODS + longName);
1520        } catch (Exception e) {
1521            logger.trace("Problem creating texttype", e);
1522            otTextType = TextType.Generic;
1523        }
1524        
1525        Data data = getData(getDataInstance());
1526        createTrackDisplay(data, true);
1527        dataSource = getDataSource();
1528        try {
1529            navDsp = getNavigatedDisplay();
1530            float defaultZ = getMapViewManager().getDefaultMapPosition();
1531            // we're just nudging a bit so tracks (and their labels) get drawn below
1532            // ground stations (and their labels), which get drawn over default map level
1533            // user can change this in map controls if they prefer maps on top
1534            gsZ = defaultZ + 0.02f;
1535            trackZ = defaultZ + 0.01f;
1536            // range on "map level" stuff is -1 to 1, stay within these limits
1537            if (trackZ > 1.0f) trackZ = 0.9f;
1538            if (gsZ > 1.0f) gsZ = 1.0f;
1539            if (! inGlobeDisplay()) {
1540                applyDisplayableLevels();
1541            }
1542        } catch (Exception e) {
1543            logger.error("Problem getting display center", e);
1544        }
1545        
1546        // set the default legend label template
1547        setLegendLabelTemplate(DisplayControlBase.MACRO_DISPLAYNAME);
1548        
1549        return result;
1550    }
1551    
1552    @Override
1553    public void itemStateChanged(ItemEvent ie) {
1554        
1555        // now we have multiple checkboxes, so first see which one applies
1556        String source = ((JCheckBox) ie.getSource()).getName();
1557        try {
1558            if (source.equals(CHECKBOX_LABELS)) {
1559                if (ie.getStateChange() == ItemEvent.DESELECTED) {
1560                    timeLabelDsp.setVisible(false);
1561                } else {
1562                    if (timeLabelDsp.displayableCount() > 0) {
1563                        // TJJ Apr 2019 - see if scale changed, if so need to redraw
1564                        float currentScale = getViewManager().getMaster().getDisplayScale();
1565                        if (Float.compare(currentScale, scale) != 0) {
1566                            scale = currentScale;
1567                            redrawAll();
1568                        } else {
1569                            timeLabelDsp.setVisible(true);
1570                        }
1571                    } else {
1572                        redrawAll();
1573                    }
1574                }
1575            }
1576            if (source.equals(CHECKBOX_SWATH_EDGES)) {
1577                // There must first be a valid swath width before we can draw edges
1578                // Test the current value in the text input and update if appropriate
1579                if (ie.getStateChange() == ItemEvent.DESELECTED) {
1580                    swathEdgeDsp.setVisible(false);
1581                    swathEdgesOn = false;
1582                } else {
1583                    swathEdgesOn = true;
1584                    float newSwathWidth = validateSwathWidthField();
1585                    if (newSwathWidth > 0) {
1586                        curWidth = newSwathWidth;
1587                        if (Double.compare(curWidth, prvWidth) != 0) {
1588                            prvWidth = curWidth;
1589                            redrawAll();
1590                        } else {
1591                            if (swathEdgeDsp.displayableCount() > 0) {
1592                                swathEdgeDsp.setVisible(true);
1593                            } else {
1594                                redrawAll();
1595                            }
1596                        }
1597                    }
1598                }
1599            }
1600        } catch (VisADException | RemoteException e) {
1601            logger.error("Problem handing state change", e);
1602        }
1603        
1604    }
1605    
1606    private TextDisplayable labelGroundStation(GroundStation station) {
1607        TextDisplayable groundStationDsp = null;
1608        try {
1609            String str = "+ " + station;
1610            logger.debug("Drawing station: {}", str);
1611            TextType tt = new TextType(STATION_MODS + ttCounter);
1612            groundStationDsp =
1613                new TextDisplayable(STATION_MODS + jcbStationsPlotted.getItemCount(), tt);
1614            ttCounter++;
1615            groundStationDsp.setJustification(TextControl.Justification.LEFT);
1616            groundStationDsp.setVerticalJustification(TextControl.Justification.CENTER);
1617            groundStationDsp.setColor(station.getColor());
1618            groundStationDsp.setFont(station.getFont());
1619            groundStationDsp.setTextSize((float) scale * station.getFont().getSize() / FONT_SCALE_FACTOR);
1620            groundStationDsp.setSphere(inGlobeDisplay());
1621            DisplayRealType dispType = navDsp.getDisplayAltitudeType();
1622            groundStationDsp.setConstantPosition(gsZ, dispType);
1623            
1624            double dlat = station.getElt().getValues()[0];
1625            double dlon = station.getElt().getValues()[1];
1626            RealTuple lonLat =
1627                new RealTuple(RealTupleType.SpatialEarth2DTuple,
1628                    new double[] { dlon, dlat });
1629            Tuple tup = new Tuple(makeTupleType(tt),
1630                new Data[] { lonLat, new Text(tt, str)});
1631            groundStationDsp.setData(tup);
1632            stationToText.put(station, groundStationDsp);
1633        } catch (Exception e) {
1634            logger.error("Problem drawing station", e);
1635        }
1636        return groundStationDsp;
1637    }
1638    
1639    private JPanel makeGroundStationPanel() {
1640        JPanel jp = new JPanel(new MigLayout());
1641        jp.setBorder(BorderFactory.createTitledBorder(" Ground Station Controls "));
1642        
1643        jcbStationLineStyle = new JComboBox<>(Constants.lineStyles);
1644        jcbStationLineStyle.addActionListener(this);
1645        jcbStationLineStyle.setSelectedIndex(1);
1646        
1647        locationComboBox = new JComboBox<>();
1648        jcbStationsPlotted = new JComboBox<>();
1649        jcbStationsPlotted.addItemListener(event -> {
1650            logger.debug("Active Station changed...");
1651            GroundStation gs = (GroundStation) jcbStationsPlotted.getSelectedItem();
1652            if (gs != null) {
1653                // Update UI with settings for this station
1654                updateGroundStationWidgets(gs);
1655            }
1656        });
1657        
1658        // Ground Stations are now a natural-order map (alphabetical)
1659        GroundStations gs = new GroundStations(null);
1660        GuiUtils.setListData(locationComboBox, gs.getGroundStations());
1661        
1662        // initialize reasonable output for no stations plotted yet
1663        if (locationComboBox.getItemCount() > 0) {
1664            latLabel.setText(" - ");
1665            lonLabel.setText(" - ");
1666            altLabel.setText(" - ");
1667        }
1668        
1669        locationPanel = new JPanel();
1670        locationPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1671        locationPanel.add(new JLabel("Ground Stations Available:"));
1672        locationPanel.add(locationComboBox);
1673        JButton addButton = new JButton("Add Selected");
1674        addButton.setToolTipText(TOOLTIP_ADD_SELECTED);
1675        addButton.setActionCommand(STATION_ADD);
1676        addButton.addActionListener(this);
1677        locationPanel.add(addButton);
1678        
1679        JPanel customPanel = new JPanel();
1680        customPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1681        customPanel.add(new JLabel("Custom Ground Station:   Label:"));
1682        customLab = new JTextField(6);
1683        customLab.setToolTipText(TOOLTIP_CUSTOM_LABEL);
1684        customPanel.add(customLab);
1685        customPanel.add(new JLabel("Lat:"));
1686        customLat = new JTextField(6);
1687        customLat.setToolTipText(TOOLTIP_CUSTOM_LAT);
1688        customPanel.add(customLat);
1689        customPanel.add(new JLabel("Lon:"));
1690        customLon = new JTextField(6);
1691        customLon.setToolTipText(TOOLTIP_CUSTOM_LON);
1692        customPanel.add(customLon);
1693        customPanel.add(new JLabel("Alt:"));
1694        customAlt = new JTextField(6);
1695        customAlt.setToolTipText(TOOLTIP_CUSTOM_ALT);
1696        customPanel.add(customAlt);
1697        customPanel.add(new JLabel("m"));
1698        JButton customButton = new JButton("Add Custom");
1699        customButton.setToolTipText(TOOLTIP_ADD_CUSTOM);
1700        customButton.setActionCommand(CUSTOM_ADD);
1701        customButton.addActionListener(this);
1702        customPanel.add(customButton);
1703        
1704        JPanel plottedStationsPanel = new JPanel();
1705        plottedStationsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1706        plottedStationsPanel.add(new JLabel("Ground Stations Plotted:"));
1707        plottedStationsPanel.add(jcbStationsPlotted);
1708        JButton remButton = new JButton("Remove Selected");
1709        remButton.setActionCommand(STATION_REM);
1710        remButton.addActionListener(this);
1711        plottedStationsPanel.add(remButton);
1712        
1713        latLonAltPanel = new JPanel();
1714        latLonAltPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1715        
1716        latLonAltPanel.add(new JLabel("Current Station,  Latitude: "));
1717        latLonAltPanel.add(latLabel);
1718        latLonAltPanel.add(Box.createHorizontalStrut(5));
1719        
1720        latLonAltPanel.add(new JLabel("Longitude: "));
1721        latLonAltPanel.add(lonLabel);
1722        latLonAltPanel.add(Box.createHorizontalStrut(5));
1723        
1724        latLonAltPanel.add(new JLabel("Altitude: "));
1725        latLonAltPanel.add(altLabel);
1726        
1727        JPanel gsFontPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1728        gsFontPanel.add(new JLabel("Font: "));
1729        gsFontPanel.add(gsFontSelector.getComponent());
1730        
1731        Color swatchAntColor = getAntColor();
1732        antColorSwatch = new ColorSwatchComponent(getStore(), swatchAntColor, "Color");
1733        antColorSwatch.setPreferredSize(Constants.DEFAULT_COLOR_PICKER_SIZE);
1734        
1735        antColorPanel = new JPanel();
1736        antColorPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1737        antColorPanel.add(new JLabel("Color: "));
1738        antColorPanel.add(antColorSwatch);
1739        
1740        antColorPanel.add(Box.createHorizontalStrut(5));
1741        antColorPanel.add(new JLabel("Line Width: "));
1742        antColorPanel.add(jcbStationLineWidth);
1743        
1744        antColorPanel.add(Box.createHorizontalStrut(5));
1745        antColorPanel.add(new JLabel("Line Style: "));
1746        antColorPanel.add(jcbStationLineStyle);
1747        
1748        antColorPanel.add(Box.createHorizontalStrut(5));
1749        antColorPanel.add(new JLabel("Antenna Angle: "));
1750        antennaAngle.setToolTipText(TOOLTIP_ANTENNA_ANGLE);
1751        antColorPanel.add(antennaAngle);
1752        
1753        jp.add(locationPanel, "wrap");
1754        jp.add(customPanel, "wrap");
1755        jp.add(plottedStationsPanel, "wrap");
1756        jp.add(latLonAltPanel, "wrap");
1757        jp.add(Box.createVerticalStrut(5), "wrap");
1758        jp.add(gsFontPanel, "wrap");
1759        jp.add(antColorPanel, "wrap");
1760        jp.add(makeBottomRow(STATION_MODS));
1761        return jp;
1762    }
1763    
1764    /**
1765     * Create the {@literal "Apply"} button used by both sections of the 
1766     * control's GUI.
1767     * 
1768     * @param command {@literal "Command"} used in 
1769     *                {@link #actionPerformed(ActionEvent)}.
1770     * 
1771     * @return {@code JPanel} containing our {@literal "Apply"} button, 
1772     *         suitable for adding to the end of the control's bordered panels.
1773     */
1774
1775    private JPanel makeBottomRow(String command) {
1776        JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT));
1777        JButton applyButton = new JButton("Apply");
1778        applyButton.setActionCommand(command);
1779        applyButton.addActionListener(this);
1780        row.add(applyButton);
1781        return row;
1782    }
1783    
1784    private JPanel makeSwathWidthPanel() {
1785        if (dataChoice != null)
1786            satelliteName = new JLabel(dataChoice.getName());
1787        swathWidthFld = new JTextField("" + curWidth, 5);
1788        if (curWidth == 0) swathWidthFld.setText(SWATH_NA);
1789        
1790        JPanel jp = new JPanel(new FlowLayout(FlowLayout.LEFT));
1791        
1792        // first on this panel, check box to turn on/off swath line edges
1793        jp.add(jcbSwathEdges);
1794        
1795        jp.add(Box.createHorizontalStrut(5));
1796        jp.add(new JLabel("Satellite: "));
1797        jp.add(satelliteName);
1798        jp.add(Box.createHorizontalStrut(5));
1799        jp.add(new JLabel("Swath Width: "));
1800        swathWidthFld.setToolTipText(TOOLTIP_SWATH_WIDTH);
1801        jp.add(swathWidthFld);
1802        jp.add(kmLabel);
1803        jp.add(Box.createHorizontalStrut(5));
1804        
1805        return jp;
1806    }
1807    
1808    private TupleType makeTupleType(TextType tt) {
1809        TupleType t = null;
1810        try {
1811            t = new TupleType(new MathType[] {RealTupleType.SpatialEarth2DTuple, tt});
1812        } catch (Exception e) {
1813            logger.error("Problem creating TupleType", e);
1814        }
1815        return t;
1816    }
1817    
1818    private void addGroundStation(GroundStation gs, boolean isCustom) {
1819        
1820        logger.debug("addGroundStation() in, name: {}", gs.getName());
1821        
1822        try {
1823            
1824            EarthLocationTuple elt = gs.getElt();
1825            latLabel.setText(elt.getLatitude().toString());
1826            lonLabel.setText(elt.getLongitude().toString());
1827            altLabel.setText(elt.getAltitude().toString());
1828            
1829            // quick and easy way to limit sig digits to something not too crazy
1830            if (altLabel.getText().length() > 10) altLabel.setText(altLabel.getText().substring(0, 9));
1831            latitude = Double.parseDouble(latLabel.getText());
1832            longitude = Double.parseDouble(lonLabel.getText());
1833
1834            // For non-custom, "stock" groundstations, compute altitude
1835            // For custom, the user will have specified it
1836            if (! isCustom) {
1837                double altitude = dataSource.getNearestAltToGroundStation(latitude, longitude) / 1000.0;
1838                gs.setAltitude(altitude);
1839            }
1840            gs.setColor(antColorSwatch.getColor());
1841            gs.setLineWidth(jcbStationLineWidth.getSelectedIndex() + 1);
1842            gs.setLineStyle(jcbStationLineStyle.getSelectedIndex());
1843            gs.setFont(gsFontSelector.getFont());
1844            CurveDrawer cd = makeCoverageCircle(gs);
1845            
1846            if (cd != null) {
1847                logger.debug("Adding ground station, station name: {}", gs.getName());
1848                TextDisplayable label = labelGroundStation(gs);
1849                cd.setConstantPosition(gsZ, navDsp.getDisplayAltitudeType());
1850                addDisplayable(cd);
1851                stationToCurve.put(gs, cd);
1852                stationToText.put(gs, label);
1853            } else {
1854                logger.error("could not draw curve!!");
1855            }
1856            
1857            TextDisplayable td = stationToText.get(gs);
1858            td.setConstantPosition(gsZ, navDsp.getDisplayAltitudeType());
1859            addDisplayable(td);
1860        } catch (Exception e) {
1861            logger.error("Problem adding ground station", e);
1862        }
1863    }
1864    
1865    private void updateStationColor(GroundStation gs, Color newColor)
1866        throws VisADException, RemoteException
1867    {
1868        gs.setColor(newColor);
1869        stationToCurve.get(gs).setColor(newColor);
1870        stationToText.get(gs).setColor(newColor);
1871    }
1872    
1873    private void updateStationFont(GroundStation gs, Font newFont)
1874        throws VisADException, RemoteException
1875    {
1876        gs.setFont(newFont);
1877        TextDisplayable label = stationToText.get(gs);
1878        label.setFont(newFont);
1879        scale = getViewManager().getMaster().getDisplayScale();
1880        label.setTextSize((float) scale * newFont.getSize() / FONT_SCALE_FACTOR);
1881    }
1882    
1883    private void replaceCurve(GroundStation gs)
1884        throws VisADException, RemoteException
1885    {
1886        gs.setLineWidth(jcbStationLineWidth.getSelectedIndex() + 1);
1887        gs.setLineStyle(jcbStationLineStyle.getSelectedIndex());
1888        CurveDrawer cdOld = stationToCurve.get(gs);
1889        CurveDrawer cdNew = makeCoverageCircle(gs);
1890        TextDisplayable label = stationToText.get(gs);
1891        cdNew.setLineWidth(gs.getLineWidth());
1892        removeDisplayable(cdOld);
1893        removeDisplayable(label);
1894        addDisplayable(cdNew);
1895        addDisplayable(label);
1896        stationToCurve.put(gs, cdNew);
1897    }
1898    
1899    private void updateGroundStationWidgets(GroundStation gs) {
1900        gsFontSelector.setFont(gs.getFont());
1901        antColorSwatch.setBackground(gs.getColor());
1902        jcbStationLineStyle.setSelectedIndex(gs.getLineStyle());
1903        jcbStationLineWidth.setSelectedIndex(gs.getLineWidth() - 1);
1904        antennaAngle.setText(String.valueOf(gs.getAntennaAngle()));
1905        curAngle = gs.getAntennaAngle();
1906        curElevation = (int) gs.getAltitude();
1907        
1908        EarthLocation elt = gs.getElt();
1909        latLabel.setText(String.valueOf(elt.getLatitude().getValue()));
1910        lonLabel.setText(String.valueOf(elt.getLongitude().getValue()));
1911        altLabel.setText(String.valueOf(elt.getAltitude().getValue()));
1912    }
1913    
1914    public void setStations(List<GroundStation> newStations) {
1915        stations.clear();
1916        stations.addAll(newStations);
1917    }
1918    
1919    public List<GroundStation> getStations() {
1920        // this clear() call is important!
1921        // if not done, saving more than one bundle will result in the 
1922        // "stations" list containing the contents of "jcbStationsPlotted"
1923        // for *every call to getStations()!*
1924        stations.clear();
1925        for (int i = 0; i < jcbStationsPlotted.getItemCount(); i++) {
1926            stations.add(jcbStationsPlotted.getItemAt(i));
1927        }
1928        return stations;
1929    }
1930    
1931    public void setShowingLabels(boolean newValue) {
1932        showingLabels = newValue;
1933    }
1934    
1935    public boolean getShowingLabels() {
1936        return jcbLabels.isSelected();
1937    }
1938    
1939    /* (non-Javadoc)
1940     * @see ucar.unidata.idv.control.DisplayControlImpl#projectionChanged()
1941     */
1942    @Override
1943    public void projectionChanged() {
1944        super.projectionChanged();
1945        applyDisplayableLevels();
1946    }
1947    
1948    public void setAntColor(Color c) {
1949        if (c == null) c = defaultAntColor;
1950        try {
1951            antColor = c;
1952        } catch (Exception e) {
1953            logger.error("Exception in PolarOrbitTrackControl.setAntColor", e);
1954        }
1955    }
1956    
1957    public void setStation(String val) {
1958        station = val.trim();
1959    }
1960    
1961    private float validateSwathWidthField() {
1962        String s = swathWidthFld.getText().trim();
1963        float val = -1.0f;
1964        try {
1965            val = Float.parseFloat(s);
1966        } catch (NumberFormatException nfe) {
1967            // TJJ Jun 2015 - if GEO sensor, N/A means return invalid, but no warning msg needed
1968            if ((s != null) && (s.equals(SWATH_NA))) {
1969                return -2;
1970            }
1971            // throw up a dialog to tell user the problem
1972            JOptionPane.showMessageDialog(latLonAltPanel,
1973                "Invalid swath width: must be a decimal value in km");
1974            return -1;
1975        }
1976        
1977        // Need <= on low end because value must be positive
1978        if ((val <= SWATH_WIDTH_MIN) || (val > SWATH_WIDTH_MAX)) {
1979            // throw up a dialog to tell user the problem
1980            JOptionPane.showMessageDialog(latLonAltPanel,
1981                "Swath width valid range is > " + SWATH_WIDTH_MIN +
1982                    " to " + SWATH_WIDTH_MAX + " km");
1983            return -1;
1984        }
1985        return val;
1986    }
1987    
1988    /* (non-Javadoc)
1989     * @see ucar.unidata.idv.control.DisplayControlImpl#showProperties()
1990     * We need this because the TimeSelection widget in preview window
1991     * and properties window are two different objects we are trying to
1992     * keep in sync, and only redraw the display when necessary.
1993     */
1994    
1995    @Override public void showProperties() {
1996        oldProps = new Hashtable(getDataInstance().getDataSelection().getProperties());
1997        
1998        JTabbedPane jtp = new JTabbedPane();
1999        addPropertiesComponents(jtp);
2000        final JDialog propertiesDialog = GuiUtils.createDialog("Properties -- " + getTitle(), true);
2001        ActionListener listener = event -> {
2002            String cmd = event.getActionCommand();
2003            if (cmd.equals(GuiUtils.CMD_OK) || cmd.equals(GuiUtils.CMD_APPLY)) {
2004                if (! applyProperties()) {
2005                    return;
2006                }
2007                PolarOrbitTrackDataSource ds = getDataSource();
2008                ds.setSelectionProps(getDataSelection().getProperties());
2009                if (! oldProps.equals(getDataSelection().getProperties())) {
2010                    redrawAll();
2011                    oldProps = getDataSelection().getProperties();
2012                }
2013            }
2014            if (cmd.equals(GuiUtils.CMD_OK) || cmd.equals(GuiUtils.CMD_CANCEL)) {
2015                propertiesDialog.dispose();
2016            }
2017        };
2018        Window f = GuiUtils.getWindow(getContents());
2019        JComponent buttons = GuiUtils.makeApplyOkCancelButtons(listener);
2020        JComponent propContents = inset(centerBottom(jtp, buttons), 5);
2021        Msg.translateTree(jtp, true);
2022        propertiesDialog.getContentPane().add(propContents);
2023        propertiesDialog.pack();
2024        if (f != null) {
2025            GuiUtils.showDialogNearSrc(f, propertiesDialog);
2026        } else {
2027            propertiesDialog.setVisible(true);
2028        }
2029    }
2030}