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).
881 lines
26 KiB
C
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));
|
|
}
|
|
}
|