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