001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2017
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.control;
030
031import edu.wisc.ssec.mcidasv.McIdasPreferenceManager;
032
033import edu.wisc.ssec.mcidasv.data.GroundStations;
034import edu.wisc.ssec.mcidasv.data.PolarOrbitTrackDataSource;
035import edu.wisc.ssec.mcidasv.data.TimeRangeSelection;
036import edu.wisc.ssec.mcidasv.data.hydra.CurveDrawer;
037import edu.wisc.ssec.mcidasv.ui.ColorSwatchComponent;
038import edu.wisc.ssec.mcidasv.util.XmlUtil;
039
040import java.awt.Color;
041import java.awt.Container;
042import java.awt.Dimension;
043import java.awt.FlowLayout;
044import java.awt.Font;
045import java.awt.event.ActionEvent;
046import java.awt.event.ItemEvent;
047import java.lang.Math;
048import java.rmi.RemoteException;
049import java.util.ArrayList;
050import java.util.HashMap;
051import java.util.List;
052import java.util.TreeSet;
053
054import javax.swing.BorderFactory;
055import javax.swing.Box;
056import javax.swing.BoxLayout;
057import javax.swing.JButton;
058import javax.swing.JCheckBox;
059import javax.swing.JComboBox;
060import javax.swing.JLabel;
061import javax.swing.JOptionPane;
062import javax.swing.JPanel;
063import javax.swing.JSpinner;
064import javax.swing.JTextField;
065import javax.swing.SpinnerNumberModel;
066
067import name.gano.astro.AstroConst;
068import net.miginfocom.swing.MigLayout;
069
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072import org.w3c.dom.Element;
073import org.w3c.dom.NodeList;
074
075import ucar.unidata.data.DataChoice;
076import ucar.unidata.data.DataSourceImpl;
077import ucar.unidata.idv.control.DisplayControlImpl;
078import ucar.unidata.ui.FontSelector;
079import ucar.unidata.util.GuiUtils;
080import ucar.unidata.util.IOUtil;
081import ucar.unidata.view.geoloc.NavigatedDisplay;
082import ucar.visad.UtcDate;
083import ucar.visad.Util;
084import ucar.visad.display.CompositeDisplayable;
085import ucar.visad.display.TextDisplayable;
086
087import visad.Data;
088import visad.DisplayRealType;
089import visad.Gridded2DSet;
090import visad.MathType;
091import visad.RealTuple;
092import visad.RealTupleType;
093import visad.SampledSet;
094import visad.Text;
095import visad.TextControl;
096import visad.TextType;
097import visad.Tuple;
098import visad.TupleType;
099import visad.UnionSet;
100import visad.VisADException;
101import visad.georef.EarthLocationTuple;
102import visad.georef.LatLonTuple;
103
104/**
105 * {@link ucar.unidata.idv.control.DisplayControlImpl} with some McIDAS-V
106 * specific extensions. Namely parameter sets and support for inverted 
107 * parameter defaults.
108 */
109
110public class PolarOrbitTrackControl extends DisplayControlImpl {
111
112    private static final Logger logger = LoggerFactory.getLogger(PolarOrbitTrackControl.class);
113
114    private JLabel satelliteName = new JLabel("");
115    private static final JLabel kmLabel = new JLabel("km");
116    private JTextField swathWidthFld = null;
117    private JPanel swathWidthPanel;
118    
119    // Ground Station hashmap
120    private HashMap<String, EarthLocationTuple> stationMap = null;
121
122    private double latitude;
123    private double longitude;
124    private JPanel fontSizePanel;
125    private JPanel colorPanel;
126    private JPanel antColorPanel;
127    private JPanel locationPanel;
128    private JPanel latLonAltPanel;
129
130    /** Property name to get the list or URLs */
131    public final String PREF_GROUNDSTATIONS = "mcv.groundstations";
132
133    private JComboBox locationComboBox;
134    private JComboBox jcbStationsPlotted;
135    String [] lineStyles = new String[] { "_____", "_ _ _", ".....", "_._._" };
136    private JComboBox jcbTrackLineStyle = new JComboBox(lineStyles);
137    private JComboBox jcbEdgeLineStyle = new JComboBox(lineStyles);
138    private JComboBox jcbStationLineStyle = new JComboBox(lineStyles);
139    private JCheckBox jcbLabels;
140    private JCheckBox jcbSwathEdges;
141    
142    // names to distinguish checkbox event sources
143    private static final String CHECKBOX_LABELS = "CHECKBOX_LABELS";
144    private static final String CHECKBOX_SWATH_EDGES = "CHECKBOX_SWATH_EDGES";
145
146    private String station = "";
147
148    private static final int SWATH_WIDTH_MIN = 1;
149    // swath width not applicable, e.g. GEO sensor
150    private static final String SWATH_NA = "N/A";
151    // TJJ Feb 2014 - need to determine max of any sensor. VIIRS is over 3000 km
152    private static final int SWATH_WIDTH_MAX = 4000;
153    private static final int DEFAULT_ANTENNA_ANGLE = 5;
154    private static final int MAX_ANTENNA_ANGLE = 90;
155    private int curAngle = DEFAULT_ANTENNA_ANGLE;
156    private static final double LABEL_DISTANCE_THRESHOLD = 5.0d;
157
158    private DataChoice dataChoice;
159
160    private JLabel latLabel;
161    private JLabel lonLabel;
162    private JLabel altLabel;
163    private JTextField antennaAngle = new JTextField("" + DEFAULT_ANTENNA_ANGLE, DEFAULT_ANTENNA_ANGLE);
164    
165    // custom ground station UI components
166    JTextField customLat = null;
167    JTextField customLon = null;
168    JTextField customLab = null;
169
170    /** the font selectors, Orbit Track (ot) and Ground Station (gs) */
171    private FontSelector otFontSelector;
172    private Font otCurFont = FontSelector.DEFAULT_FONT;
173    private FontSelector gsFontSelector;
174    private Font gsCurFont = FontSelector.DEFAULT_FONT;
175    
176    // line width combo boxes, GS: Ground Station, SC: Swath Center, SE: Swath Edge
177    private JComboBox jcbGSLineWidth;
178    private JComboBox jcbSCLineWidth;
179    private JComboBox jcbSELineWidth;
180    private JSpinner js = null;
181
182    private CompositeDisplayable trackDsp;
183    private CompositeDisplayable timeLabelDsp;
184    private CompositeDisplayable stationLabelDsp;
185    private CompositeDisplayable swathEdgeDsp;
186    private CompositeDisplayable circleDsp;
187    
188    // time label variables
189    private static final int DEFAULT_LABEL_INTERVAL = 5;
190    private int labelInterval = DEFAULT_LABEL_INTERVAL;
191
192    private ColorSwatchComponent colorSwatch;
193
194    private static final Color DEFAULT_COLOR = Color.GREEN;
195    private Color curSwathColor = DEFAULT_COLOR;
196    private Color prvSwathColor = null;
197    
198    private ColorSwatchComponent antColorSwatch;
199    private Color antColor;
200    private Color defaultAntColor = Color.MAGENTA;
201    private PolarOrbitTrackDataSource dataSource;
202
203    private double satelliteAltitude = 0.0;
204
205    private double trackZ = 0.0d;
206    private double gsZ = 0.0d;
207    private NavigatedDisplay navDsp = null;
208    private TextType otTextType = null;
209    private TextType gsTextType = null;
210    private double curWidth = 0.0d;
211    private double prvWidth = 0.0d;
212    // TODO: event handler for ground station controls needs conditional handling checks
213    // e.g., utilize the unused variable below
214    private int prvStationLineStyle = -1;
215    private int prvTrackLineStyle = -1;
216    private int prvEdgeLineStyle = -1;
217    private int curTrackLineStyle = -1;
218    private int curEdgeLineStyle = -1;
219    private static final float FONT_SCALE_FACTOR = 12.0f;
220    
221    // line width for drawing track center and swath edges
222    private float prvSwathCenterWidth = 2.0f;
223    private float curSwathCenterWidth = 2.0f;
224    private float prvSwathEdgeWidth = 1.0f;
225    private float curSwathEdgeWidth = 1.0f;
226
227    /** Path to the McV swathwidths.xml */
228    private static final String SWATH_WIDTHS = "/edu/wisc/ssec/mcidasv/resources/swathwidths.xml";
229    private static final String TAG_SATELLITE = "satellite";
230    private static final String ATTR_NAME = "name";
231    private static final String ATTR_WIDTH = "width";
232
233        private static final String SWATH_MODS = "OrbitTrack";
234        private static final String STATION_MODS = "GroundStation";
235        private static final String STATION_ADD = "AddStation";
236        private static final String STATION_REM = "RemStation";
237        private static final String CUSTOM_ADD = "AddCustom";
238
239    private Element root = null;
240
241    public PolarOrbitTrackControl() {
242        super();
243        logger.trace("created new PolarOrbitTrackControl...");
244        setAttributeFlags(FLAG_COLORTABLE);
245        try {
246            final String xml =
247                IOUtil.readContents(SWATH_WIDTHS, McIdasPreferenceManager.class);
248            root = XmlUtil.getRoot(xml);
249        } catch (Exception e) {
250            logger.error("problem reading swathwidths.xml");
251            e.printStackTrace();
252        }
253    }
254
255    /**
256     * Deal with action events
257     *
258     * @param  ae the ActionEvent fired when the user applies changes
259     */
260
261    public void actionPerformed(ActionEvent ae) {
262        
263        // user trying to add a custom ground station
264        if (CUSTOM_ADD.equals(ae.getActionCommand())) {
265                logger.debug("Custom Ground Station...");
266                String labStr = customLab.getText();
267                if ((labStr == null) || (labStr.isEmpty())) {
268                JOptionPane.showMessageDialog(null, 
269                        "Please provide a label for the custom ground station.");
270                return;
271                }
272                float fLat;
273                float fLon;
274                try {
275                                fLat = Float.parseFloat(customLat.getText());
276                                fLon = Float.parseFloat(customLon.getText());
277                        } catch (NumberFormatException nfe) {
278                JOptionPane.showMessageDialog(null, 
279                        "Latitude and Longitude must be floating point numbers, please correct.");
280                return;
281                        }
282                if ((fLat < -90) || (fLat > 90)) {
283                JOptionPane.showMessageDialog(null, 
284                        "Latitude is out of valid range: " + fLat);
285                return;
286                }
287                if ((fLon < -180) || (fLon > 180)) {
288                JOptionPane.showMessageDialog(null, 
289                        "Longitude is out of valid range: " + fLon);
290                return;
291                }
292                // last check, is this label already used?
293                int numPlotted = jcbStationsPlotted.getItemCount();
294                for (int i = 0; i < numPlotted; i++) {
295                        String s = (String) jcbStationsPlotted.getItemAt(i);
296                        if ((s != null) && s.equals(station)) {
297                        JOptionPane.showMessageDialog(null, 
298                                "A station with this label has already been plotted: " + s);
299                    return;
300                        }
301                }
302                // if we made it this far, fields are valid, we can create a custom ground station
303                // create new earth location, add it to stations plotted, set index, 
304                        jcbStationsPlotted.addItem(labStr);
305                        jcbStationsPlotted.setSelectedItem(labStr);
306                        // make an Earth location
307                        double dAlt = dataSource.getNearestAltToGroundStation(latitude, longitude) / 1000.0;
308                        EarthLocationTuple elt = null;
309                        try {
310                                elt = new EarthLocationTuple(fLat, fLon, dAlt);
311                        } catch (RemoteException e) {
312                                e.printStackTrace();
313                        } catch (VisADException e) {
314                                e.printStackTrace();
315                        }
316                        stationMap.put(labStr, elt);
317                        plotCoverageCircles();
318                        updateDisplayList();
319                return;
320        }
321        
322        // user trying to add a new ground station to those plotted on display
323        if (STATION_ADD.equals(ae.getActionCommand())) {
324                logger.debug("Add Station...");
325                String station = (String) locationComboBox.getSelectedItem();
326                boolean alreadyPlotted = false;
327                int numPlotted = jcbStationsPlotted.getItemCount();
328                for (int i = 0; i < numPlotted; i++) {
329                        String s = (String) jcbStationsPlotted.getItemAt(i);
330                        if ((s != null) && s.equals(station)) {
331                                alreadyPlotted = true;
332                                break;
333                        }
334                }
335                if (alreadyPlotted) {
336                JOptionPane.showMessageDialog(null, 
337                        "Station already plotted on display: " + station);
338                } else {
339                        jcbStationsPlotted.addItem(station);
340                        jcbStationsPlotted.setSelectedItem(station);
341                        plotCoverageCircles();
342                }
343                updateDisplayList();
344                return;
345        }
346        
347        // user removing a ground station from the display
348        if (STATION_REM.equals(ae.getActionCommand())) {
349                logger.debug("Rem Station...");
350                String station = (String) jcbStationsPlotted.getSelectedItem();
351                if (station == null) {
352                JOptionPane.showMessageDialog(null, 
353                        "Nothing to remove");
354                } else {
355                        jcbStationsPlotted.removeItem(station);
356                        plotCoverageCircles();
357                }
358                updateDisplayList();
359                return;
360        }
361        
362        // swath-related changes
363        if (SWATH_MODS.equals(ae.getActionCommand())) {
364                logger.debug("Apply Swath Mods...");
365                
366                boolean fontChanged = true;
367                boolean swathChanged = false;
368                curSwathCenterWidth = jcbSCLineWidth.getSelectedIndex() + 1;
369                curSwathEdgeWidth = jcbSELineWidth.getSelectedIndex() + 1;
370                if (curSwathCenterWidth != prvSwathCenterWidth) swathChanged = true;
371                if (curSwathEdgeWidth != prvSwathEdgeWidth) swathChanged = true;
372                
373                curTrackLineStyle = jcbTrackLineStyle.getSelectedIndex();
374                if (curTrackLineStyle != prvTrackLineStyle) swathChanged = true; 
375                curEdgeLineStyle = jcbEdgeLineStyle.getSelectedIndex();
376                if (curEdgeLineStyle != prvEdgeLineStyle) swathChanged = true;
377                
378                curSwathColor = colorSwatch.getColor();
379                if (! curSwathColor.equals(prvSwathColor)) swathChanged = true;
380                
381                int newSwathWidth = validateSwathWidthField();
382                if (newSwathWidth > 0) {
383                        curWidth = newSwathWidth;
384                        if (curWidth != prvWidth) swathChanged = true;
385                }
386                
387                // update font attributes if necessary
388                Font f = otFontSelector.getFont();
389                if (! f.equals(otCurFont)) {
390                        otCurFont = f;
391                        fontChanged = true;
392                }
393                
394                // see if label interval has changed
395                SpinnerNumberModel snm = (SpinnerNumberModel) (js.getModel());
396                int tmpLabelInterval = ((Integer) snm.getValue()).intValue();
397                if ((tmpLabelInterval != labelInterval) || fontChanged) {
398                        logger.debug("Label interval change from: " + labelInterval +
399                                        " to: " + tmpLabelInterval);
400                        labelInterval = tmpLabelInterval;
401                        try {
402                                // remove the current set of labels
403                                int numLabels = timeLabelDsp.displayableCount();
404                                for (int i = 0; i < numLabels; i++) {
405                                        timeLabelDsp.removeDisplayable(0);
406                                }
407                                // get the currently loaded data
408                                Data data = getData(getDataInstance());
409                                        if (data instanceof Tuple) {
410                                Data[] dataArr = ((Tuple) data).getComponents();
411
412                                int npts = dataArr.length;
413                                double distance = 0.0d;
414                                LatLonTuple prvPoint = null;
415
416                                for (int i = 0; i < npts; i++) {
417                                    Tuple t = (Tuple) dataArr[i];
418                                    Data[] tupleComps = t.getComponents();
419
420                                    LatLonTuple llt = (LatLonTuple) tupleComps[1];
421                                    double dlat = llt.getLatitude().getValue();
422                                    double dlon = llt.getLongitude().getValue();
423
424                                if ((i % labelInterval) == 0) {
425                                        
426                                            if (prvPoint != null) {
427                                                distance = Util.distance(prvPoint, llt);
428                                                if (distance < LABEL_DISTANCE_THRESHOLD) {
429                                                        continue;
430                                                }
431                                            }
432                                            
433                                    String str = ((Text) tupleComps[0]).getValue();
434                                    logger.debug("Adding time for str: " + str);
435                                    int indx = str.indexOf(" ") + 1;
436                                    String subStr = "- " + str.substring(indx, indx+5);
437                                    TextDisplayable time = new TextDisplayable(SWATH_MODS, otTextType);
438                                    time.setJustification(TextControl.Justification.LEFT);
439                                    time.setVerticalJustification(TextControl.Justification.CENTER);
440                                    time.setColor(curSwathColor);
441                                    time.setFont(otFontSelector.getFont());
442                                    time.setTextSize((float) otFontSelector.getFontSize() / FONT_SCALE_FACTOR);
443                                    time.setSphere(inGlobeDisplay());
444                                    
445                                    RealTuple lonLat =
446                                        new RealTuple(RealTupleType.SpatialEarth2DTuple,
447                                            new double[] { dlon, dlat });
448                                    Tuple tup = new Tuple(makeTupleType(SWATH_MODS),
449                                        new Data[] { lonLat, new Text(otTextType, subStr)});
450                                    time.setData(tup);
451                                    timeLabelDsp.addDisplayable(time);
452                                    
453                                        prvPoint = llt;
454                                }
455
456                                }
457                            }
458                                        
459                                // check swath width field, update if necessary
460                                if (swathChanged) changeSwathWidth();
461                                
462                                } catch (RemoteException re) {
463                                        re.printStackTrace();
464                                } catch (VisADException vade) {
465                                        vade.printStackTrace();
466                                }
467                }
468                
469                updateDisplayList();
470                return;
471        }
472        
473        // Ground station mods
474        if (STATION_MODS.equals(ae.getActionCommand())) {
475                
476                logger.debug("Apply Station Mods...");
477                
478                // flag indicates user changed some parameter
479                boolean somethingChanged = true;
480                
481                setAntColor(antColorSwatch.getColor());
482                
483                // update font attributes if necessary
484                Font f = gsFontSelector.getFont();
485                if (! f.equals(gsCurFont)) {
486                        gsCurFont = f;
487                        somethingChanged = true;
488                }
489                
490                // validate antenna angle text field, redraw if necessary
491            String s = antennaAngle.getText();
492            int newAngle = curAngle;
493            try {
494                newAngle = Integer.parseInt(s);
495                if (newAngle != curAngle) {
496                        // TJJ Jun 2015 range check
497                        if ((newAngle < DEFAULT_ANTENNA_ANGLE) ||
498                            (newAngle > MAX_ANTENNA_ANGLE)) {
499                                throw new NumberFormatException();
500                        } else {
501                                curAngle = newAngle;
502                                somethingChanged = true;
503                        }
504                }
505            } catch (NumberFormatException nfe) {
506                JOptionPane.showMessageDialog(latLonAltPanel, 
507                        "Antenna angle valid range is " + DEFAULT_ANTENNA_ANGLE + 
508                        " to " + MAX_ANTENNA_ANGLE + " degrees");
509                return;
510            }
511            
512            if (somethingChanged) {             
513                plotCoverageCircles();  
514            }
515            
516            updateDisplayList();
517                return;
518                
519        }
520
521    }
522
523    /**
524     * Apply the map (height) position to the displays
525     */
526    
527    private void applyTrackPosition() {
528        try {
529            DisplayRealType dispType = navDsp.getDisplayAltitudeType();
530            trackDsp.setConstantPosition(trackZ, dispType);
531            timeLabelDsp.setConstantPosition(trackZ, dispType);
532            stationLabelDsp.setConstantPosition(gsZ, dispType);
533        } catch (Exception e) {
534            e.printStackTrace();
535        }
536    }
537
538    private void changeSwathWidth() {
539
540        logger.debug("changeSwathWidth() in...");
541        if ((curWidth != prvWidth) || 
542                        (curTrackLineStyle != prvTrackLineStyle) ||
543                        (curEdgeLineStyle != prvEdgeLineStyle) ||
544                        (curSwathCenterWidth != prvSwathCenterWidth) ||
545                        (curSwathEdgeWidth != prvSwathEdgeWidth) ||
546                        (curSwathColor != prvSwathColor)) {
547                prvWidth = curWidth;
548                prvSwathCenterWidth = curSwathCenterWidth;
549                prvSwathEdgeWidth = curSwathEdgeWidth;
550                prvSwathColor = curSwathColor;
551                prvTrackLineStyle = curTrackLineStyle;
552                prvEdgeLineStyle = curEdgeLineStyle;
553                try {
554                        removeDisplayable(swathEdgeDsp);
555                        removeDisplayable(trackDsp);
556                        swathEdgeDsp = null;
557                        trackDsp = null;
558                        Data data = getData(getDataInstance());
559                        swathEdgeDsp = new CompositeDisplayable();
560                        trackDsp = new CompositeDisplayable();
561                        createTrackDisplay(data, true);
562                } catch (Exception e) {
563                        e.printStackTrace();
564                }
565        }
566    }
567    
568    private void createTrackDisplay(Data data, boolean doTrack) {
569        logger.debug("createTrackDisplay() in...");
570        try {
571            List<String> dts = new ArrayList<String>();
572            if (data instanceof Tuple) {
573                Data[] dataArr = ((Tuple) data).getComponents();
574
575                int npts = dataArr.length;
576                float[][] latlon = new float[2][npts];
577                double distance = 0.0d;
578                LatLonTuple prvPoint = null;
579
580                for (int i = 0; i < npts; i++) {
581                    Tuple t = (Tuple) dataArr[i];
582                    Data[] tupleComps = t.getComponents();
583
584                    LatLonTuple llt = (LatLonTuple) tupleComps[1];
585                    double dlat = llt.getLatitude().getValue();
586                    double dlon = llt.getLongitude().getValue();
587
588                    if (doTrack) {
589                        if ((i % labelInterval) == 0) {
590                                    
591                                if (prvPoint != null) {
592                                        distance = Util.distance(prvPoint, llt);
593                                        if (distance < LABEL_DISTANCE_THRESHOLD) {
594                                    latlon[0][i] = (float) dlat;
595                                    latlon[1][i] = (float) dlon;
596                                                continue;
597                                        }
598                                }
599                                
600                            String str = ((Text) tupleComps[0]).getValue();
601                            dts.add(str);
602                            int indx = str.indexOf(" ") + 1;
603                            String subStr = "- " + str.substring(indx, indx + 5);
604                            TextDisplayable time = new TextDisplayable(SWATH_MODS, otTextType);
605                            time.setJustification(TextControl.Justification.LEFT);
606                            time.setVerticalJustification(TextControl.Justification.CENTER);
607                            time.setColor(curSwathColor);
608                                time.setTextSize((float) otFontSelector.getFontSize() / FONT_SCALE_FACTOR);
609                                time.setFont(otFontSelector.getFont());
610                                time.setSphere(inGlobeDisplay());
611                            
612                            RealTuple lonLat =
613                                new RealTuple(RealTupleType.SpatialEarth2DTuple,
614                                    new double[] { dlon, dlat });
615                            Tuple tup = new Tuple(makeTupleType(SWATH_MODS),
616                                new Data[] { lonLat, new Text(otTextType, subStr)});
617                            time.setData(tup);
618                            timeLabelDsp.addDisplayable(time);
619                            
620                            prvPoint = llt;
621                        }
622                    }
623                    latlon[0][i] = (float) dlat;
624                    latlon[1][i] = (float) dlon;
625                }
626
627                if (doTrack) {
628                    Gridded2DSet track = new Gridded2DSet(RealTupleType.LatitudeLongitudeTuple,
629                               latlon, npts);
630                    SampledSet[] set = new SampledSet[1];
631                    set[0] = track;
632                    UnionSet uset = new UnionSet(set);
633                    CurveDrawer trackLines = new CurveDrawer(uset);
634                    trackLines.setData(uset);
635                    trackLines.setDrawingEnabled(false);
636                    trackLines.setLineStyle(jcbTrackLineStyle.getSelectedIndex());
637                    trackDsp.addDisplayable(trackLines);
638                    trackDsp.setColor(curSwathColor);
639                    trackDsp.setLineWidth(jcbSCLineWidth.getSelectedIndex() + 1);
640
641                    addDisplayable(trackDsp, FLAG_COLORTABLE);
642                    addDisplayable(timeLabelDsp, FLAG_COLORTABLE);
643                    addDisplayable(stationLabelDsp, FLAG_COLORTABLE);
644                }
645
646                float[][][] crv = getSwath(latlon);
647                int npt = crv[0][0].length;
648                float[][] leftC = new float[2][npt];
649                float[][] rightC = new float[2][npt];
650                for (int i = 0; i < npt; i++) {
651                    leftC[0][i] = crv[0][0][i];
652                    leftC[1][i] = crv[0][1][i];
653                    rightC[0][i] = crv[1][0][i];
654                    rightC[1][i] = crv[1][1][i];
655                }
656                Gridded2DSet left = new Gridded2DSet(RealTupleType.LatitudeLongitudeTuple,
657                           leftC, npt);
658                SampledSet[] lSet = new SampledSet[1];
659                lSet[0] = left;
660                UnionSet lUSet = new UnionSet(lSet);
661                CurveDrawer leftLines = new CurveDrawer(lUSet);
662                leftLines.setLineStyle(jcbEdgeLineStyle.getSelectedIndex());
663                leftLines.setData(lUSet);
664                swathEdgeDsp.addDisplayable(leftLines);
665                leftLines.setDrawingEnabled(false);
666
667                Gridded2DSet right = new Gridded2DSet(RealTupleType.LatitudeLongitudeTuple,
668                           rightC, npt);
669                SampledSet[] rSet = new SampledSet[1];
670                rSet[0] = right;
671                UnionSet rUSet = new UnionSet(rSet);
672                CurveDrawer rightLines = new CurveDrawer(rUSet);
673                rightLines.setLineStyle(jcbEdgeLineStyle.getSelectedIndex());
674                rightLines.setData(rUSet);
675                swathEdgeDsp.addDisplayable(rightLines);
676                rightLines.setDrawingEnabled(false);
677                
678                swathEdgeDsp.setColor(curSwathColor);
679                swathEdgeDsp.setLineWidth(curSwathEdgeWidth);
680                addDisplayable(swathEdgeDsp, FLAG_COLORTABLE);
681            }
682        } catch (Exception e) {
683            e.printStackTrace();
684        }
685        return;
686    }
687
688    /**
689     * Called by doMakeWindow in DisplayControlImpl, which then calls its
690     * doMakeMainButtonPanel(), which makes more buttons.
691     *
692     * @return container of contents
693     */
694    
695    public Container doMakeContents() {
696        
697        fontSizePanel = new JPanel();
698        fontSizePanel.setLayout(new BoxLayout(fontSizePanel, BoxLayout.Y_AXIS));
699        JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
700        jcbLabels = new JCheckBox("Labels On/Off");
701        jcbLabels.setSelected(true);
702        jcbLabels.setName(CHECKBOX_LABELS);
703        jcbLabels.addItemListener(this);
704        labelPanel.add(jcbLabels);
705        
706        // same row, add label interval spinner
707        Integer defaultInterval = new Integer(5); 
708        Integer minInterval = new Integer(1);
709        Integer maxInterval = new Integer(120); 
710        Integer intervalStep = new Integer(1); 
711        SpinnerNumberModel snm = 
712                        new SpinnerNumberModel(defaultInterval, minInterval, maxInterval, intervalStep);
713        JLabel intervalLabel = new JLabel("Label Interval:");
714        JLabel intervalUnits = new JLabel("minutes");
715        js = new JSpinner(snm);
716        labelPanel.add(Box.createHorizontalStrut(5));
717        labelPanel.add(intervalLabel);
718        labelPanel.add(js);
719        labelPanel.add(intervalUnits);
720        
721        // line style for drawing swath track and width edges
722        jcbTrackLineStyle.addActionListener(this);
723        // init to solid
724        jcbTrackLineStyle.setSelectedIndex(0);
725        curTrackLineStyle = jcbTrackLineStyle.getSelectedIndex();
726
727        jcbEdgeLineStyle.addActionListener(this);
728        // init to dashed
729        jcbEdgeLineStyle.setSelectedIndex(1);
730        curEdgeLineStyle = jcbEdgeLineStyle.getSelectedIndex();
731        
732        fontSizePanel.add(labelPanel);
733        JPanel botPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
734        botPanel.add(new JLabel("Font: "));
735        botPanel.add(otFontSelector.getComponent());
736        fontSizePanel.add(botPanel);
737
738        colorSwatch = new ColorSwatchComponent(getStore(), curSwathColor, "Color");
739        colorSwatch.setPreferredSize(new Dimension(30, 30));
740        
741        colorPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
742        colorPanel.add(new JLabel("Color: "));
743        colorPanel.add(colorSwatch);
744        
745        colorPanel.add(Box.createHorizontalStrut(5));
746        colorPanel.add(new JLabel("Track Width: "));
747        colorPanel.add(jcbSCLineWidth);
748        
749        colorPanel.add(Box.createHorizontalStrut(4));
750        colorPanel.add(new JLabel("Track Style: "));
751        colorPanel.add(jcbTrackLineStyle);
752        
753        colorPanel.add(Box.createHorizontalStrut(5));
754        colorPanel.add(new JLabel("Edge Width: "));
755        colorPanel.add(jcbSELineWidth);
756        
757        colorPanel.add(Box.createHorizontalStrut(4));
758        colorPanel.add(new JLabel("Edge Style: "));
759        colorPanel.add(jcbEdgeLineStyle);
760        
761        JPanel groundStationPanel = makeGroundStationPanel();
762
763        swathWidthPanel = makeSwathWidthPanel();
764        
765        JPanel outerPanel = new JPanel(new MigLayout());
766        
767        JPanel mainPanel = new JPanel(new MigLayout());
768        mainPanel.setBorder(BorderFactory.createTitledBorder(" Swath Controls "));
769        mainPanel.add(swathWidthPanel, "wrap");
770        mainPanel.add(fontSizePanel, "wrap");
771        mainPanel.add(colorPanel, "wrap");
772        JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
773        JButton applySwathMods = new JButton("Apply");
774        applySwathMods.setActionCommand(SWATH_MODS);
775        applySwathMods.addActionListener(this);
776        bottomPanel.add(applySwathMods);
777        mainPanel.add(bottomPanel);
778        
779        outerPanel.add(mainPanel, "wrap");
780        outerPanel.add(groundStationPanel, "wrap");
781        
782        return outerPanel;
783    }
784
785    private CurveDrawer makeCoverageCircle(double lat, double lon, double satAlt, Color color) {
786
787        /* mean Earth radius in km */
788        double earthRadius = AstroConst.R_Earth_mean / 1000.0;
789        satAlt += earthRadius;
790        double SAC = Math.PI / 2.0 + Math.toRadians(curAngle);
791        double sinASC = earthRadius * Math.sin(SAC) / satAlt;
792        double dist = earthRadius * (Math.PI - SAC - Math.asin(sinASC));
793        double rat = dist / earthRadius;
794
795        int npts = 360;
796        float[][] latlon = new float[2][npts];
797        double cosDist = Math.cos(rat);
798        double sinDist = Math.sin(rat);
799        double sinLat = Math.sin(lat);
800        double cosLat = Math.cos(lat);
801        double sinLon = -Math.sin(lon);
802        double cosLon = Math.cos(lon);
803        for (int i = 0; i < npts; i++) {
804            double azimuth = Math.toRadians((double) i);
805            double cosBear = Math.cos(azimuth);
806            double sinBear = Math.sin(azimuth);
807            double z = cosDist * sinLat +
808                       sinDist * cosLat * cosBear;
809            double y = cosLat * cosLon * cosDist +
810                       sinDist * (sinLon * sinBear - sinLat * cosLon * cosBear);
811            double x = cosLat * sinLon * cosDist -
812                       sinDist * (cosLon * sinBear + sinLat * sinLon * cosBear);
813            double r = Math.sqrt(x * x + y * y);
814            double latRad = Math.atan2(z, r);
815            double lonRad = 0.0;
816            if (r > 0.0) lonRad = -Math.atan2(x, y);
817            latlon[0][i] = (float) Math.toDegrees(latRad);
818            latlon[1][i] = (float) Math.toDegrees(lonRad);
819        }
820        CurveDrawer coverageCircle = null;
821        try {
822            Gridded2DSet circle = new Gridded2DSet(RealTupleType.LatitudeLongitudeTuple,
823                               latlon, npts);
824            SampledSet[] set = new SampledSet[1];
825            set[0] = circle;
826            UnionSet uset = new UnionSet(set);
827            coverageCircle = new CurveDrawer(uset);
828            coverageCircle.setLineWidth(jcbGSLineWidth.getSelectedIndex() + 1);
829            coverageCircle.setLineStyle(jcbStationLineStyle.getSelectedIndex());
830            coverageCircle.setColor(getAntColor());
831            coverageCircle.setData(uset);
832            coverageCircle.setDrawingEnabled(false);
833            if (! inGlobeDisplay()) 
834                coverageCircle.setConstantPosition(gsZ, navDsp.getDisplayAltitudeType());
835        } catch (Exception e) {
836            e.printStackTrace();
837            return null;
838        }
839        return coverageCircle;
840    }
841
842    public Color getAntColor() {
843        if (antColor == null) antColor = defaultAntColor;
844        return antColor;
845    }
846
847    public PolarOrbitTrackDataSource getDataSource() {
848        DataSourceImpl ds = null;
849        List dataSources = getDataSources();
850        boolean gotit = false;
851        if (!dataSources.isEmpty()) {
852            int nsrc = dataSources.size();
853            for (int i = 0; i < nsrc; i++) {
854                ds = (DataSourceImpl) dataSources.get(nsrc - i - 1);
855                if (ds instanceof PolarOrbitTrackDataSource) {
856                    gotit = true;
857                    break;
858                }
859            }
860        }
861        if (!gotit) return null;
862        return (PolarOrbitTrackDataSource) ds;
863    }
864
865        /* (non-Javadoc)
866         * @see ucar.unidata.idv.control.DisplayControlImpl#getDisplayListData()
867         */
868        @Override
869        protected Data getDisplayListData() {
870                // get time range that was specified in the Field Selector
871                String startTime = (String) getDataInstance().getDataSelection().getProperties().get(TimeRangeSelection.PROP_BEGTIME);
872                String endTime = (String) getDataInstance().getDataSelection().getProperties().get(TimeRangeSelection.PROP_ENDTIME);
873                
874                // get the template used for the Display Properties Layer Label
875                String labelTemplate = getDisplayListTemplate();
876
877                // see if time macro is enabled
878                boolean hasTimeMacro = UtcDate.containsTimeMacro(labelTemplate);
879                
880                // fetch the label superclass would normally generate
881                Data data = super.getDisplayListData();
882                
883                // if so, modify label with time range for this selection
884                if (hasTimeMacro) {
885                        try {
886                                TextType tt = TextType.getTextType(DISPLAY_LIST_NAME);
887                                data  = new Text(tt, data.toString() + startTime + " - " + endTime);
888                        } catch (VisADException vade) {
889                                vade.printStackTrace();
890                        }
891                }
892                
893                // return either original or modified data object
894                return data;
895        }
896
897        public double getLatitude() {
898        return latitude;
899    }
900
901    public double getLongitude() {
902        return longitude;
903    }
904
905    public String getStation() {
906        return station;
907    }
908
909    private float[][][] getSwath(float[][] track) {
910        double earthRadius = AstroConst.R_Earth_mean / 1000.0;
911        int npt = track[0].length-1;
912        float[][][] ret = new float[2][2][npt - 1];
913        try {
914            int indx = 0;
915            for (int i = 1; i < npt; i++) {
916                double latA = Math.toRadians(track[0][i - 1]);
917                double lonA = Math.toRadians(track[1][i - 1]);
918
919                double latB = Math.toRadians(track[0][i + 1]);
920                double lonB = Math.toRadians(track[1][i + 1]);
921
922                double diffLon = lonB - lonA;
923                double bX = Math.cos(latB) * Math.cos(diffLon);
924                double bY = Math.cos(latB) * Math.sin(diffLon);
925                double xFac = Math.cos(latA) + bX;
926                double latC = Math.atan2(Math.sin(latA) + Math.sin(latB), Math.sqrt(xFac * xFac + bY * bY));
927                double lonC = lonA + Math.atan2(bY, xFac);
928
929                double bearing = Math.atan2(Math.sin(diffLon) * Math.cos(latB),
930                                 Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(diffLon))
931                                 + Math.PI / 2.0;
932                double dist = curWidth / 2.0;
933                dist /= earthRadius;
934                double lat = Math.asin(Math.sin(latC) * Math.cos(dist) +
935                                       Math.cos(latC) * Math.sin(dist) * Math.cos(bearing));
936                double lon = lonC + Math.atan2(Math.sin(bearing) * Math.sin(dist) * Math.cos(latC),
937                                               Math.cos(dist) - Math.sin(latC) * Math.sin(lat));
938                float latD = (float) Math.toDegrees(lat);
939                float lonD = (float) Math.toDegrees(lon);
940
941                bearing += Math.PI;
942                lat = Math.asin(Math.sin(latC) * Math.cos(dist) +
943                                       Math.cos(latC) * Math.sin(dist) * Math.cos(bearing));
944                lon = lonC + Math.atan2(Math.sin(bearing) * Math.sin(dist) * Math.cos(latC),
945                                               Math.cos(dist) - Math.sin(latC) * Math.sin(lat));
946                float latE = (float) Math.toDegrees(lat);
947                float lonE = (float) Math.toDegrees(lon);
948
949                ret[0][0][indx] = latD;
950                ret[0][1][indx] = lonD;
951
952                ret[1][0][indx] = latE;
953                ret[1][1][indx] = lonE;
954                ++indx;
955            }
956        } catch (Exception e) {
957            e.printStackTrace();
958            return null;
959        }
960        return ret;
961    }
962
963    @Override public boolean init(DataChoice dataChoice) 
964        throws VisADException, RemoteException 
965    {
966        logger.debug("init() in...");
967        
968        PolarOrbitTrackDataSource potdc = getDataSource();
969
970        // validate time range before going ahead with control initialization
971        if (! potdc.getTrs().begTimeOk()) {
972                JOptionPane.showMessageDialog(null, 
973                                "Invalid start time, must follow format HH:MM:SS", 
974                                "Time Range Selection Error", JOptionPane.ERROR_MESSAGE);
975                return false;
976        }
977        
978        if (! potdc.getTrs().endTimeOk()) {
979                JOptionPane.showMessageDialog(null, 
980                                "Invalid end time, must follow format HH:MM:SS", 
981                                "Time Range Selection Error", JOptionPane.ERROR_MESSAGE);
982                return false;
983        }
984        
985        if (! potdc.getTrs().timeRangeOk()) {
986                JOptionPane.showMessageDialog(null, 
987                                "Invalid time range selection, please correct", 
988                                "Time Range Selection Error", JOptionPane.ERROR_MESSAGE);
989                return false;
990        }
991        
992        // allow at most two full days of orbit tracks - more than this will
993        // at best clutter the display and at worst grind McV indefinitely
994        long timeDiff = potdc.getTrs().getTimeRangeInSeconds();
995        if (timeDiff >= (60 * 60 * 24 * 2)) {
996                JOptionPane.showMessageDialog(null, 
997                                "Time range greater than two full days is not allowed, please correct", 
998                                "Time Range Selection Error", JOptionPane.ERROR_MESSAGE);
999                return false;           
1000        }
1001        
1002        // instantiate components we need to exist at initialization
1003        latLabel = new JLabel();
1004        lonLabel = new JLabel();
1005        altLabel = new JLabel();
1006        String [] lineWidths = {"1", "2", "3", "4"};
1007        jcbGSLineWidth = new JComboBox(lineWidths);
1008        jcbSCLineWidth = new JComboBox(lineWidths);
1009        // initialize swath center (track line) to width 2
1010        jcbSCLineWidth.setSelectedIndex(1);
1011        jcbEdgeLineStyle.setSelectedIndex(1);
1012        jcbSELineWidth = new JComboBox(lineWidths);
1013        otFontSelector = new FontSelector(FontSelector.COMBOBOX_UI, false, false);
1014        otFontSelector.setFont(FontSelector.DEFAULT_FONT);
1015        gsFontSelector = new FontSelector(FontSelector.COMBOBOX_UI, false, false);
1016        gsFontSelector.setFont(FontSelector.DEFAULT_FONT);
1017        this.dataChoice = dataChoice;
1018        String choiceName = dataChoice.getName();
1019        NodeList nodeList = root.getElementsByTagName(TAG_SATELLITE);
1020        int num = nodeList.getLength();
1021        if (num > 0) {
1022            for (int i = 0; i < num; i++) {
1023                Element n = (Element) (nodeList.item(i));
1024                String satName = n.getAttribute(ATTR_NAME);
1025                if (satName.equals(choiceName)) {
1026                    String strWidth = n.getAttribute(ATTR_WIDTH);
1027                    if (strWidth.isEmpty()) strWidth = "0";
1028                    Double dWidth = new Double(strWidth);
1029                    curWidth = dWidth.doubleValue();
1030                    break;
1031                }
1032            }
1033        }
1034        try {
1035            trackDsp = new CompositeDisplayable();
1036            timeLabelDsp = new CompositeDisplayable();
1037            stationLabelDsp = new CompositeDisplayable();
1038            swathEdgeDsp = new CompositeDisplayable();
1039            circleDsp = new CompositeDisplayable();
1040        } catch (Exception e) {
1041            logger.error("problem creating composite displayable");
1042            e.printStackTrace();
1043            return false;
1044        }
1045        boolean result = super.init((DataChoice) this.getDataChoices().get(0));
1046
1047        String dispName = getDisplayName();
1048        setDisplayName(getLongParamName() + " " + dispName);
1049        logger.debug("Setting display name: " + getDisplayName());
1050        try {
1051            String longName = getLongParamName().replaceAll(" ", "");
1052            otTextType = new TextType(SWATH_MODS + longName);
1053            gsTextType = new TextType(STATION_MODS + longName);
1054        } catch (Exception e) {
1055                e.printStackTrace();
1056            otTextType = TextType.Generic;
1057            gsTextType = TextType.Generic;
1058        }
1059
1060        Data data = getData(getDataInstance());
1061        createTrackDisplay(data, true);
1062        dataSource = getDataSource();
1063        try {
1064            navDsp = getNavigatedDisplay();
1065            float defaultZ = getMapViewManager().getDefaultMapPosition();
1066            // we're just nudging a bit so tracks (and their labels) get drawn over
1067            // ground stations (and their labels), which get drawn over default map level
1068            // user can change this in map controls if they prefer maps on top
1069            gsZ = defaultZ + 0.01f;
1070            trackZ = defaultZ + 0.02f;
1071            // range on "map level" stuff is -1 to 1, stay within these limits
1072            if (trackZ > 1.0f) trackZ = 1.0f;
1073            if (gsZ > 1.0f) gsZ = 1.0f;
1074            if (! inGlobeDisplay()) {
1075                applyTrackPosition();
1076            }
1077        } catch (Exception e) {
1078            logger.error("get display center e=" + e);
1079        }
1080
1081        return result;
1082    }
1083
1084    public void itemStateChanged(ItemEvent ie) {
1085
1086        // now we got multiple checkboxes, so first see which one applies
1087        String source = ((JCheckBox) ie.getSource()).getName();
1088        try {
1089                if (source.equals(CHECKBOX_LABELS)) {
1090                        if (ie.getStateChange() == ItemEvent.DESELECTED) {
1091                                timeLabelDsp.setVisible(false);
1092                        } else {
1093                                timeLabelDsp.setVisible(true);
1094                        }
1095                                updateDisplayList();
1096                }
1097                if (source.equals(CHECKBOX_SWATH_EDGES)) {
1098                        if (ie.getStateChange() == ItemEvent.DESELECTED) {
1099                                swathEdgeDsp.setVisible(false);
1100                        } else {
1101                                swathEdgeDsp.setVisible(true);
1102                        }
1103                                updateDisplayList();
1104                }
1105        } catch (RemoteException re) {
1106                re.printStackTrace();
1107        } catch (VisADException vade) {
1108                vade.printStackTrace();
1109        }
1110
1111    }
1112
1113    private void labelGroundStation(String station) {
1114        try {
1115                String str = "+ " + station;
1116                logger.debug("Drawing station: " + str);
1117
1118                TextDisplayable groundStationDsp = new TextDisplayable(STATION_MODS, gsTextType);
1119                groundStationDsp.setJustification(TextControl.Justification.LEFT);
1120                groundStationDsp.setVerticalJustification(TextControl.Justification.CENTER);
1121                groundStationDsp.setColor(getAntColor());
1122                groundStationDsp.setFont(gsFontSelector.getFont());
1123                groundStationDsp.setTextSize((float) gsFontSelector.getFontSize() / FONT_SCALE_FACTOR);
1124                groundStationDsp.setSphere(inGlobeDisplay());
1125
1126                double dlat = getLatitude();
1127                double dlon = getLongitude();
1128                RealTuple lonLat =
1129                                new RealTuple(RealTupleType.SpatialEarth2DTuple,
1130                                                new double[] { dlon, dlat });
1131                Tuple tup = new Tuple(makeTupleType(STATION_MODS),
1132                                new Data[] { lonLat, new Text(gsTextType, str)});
1133                groundStationDsp.setData(tup);
1134                if (! inGlobeDisplay()) 
1135                        groundStationDsp.setConstantPosition(gsZ, navDsp.getDisplayAltitudeType());
1136                stationLabelDsp.addDisplayable(groundStationDsp);
1137        } catch (Exception e) {
1138                e.printStackTrace();
1139        }
1140    }
1141
1142    private JPanel makeGroundStationPanel() {
1143        
1144        JPanel jp = new JPanel(new MigLayout());
1145        jp.setBorder(BorderFactory.createTitledBorder(" Ground Station Controls "));
1146
1147        jcbStationLineStyle = new JComboBox(lineStyles);
1148        jcbStationLineStyle.addActionListener(this);
1149        jcbStationLineStyle.setSelectedIndex(1);
1150        prvStationLineStyle = jcbStationLineStyle.getSelectedIndex();
1151        
1152        locationComboBox = new JComboBox();
1153        jcbStationsPlotted = new JComboBox();
1154
1155        // Ground Stations are now a natural-order map (alphabetical)
1156        GroundStations gs = new GroundStations(null);
1157        stationMap = gs.getGroundStations();
1158        TreeSet<String> keySet = new TreeSet<String>(stationMap.keySet());
1159
1160        GuiUtils.setListData(locationComboBox, keySet.toArray());
1161
1162        // initialize with first Earth Location in our map
1163        if (! stationMap.isEmpty()) {
1164                EarthLocationTuple elt = stationMap.get(locationComboBox.getSelectedItem());
1165                latLabel.setText(elt.getLatitude().toString());
1166                lonLabel.setText(elt.getLongitude().toString());
1167                altLabel.setText(elt.getAltitude().toString());
1168        }
1169        
1170        locationPanel = new JPanel();
1171        locationPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1172        locationPanel.add(new JLabel("Ground Stations Available:"));
1173        locationPanel.add(locationComboBox);
1174        JButton addButton = new JButton("Add Selected");
1175        addButton.setActionCommand(STATION_ADD);
1176        addButton.addActionListener(this);
1177        locationPanel.add(addButton);
1178        
1179        JPanel customPanel = new JPanel();
1180        customPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1181        customPanel.add(new JLabel("Custom Ground Station:    Label: "));
1182        customLab = new JTextField(6);
1183        customPanel.add(customLab);
1184        customPanel.add(new JLabel("Latitude: "));
1185        customLat = new JTextField(6);
1186        customPanel.add(customLat);
1187        customPanel.add(new JLabel("Longitude: "));
1188        customLon = new JTextField(6);
1189        customPanel.add(customLon);
1190        JButton customButton = new JButton("Add Custom");
1191        customButton.setActionCommand(CUSTOM_ADD);
1192        customButton.addActionListener(this);
1193        customPanel.add(customButton);
1194        
1195        JPanel plottedStationsPanel = new JPanel();
1196        plottedStationsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1197        plottedStationsPanel.add(new JLabel("Ground Stations Plotted:"));
1198        plottedStationsPanel.add(jcbStationsPlotted);
1199        JButton remButton = new JButton("Remove Selected");
1200        remButton.setActionCommand(STATION_REM);
1201        remButton.addActionListener(this);
1202        plottedStationsPanel.add(remButton);
1203        
1204        latLonAltPanel = new JPanel();
1205        latLonAltPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1206        
1207        latLonAltPanel.add(new JLabel("Last Station Plotted,  Latitude: "));
1208        latLonAltPanel.add(latLabel);
1209        latLonAltPanel.add(Box.createHorizontalStrut(5));
1210        
1211        latLonAltPanel.add(new JLabel("Longitude: "));
1212        latLonAltPanel.add(lonLabel);
1213        latLonAltPanel.add(Box.createHorizontalStrut(5));
1214        
1215        latLonAltPanel.add(new JLabel("Altitude: "));
1216        latLonAltPanel.add(altLabel);
1217        
1218        JPanel gsFontPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1219        gsFontPanel.add(new JLabel("Font: "));
1220        gsFontPanel.add(gsFontSelector.getComponent());
1221        
1222        Color swatchAntColor = getAntColor();
1223        antColorSwatch = new ColorSwatchComponent(getStore(), swatchAntColor, "Color");
1224        antColorSwatch.setPreferredSize(new Dimension(30, 30));
1225        
1226        antColorPanel = new JPanel();
1227        antColorPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
1228        antColorPanel.add(new JLabel("Color: "));
1229        antColorPanel.add(antColorSwatch);
1230        
1231        antColorPanel.add(Box.createHorizontalStrut(5));
1232        antColorPanel.add(new JLabel("Line Width: "));
1233        antColorPanel.add(jcbGSLineWidth);
1234        
1235        antColorPanel.add(Box.createHorizontalStrut(5));
1236        antColorPanel.add(new JLabel("Line Style: "));
1237        antColorPanel.add(jcbStationLineStyle);
1238        
1239        antColorPanel.add(Box.createHorizontalStrut(5));
1240        antColorPanel.add(new JLabel("Antenna Angle: "));
1241        antColorPanel.add(antennaAngle);
1242        
1243        jp.add(locationPanel, "wrap");
1244        jp.add(customPanel, "wrap");
1245        jp.add(plottedStationsPanel, "wrap");
1246        jp.add(latLonAltPanel, "wrap");
1247        jp.add(Box.createVerticalStrut(5), "wrap");
1248        jp.add(gsFontPanel, "wrap");
1249        jp.add(antColorPanel, "wrap");
1250        
1251        JPanel bottomRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
1252        JButton applyGroundStationMods = new JButton("Apply");
1253        applyGroundStationMods.setActionCommand(STATION_MODS);
1254        applyGroundStationMods.addActionListener(this);
1255        bottomRow.add(applyGroundStationMods);
1256        jp.add(bottomRow);
1257        
1258        return jp;
1259    }
1260
1261    private JPanel makeSwathWidthPanel() {
1262        if (dataChoice != null)
1263            satelliteName = new JLabel(dataChoice.getName());
1264        Integer isw = new Integer((int) curWidth);
1265        swathWidthFld = new JTextField(isw.toString(), 5);
1266        if (curWidth == 0) swathWidthFld.setText(SWATH_NA);
1267
1268        JPanel jp = new JPanel(new FlowLayout(FlowLayout.LEFT));
1269        
1270        // first on this panel, check box to turn on/off swath line edges
1271        jcbSwathEdges = new JCheckBox("Swath Edges On/Off");
1272        jcbSwathEdges.setSelected(true);
1273        jcbSwathEdges.setName(CHECKBOX_SWATH_EDGES);
1274        jcbSwathEdges.addItemListener(this);
1275        jp.add(jcbSwathEdges);
1276        
1277        jp.add(Box.createHorizontalStrut(5));
1278        jp.add(new JLabel("Satellite: "));
1279        jp.add(satelliteName);
1280        jp.add(Box.createHorizontalStrut(5));
1281        jp.add(new JLabel("Swath Width: "));
1282        jp.add(swathWidthFld);
1283        jp.add(kmLabel);
1284        jp.add(Box.createHorizontalStrut(5));
1285        
1286        return jp;
1287    }
1288
1289    private TupleType makeTupleType(String prefix) {
1290        TupleType t = null;
1291        try {
1292                if (prefix.equals(SWATH_MODS))
1293                        t = new TupleType(new MathType[] {RealTupleType.SpatialEarth2DTuple,
1294                                              otTextType});
1295                if (prefix.equals(STATION_MODS))
1296                        t = new TupleType(new MathType[] {RealTupleType.SpatialEarth2DTuple,
1297                                              gsTextType});
1298        } catch (Exception e) {
1299            e.printStackTrace();
1300        }
1301        return t;
1302    }
1303
1304    private void plotCoverageCircles() {
1305        try {
1306
1307                int num = circleDsp.displayableCount();
1308            for (int i = 0; i < num; i++) {
1309                // yes, always 0th it's a queue
1310                circleDsp.removeDisplayable(0);
1311            }
1312            removeDisplayable(circleDsp);
1313            circleDsp = null;
1314                int numLabels = stationLabelDsp.displayableCount();
1315                for (int i = 0; i < numLabels; i++) {
1316                        // yes, always 0th it's a queue
1317                        stationLabelDsp.removeDisplayable(0);
1318                }
1319
1320                int numPlotted = jcbStationsPlotted.getItemCount();
1321            circleDsp = new CompositeDisplayable();
1322            if (! inGlobeDisplay()) 
1323                circleDsp.setConstantPosition(gsZ, navDsp.getDisplayAltitudeType());
1324                for (int i = 0; i < numPlotted; i++) {
1325                        String s = (String) jcbStationsPlotted.getItemAt(i);
1326                EarthLocationTuple elt = stationMap.get(s);
1327                latLabel.setText(elt.getLatitude().toString());
1328                lonLabel.setText(elt.getLongitude().toString());
1329                altLabel.setText(elt.getAltitude().toString());
1330                // quick and easy way to limit sig digits to something not too crazy
1331                if (altLabel.getText().length() > 10) altLabel.setText(altLabel.getText().substring(0, 9));
1332                latitude = Double.parseDouble(latLabel.getText());
1333                longitude = Double.parseDouble(lonLabel.getText());
1334                setSatelliteAltitude(dataSource.getNearestAltToGroundStation(latitude, longitude) / 1000.0);
1335                
1336                CurveDrawer cd = makeCoverageCircle(Math.toRadians(latitude), Math.toRadians(longitude),
1337                        satelliteAltitude, getAntColor());
1338                if (cd != null) {
1339                        logger.debug("Drawing ground station, station name: " + s);
1340                    labelGroundStation(s);
1341                    circleDsp.setColor(getAntColor());
1342                    cd.setLineWidth(jcbGSLineWidth.getSelectedIndex() + 1);
1343                    circleDsp.addDisplayable(cd);
1344                }
1345                }
1346                addDisplayable(circleDsp, FLAG_COLORTABLE);
1347
1348        } catch (Exception e) {
1349            e.printStackTrace();
1350        }
1351    }
1352
1353    public void setAntColor(Color c) {
1354        if (c == null) c = defaultAntColor;
1355        try {
1356            antColor = c;
1357            circleDsp.setColor(c);
1358        } catch (Exception e) {
1359            logger.error("Exception in PolarOrbitTrackControl.setAntColor e=" + e);
1360        }
1361    }
1362        
1363    private void setSatelliteAltitude(double val) {
1364        satelliteAltitude = val;
1365    }
1366    
1367    public void setStation(String val) {
1368        station = val.trim();
1369    }
1370    
1371    private int validateSwathWidthField() {
1372        String s = swathWidthFld.getText().trim();
1373        int val = -1;
1374        try {
1375                val = Integer.parseInt(s);
1376        } catch (NumberFormatException nfe) {
1377                // TJJ Jun 2015 - if GEO sensor, N/A means return invalid, but no warning msg needed
1378                if ((s != null) && (s.equals(SWATH_NA))) {
1379                        return -1;
1380                }
1381                // throw up a dialog to tell user the problem
1382                JOptionPane.showMessageDialog(latLonAltPanel, 
1383                                "Invalid swath width: must be an integer value in km");
1384                return -1;
1385        }
1386
1387        if ((val < SWATH_WIDTH_MIN) || (val > SWATH_WIDTH_MAX)) {
1388                // throw up a dialog to tell user the problem
1389                JOptionPane.showMessageDialog(latLonAltPanel, 
1390                                "Swath width valid range is " + SWATH_WIDTH_MIN + 
1391                                " to " + SWATH_WIDTH_MAX + " km");
1392                return -1;
1393        }
1394        return val;
1395    }
1396    
1397}