How to get satellite LLA (Latitude, Longitude and Altitude) from TLE?

Like the title says, I am trying to use the TLE of the ISS to get its LLA at a specific date.
I am very new to astrodynamics and orekit. I am trying to follow the examples and tutorials in the documentation, but the results I am getting are completely off. If I had to guess, I’d guess the problem is coming from an incorrect setup in my TLELocation constructor or the method getLocation(…).

Thank you for any help or suggestions.

TLELocation.java

public class TLELocation {
    
    private TLE input;
    private Frame inertialFrame;
    private Frame earthFrame;
    private TLEPropagator testPropagator;
    private BodyShape earth;

    public TLELocation(String line1, String line2)
    {
        if(line1.length() < 69 || line2.length() < 69)
            throw new InvalidTLEException("Invalid TLE");
        input = new TLE(line1, line2, DateTimeUtil.getUTC());
        //The TEME frame is used for the SGP4 model in TLE propagation.
        inertialFrame = FramesFactory.getEME2000();
        //Get an unspecified International Terrestrial Reference Frame.
        earthFrame = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
        //Modeling of a one-axis ellipsoid.
        //One-axis ellipsoids is a good approximate model for most planet-size and larger natural bodies.
        earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
                                     Constants.WGS84_EARTH_FLATTENING,
                                     earthFrame);
        testPropagator = TLEPropagator.selectExtrapolator(input);
    }

    public HVector3D getLocation(AbsoluteDate date)
    {
        PVCoordinates inertial = testPropagator.getPVCoordinates(date);
        PVCoordinates fixedEarth = inertialFrame.getTransformTo(earthFrame, date).transformPVCoordinates(inertial);
        return new HVector3D(fixedEarth.getPosition());
    }

    public Vector3D geodeticLocation(AbsoluteDate date)
    {
        GeodeticPoint point = earth.transform(getLocation(date).getVector3D(), earthFrame, date);
        Vector3D vector3D = new Vector3D(FastMath.toDegrees(point.getLatitude()),
                            FastMath.toDegrees(point.getLongitude()),
                            point.getAltitude());
        return vector3D;
    }
}

TLELocationTest.java

public class TLELocationTest {

    private TLELocation tleLocation;
    private TLE tle;
    private String dateStr;
    private AbsoluteDate absoluteDate;

    @BeforeAll
    public void setUp() throws IOException {
        File orekitData = new File("orekit_data");
        DataProvidersManager manager = DataContext.getDefault().getDataProvidersManager();
        manager.addProvider(new DirectoryCrawler(orekitData));
        //==========================================================//
        tle = getIssTleFromCelesTrackAPI();
        tleLocation = new TLELocation(tle.getLine1(), tle.getLine2());
        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
        Long currentTime = new Date().getTime();
        dateStr = formatter.format(new Date(currentTime));
        absoluteDate = DateTimeUtil.parseDate(dateStr);
    }
 @Test //Latitude
    public void testGetGeodeticLocationGetLatitude() throws IOException {
        Vector3D vector3D = tleLocation.geodeticLocation(absoluteDate);
        Location actualLocation = new Location(vector3D.getX(), vector3D.getY());
        Location expectedLocation = getISSLocation();
        Assertions.assertEquals(expectedLocation.getLatitude(), actualLocation.getLatitude(), 1.0);
    }

    @Test //Longitude
    public void testGetGeodeticLocationGetLongitude() throws IOException {
        Vector3D vector3D = tleLocation.geodeticLocation(absoluteDate);
        Location actualLocation = new Location(vector3D.getX(), vector3D.getY());
        Location expectedLocation = getISSLocation();
        Assertions.assertEquals(expectedLocation.getLongitude(), actualLocation.getLongitude(), 1.0);
    }

//private methods
.
.
.
}

HVector.java

public class HVector3D {
    private final double a, b, c, d;

    public HVector3D(Vector3D v)
    {
        this.a = v.getX();
        this.b = v.getY();
        this.c = v.getZ();
        this.d = 1;
    }

    public HVector3D(Vector3D v, double d)
    {
        this.a = v.getX();
        this.b = v.getY();
        this.c = v.getZ();
        this.d = d;
    }

    public HVector3D(double a, double b, double c, double d)
    {
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
    }

