skyfielders / python-skyfield

Elegant astronomy for Python
MIT License
1.43k stars 213 forks source link

Is there a way to get the TLE data of a satellite by ID, not by name? #167

Closed tmamedzadeh closed 6 years ago

tmamedzadeh commented 6 years ago

There is a code, describing the retrieval of satellite data from the TLE:

stations_url = 'http://celestrak.com/NORAD/elements/stations.txt'
satellites = load.tle(stations_url)
satellite = satellites['ISS (ZARYA)']

However, the names sometimes are changed, but ID is constant. Is there a way to get the data by ID, something like satellites[40053]? For now, my solution is just looping through the lines.

tmamedzadeh commented 6 years ago

Solved, loading the TLE directly from URL celestrak.com/cgi-bin/TLE.pl?CATNR=40052 and parsing achieved 3 lines.

Closing this issue, however this functionality would be useful.

brandon-rhodes commented 6 years ago

I didn't know that names changed! I think this is a good idea, and I've re-opened this issue so that I remember to add that feature (or so that someone else can).

murison commented 6 years ago

FWIW, another user finds it essential to be able to do satellites[cat Id #] instead of satellites[name]. I know you have a gazillion things to do, but do know this is not just a "gee, it'd be nice if..." need for some of us.

brandon-rhodes commented 6 years ago

If someone needs this quickly, I believe they can build their own dictionary with something like:

satcat = {sat.model.satnum: sat for sat in satellites.values()}

Then they could say satcat[...cat Id #...] — if someone could test this out and see if it works, a follow-up comment letting other users know will doubtless be appreciated!

murison commented 6 years ago

Oh, duh! Good idea. This, for example, works:

geosats = loader.tle(celestrak+'geo.txt')
[...]
satcats = [geosats, gpssats, sciencesats, stationsats, tdrss, visualsats]
sats = {}
for cat in satcats:
    for name in cat.keys():
        sat = cat[name]
        sats.update( {sat.model.satnum:sat, name:sat} )

Then one can access either way:

sats['GOES 16']
sats[41866]
brandon-rhodes commented 6 years ago

Very nice! You might find it slightly more efficient to:

sats[sat.model.satnum] = sat
sats[name] = sat

instead of building and then disposing of an entire new dictionary — but, honestly, you'll never notice the difference since it's plenty fast either way for the number of satellites involved. :)

murison commented 6 years ago

For completeness, and in case someone else might find it useful at some point:

import urllib
from skyfield import api as sf
datadir = os.environ['HOME']+'/programming/python/astro/skyfield-data/'
loader = sf.Loader(datadir, expire=False)
# Satellite TLEs.
celestrak = 'http://celestrak.com/NORAD/elements/'
geosats = loader.tle(celestrak+'geo.txt')
gpssats = loader.tle(celestrak+'supplemental/gps.txt')
sciencesats = loader.tle(celestrak+'science.txt')
stationsats = loader.tle(celestrak+'stations.txt')
tdrss = loader.tle(celestrak+'tdrss.txt')
visualsats = loader.tle(celestrak+'visual.txt')
# Make catalogs indexable by either name or catalog number.
# Also create sats, a merge of the individual ones.
satcats = [geosats, gpssats, sciencesats, stationsats, tdrss, visualsats]
sats = {}
for cat in satcats:
    names = [key for key in cat.keys()]
    for name in names:
        sat = cat[name]
        satnum = sat.model.satnum
        cat[satnum]  = sat
        sats[satnum] = sat
        sats[name]   = sat
def getsat(satid):
    """
    Return a skyfield EarthSatellite.

    <satid> is case independent if it is a satellite name (str).

    Retrieve directly from CelesTrak by catalog number if not in local database.
    """
    if isinstance(satid, str):
        satid = satid.upper()
    if satid in sats.keys():
        return sats[satid]
    if not isinstance(satid, int):
        msg = 'satid must be an integer for satellites not in the local set'
        raise Exception(msg)
    base = 'http://celestrak.com/cgi-bin/TLE.pl?CATNR='
    url = base + str(satid)
    with urllib.request.urlopen(url) as fd:
        lines = fd.readlines()
    for k, line in enumerate(lines):
        if 'PRE' in line.decode():
            name = lines[k+1].decode().strip()
            if name == 'No TLE found':
                msg = '%i is not in the CelesTrak database!' % satid
                raise Exception(msg)
            tle1 = lines[k+2].decode().strip()
            tle2 = lines[k+3].decode().strip()
            break
    sat = sf.EarthSatellite(tle1, tle2, name)
    return sat

>> sat = getsat(28129)
...print(sat)
...sat = getsat('goes 16')
...print(sat)

EarthSatellite 'GPS BIIR-10 (PRN 22)' number=28129 epoch=2018-04-21T19:50:06Z
EarthSatellite 'GOES 16' number=41866 epoch=2018-04-18T15:14:01Z
blast-inc commented 2 years ago

For completeness, and in case someone else might find it useful at some point:

import urllib
from skyfield import api as sf
datadir = os.environ['HOME']+'/programming/python/astro/skyfield-data/'
loader = sf.Loader(datadir, expire=False)
# Satellite TLEs.
celestrak = 'http://celestrak.com/NORAD/elements/'
geosats = loader.tle(celestrak+'geo.txt')
gpssats = loader.tle(celestrak+'supplemental/gps.txt')
sciencesats = loader.tle(celestrak+'science.txt')
stationsats = loader.tle(celestrak+'stations.txt')
tdrss = loader.tle(celestrak+'tdrss.txt')
visualsats = loader.tle(celestrak+'visual.txt')
# Make catalogs indexable by either name or catalog number.
# Also create sats, a merge of the individual ones.
satcats = [geosats, gpssats, sciencesats, stationsats, tdrss, visualsats]
sats = {}
for cat in satcats:
    names = [key for key in cat.keys()]
    for name in names:
        sat = cat[name]
        satnum = sat.model.satnum
        cat[satnum]  = sat
        sats[satnum] = sat
        sats[name]   = sat
def getsat(satid):
    """
    Return a skyfield EarthSatellite.

    <satid> is case independent if it is a satellite name (str).

    Retrieve directly from CelesTrak by catalog number if not in local database.
    """
    if isinstance(satid, str):
        satid = satid.upper()
    if satid in sats.keys():
        return sats[satid]
    if not isinstance(satid, int):
        msg = 'satid must be an integer for satellites not in the local set'
        raise Exception(msg)
    base = 'http://celestrak.com/cgi-bin/TLE.pl?CATNR='
    url = base + str(satid)
    with urllib.request.urlopen(url) as fd:
        lines = fd.readlines()
    for k, line in enumerate(lines):
        if 'PRE' in line.decode():
            name = lines[k+1].decode().strip()
            if name == 'No TLE found':
                msg = '%i is not in the CelesTrak database!' % satid
                raise Exception(msg)
            tle1 = lines[k+2].decode().strip()
            tle2 = lines[k+3].decode().strip()
            break
    sat = sf.EarthSatellite(tle1, tle2, name)
    return sat

>> sat = getsat(28129)
...print(sat)
...sat = getsat('goes 16')
...print(sat)

EarthSatellite 'GPS BIIR-10 (PRN 22)' number=28129 epoch=2018-04-21T19:50:06Z
EarthSatellite 'GOES 16' number=41866 epoch=2018-04-18T15:14:01Z

thanks a lot for this code. I am getting a strange error when i call the function with your example:

sat = getsat(28129) ...print(sat)

line 66, in getsat sat = sf.EarthSatellite(tle1, tle2, name)

UnboundLocalError: local variable 'tle1' referenced before assignment

???