pg_orrery/src/pass_funcs.c
Ryan Malloy b33d63034b Add v0.9.0 apparent position features: equatorial type, refraction, proper motion, light-time
New equatorial type (24 bytes: RA/Dec/distance) captures apparent coordinates
of date — what the observation pipeline computes at precession step 3 but was
discarding before hour angle conversion. Matches telescope GoTo mount conventions.

24 new SQL functions (82 → 106 total):
- equatorial type I/O + 3 accessors (eq_ra, eq_dec, eq_distance)
- Satellite RA/Dec: eci_to_equatorial (topocentric), eci_to_equatorial_geo (geocentric)
- Solar system equatorial: planet/sun/moon/small_body_equatorial
- Atmospheric refraction: Bennett (1982) with domain clamp at -1 deg
- Refracted pass prediction: predict_passes_refracted (horizon at -0.569 deg)
- Stellar proper motion: star_observe_pm, star_equatorial_pm (Hipparcos/Gaia convention)
- Light-time correction: planet/sun/small_body_observe_apparent, *_equatorial_apparent
- DE equatorial variants: planet_equatorial_de, moon_equatorial_de

Also includes v0.8.0 orbital_elements type (MPC parser, small_body_observe),
GiST 0-based indexing fix, llms.txt updates, and doc improvements.

All 18 regression suites pass. Zero build warnings (GCC + Clang).
2026-02-21 15:31:46 -07:00

881 lines
26 KiB
C

