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