    public HVector3D()
    {
        this.a = 0.0;
        this.b = 0.0;
        this.c = 0.0;
        this.d = 1.0;
    }

    public double dotProduct(final HVector3D v)
    {
        return MathArrays.linearCombination(a, v.a, b, v.b, c, v.c, d, v.d);
    }

    public double getA()
    {
        return this.a;
    }

    public double getB()
    {
        return this.b;
    }

    public double getC()
    {
        return this.c;
    }

    public double getD()
    {
        return this.d;
    }

    public HVector3D toHessianNormal()
    {
        Vector3D temp = new Vector3D(this.a, this.b, this.c);
        double distance = this.d / temp.getNorm();
        return new HVector3D(temp.normalize(), distance);
    }

    @Override
    public String toString()
    {
        return this.a + " " + this.b + " " + this.c + " " + this.d;
    }

    public Vector3D getVector3D()
    {
        return new Vector3D(a/d, b/d, c/d);
    }

    public double distance(HVector3D v)
    {
        final double dx = FastMath.abs(v.a - a);
        final double dy = FastMath.abs(v.b - b);
        final double dz = FastMath.abs(v.c - c);
        return dx + dy + dz;
    }
}

Hi @jacobv25 , welcome.

The TLE propagator uses the TEME frame, not the EME2000 frame; so your inertialFrame variable is inconsistent with the propagator output.

Anyway, you can just ignore which inertial frame is used and avoid performing the frame transform by yourself is instead of calling testPropagator.getPVCoordinates(date) you call testPropagator.propagate(date).getPVCoordinates(earthFrame).

Storing geodetic coordinates converted into degrees in a Vector3D variable should really be avoided. Vector3D is intended to store Cartesian coordinates and perform geometric computation, it is not intended to store three arbitrary double values. I therefore suggest you change the signature of your getLocation method, it is misleading.