/*
* pass_funcs.c -- Satellite pass prediction for pg_orrery
*
* Finds visibility windows (AOS/LOS) for a satellite relative to a
* ground observer. Uses bisection on the elevation function to pin
* zero-crossings, then ternary search for peak elevation.
*
* The coarse scan steps at 30-second intervals -- fine enough for LEO
* orbits (~90 min period) that no pass shorter than a minute gets
* missed, coarse enough that a 7-day window doesn't require millions
* of propagation calls.
*/
#include "postgres.h"
#include "fmgr.h"
#include "funcapi.h"
#include "utils/timestamp.h"
#include "utils/builtins.h"
#include "libpq/pqformat.h"
#include "norad.h"
#include "types.h"
#include <math.h>
#include <string.h>
PG_FUNCTION_INFO_V1(pass_event_in);
PG_FUNCTION_INFO_V1(pass_event_out);
PG_FUNCTION_INFO_V1(pass_event_recv);
PG_FUNCTION_INFO_V1(pass_event_send);
PG_FUNCTION_INFO_V1(pass_aos_time);
PG_FUNCTION_INFO_V1(pass_max_el_time);
PG_FUNCTION_INFO_V1(pass_los_time);
PG_FUNCTION_INFO_V1(pass_max_elevation);
PG_FUNCTION_INFO_V1(pass_aos_azimuth);
PG_FUNCTION_INFO_V1(pass_los_azimuth);
PG_FUNCTION_INFO_V1(pass_duration);
PG_FUNCTION_INFO_V1(next_pass);
PG_FUNCTION_INFO_V1(predict_passes);
PG_FUNCTION_INFO_V1(pass_visible);
PG_FUNCTION_INFO_V1(predict_passes_refracted);
#define DEG_TO_RAD (M_PI / 180.0)
#define RAD_TO_DEG (180.0 / M_PI)
#define COARSE_STEP_JD (30.0 / 86400.0) /* 30 seconds */
#define BISECT_TOL_JD (0.1 / 86400.0) /* 0.1 second */
#define MIN_PASS_DURATION_JD (10.0 / 86400.0) /* 10 seconds */
#define DEFAULT_WINDOW_DAYS 7.0
#define POST_LOS_GAP_JD (60.0 / 86400.0) /* 1 minute */
#define TERNARY_ITERATIONS 50
/* ----------------------------------------------------------------
* Static helpers -- duplicated from coord_funcs.c because both
* files need them and they're too small to warrant a shared module.
* ----------------------------------------------------------------
*/
/*
* Convert pg_tle to sat_code's tle_t. No unit conversion needed --
* both store radians, radians/min, Julian dates.
*/
static void
pg_tle_to_sat_code(const pg_tle *src, tle_t *dst)
{
memset(dst, 0, sizeof(tle_t));
dst->epoch = src->epoch;
dst->xincl = src->inclination;
dst->xnodeo = src->raan;
dst->eo = src->eccentricity;
dst->omegao = src->arg_perigee;
dst->xmo = src->mean_anomaly;
dst->xno = src->mean_motion;
dst->xndt2o = src->mean_motion_dot;
dst->xndd6o = src->mean_motion_ddot;
dst->bstar = src->bstar;
dst->norad_number = src->norad_id;
dst->bulletin_number = src->elset_num;
dst->revolution_number = src->rev_num;
dst->classification = src->classification;
dst->ephemeris_type = src->ephemeris_type;
memcpy(dst->intl_desig, src->intl_desig, 9);
}
/*
* Propagate TLE to a Julian date. Returns sat_code error code.
* pos[3] in km (TEME), vel[3] in km/min (TEME).
*/
static int
do_propagate(const pg_tle *tle, double jd, double *pos, double *vel)
{
tle_t sat;
double *params;
int is_deep;
int err;
double tsince;
pg_tle_to_sat_code(tle, &sat);
is_deep = select_ephemeris(&sat);
if (is_deep < 0)
return -99; /* invalid TLE */
tsince = jd_to_minutes_since_epoch(jd, sat.epoch);
params = palloc(sizeof(double) * N_SAT_PARAMS);
if (is_deep)
{
SDP4_init(params, &sat);
err = SDP4(tsince, &sat, params, pos, vel);
}
else
{
SGP4_init(params, &sat);
err = SGP4(tsince, &sat, params, pos, vel);
}
pfree(params);
return err;
}
/*
* Greenwich Mean Sidereal Time from Julian date.
* Returns GMST in radians. Uses the IAU 1982 formula matching
* the low-precision model inside SGP4.
*/
static double
gmst_from_jd(double jd)
{
double ut1, tu;
double gmst;
/* Julian centuries of UT1 from J2000.0 */
ut1 = jd - J2000_JD;
tu = ut1 / 36525.0;
/* GMST in seconds at 0h UT1, then add fractional day rotation */
gmst = 67310.54841
+ (876600.0 * 3600.0 + 8640184.812866) * tu
+ 0.093104 * tu * tu
- 6.2e-6 * tu * tu * tu;
/* Convert seconds to radians, mod 2pi */
gmst = fmod(gmst * M_PI / 43200.0, 2.0 * M_PI);
if (gmst < 0.0)
gmst += 2.0 * M_PI;
return gmst;
}
/*
* Rotate TEME position (and optionally velocity) to ECEF via GMST.
* Only the z-axis rotation matters for SGP4's simplified nutation.
*/
static void
teme_to_ecef(const double *pos_teme, const double *vel_teme,
double gmst, double *pos_ecef, double *vel_ecef)
{
double cg = cos(gmst);
double sg = sin(gmst);
pos_ecef[0] = cg * pos_teme[0] + sg * pos_teme[1];
pos_ecef[1] = -sg * pos_teme[0] + cg * pos_teme[1];
pos_ecef[2] = pos_teme[2];
if (vel_teme && vel_ecef)
{
/* Earth rotation rate, rad/min */
double omega_e = 7.29211514670698e-5 * 60.0;
vel_ecef[0] = cg * vel_teme[0] + sg * vel_teme[1]
+ omega_e * pos_ecef[1];
vel_ecef[1] = -sg * vel_teme[0] + cg * vel_teme[1]
- omega_e * pos_ecef[0];
vel_ecef[2] = vel_teme[2];
}
}
/*
* Observer geodetic (radians, meters) to ECEF (km).
* Uses WGS-84 ellipsoid for ground station positioning.
*/
static void
observer_to_ecef(const pg_observer *obs, double *ecef)
{
double sinlat = sin(obs->lat);
double coslat = cos(obs->lat);
double sinlon = sin(obs->lon);
double coslon = cos(obs->lon);
double N; /* radius of curvature in the prime vertical */
double alt_km = obs->alt_m / 1000.0;
N = WGS84_A / sqrt(1.0 - WGS84_E2 * sinlat * sinlat);
ecef[0] = (N + alt_km) * coslat * coslon;
ecef[1] = (N + alt_km) * coslat * sinlon;
ecef[2] = (N * (1.0 - WGS84_E2) + alt_km) * sinlat;
}
/*
* Compute topocentric azimuth, elevation, and range from ECEF positions.
* Azimuth: 0=N, 90=E, 180=S, 270=W (radians).
* Elevation: positive above horizon (radians).
*/
static void
ecef_to_topocentric(const double *sat_ecef, const double *obs_ecef,
double obs_lat, double obs_lon,
double *az, double *el, double *range_km)
{
double dx, dy, dz;
double sinlat, coslat, sinlon, coslon;
double south, east, up;
double rng;
dx = sat_ecef[0] - obs_ecef[0];
dy = sat_ecef[1] - obs_ecef[1];
dz = sat_ecef[2] - obs_ecef[2];
sinlat = sin(obs_lat);
coslat = cos(obs_lat);
sinlon = sin(obs_lon);
coslon = cos(obs_lon);
/* Rotate difference vector into SEZ (south-east-zenith) frame */
south = sinlat * coslon * dx + sinlat * sinlon * dy - coslat * dz;
east = -sinlon * dx + coslon * dy;
up = coslat * coslon * dx + coslat * sinlon * dy + sinlat * dz;
rng = sqrt(dx * dx + dy * dy + dz * dz);
*range_km = rng;
*el = asin(up / rng);
/* Azimuth from north, measured clockwise */
*az = atan2(east, -south);
if (*az < 0.0)
*az += 2.0 * M_PI;
}
/* ----------------------------------------------------------------
* elevation_at_jd -- the function we bisect on
*
* Returns satellite elevation in radians relative to the observer's
* local horizon. Negative means below horizon. On hard propagation
* errors, returns -pi (well below any real horizon).
* ----------------------------------------------------------------
*/
static double
elevation_at_jd(const pg_tle *tle, const pg_observer *obs,
double jd, double *az_out)
{
double pos[3], vel[3];
double gmst, pos_ecef[3], obs_ecef[3];
double az, el, range_km;
int err;
err = do_propagate(tle, jd, pos, vel);
/* On hard errors, return well below horizon */
if (err == SXPX_ERR_NEARLY_PARABOLIC ||
err == SXPX_ERR_NEGATIVE_MAJOR_AXIS ||
err == SXPX_ERR_NEGATIVE_XN ||
err == SXPX_ERR_CONVERGENCE_FAIL ||
err == -99)
return -M_PI;
gmst = gmst_from_jd(jd);
teme_to_ecef(pos, NULL, gmst, pos_ecef, NULL);
observer_to_ecef(obs, obs_ecef);
ecef_to_topocentric(pos_ecef, obs_ecef, obs->lat, obs->lon,
&az, &el, &range_km);
if (az_out)
*az_out = az;
return el;
}
/* ----------------------------------------------------------------
* find_next_pass -- core pass-finding algorithm
*
* Scans from start_jd to stop_jd looking for an elevation zero
* crossing (rising edge). When found, bisects to refine AOS,
* scans forward to find LOS, bisects to refine LOS, then uses
* ternary search to locate peak elevation.
*
* Passes below min_el_rad are silently skipped; scanning resumes
* from their LOS.
*
* Returns true if a qualifying pass was found.
* ----------------------------------------------------------------
*/
static bool
find_next_pass(const pg_tle *tle, const pg_observer *obs,
double start_jd, double stop_jd,
double min_el_rad, double threshold_rad,
double *aos_jd, double *los_jd,
double *max_el_jd, double *max_el,
double *aos_az, double *los_az)
{
double jd = start_jd;
double prev_el, curr_el;
double az;
prev_el = elevation_at_jd(tle, obs, jd, NULL);
while (jd < stop_jd)
{
jd += COARSE_STEP_JD;
if (jd > stop_jd)
jd = stop_jd;
curr_el = elevation_at_jd(tle, obs, jd, NULL);
/* Rising edge: was below threshold, now above */
if (prev_el <= threshold_rad && curr_el > threshold_rad)
{
double lo, hi, mid;
double peak_el;
double scan_jd, scan_el;
/* Bisect to find AOS */
lo = jd - COARSE_STEP_JD;
hi = jd;
while (hi - lo > BISECT_TOL_JD)
{
mid = (lo + hi) / 2.0;
if (elevation_at_jd(tle, obs, mid, NULL) > threshold_rad)
hi = mid;
else
lo = mid;
}
*aos_jd = (lo + hi) / 2.0;
elevation_at_jd(tle, obs, *aos_jd, &az);
*aos_az = az;
/* Scan forward to find LOS */
scan_jd = *aos_jd;
peak_el = 0.0;
while (scan_jd < stop_jd)
{
scan_jd += COARSE_STEP_JD;
scan_el = elevation_at_jd(tle, obs, scan_jd, NULL);
if (scan_el > peak_el)
peak_el = scan_el;
if (scan_el <= threshold_rad)
{
/* Bisect to find LOS */
lo = scan_jd - COARSE_STEP_JD;
hi = scan_jd;
while (hi - lo > BISECT_TOL_JD)
{
mid = (lo + hi) / 2.0;
if (elevation_at_jd(tle, obs, mid, NULL) > threshold_rad)
lo = mid;
else
hi = mid;
}
*los_jd = (lo + hi) / 2.0;
break;
}
}
/* Ran past the search window without finding LOS -- clip */
if (scan_jd >= stop_jd)
*los_jd = stop_jd;
/* Skip degenerate passes */
if (*los_jd - *aos_jd < MIN_PASS_DURATION_JD)
{
jd = *los_jd;
prev_el = threshold_rad - 0.01;
continue;
}
/* Refine peak elevation with ternary search */
lo = *aos_jd;
hi = *los_jd;
for (int i = 0; i < TERNARY_ITERATIONS; i++)
{
double m1 = lo + (hi - lo) / 3.0;
double m2 = hi - (hi - lo) / 3.0;
if (elevation_at_jd(tle, obs, m1, NULL) <
elevation_at_jd(tle, obs, m2, NULL))
lo = m1;
else
hi = m2;
}
*max_el_jd = (lo + hi) / 2.0;
*max_el = elevation_at_jd(tle, obs, *max_el_jd, NULL);
elevation_at_jd(tle, obs, *los_jd, &az);
*los_az = az;
/* Below the caller's minimum elevation threshold -- skip */
if (*max_el < min_el_rad)
{
jd = *los_jd;
prev_el = threshold_rad - 0.01;
continue;
}
return true;
}
prev_el = curr_el;
}
return false;
}
/* ----------------------------------------------------------------
* pass_event type I/O
* ----------------------------------------------------------------
*/
/*
* pass_event_in -- parse text to pg_pass_event
*
* Format: (aos_ts,maxel_ts,los_ts,max_el_deg,aos_az_deg,los_az_deg)
* Timestamps are raw int64 microseconds (PG internal representation).
*/
Datum
pass_event_in(PG_FUNCTION_ARGS)
{
char *str = PG_GETARG_CSTRING(0);
pg_pass_event *result;
long long aos_raw, maxel_raw, los_raw;
double max_el_deg, aos_az_deg, los_az_deg;
int nfields;
result = (pg_pass_event *) palloc(sizeof(pg_pass_event));
nfields = sscanf(str, " ( %lld , %lld , %lld , %lf , %lf , %lf )",
&aos_raw, &maxel_raw, &los_raw,
&max_el_deg, &aos_az_deg, &los_az_deg);
if (nfields != 6)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("invalid input syntax for type pass_event: \"%s\"", str),
errhint("Expected (aos_usec,maxel_usec,los_usec,max_el_deg,aos_az_deg,los_az_deg).")));
result->aos_time = (int64) aos_raw;
result->max_el_time = (int64) maxel_raw;
result->los_time = (int64) los_raw;
result->max_elevation = max_el_deg;
result->aos_azimuth = aos_az_deg;
result->los_azimuth = los_az_deg;
PG_RETURN_POINTER(result);
}
/*
* pass_event_out -- pg_pass_event to human-readable text
*
* Format: (2024-01-01 12:00:00+00,2024-01-01 12:05:00+00,2024-01-01 12:10:00+00,45.2,180.0,350.0)
* Timestamps formatted via DirectFunctionCall1(timestamptz_out, ...).
*/
Datum
pass_event_out(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
char *aos_str;
char *maxel_str;
char *los_str;
aos_str = DatumGetCString(
DirectFunctionCall1(timestamptz_out,
Int64GetDatum(pe->aos_time)));
maxel_str = DatumGetCString(
DirectFunctionCall1(timestamptz_out,
Int64GetDatum(pe->max_el_time)));
los_str = DatumGetCString(
DirectFunctionCall1(timestamptz_out,
Int64GetDatum(pe->los_time)));
PG_RETURN_CSTRING(psprintf("(%s,%s,%s,%.1f,%.1f,%.1f)",
aos_str, maxel_str, los_str,
pe->max_elevation,
pe->aos_azimuth,
pe->los_azimuth));
}
/*
* pass_event_recv -- binary input (3 int64 + 3 float8 = 48 bytes)
*/
Datum
pass_event_recv(PG_FUNCTION_ARGS)
{
StringInfo buf = (StringInfo) PG_GETARG_POINTER(0);
pg_pass_event *result;
result = (pg_pass_event *) palloc(sizeof(pg_pass_event));
result->aos_time = pq_getmsgint64(buf);
result->max_el_time = pq_getmsgint64(buf);
result->los_time = pq_getmsgint64(buf);
result->max_elevation = pq_getmsgfloat8(buf);
result->aos_azimuth = pq_getmsgfloat8(buf);
result->los_azimuth = pq_getmsgfloat8(buf);
PG_RETURN_POINTER(result);
}
/*
* pass_event_send -- binary output
*/
Datum
pass_event_send(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
StringInfoData buf;
pq_begintypsend(&buf);
pq_sendint64(&buf, pe->aos_time);
pq_sendint64(&buf, pe->max_el_time);
pq_sendint64(&buf, pe->los_time);
pq_sendfloat8(&buf, pe->max_elevation);
pq_sendfloat8(&buf, pe->aos_azimuth);
pq_sendfloat8(&buf, pe->los_azimuth);
PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
}
/* ----------------------------------------------------------------
* pass_event accessor functions
* ----------------------------------------------------------------
*/
Datum
pass_aos_time(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
PG_RETURN_TIMESTAMPTZ(pe->aos_time);
}
Datum
pass_max_el_time(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
PG_RETURN_TIMESTAMPTZ(pe->max_el_time);
}
Datum
pass_los_time(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
PG_RETURN_TIMESTAMPTZ(pe->los_time);
}
Datum
pass_max_elevation(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
PG_RETURN_FLOAT8(pe->max_elevation);
}
Datum
pass_aos_azimuth(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
PG_RETURN_FLOAT8(pe->aos_azimuth);
}
Datum
pass_los_azimuth(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
PG_RETURN_FLOAT8(pe->los_azimuth);
}
/*
* pass_duration -- time from AOS to LOS as a PostgreSQL interval
*/
Datum
pass_duration(PG_FUNCTION_ARGS)
{
pg_pass_event *pe = (pg_pass_event *) PG_GETARG_POINTER(0);
Interval *result;
result = (Interval *) palloc(sizeof(Interval));
result->time = pe->los_time - pe->aos_time; /* microseconds */
result->day = 0;
result->month = 0;
PG_RETURN_INTERVAL_P(result);
}
/* ----------------------------------------------------------------
* next_pass(tle, observer, from_time) -> pass_event
*
* Finds the next pass above the horizon starting from from_time.
* Searches a 7-day window. Returns NULL if no pass is found.
* ----------------------------------------------------------------
*/
Datum
next_pass(PG_FUNCTION_ARGS)
{
pg_tle *tle = (pg_tle *) PG_GETARG_POINTER(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 from_ts = PG_GETARG_INT64(2);
double start_jd, stop_jd;
double aos_jd, los_jd, max_el_jd, max_el;
double aos_az, los_az;
pg_pass_event *result;
start_jd = timestamptz_to_jd(from_ts);
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
if (!find_next_pass(tle, obs, start_jd, stop_jd,
0.0, /* minimum elevation = 0 degrees */
0.0, /* threshold = geometric horizon */
&aos_jd, &los_jd,
&max_el_jd, &max_el,
&aos_az, &los_az))
PG_RETURN_NULL();
result = (pg_pass_event *) palloc(sizeof(pg_pass_event));
result->aos_time = jd_to_timestamptz(aos_jd);
result->max_el_time = jd_to_timestamptz(max_el_jd);
result->los_time = jd_to_timestamptz(los_jd);
result->max_elevation = max_el * RAD_TO_DEG;
result->aos_azimuth = aos_az * RAD_TO_DEG;
result->los_azimuth = los_az * RAD_TO_DEG;
PG_RETURN_POINTER(result);
}
/* ----------------------------------------------------------------
* predict_passes(tle, observer, start, stop [, min_elevation])
* -> SETOF pass_event
*
* Returns all passes in the given time window. Optional 5th arg
* sets the minimum peak elevation filter in degrees (default 0).
* ----------------------------------------------------------------
*/
typedef struct
{
pg_tle tle;
pg_observer obs;
double current_jd;
double stop_jd;
double min_el_rad;
} predict_passes_ctx;
Datum
predict_passes(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
predict_passes_ctx *ctx;
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldctx;
pg_tle *tle;
pg_observer *obs;
int64 start_ts;
int64 stop_ts;
double min_el_deg;
funcctx = SRF_FIRSTCALL_INIT();
oldctx = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
tle = (pg_tle *) PG_GETARG_POINTER(0);
obs = (pg_observer *) PG_GETARG_POINTER(1);
start_ts = PG_GETARG_INT64(2);
stop_ts = PG_GETARG_INT64(3);
min_el_deg = (PG_NARGS() > 4 && !PG_ARGISNULL(4))
? PG_GETARG_FLOAT8(4)
: 0.0;
if (stop_ts <= start_ts)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("stop time must be after start time")));
ctx = (predict_passes_ctx *)
palloc0(sizeof(predict_passes_ctx));
memcpy(&ctx->tle, tle, sizeof(pg_tle));
memcpy(&ctx->obs, obs, sizeof(pg_observer));
ctx->current_jd = timestamptz_to_jd(start_ts);
ctx->stop_jd = timestamptz_to_jd(stop_ts);
ctx->min_el_rad = min_el_deg * DEG_TO_RAD;
funcctx->user_fctx = ctx;
MemoryContextSwitchTo(oldctx);
}
funcctx = SRF_PERCALL_SETUP();
ctx = (predict_passes_ctx *) funcctx->user_fctx;
{
double aos_jd, los_jd, max_el_jd, max_el;
double aos_az, los_az;
pg_pass_event *result;
if (!find_next_pass(&ctx->tle, &ctx->obs,
ctx->current_jd, ctx->stop_jd,
ctx->min_el_rad,
0.0, /* threshold = geometric horizon */
&aos_jd, &los_jd,
&max_el_jd, &max_el,
&aos_az, &los_az))
SRF_RETURN_DONE(funcctx);
result = (pg_pass_event *) palloc(sizeof(pg_pass_event));
result->aos_time = jd_to_timestamptz(aos_jd);
result->max_el_time = jd_to_timestamptz(max_el_jd);
result->los_time = jd_to_timestamptz(los_jd);
result->max_elevation = max_el * RAD_TO_DEG;
result->aos_azimuth = aos_az * RAD_TO_DEG;
result->los_azimuth = los_az * RAD_TO_DEG;
/* Advance past this pass before the next call */
ctx->current_jd = los_jd + POST_LOS_GAP_JD;
SRF_RETURN_NEXT(funcctx, PointerGetDatum(result));
}
}
/* ----------------------------------------------------------------
* pass_visible(tle, observer, start, stop) -> bool
*
* Returns true if any pass crosses above the horizon in the window.
* Cheaper than predict_passes when you only need a yes/no answer.
* ----------------------------------------------------------------
*/
Datum
pass_visible(PG_FUNCTION_ARGS)
{
pg_tle *tle = (pg_tle *) PG_GETARG_POINTER(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 start_ts = PG_GETARG_INT64(2);
int64 stop_ts = PG_GETARG_INT64(3);
double start_jd, stop_jd;
double aos_jd, los_jd, max_el_jd, max_el;
double aos_az, los_az;
start_jd = timestamptz_to_jd(start_ts);
stop_jd = timestamptz_to_jd(stop_ts);
PG_RETURN_BOOL(find_next_pass(tle, obs, start_jd, stop_jd,
0.0,
0.0, /* threshold = geometric horizon */
&aos_jd, &los_jd,
&max_el_jd, &max_el,
&aos_az, &los_az));
}
/* ----------------------------------------------------------------
* predict_passes_refracted(tle, observer, start, stop [, min_elevation])
* -> SETOF pass_event
*
* Same as predict_passes but uses refracted horizon threshold.
* Bennett's refraction at 0 deg geometric elevation is ~0.569 deg,
* so the threshold is -0.569 deg = -0.00993 rad. This means AOS
* triggers when the satellite's geometric elevation crosses -0.569
* deg (the point at which refraction bends it to the apparent
* horizon).
* ----------------------------------------------------------------
*/
#define REFRACTED_HORIZON_RAD (-0.00993) /* -0.569 deg, Bennett at h=0 */
typedef struct
{
pg_tle tle;
pg_observer obs;
double current_jd;
double stop_jd;
double min_el_rad;
} predict_passes_refracted_ctx;
Datum
predict_passes_refracted(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
predict_passes_refracted_ctx *ctx;
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldctx;
pg_tle *tle;
pg_observer *obs;
int64 start_ts;
int64 stop_ts;
double min_el_deg;
funcctx = SRF_FIRSTCALL_INIT();
oldctx = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
tle = (pg_tle *) PG_GETARG_POINTER(0);
obs = (pg_observer *) PG_GETARG_POINTER(1);
start_ts = PG_GETARG_INT64(2);
stop_ts = PG_GETARG_INT64(3);
min_el_deg = (PG_NARGS() > 4 && !PG_ARGISNULL(4))
? PG_GETARG_FLOAT8(4)
: 0.0;
if (stop_ts <= start_ts)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("stop time must be after start time")));
ctx = (predict_passes_refracted_ctx *)
palloc0(sizeof(predict_passes_refracted_ctx));
memcpy(&ctx->tle, tle, sizeof(pg_tle));
memcpy(&ctx->obs, obs, sizeof(pg_observer));
ctx->current_jd = timestamptz_to_jd(start_ts);
ctx->stop_jd = timestamptz_to_jd(stop_ts);
ctx->min_el_rad = min_el_deg * DEG_TO_RAD;
funcctx->user_fctx = ctx;
MemoryContextSwitchTo(oldctx);
}
funcctx = SRF_PERCALL_SETUP();
ctx = (predict_passes_refracted_ctx *) funcctx->user_fctx;
{
double aos_jd, los_jd, max_el_jd, max_el;
double aos_az, los_az;
pg_pass_event *result;
if (!find_next_pass(&ctx->tle, &ctx->obs,
ctx->current_jd, ctx->stop_jd,
ctx->min_el_rad,
REFRACTED_HORIZON_RAD,
&aos_jd, &los_jd,
&max_el_jd, &max_el,
&aos_az, &los_az))
SRF_RETURN_DONE(funcctx);
result = (pg_pass_event *) palloc(sizeof(pg_pass_event));
result->aos_time = jd_to_timestamptz(aos_jd);
result->max_el_time = jd_to_timestamptz(max_el_jd);
result->los_time = jd_to_timestamptz(los_jd);
result->max_elevation = max_el * RAD_TO_DEG;
result->aos_azimuth = aos_az * RAD_TO_DEG;
result->los_azimuth = los_az * RAD_TO_DEG;
/* Advance past this pass before the next call */
ctx->current_jd = los_jd + POST_LOS_GAP_JD;
SRF_RETURN_NEXT(funcctx, PointerGetDatum(result));
}
}