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.data;
030
031import java.io.BufferedReader;
032import java.io.File;
033import java.io.InputStreamReader;
034import java.net.MalformedURLException;
035import java.net.URI;
036import java.net.URL;
037import java.net.URLConnection;
038import java.nio.file.Paths;
039import java.rmi.RemoteException;
040import java.util.ArrayList;
041import java.util.Hashtable;
042import java.util.List;
043import java.util.Objects;
044import java.util.Vector;
045
046import javax.swing.JOptionPane;
047
048import com.google.common.net.InternetDomainName;
049import jsattrak.objects.SatelliteTleSGP4;
050import jsattrak.utilities.TLE;
051import name.gano.astro.propogators.sgp4_cssi.SGP4SatData;
052import name.gano.astro.time.Time;
053
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057import ucar.unidata.data.DataCategory;
058import ucar.unidata.data.DataChoice;
059import ucar.unidata.data.DataSelection;
060import ucar.unidata.data.DataSelectionComponent;
061import ucar.unidata.data.DataSourceDescriptor;
062import ucar.unidata.data.DataSourceImpl;
063import ucar.unidata.data.DirectDataChoice;
064import ucar.unidata.idv.IntegratedDataViewer;
065import ucar.unidata.util.IOUtil;
066import ucar.unidata.util.StringUtil;
067import visad.Data;
068import visad.Text;
069import visad.Tuple;
070import visad.VisADException;
071import visad.georef.LatLonTuple;
072import edu.wisc.ssec.mcidas.adde.AddeTextReader;
073import edu.wisc.ssec.mcidasv.chooser.PolarOrbitTrackChooser;
074import edu.wisc.ssec.mcidasv.util.XmlUtil;
075
076/**
077 * Class for Two-Line-Element data sources, to plot orbit tracks
078 * on McIDAS-V display window.
079 *
080 * @author Gail Dengel and Tommy Jasmin
081 * @version $Revision$
082 */
083
084public class PolarOrbitTrackDataSource extends DataSourceImpl {
085
086    private static final Logger logger =
087        LoggerFactory.getLogger(PolarOrbitTrackDataSource.class);
088
089    private ArrayList<String> tleCards = new ArrayList<>();
090    private ArrayList<String> choices = new ArrayList<>();
091
092    private SGP4SatData data = new SGP4SatData();
093    private TLE tle;
094
095    private Hashtable selectionProps;
096
097    /** time step between data points */
098    private int dTime = 1;
099
100    private SatelliteTleSGP4 prop = null;
101    private double julDate0 = 0.0;
102    private double julDate1 = 0.0;
103    
104    TimeRangeSelection trs = null;
105
106    /**
107     * Default bean constructor for persistence; does nothing.
108     */
109    public PolarOrbitTrackDataSource() {}
110
111    /**
112     * Create a new PolarOrbitTrackDataSource
113     *
114     * @param descriptor    descriptor for this source
115     * @param filename      ADDE URL
116     * @param properties    extra properties for this source
117     *
118     */
119
120    public PolarOrbitTrackDataSource(DataSourceDescriptor descriptor,
121                                     String filename,
122                                     Hashtable properties)
123    {
124        super(descriptor, filename, null, properties);
125        tleCards = new ArrayList<>();
126        choices = new ArrayList<>();
127        
128        // we dealing with a local file?
129        if (properties.containsKey(PolarOrbitTrackChooser.LOCAL_FILE_KEY)) {
130            File f = (File) (properties.get(PolarOrbitTrackChooser.LOCAL_FILE_KEY));
131            logger.debug("Local file: {}", f.getName());
132            URI uri = f.toURI();
133            properties.put(PolarOrbitTrackChooser.URL_NAME_KEY, uri.toString());
134        }
135        
136        // not a file, must be URL or ADDE request
137        String key = PolarOrbitTrackChooser.TLE_SERVER_NAME_KEY;
138        if (properties.containsKey(key)) {
139            logger.debug("ADDE request...");
140            Object server = properties.get(key);
141            key = PolarOrbitTrackChooser.TLE_GROUP_NAME_KEY;
142            Object group = properties.get(key);
143            key = PolarOrbitTrackChooser.TLE_USER_ID_KEY;
144            Object user = properties.get(key);
145            key = PolarOrbitTrackChooser.TLE_PROJECT_NUMBER_KEY;
146            Object proj = properties.get(key);
147            key = PolarOrbitTrackChooser.DATASET_NAME_KEY;
148            String descr = (String) properties.get(key);
149            String url = "adde://" + server + "/textdata?&PORT=112&COMPRESS=gzip&USER=" + user + "&PROJ=" + proj + "&GROUP=" + group + "&DESCR=" + descr;
150            AddeTextReader reader = new AddeTextReader(url);
151            List lines = null;
152            if ("OK".equals(reader.getStatus())) {
153                lines = reader.getLinesOfText();
154            }
155            if (lines == null) {
156                notTLE();
157                return;
158            } else {
159                String[] cards = StringUtil.listToStringArray(lines);
160                for (int i = 0; i < cards.length; i++) {
161                    String str = cards[i];
162                    if (str.length() > 0) {
163                        
164                        // TJJ Mar 2018 - only take records that match descriptor
165                        // remove trailing description part of passed in descriptor
166                        String satOnly = descr;
167                        if (satOnly.indexOf(" ") > 0) {
168                            satOnly = satOnly.substring(0, satOnly.indexOf(" "));
169                        }
170                        if (str.startsWith(satOnly)) {
171                            tleCards.add(XmlUtil.stripNonValidXMLCharacters(cards[i]));
172                            
173                            // TJJ - wish I knew what these lines were all about!
174                            // I think I added them at some point - skip blank lines maybe?
175                            // Better just leave it for now.
176                            int indx = cards[i].indexOf(" ");
177                            if (indx < 0) {
178                                choices.add(XmlUtil.stripNonValidXMLCharacters(cards[i]));
179                            }
180                            
181                            // Grab the next two lines and exit loop
182                            tleCards.add(XmlUtil.stripNonValidXMLCharacters(cards[i + 1]));
183                            tleCards.add(XmlUtil.stripNonValidXMLCharacters(cards[i + 2]));
184                            break;
185                        }
186
187                    }
188                }
189            }
190        } else {
191            try {
192                key = PolarOrbitTrackChooser.URL_NAME_KEY;
193                String urlStr = (String)(properties.get(key));
194                logger.debug("URL request: {}", urlStr);
195
196                URLConnection urlCon = IOUtil.getUrlConnection(urlStr);
197                if (urlStr.startsWith("file:/")) {
198                    // "file:/" protocol prefix causes windows to barf
199                    // Windows path will look like "file:/C:/temp/weather.txt"
200                    String substr = urlStr.substring(6);
201                    setName(Paths.get(substr).getFileName().toString());
202                } else {
203                    setName(makeNameForRemoteSource(urlStr));
204                }
205                InputStreamReader isr = new InputStreamReader(urlCon.getInputStream());
206                BufferedReader tleReader = new BufferedReader(isr);
207                String nextLine = null;
208                while ((nextLine = tleReader.readLine()) != null) {
209                    if (!nextLine.isEmpty()) {
210                        tleCards.add(XmlUtil.stripNonValidXMLCharacters(nextLine));
211                        if (nextLine.length() < 50) {
212                            choices.add(XmlUtil.stripNonValidXMLCharacters(nextLine));
213                        }
214                    }
215                }
216            } catch (Exception e) {
217                notTLE();
218                logger.error("Could not complete URL request", e);
219                return;
220            }
221        }
222        checkFirstEntry();
223    }
224    
225    /**
226     * Create a nice looking name for this instance.
227     * 
228     * <p>Given a URL like
229     * {@code http://celestrak.com/NORAD/elements/weather.txt}, this method
230     * will return {@code celestrak: /NORAD/elements/weather.txt}.</p>
231     * 
232     * <p>If the hostname from {@code urlStr} could not be sufficiently reduced,
233     * this method will simply use the entire hostname. A URL like
234     * {@code http://adde.ssec.wisc.edu/weather.txt} will return
235     * {@code adde.ssec.wisc.edu: weather.txt}.</p>
236     * 
237     * <p>If there was a problem parsing {@code urlStr}, the method will try
238     * to return the filename. A URL like 
239     * {@code http://celestrak.com/NORAD/elements/weather.txt} would return 
240     * {@code weather.txt}.</p>
241     * 
242     * <p>If all of the above fails, {@code urlStr} will be returned.</p>
243     * 
244     * @param urlStr URL of the TLE information. Cannot be {@code null}.
245     * 
246     * @return Either the name as described above, or {@code null} if there was
247     *         a problem.
248     */
249    public static String makeNameForRemoteSource(String urlStr) {
250        Objects.requireNonNull(urlStr, "Cannot use a null URL string");
251        String result;
252        try {
253            URL url = new URL(urlStr);
254            String host = url.getHost();
255            String path = url.getPath();
256            
257            // thank you, guava!
258            InternetDomainName domain = InternetDomainName.from(host);
259            
260            // suffix will be something like 'com' or 'co.uk', so suffixStart
261            // needs to start one character earlier to remove the trailing '.'
262            String suffix = domain.publicSuffix().toString();
263            int suffixStart = host.indexOf(suffix) - 1;
264            String trimmed = host.substring(0, suffixStart);
265            
266            // Trying this with 'http://adde.ssec.wisc.edu/weather.txt' will
267            // result in trimmed being 'adde.ssec.wisc', and I imagine there
268            // are more edge cases. With that in mind, we just use the hostname
269            // if it looks like trimmed doesn't look nice
270            if (trimmed.indexOf('.') >  -1) {
271                result = host + ": " + path;
272            } else {
273                result = trimmed + ": " + path;
274            }
275        } catch (IllegalArgumentException e) {
276            // InternetDomainName.from() call likely failed; simply return
277            // original URL string as specified by the javadoc!
278            result = urlStr;
279            logger.warn("Problem with URL '"+urlStr+'\'', e);
280        } catch (MalformedURLException e) {
281            logger.error("Bad URL", e);
282            int lastSlash = urlStr.lastIndexOf('/');
283            if (lastSlash > -1) {
284                // need the "+1" to get rid of the slash
285                result = urlStr.substring(lastSlash + 1);
286            } else {
287                result = urlStr;
288            }
289        }
290        return result;
291    }
292    
293    private void checkFirstEntry() {
294        if (tleCards.isEmpty()) {
295            notTLE();
296            return;
297        }
298        String card = (String) tleCards.get(1);
299        decodeCard1(card);
300    }
301
302    private int checksum(String str) {
303        int sum = 0;
304        byte[] bites = str.getBytes();
305        for (int i = 0; i < bites.length; i++) {
306            int val = (int) bites[i];
307            if ((val > 47) && (val < 58)) {
308                sum += val - 48;
309            } else if (val == 45) {
310                ++sum;
311            }
312        }
313        return sum % 10;
314    }
315
316    private int decodeCard1(String card) {
317        logger.debug("Decoding card: {}", card);
318        int satId = 0;
319        double ddd = 1.0;
320        double firstDev = 1.0;
321        int ephemerisType = 0;
322        int elementNumber = 0;
323
324        int ret = 0;
325        if (card.length() < 69) {
326            notTLE();
327            return -1;
328        }
329        int ck1 = checksum(card.substring(0, 68));
330        String str = card.substring(0, 1);
331        if (str.equals("1")) {
332            satId = getInt(2, 7, card);
333            //System.out.println("    satId = " + satId);
334            data.satnum = satId;
335            ++ret;
336
337            data.classification = card.substring(7, 8);
338            data.intldesg = card.substring(9, 17);
339            int yy = getInt(18, 20, card);
340            data.epochyr = yy;
341            ++ret;
342
343            ddd = getDouble(20, 32, card);
344            //System.out.println("    ddd = " + ddd);
345            data.epochdays = ddd;
346            ++ret;
347
348            firstDev = getDouble(33, 43, card);
349            //System.out.println("    firstDev = " + firstDev);
350            data.ndot = firstDev;
351            ++ret;
352
353            if((card.substring(44, 52)).equals("        "))
354            {
355                data.nddot = 0;
356                data.nexp = 0;
357            }
358            else
359            {
360                data.nddot = getDouble(44, 50, card) / 1.0E5;
361                data.nexp = getInt(50, 52, card);
362            }
363            //System.out.println("    nddot=" + data.nddot);
364            //System.out.println("    nexp=" + data.nexp);
365
366            data.bstar = getDouble(53, 59, card) / 1.0E5;
367            data.ibexp = getInt(59, 61, card);
368            //System.out.println("    bstar=" + data.bstar);
369            //System.out.println("    ibexp=" + data.ibexp);
370
371            try {
372                ephemerisType = getInt(62, 63, card);
373                //System.out.println("    ephemerisType = " + ephemerisType);
374                data.numb = ephemerisType;
375                ++ret;
376
377                elementNumber = getInt(64, 68, card);
378                //System.out.println("    elementNumber = " + elementNumber);
379                data.elnum = elementNumber;
380                ++ret;
381            } catch (Exception e) {
382                logger.error("Warning: Error Reading numb or elnum from TLE line 1 sat#:" + data.satnum);
383            }
384
385            int check = card.codePointAt(68) - 48;
386            if (check != ck1) {
387                notTLE();
388//                logger.error("***** Failed checksum *****");
389                ret = -1;
390            }
391        }
392        return ret;
393    }
394
395    private int decodeCard2(String card) {
396/*
397        System.out.println("\ndecodeCard2:");
398        System.out.println("    card=" + card);
399        System.out.println("    length=" + card.length());
400*/
401        double inclination = 1.0;
402        double rightAscension = 1.0;
403        double eccentricity = 1.0;
404        double argOfPerigee = 1.0;
405        double meanAnomaly = 1.0;
406        double meanMotion = 1.0;
407        int revolutionNumber = 0;
408
409        int ret = 0;
410        //System.out.println("\n" + card);
411        if (card.length() < 69) {
412            notTLE();
413            return -1;
414        }
415        int ck1 = checksum(card.substring(0, 68));
416        String str = card.substring(0, 1);
417        if (str.equals("2")) {
418            int nsat = getInt(2, 7, card);
419            //System.out.println("    nsat = " + nsat + " data.satnum=" + data.satnum);
420            if (nsat != data.satnum) {
421                logger.error("Warning TLE line 2 Sat Num doesn't match line1 for sat: " + data.name);
422            } else {
423                inclination = getDouble(8, 16, card);
424                data.inclo = inclination;
425                //System.out.println("    inclo = " + data.inclo);
426                ++ret;
427
428                rightAscension = getDouble(17, 25, card);
429                data.nodeo = rightAscension;
430                //System.out.println("    nodeo = " + data.nodeo);
431                ++ret;
432
433                eccentricity = getDouble(26, 33, card) / 1.0E7;
434                data.ecco = eccentricity;
435                //System.out.println("    ecco = " + data.ecco);
436                ++ret;
437
438                argOfPerigee = getDouble(34, 42, card);
439                data.argpo = argOfPerigee;
440                //System.out.println("    argpo = " + data.argpo);
441                ++ret;
442
443                meanAnomaly = getDouble(43, 51, card);
444                data.mo = meanAnomaly;
445                //System.out.println("    mo = " + data.mo);
446                ++ret;
447
448                meanMotion = getDouble(52, 63, card);
449                data.no = meanMotion;
450                //System.out.println("    no = " + data.no);
451                ++ret;
452
453                try {
454                    revolutionNumber = getInt(63, 68, card);
455                    data.revnum = revolutionNumber;
456                    //System.out.println("    revnum = " + data.revnum);
457                    ++ret;
458                } catch (Exception e) {
459                    logger.error("Warning: Error Reading revnum from TLE line 2 sat#:" + data.satnum + "\n" + e.toString());
460                    data.revnum = -1;
461                }
462
463                int check = card.codePointAt(68) - 48;
464                if (check != ck1) {
465                    notTLE();
466//                    logger.error("***** Failed checksum *****");
467                    ret = -1;
468                }
469            }
470        }
471        return ret;
472    }
473
474    /**
475     * Make the data choices associated with this source.
476     */
477    protected void doMakeDataChoices() {
478        String category = "TLE";
479        for (int i = 0; i < choices.size(); i++) {
480            String name  = ((String) choices.get(i)).trim();
481            addDataChoice(
482                new DirectDataChoice(
483                    this, name, name, name,
484                    DataCategory.parseCategories(category, false)));
485        }
486    }
487
488    /**
489     * Actually get the data identified by the given DataChoce. The default is
490     * to call the getDataInner that does not take the requestProperties. This
491     * allows other, non unidata.data DataSource-s (that follow the old API)
492     * to work.
493     *
494     * @param dataChoice        The data choice that identifies the requested
495     *                          data.
496     * @param category          The data category of the request.
497     * @param dataSelection     Identifies any subsetting of the data.
498     * @param requestProperties Hashtable that holds any detailed request
499     *                          properties.
500     *
501     * @return The visad.Text object
502     *
503     * @throws RemoteException    Java RMI problem
504     * @throws VisADException     VisAD problem
505     */
506    
507    protected Data getDataInner(DataChoice dataChoice, DataCategory category,
508                                DataSelection dataSelection,
509                                Hashtable requestProperties)
510            throws VisADException, RemoteException {
511
512        boolean gotit = false;
513        int index = -1;
514        String choiceName = dataChoice.getName();
515        String tleLine1 = "";
516        String tleLine2 = "";
517
518        while (!gotit) {
519            index++;
520            String name = ((String) tleCards.get(index)).trim();
521            if (name.equals(choiceName)) {
522                data.name = name; 
523/*
524                System.out.println("\n" + tleCards.get(index));
525                System.out.println(tleCards.get(index+1));
526                System.out.println(tleCards.get(index+2) + "\n");
527*/
528                index++;
529                String card = (String) tleCards.get(index);
530                tleLine1 = card;
531                int ncomps = decodeCard1(card);
532                if (ncomps < 0) return null;
533                index++;
534                card = (String) tleCards.get(index);
535                tleLine2 = card;
536                ncomps += decodeCard2(card);
537                gotit= true;
538            }
539            if (index+3 > tleCards.size()) gotit = true;
540        }
541        if (gotit == false) return null;
542
543        this.selectionProps = dataSelection.getProperties();
544/*
545        Enumeration propEnum = this.selectionProps.keys();
546        for (int i = 0; propEnum.hasMoreElements(); i++) {
547            String key = propEnum.nextElement().toString();
548            String val = (String)this.selectionProps.get(key);
549            System.out.println("key=" + key + " val=" + val);
550        }
551*/
552        tle = new TLE(choiceName, tleLine1, tleLine2);
553
554        String endStr = (String) this.selectionProps.get("ETime");
555        Double dEnd = new Double(endStr);
556        double endJulianDate = dEnd.doubleValue();
557        julDate1 = endJulianDate;
558
559                try {
560                        prop = new SatelliteTleSGP4(tle.getSatName(), tle.getLine1(),
561                                        tle.getLine2());
562                        prop.setShowGroundTrack(false);
563                } catch (Exception e) {
564                        logger.error("Error Creating SGP4 Satellite", e);
565                        // WTF
566                        //System.exit(1);
567                }
568
569        Time time = new Time(
570                        (new Integer((String)this.selectionProps.get("Year"))).intValue(),
571                        (new Integer((String)this.selectionProps.get("Month"))).intValue(),
572                        (new Integer((String)this.selectionProps.get("Day"))).intValue(),
573                        (new Integer((String)this.selectionProps.get("Hours"))).intValue(),
574                        (new Integer((String)this.selectionProps.get("Mins"))).intValue(),
575                        (new Double((String)this.selectionProps.get("Secs"))).doubleValue());
576        double julianDate = time.getJulianDate();
577        julDate0 = julianDate;
578        Vector v = new Vector();
579
580        while (julianDate <= julDate1) {
581            // prop to the desired time
582            prop.propogate2JulDate(julianDate);
583
584            // get the lat/long/altitude [radians, radians, meters]
585            double[] lla = prop.getLLA();
586            double lat = lla[0] * 180.0 / Math.PI;
587            double lon = lla[1] * 180.0 / Math.PI;
588
589/*
590             System.out.println(time.getDateTimeStr() + " Lat: " + lat
591                                                      + " Lon: " + lon
592                                                      + " Alt: " + alt);
593 */
594            Tuple data = new Tuple(new Data[] { new Text(time.getDateTimeStr()),
595                                                new LatLonTuple(
596                                                    lat,
597                                                    lon
598                                                )}
599                         );
600            v.add(data);
601            time.add(Time.MINUTE, dTime);
602            julianDate = time.getJulianDate();
603        }
604
605        return new Tuple((Data[]) v.toArray(new Data[v.size()]), false);
606    }
607
608    private double getDouble(int beg, int end, String card) {
609        String str = card.substring(beg, end);
610        str = str.trim();
611        return (new Double(str)).doubleValue();
612    }
613
614    public int getDTime() {
615        return dTime;
616    }
617
618    private int getInt(int beg, int end, String card) {
619        String str = card.substring(beg, end);
620        str = str.trim();
621        int tmp = -1;
622        try {
623            tmp = Integer.valueOf(str);
624        } catch (NumberFormatException nfe) {
625            JOptionPane.showMessageDialog(
626                null, 
627                "Error parsing integer value from TLE card, potentially corrupt data source.", 
628                "Orbit Track Data Source Error", 
629                JOptionPane.ERROR_MESSAGE
630            );
631        }
632        return tmp;
633    }
634
635    /**
636     * choices needs to persist to support bundles
637     * @return the choices
638     */
639
640    public ArrayList<String> getChoices() {
641        return choices;
642    }
643
644    /**
645     * choices needs to persist to support bundles
646     * @param choices the choices to set
647     */
648
649    public void setChoices(ArrayList<String> choices) {
650        this.choices = choices;
651    }
652
653    /**
654     * tleCards needs to persist to support bundles
655     * @return the tleCards
656     */
657
658    public ArrayList<String> getTleCards() {
659        return tleCards;
660    }
661
662    /**
663     * tleCards needs to persist to support bundles
664     * @param tleCards the tleCards to set
665     */
666
667    public void setTleCards(ArrayList<String> tleCards) {
668        this.tleCards = tleCards;
669    }
670
671    /**
672     * @return the trs
673     */
674    public TimeRangeSelection getTrs() {
675        return trs;
676    }
677
678    public double getNearestAltToGroundStation(double gsLat, double gsLon) {
679        double retAlt = 0.0;
680        Time time = new Time(
681            (new Integer((String)this.selectionProps.get("Year"))).intValue(),
682            (new Integer((String)this.selectionProps.get("Month"))).intValue(),
683            (new Integer((String)this.selectionProps.get("Day"))).intValue(),
684            (new Integer((String)this.selectionProps.get("Hours"))).intValue(),
685            (new Integer((String)this.selectionProps.get("Mins"))).intValue(),
686            (new Double((String)this.selectionProps.get("Secs"))).doubleValue());
687
688        double minDist = 999999.99;
689        double julianDate = julDate0;
690
691        while (julianDate <= julDate1) {
692            // prop to the desired time
693            prop.propogate2JulDate(julianDate);
694
695            // get the lat/long/altitude [radians, radians, meters]
696            double[] lla = prop.getLLA();
697            double lat = lla[0] * 180.0 / Math.PI;
698            double lon = lla[1] * 180.0 / Math.PI;
699            double alt = lla[2];
700            //System.out.println("    " + time.getDateTimeStr() + ": lat=" + lat + " lon=" + lon + " alt=" + alt);
701
702            double latDiff = (gsLat - lat) * (gsLat - lat);
703            double lonDiff = (gsLon - lon) * (gsLon - lon);
704            double dist = Math.sqrt(latDiff+lonDiff);
705            if (dist < minDist) {
706                minDist = dist;
707                retAlt = alt;
708            }
709            time.add(Time.MINUTE, dTime);
710            julianDate = time.getJulianDate();
711        }
712
713        return retAlt;
714    }
715    
716    @Override public boolean canSaveDataToLocalDisk() {
717        return true;
718    }
719    
720    @Override protected List saveDataToLocalDisk(String filePrefix,
721                                                 Object loadId,
722                                                 boolean changeLinks)
723        throws Exception
724    {
725        Hashtable props = getProperties();
726        List result;
727        if (props.containsKey(PolarOrbitTrackChooser.URL_NAME_KEY)) {
728            List<String> urls = new ArrayList<>(1);
729            urls.add((String)props.get(PolarOrbitTrackChooser.URL_NAME_KEY));
730            result = IOUtil.writeTo(urls, filePrefix, ".txt", loadId);
731            String newUrl = "file:"+result.get(0);
732            props.put(PolarOrbitTrackChooser.URL_NAME_KEY, newUrl);
733        } else {
734            result = super.saveDataToLocalDisk(filePrefix, loadId, changeLinks);
735        }
736        return result;
737    }
738    
739    protected void initDataSelectionComponents(
740        List<DataSelectionComponent> components, final DataChoice dataChoice) {
741/*
742        System.out.println("\ninitDataSelectionComponents:");
743        System.out.println("    components=" + components);
744        System.out.println("    dataChoice=" + dataChoice);
745        System.out.println("        categories=" + dataChoice.getCategories());
746        System.out.println("        displayCategory=" + dataChoice.getDisplayCategory());
747*/
748        clearTimes();
749        IntegratedDataViewer idv = getDataContext().getIdv();
750        idv.showWaitCursor();
751        try {
752            trs = new TimeRangeSelection(this);
753            if (selectionProps != null) {
754                trs.applyFromDataSelectionProperties(selectionProps);
755            }
756            components.add(trs);
757        } catch (Exception e) {
758            logger.error("problem creating TimeRangeSelection e=" + e);
759        }
760        idv.showNormalCursor();
761    }
762
763    private void notTLE() {
764        tleCards = new ArrayList<>();
765        choices = new ArrayList<>();
766        setInError(true, "\nSource does not contain TLE data");
767    }
768
769    public void setDTime(int val) {
770        dTime = val;
771    }
772
773    /**
774     * Show the dialog
775     *
776     * @param initTabName What tab should we show. May be null.
777     * @param modal Is dialog modal
778     *
779     * @return success
780     */
781
782    public boolean showPropertiesDialog(String initTabName, boolean modal) {
783        boolean ret = super.showPropertiesDialog(initTabName, modal);
784        return ret;
785    }
786    
787    public void setSelectionProps(Hashtable newProperties) {
788        selectionProps.putAll(newProperties);
789    }
790
791}
792