Skip to content

Commit e5bace2

Browse files
committed
fix: use WGS-84 ellipsoid for observer positioning instead of spherical Earth
Observer ECEF/ECI conversion treated geodetic lat/lon as geocentric, placing the observer up to ~21.7 km off at mid-latitudes. This caused up to ~1° azimuth error on east/west LEO passes and ~2.3% slant range error. Add shared geodetic helpers (geodeticToEcef, geodeticToEci) using WGS-84 prime vertical radius of curvature and replace spherical formula in az-el, doppler, and magnitude modules.
1 parent 7043a76 commit e5bace2

5 files changed

Lines changed: 86 additions & 47 deletions

File tree

src/astro/az-el.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,49 @@
22
* Compute azimuth and elevation from a ground observer to a satellite.
33
* Pure math — no THREE.js or DOM dependencies. Usable in Web Workers.
44
*
5-
* Uses spherical Earth model (R = 6371 km).
5+
* Uses WGS-84 ellipsoid for observer positioning.
66
*
77
* @param eciX Standard ECI X position (km) — toward vernal equinox
88
* @param eciY Standard ECI Y position (km) — completes right-hand system
99
* @param eciZ Standard ECI Z position (km) — toward north pole
1010
* @param gmstRad Greenwich Mean Sidereal Time in radians
11-
* @param obsLatDeg Observer latitude in degrees (-90 to 90)
12-
* @param obsLonDeg Observer longitude in degrees (-180 to 180)
13-
* @param obsAltM Observer altitude in meters above sea level
11+
* @param obsLatDeg Observer geodetic latitude in degrees (-90 to 90)
12+
* @param obsLonDeg Observer geodetic longitude in degrees (-180 to 180)
13+
* @param obsAltM Observer altitude in meters above WGS-84 ellipsoid
1414
* @returns { az, el } in degrees. Az: 0=N, 90=E, 180=S, 270=W. El: 0=horizon, 90=zenith.
1515
*/
16+
import { geodeticToEcef } from './geodetic';
17+
1618
export function getAzEl(
1719
eciX: number, eciY: number, eciZ: number,
1820
gmstRad: number,
1921
obsLatDeg: number, obsLonDeg: number, obsAltM: number,
2022
): { az: number; el: number } {
2123
const DEG2RAD = Math.PI / 180;
22-
const EARTH_R = 6371.0; // km
2324

2425
const satR = Math.sqrt(eciX * eciX + eciY * eciY + eciZ * eciZ);
2526
if (satR === 0) return { az: 0, el: -90 };
2627

27-
// Satellite geodetic from ECI
28+
// Satellite geocentric lat/lon from ECI, then to ECEF
2829
const satLat = Math.asin(eciZ / satR);
2930
const satLonEci = Math.atan2(eciY, eciX);
3031
const satLonEcef = satLonEci - gmstRad;
3132

32-
// Satellite ECEF position
3333
const sx = satR * Math.cos(satLat) * Math.cos(satLonEcef);
3434
const sy = satR * Math.cos(satLat) * Math.sin(satLonEcef);
3535
const sz = satR * Math.sin(satLat);
3636

37-
// Observer ECEF position
38-
const latRad = obsLatDeg * DEG2RAD;
39-
const lonRad = obsLonDeg * DEG2RAD;
40-
const obsR = EARTH_R + obsAltM / 1000;
41-
const ox = obsR * Math.cos(latRad) * Math.cos(lonRad);
42-
const oy = obsR * Math.cos(latRad) * Math.sin(lonRad);
43-
const oz = obsR * Math.sin(latRad);
37+
// Observer ECEF position (WGS-84 ellipsoid)
38+
const obs = geodeticToEcef(obsLatDeg, obsLonDeg, obsAltM);
4439

4540
// Range vector (ECEF)
46-
const dx = sx - ox;
47-
const dy = sy - oy;
48-
const dz = sz - oz;
41+
const dx = sx - obs.x;
42+
const dy = sy - obs.y;
43+
const dz = sz - obs.z;
4944

50-
// Rotate to topocentric East-North-Up
45+
// Rotate to topocentric East-North-Up (geodetic lat/lon defines the local frame)
46+
const latRad = obsLatDeg * DEG2RAD;
47+
const lonRad = obsLonDeg * DEG2RAD;
5148
const clat = Math.cos(latRad), slat = Math.sin(latRad);
5249
const clon = Math.cos(lonRad), slon = Math.sin(lonRad);
5350

src/astro/doppler.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import { twoline2satrec, json2satrec, propagate } from 'satellite.js';
88
import type { SatRec } from 'satellite.js';
99
import { epochToUnix, epochToGmst } from './epoch';
10-
import { EARTH_RADIUS_KM, DEG2RAD } from '../constants';
10+
import { DEG2RAD } from '../constants';
11+
import { geodeticToEcef } from './geodetic';
1112

1213
const C_KM_S = 299792.458; // speed of light (km/s)
1314
const OMEGA_E = 7.2921159e-5; // Earth rotation rate (rad/s)
@@ -18,18 +19,6 @@ export interface DopplerResult {
1819
rangeRateKmS: number; // range rate (km/s, positive = receding)
1920
}
2021

21-
/** Observer ECEF position from geodetic (spherical Earth). */
22-
function observerEcef(latDeg: number, lonDeg: number, altM: number) {
23-
const lat = latDeg * DEG2RAD;
24-
const lon = lonDeg * DEG2RAD;
25-
const R = EARTH_RADIUS_KM + altM / 1000;
26-
return {
27-
x: R * Math.cos(lat) * Math.cos(lon),
28-
y: R * Math.cos(lat) * Math.sin(lon),
29-
z: R * Math.sin(lat),
30-
};
31-
}
32-
3322
/**
3423
* Rotate a standard-ECI vector to ECEF via Rz(-GMST).
3524
* Standard ECI: x=vernal equinox, y=90° ahead, z=north pole.
@@ -83,8 +72,8 @@ export function calculateDopplerShift(
8372
z: velRot.z,
8473
};
8574

86-
// Observer (stationary in ECEF)
87-
const obs = observerEcef(obsLatDeg, obsLonDeg, obsAltM);
75+
// Observer (stationary in ECEF, WGS-84 ellipsoid)
76+
const obs = geodeticToEcef(obsLatDeg, obsLonDeg, obsAltM);
8877

8978
// Range vector (observer → satellite)
9079
const dx = satPos.x - obs.x;

src/astro/geodetic.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* WGS-84 geodetic coordinate conversions.
3+
* Pure math — no THREE.js dependency. Usable in Web Workers.
4+
*/
5+
6+
import { DEG2RAD, WGS84_A, WGS84_E2 } from '../constants';
7+
8+
/**
9+
* Convert geodetic coordinates to ECEF (Earth-Centered, Earth-Fixed) in km.
10+
* Uses the WGS-84 ellipsoid for accurate positioning.
11+
*
12+
* @param latDeg Geodetic latitude (degrees)
13+
* @param lonDeg Geodetic longitude (degrees)
14+
* @param altM Altitude above ellipsoid (meters)
15+
*/
16+
export function geodeticToEcef(
17+
latDeg: number, lonDeg: number, altM: number,
18+
): { x: number; y: number; z: number } {
19+
const latRad = latDeg * DEG2RAD;
20+
const lonRad = lonDeg * DEG2RAD;
21+
const sinLat = Math.sin(latRad);
22+
const cosLat = Math.cos(latRad);
23+
const altKm = altM / 1000;
24+
const N = WGS84_A / Math.sqrt(1 - WGS84_E2 * sinLat * sinLat);
25+
return {
26+
x: (N + altKm) * cosLat * Math.cos(lonRad),
27+
y: (N + altKm) * cosLat * Math.sin(lonRad),
28+
z: (N * (1 - WGS84_E2) + altKm) * sinLat,
29+
};
30+
}
31+
32+
/**
33+
* Convert geodetic coordinates to standard ECI (Earth-Centered Inertial) in km.
34+
* Rotates from ECEF by GMST to align with the vernal equinox frame.
35+
*
36+
* Standard ECI: x = vernal equinox, y = 90° ahead, z = north pole.
37+
*
38+
* @param latDeg Geodetic latitude (degrees)
39+
* @param lonDeg Geodetic longitude (degrees)
40+
* @param altM Altitude above ellipsoid (meters)
41+
* @param gmstRad Greenwich Mean Sidereal Time (radians)
42+
*/
43+
export function geodeticToEci(
44+
latDeg: number, lonDeg: number, altM: number, gmstRad: number,
45+
): { x: number; y: number; z: number } {
46+
const latRad = latDeg * DEG2RAD;
47+
const sinLat = Math.sin(latRad);
48+
const cosLat = Math.cos(latRad);
49+
const altKm = altM / 1000;
50+
const N = WGS84_A / Math.sqrt(1 - WGS84_E2 * sinLat * sinLat);
51+
const theta = gmstRad + lonDeg * DEG2RAD;
52+
return {
53+
x: (N + altKm) * cosLat * Math.cos(theta),
54+
y: (N + altKm) * cosLat * Math.sin(theta),
55+
z: (N * (1 - WGS84_E2) + altKm) * sinLat,
56+
};
57+
}

src/astro/magnitude.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
* and cached in localStorage. See .docs/magnitude.md for details.
1414
*/
1515

16+
import { geodeticToEci } from './geodetic';
17+
1618
const DEG2RAD = Math.PI / 180.0;
1719
const RAD2DEG = 180.0 / Math.PI;
18-
const EARTH_RADIUS_KM = 6371.0;
1920

2021
/**
2122
* Lambertian (diffuse) sphere phase function.
@@ -74,27 +75,17 @@ export function computePhaseAngle(
7475
/**
7576
* Compute observer position in **standard ECI** (km) from geodetic coordinates.
7677
* Returns standard ECI — convert to render coords (x, z, -y) before mixing with sat.currentPos.
77-
* Simplified: assumes spherical Earth (good enough for magnitude estimation).
78+
* Uses WGS-84 ellipsoid for accurate positioning.
7879
*
7980
* @param latDeg Geodetic latitude (degrees)
8081
* @param lonDeg Geodetic longitude (degrees)
81-
* @param altM Altitude above sea level (meters)
82+
* @param altM Altitude above WGS-84 ellipsoid (meters)
8283
* @param gmstRad Greenwich Mean Sidereal Time (radians)
8384
*/
8485
export function observerEci(
8586
latDeg: number, lonDeg: number, altM: number, gmstRad: number,
8687
): { x: number; y: number; z: number } {
87-
const latRad = latDeg * DEG2RAD;
88-
const lonRad = lonDeg * DEG2RAD;
89-
const r = EARTH_RADIUS_KM + altM / 1000.0;
90-
const cosLat = Math.cos(latRad);
91-
const sinLat = Math.sin(latRad);
92-
const theta = gmstRad + lonRad; // sidereal angle
93-
return {
94-
x: r * cosLat * Math.cos(theta),
95-
y: r * cosLat * Math.sin(theta),
96-
z: r * sinLat,
97-
};
88+
return geodeticToEci(latDeg, lonDeg, altM, gmstRad);
9889
}
9990

10091
/**

src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,10 @@ export function satColorGl(index: number): readonly [number, number, number] {
5050
// J2 perturbation constants
5151
export const J2 = 1.08263e-3; // Earth's J2 zonal harmonic
5252
export const EARTH_RADIUS_EQ_KM = 6378.137; // WGS-84 equatorial radius (km)
53+
54+
// WGS-84 ellipsoid constants
55+
export const WGS84_A = 6378.137; // semi-major axis (km)
56+
export const WGS84_F = 1 / 298.257223563; // flattening
57+
export const WGS84_E2 = 2 * WGS84_F - WGS84_F * WGS84_F; // first eccentricity squared
5358
export const ORBIT_RECOMPUTE_INTERVAL_S = 900; // recompute orbits every 15 sim-minutes
5459
export const MOBILE_BREAKPOINT = 768;

0 commit comments

Comments
 (0)