/* * 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 #include 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)); } }