When you say results are really off, what are the kind of errors you observe? You may try to display the evolution over time of the coordinates (both expectedLocation and actualLocation) on a map. If for example you have correct latitude range but shifted longitudes, this may be a time reference issue (typically, your getISSLocation() should probably be given a date and it should be the same date that your TLELocation.getLocation method uses. Did you check units too?

Hello, thanks for the advice and quick response.

When you mentioned I can “ignore which inertial frame is used”, do you mean I can comment out these lines of code and replace them with the one you suggested?

public HVector3D getLocation(AbsoluteDate date)
    {
//        PVCoordinates inertial = testPropagator.getPVCoordinates(date);
        PVCoordinates pvCoordinates = testPropagator.propagate(date).getPVCoordinates(earthFrame );
//        PVCoordinates fixedEarth = inertialFrame.getTransformTo(earthFrame, date).transformPVCoordinates(inertial);
//        return new HVector3D(fixedEarth.getPosition());
        return new HVector3D(pvCoordinates.getPosition());
    }

I’ll plot the coordinates on a map to try and see a pattern, but I probably won’t get to that until next week.

Also, when you mentioned “did I check the units too?”, do you mean degrees vs radians? I checked those and they seems to be in order, but I am wondering if there are other conversions I am not taking into consideration?

Yes, you can replace the lines as shown.

Concerning units, Orekit being a low level library, it uses SI units only inside the computation and in the API. Units conversion must be done at application level (except of course when Orekit parses the data by itself). So angles in the API are always in radians, distances in meters, time in seconds, velocity in meters per second, thrust in Newtons

Hello Luc,

I’ve been looking over the code and your reply this last weekend but I’m still unsure why my latitude and longitude are off.

  • I switched EME2000 to TEME
  • I am not using Vector3D to store and compute coordinates
  • my method getISSLocation makes a GET request to Open-Notify-API/ISS-Location-Now/ to get the ISS current location and I use LocalDateTime.now() to initialize the AbsoluteDate. Therefore the time reference should be accurate/the same.
  • I rewrote the code so that there are very little dependencies so I’m almost positive there are no conversions taking place besides converting GeodeticPoint from radians to degrees.

I’ve included 20 test results spread out over five second intervals.
CSV : LatLong_Results.csv (1.4 KB)
HTML : Test Results.html (314.1 KB)

I haven’t yet mapped my results because I am still looking for a web app that will let me input a CSV file of Lats and Longs and the output would be a map with markers or dots indicating the corresponding locations. I’m trying to decide if I should do it by hand, keep looking online, or build my own app.

Test Class

import org.hipparchus.util.FastMath;
import org.json.JSONObject;
import org.junit.jupiter.api.*;
import org.orekit.bodies.BodyShape;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.data.DataContext;
import org.orekit.data.DataProvidersManager;
import org.orekit.data.DirectoryCrawler;
import org.orekit.frames.Frame;
import org.orekit.frames.FramesFactory;
import org.orekit.propagation.analytical.tle.TLE;
import org.orekit.propagation.analytical.tle.TLEPropagator;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeScalesFactory;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.PVCoordinates;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDateTime;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class OrekitGetLLATest {

    private TLE tle;
    private Frame earthFrame;
    private TLEPropagator propagator;
    private BodyShape earth;
    private AbsoluteDate absoluteDate;

    @BeforeAll
    public void setUp() throws IOException {
        File orekitData = new File("orekit_data");
        DataProvidersManager manager = DataContext.getDefault().getDataProvidersManager();
        manager.addProvider(new DirectoryCrawler(orekitData));
        //=======================================================//
        tle = getIssTleFromCelesTrackAPI();
        //Get an unspecified International Terrestrial Reference Frame.
        earthFrame = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
        //Modeling of a one-axis ellipsoid.
        //One-axis ellipsoids is a good approximate model for most planet-size and larger natural bodies.
        earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
                Constants.WGS84_EARTH_FLATTENING,
                earthFrame);
        propagator = TLEPropagator.selectExtrapolator(tle);
    }

    @BeforeEach
    public void setUpBeforeEach(){
        LocalDateTime now = LocalDateTime.now();
        absoluteDate = new AbsoluteDate(
                now.getYear(),
                now.getMonthValue(),
                now.getDayOfMonth(),
                now.getHour(),
                now.getMinute(),
                now.getSecond(),
                TimeScalesFactory.getUTC());
    }

    @Test
    public void testGetLatitudeFromTLEUsingOrekit() throws IOException {
        PVCoordinates pvCoordinates = propagator.propagate(absoluteDate).getPVCoordinates(earthFrame);
        GeodeticPoint geodeticPoint = earth.transform(
                pvCoordinates.getPosition(),
                earthFrame,
                absoluteDate);
        double actualLatitude = FastMath.toDegrees(geodeticPoint.getLatitude());
        double expectedLatitude = getISSLocationNow().getLatitude();
        Assertions.assertEquals(expectedLatitude, actualLatitude, 1.0);
    }

    @Test
    public void testGetLongitudeFromTLEUsingOrekit() throws IOException {
        PVCoordinates pvCoordinates = propagator.propagate(absoluteDate).getPVCoordinates(earthFrame);
        GeodeticPoint geodeticPoint = earth.transform(
                pvCoordinates.getPosition(),
                earthFrame,
                absoluteDate);
        double actualLongitude = FastMath.toDegrees(geodeticPoint.getLongitude());
        double expectedLongitude = getISSLocationNow().getLongitude();
        Assertions.assertEquals(expectedLongitude, actualLongitude, 1.0);
    }

    //region private methods
    private LatLongLocation getISSLocationNow() throws IOException {
        StringBuffer content = getJsonContentFromIssLocationAPI();
        LatLongLocation issLocation = extractIssLocationFromJsonContent(content);
        return issLocation;
    }

    private LatLongLocation extractIssLocationFromJsonContent(StringBuffer content){
        JSONObject jsonObject = new JSONObject(content.toString());
        jsonObject = jsonObject.getJSONObject("iss_position");
        Double latitude = Double.parseDouble((String) jsonObject.get("latitude"));
        Double longitude = Double.parseDouble((String) jsonObject.get("longitude"));
        LatLongLocation issLocation = new LatLongLocation(latitude, longitude );
        return issLocation;
    }

    private StringBuffer getJsonContentFromIssLocationAPI() throws IOException {
        URL url = new URL("http://api.open-notify.org/iss-now.json");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        int status = connection.getResponseCode();
        BufferedReader in = new BufferedReader( new InputStreamReader(connection.getInputStream()));
        String inputLine;
        StringBuffer content = new StringBuffer();
        while((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        //TODO:remove when done testing
        System.out.println(content);
        in.close();
        connection.disconnect();
        return content;
    }

    private TLE getIssTleFromCelesTrackAPI() throws IOException {
        String baseURL = "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=TLE";
        URL url = new URL(baseURL);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        int status = connection.getResponseCode();
        BufferedReader in = new BufferedReader( new InputStreamReader(connection.getInputStream()));
        String inputLine;
        StringBuffer tleContent = new StringBuffer();
        while((inputLine = in.readLine()) != null) {
            tleContent.append(inputLine + System.lineSeparator());
        }
        //TODO:remove when done testing
        System.out.println(tleContent);
        in.close();
        connection.disconnect();
        String[] tleLines = tleContent.toString().split("\\n");
        TLE tle = new TLE(tleLines[1], tleLines[2]);
        return tle;
    }
    //endregion

    private class LatLongLocation{
        private Double latitude;
        private Double longitude;
        public LatLongLocation() {
            latitude = null;
            longitude = null;
        }
        public LatLongLocation(Double latitude, Double longitude) {
            this.latitude = latitude;
            this.longitude = longitude;
        }
        public Double getLatitude() {
            return latitude;
        }
        public void setLatitude(Double latitude) {
            this.latitude = latitude;
        }
        public Double getLongitude() {
            return longitude;
        }
        public void setLongitude(Double longitude) {
            this.longitude = longitude;
        }
        @Override
        public String toString() {
            return "Location{" +
                    "latitude=" + latitude +
                    ", longitude=" + longitude +
                    '}';
        }
    }
}

pom file

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>OrekitLLATest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.orekit</groupId>
            <artifactId>orekit</artifactId>
            <version>10.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.4.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20220320</version>
        </dependency>

    </dependencies>

</project>

Does this provide a date in UTC or in the time zone corresponding on your location as guessed by your computer?

I changed the code to LocalDateTime.now(ZoneOffset.UTC), and that seems to have corrected the large errors I was seeing.

Thanks so much for your help!

Another question, how accurate can Orekit guarantee the latitudes and longitudes to be for something like the ISS with their TLEs being updated so frequently?
I just tried with some hardcoded datestimes and the error is ~0.01
Do you think it’s feasible to get accuracy to 6 decimal places?
I am currently comparing my results to this website: ISSTracker ~ Space Station Historical Locations

It is not Orekit that will guarantee latitudes and longitudes, it is the model.
TLE are bad, really. The ones for ISS are better than other because they are updated more frequently, but TLE are still an old rough mean model with few perturbations, and ISS with its huge shape and very low altitude has complex dynamics.
I don’t know how ISSTracker performs its computation. The dynamics part of the TLE is most certainly exactly the same as Orekit because everyone uses the same: the reference paper written by David Vallado in 2006 “Revisiting Spacetrack Report #3”. We did validation with other reference implementations and got similar results (I don’t remember the accuracy, though, and could not find it in the non-regression tests). Another point that may create some discrepancies is the TLE used at each time by each program? Is it the latest one only, or is it a filtered result combined from the last few TLEs and fitted with an improved model?

Nevertheless, the dynamics part is not the full story. If you convert to ground positions, you also have to take into account the Earth model, and it may be different. In most cases, people use ITRF, or WGS84 which are close (sub-meter difference), but they depend on EOP which change all the time. Do you update yours everyday? Does ISSTracker update theirs everyday? What is the type of EOP used, rapid data or final data (i.e. bulletin A or bulletin B)? For near real time operation, rapid data is probably the sensible choice, but it has to be updated frequently and you have to manage very carefully your orekit-data content, as final data are prefered when available. Does ISSTracker use the same strategy or do they use only rapid data even for past events? Do they even use EOP?

When you say error is 0.01, on which parameter do you get this value and which unit is it? Are these the latitude and longitude in degrees? In this case 0.01° is a little above 1km. It is the type of error we expect from a TLE (which as I wrote earlier, are bad). Going as low as 1.0e-6 degrees would mean a position accuracy of the order of magnitude of 12 centimeters. This is simply not achievable with TLE. For such accuracy, you need a proper numerical model and process navigation measurements.

Thanks for the detailed response. This was very helpful :slight_smile: