Twilight: 6 functions (civil/nautical/astronomical × dawn/dusk) reusing the existing find_next_crossing() bisection search with Sun depression angle thresholds (-6°, -12°, -18°). Returns NULL for polar regions where the threshold is never reached. Lunar phase: 4 functions computing Sun-Earth-Moon geometry from VSOP87 + ELP2000-82B. Phase angle [0,360) via elongation + cross product z-component for waxing/waning discrimination. 8 named phases in 45° bins. Moon age approximated from phase angle and mean synodic month. Planet magnitude: Mallama & Hilton (2018) polynomial model with VSOP87 heliocentric distances and phase angle via law of cosines. All 7 planets (Mercury-Neptune, excluding Earth). Saturn ring tilt not modeled. 151 → 162 SQL objects. 26 → 27 test suites, all passing.
872 lines
28 KiB
C
872 lines
28 KiB
C
/*
|
|
* rise_set_funcs.c -- Rise/set prediction for solar system bodies
|
|
*
|
|
* Adapts the satellite pass prediction bisection algorithm from
|
|
* pass_funcs.c for planets, Sun, and Moon. The core difference:
|
|
* elevation is computed via VSOP87/ELP82B -> observe_from_geocentric()
|
|
* instead of SGP4 propagation.
|
|
*
|
|
* Coarse scan at 60-second steps (planets move slowly compared to LEO
|
|
* satellites at 30s), then bisection to 0.1-second precision.
|
|
*
|
|
* Returns NULL if the body doesn't rise/set within the search window
|
|
* (circumpolar or perpetually below horizon at observer latitude).
|
|
*/
|
|
|
|
#include "postgres.h"
|
|
#include "fmgr.h"
|
|
#include "utils/timestamp.h"
|
|
#include "utils/builtins.h"
|
|
#include "types.h"
|
|
#include "astro_math.h"
|
|
#include "vsop87.h"
|
|
#include "elp82b.h"
|
|
#include <math.h>
|
|
|
|
PG_FUNCTION_INFO_V1(planet_next_rise);
|
|
PG_FUNCTION_INFO_V1(planet_next_set);
|
|
PG_FUNCTION_INFO_V1(sun_next_rise);
|
|
PG_FUNCTION_INFO_V1(sun_next_set);
|
|
PG_FUNCTION_INFO_V1(moon_next_rise);
|
|
PG_FUNCTION_INFO_V1(moon_next_set);
|
|
PG_FUNCTION_INFO_V1(sun_next_rise_refracted);
|
|
PG_FUNCTION_INFO_V1(sun_next_set_refracted);
|
|
PG_FUNCTION_INFO_V1(planet_next_rise_refracted);
|
|
PG_FUNCTION_INFO_V1(planet_next_set_refracted);
|
|
PG_FUNCTION_INFO_V1(moon_next_rise_refracted);
|
|
PG_FUNCTION_INFO_V1(moon_next_set_refracted);
|
|
PG_FUNCTION_INFO_V1(sun_rise_set_status);
|
|
PG_FUNCTION_INFO_V1(moon_rise_set_status);
|
|
PG_FUNCTION_INFO_V1(planet_rise_set_status);
|
|
PG_FUNCTION_INFO_V1(sun_civil_dawn);
|
|
PG_FUNCTION_INFO_V1(sun_civil_dusk);
|
|
PG_FUNCTION_INFO_V1(sun_nautical_dawn);
|
|
PG_FUNCTION_INFO_V1(sun_nautical_dusk);
|
|
PG_FUNCTION_INFO_V1(sun_astronomical_dawn);
|
|
PG_FUNCTION_INFO_V1(sun_astronomical_dusk);
|
|
|
|
#define COARSE_STEP_JD (60.0 / 86400.0) /* 60 seconds */
|
|
#define BISECT_TOL_JD (0.1 / 86400.0) /* 0.1 second */
|
|
#define DEFAULT_WINDOW_DAYS 7.0
|
|
|
|
/* body_type encoding for the elevation helper */
|
|
#define BTYPE_PLANET 0
|
|
#define BTYPE_SUN 1
|
|
#define BTYPE_MOON 2
|
|
|
|
/*
|
|
* Standard almanac refraction correction for rise/set of Sun and Moon.
|
|
* The Sun/Moon are considered to rise/set when their geometric center
|
|
* is 0.833 degrees below the geometric horizon:
|
|
* 0.569 deg = atmospheric refraction at horizon (Bennett 1982)
|
|
* 0.266 deg = mean solar/lunar semidiameter
|
|
*/
|
|
#define SUN_MOON_REFRACTED_HORIZON_RAD (-0.01454) /* -0.833 deg */
|
|
|
|
/*
|
|
* Refraction-only horizon for point sources (planets).
|
|
* No semidiameter correction needed — even Jupiter at opposition
|
|
* subtends only ~24" (0.4 arcmin), negligible against 34' refraction.
|
|
* Error from treating planets as point sources: <1 second in time.
|
|
*/
|
|
#define REFRACTION_ONLY_HORIZON_RAD (-0.00993) /* -0.569 deg */
|
|
|
|
/* Twilight depression angles (geometric Sun center below horizon) */
|
|
#define CIVIL_TWILIGHT_RAD (-0.10472) /* -6.0 deg */
|
|
#define NAUTICAL_TWILIGHT_RAD (-0.20944) /* -12.0 deg */
|
|
#define ASTRONOMICAL_TWILIGHT_RAD (-0.30416) /* -18.0 deg */
|
|
|
|
|
|
/* ----------------------------------------------------------------
|
|
* elevation_at_jd_body -- compute topocentric elevation for a body
|
|
*
|
|
* Returns geometric elevation in radians. No error return path --
|
|
* VSOP87/ELP82B always succeed for reasonable dates.
|
|
* ----------------------------------------------------------------
|
|
*/
|
|
static double
|
|
elevation_at_jd_body(int body_type, int body_id,
|
|
const pg_observer *obs, double jd)
|
|
{
|
|
double earth_xyz[6];
|
|
double target_xyz[6];
|
|
double geo_ecl[3];
|
|
pg_topocentric topo;
|
|
|
|
switch (body_type)
|
|
{
|
|
case BTYPE_PLANET:
|
|
{
|
|
int vsop_body = body_id - 1;
|
|
GetVsop87Coor(jd, 2, earth_xyz); /* Earth */
|
|
GetVsop87Coor(jd, vsop_body, target_xyz);
|
|
geo_ecl[0] = target_xyz[0] - earth_xyz[0];
|
|
geo_ecl[1] = target_xyz[1] - earth_xyz[1];
|
|
geo_ecl[2] = target_xyz[2] - earth_xyz[2];
|
|
break;
|
|
}
|
|
case BTYPE_SUN:
|
|
{
|
|
GetVsop87Coor(jd, 2, earth_xyz);
|
|
geo_ecl[0] = -earth_xyz[0];
|
|
geo_ecl[1] = -earth_xyz[1];
|
|
geo_ecl[2] = -earth_xyz[2];
|
|
break;
|
|
}
|
|
case BTYPE_MOON:
|
|
{
|
|
double moon_ecl[3];
|
|
GetElp82bCoor(jd, moon_ecl);
|
|
geo_ecl[0] = moon_ecl[0];
|
|
geo_ecl[1] = moon_ecl[1];
|
|
geo_ecl[2] = moon_ecl[2];
|
|
break;
|
|
}
|
|
default:
|
|
return -M_PI; /* unreachable */
|
|
}
|
|
|
|
observe_from_geocentric(geo_ecl, jd, obs, &topo);
|
|
return topo.elevation;
|
|
}
|
|
|
|
|
|
/* ----------------------------------------------------------------
|
|
* find_next_crossing -- coarse scan + bisection for horizon crossing
|
|
*
|
|
* Scans from start_jd to stop_jd looking for the next rising or
|
|
* setting event. Returns the Julian date of the crossing, or -1
|
|
* if no crossing is found within the window.
|
|
*
|
|
* rising=true: find where elevation crosses threshold upward
|
|
* rising=false: find where elevation crosses threshold downward
|
|
* ----------------------------------------------------------------
|
|
*/
|
|
static double
|
|
find_next_crossing(int body_type, int body_id,
|
|
const pg_observer *obs,
|
|
double start_jd, double stop_jd,
|
|
double threshold_rad,
|
|
bool rising)
|
|
{
|
|
double jd = start_jd;
|
|
double prev_el, curr_el;
|
|
|
|
prev_el = elevation_at_jd_body(body_type, body_id, obs, jd);
|
|
|
|
while (jd < stop_jd)
|
|
{
|
|
jd += COARSE_STEP_JD;
|
|
if (jd > stop_jd)
|
|
jd = stop_jd;
|
|
|
|
curr_el = elevation_at_jd_body(body_type, body_id, obs, jd);
|
|
|
|
if (rising)
|
|
{
|
|
/* Rising: was below threshold, now above */
|
|
if (prev_el <= threshold_rad && curr_el > threshold_rad)
|
|
{
|
|
double lo = jd - COARSE_STEP_JD;
|
|
double hi = jd;
|
|
|
|
while (hi - lo > BISECT_TOL_JD)
|
|
{
|
|
double mid = (lo + hi) / 2.0;
|
|
if (elevation_at_jd_body(body_type, body_id, obs, mid) > threshold_rad)
|
|
hi = mid;
|
|
else
|
|
lo = mid;
|
|
}
|
|
return (lo + hi) / 2.0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Setting: was above threshold, now below */
|
|
if (prev_el > threshold_rad && curr_el <= threshold_rad)
|
|
{
|
|
double lo = jd - COARSE_STEP_JD;
|
|
double hi = jd;
|
|
|
|
while (hi - lo > BISECT_TOL_JD)
|
|
{
|
|
double mid = (lo + hi) / 2.0;
|
|
if (elevation_at_jd_body(body_type, body_id, obs, mid) > threshold_rad)
|
|
lo = mid;
|
|
else
|
|
hi = mid;
|
|
}
|
|
return (lo + hi) / 2.0;
|
|
}
|
|
}
|
|
|
|
prev_el = curr_el;
|
|
}
|
|
|
|
return -1.0; /* no crossing found */
|
|
}
|
|
|
|
|
|
/* ----------------------------------------------------------------
|
|
* classify_rise_set -- sample elevation to determine behavior
|
|
*
|
|
* Samples body elevation at N_SAMPLES equally-spaced points across
|
|
* 24 hours starting from start_jd. Classifies:
|
|
* - All above geometric horizon -> "circumpolar"
|
|
* - All below geometric horizon -> "never_rises"
|
|
* - Mixed -> "rises_and_sets"
|
|
*
|
|
* Uses geometric horizon (0 deg) for classification — this matches
|
|
* the NULL contract of the rise/set functions.
|
|
* ----------------------------------------------------------------
|
|
*/
|
|
#define RISE_SET_N_SAMPLES 48
|
|
|
|
static const char *
|
|
classify_rise_set(int body_type, int body_id,
|
|
const pg_observer *obs, double start_jd)
|
|
{
|
|
int above = 0;
|
|
int below = 0;
|
|
int i;
|
|
double step = 1.0 / (double)RISE_SET_N_SAMPLES; /* 24h / N = 30 min */
|
|
|
|
for (i = 0; i < RISE_SET_N_SAMPLES; i++)
|
|
{
|
|
double jd = start_jd + i * step;
|
|
double el = elevation_at_jd_body(body_type, body_id, obs, jd);
|
|
|
|
if (el > 0.0)
|
|
above++;
|
|
else
|
|
below++;
|
|
|
|
/* Early exit: once we have both above and below, it's mixed */
|
|
if (above > 0 && below > 0)
|
|
return "rises_and_sets";
|
|
}
|
|
|
|
if (above == RISE_SET_N_SAMPLES)
|
|
return "circumpolar";
|
|
else
|
|
return "never_rises";
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* planet_next_rise(body_id, observer, timestamptz) -> timestamptz
|
|
*
|
|
* Returns the next time a planet rises above the geometric horizon.
|
|
* NULL if the planet doesn't rise within 7 days (circumpolar or
|
|
* perpetually below horizon).
|
|
*
|
|
* Body IDs: 1=Mercury, ..., 8=Neptune (not Sun, Earth, or Moon)
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
planet_next_rise(PG_FUNCTION_ARGS)
|
|
{
|
|
int32 body_id = PG_GETARG_INT32(0);
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
|
int64 ts = PG_GETARG_INT64(2);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
errmsg("planet_next_rise: body_id %d must be 1-8 (Mercury-Neptune)",
|
|
body_id)));
|
|
|
|
if (body_id == BODY_EARTH)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("cannot observe Earth from Earth")));
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_PLANET, body_id, obs,
|
|
start_jd, stop_jd, 0.0, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* planet_next_set(body_id, observer, timestamptz) -> timestamptz
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
planet_next_set(PG_FUNCTION_ARGS)
|
|
{
|
|
int32 body_id = PG_GETARG_INT32(0);
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
|
int64 ts = PG_GETARG_INT64(2);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
errmsg("planet_next_set: body_id %d must be 1-8 (Mercury-Neptune)",
|
|
body_id)));
|
|
|
|
if (body_id == BODY_EARTH)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("cannot observe Earth from Earth")));
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_PLANET, body_id, obs,
|
|
start_jd, stop_jd, 0.0, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_next_rise(observer, timestamptz) -> timestamptz
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_next_rise(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd, 0.0, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_next_set(observer, timestamptz) -> timestamptz
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_next_set(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd, 0.0, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* moon_next_rise(observer, timestamptz) -> timestamptz
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
moon_next_rise(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_MOON, 0, obs,
|
|
start_jd, stop_jd, 0.0, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* moon_next_set(observer, timestamptz) -> timestamptz
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
moon_next_set(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_MOON, 0, obs,
|
|
start_jd, stop_jd, 0.0, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_next_rise_refracted(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Uses -0.833 degree threshold (standard almanac: 0.569 deg refraction
|
|
* at horizon + 0.266 deg solar semidiameter). Refracted sunrise is
|
|
* earlier than geometric by ~4 minutes at mid-latitudes.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_next_rise_refracted(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
SUN_MOON_REFRACTED_HORIZON_RAD, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_next_set_refracted(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Refracted sunset is later than geometric by ~4 minutes.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_next_set_refracted(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
SUN_MOON_REFRACTED_HORIZON_RAD, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* planet_next_rise_refracted(body_id, observer, timestamptz) -> timestamptz
|
|
*
|
|
* Uses -0.569 degree threshold (refraction only, point source).
|
|
* Planets are too small for semidiameter to matter — Jupiter at
|
|
* opposition is 24 arcseconds, <1 second of time error.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
planet_next_rise_refracted(PG_FUNCTION_ARGS)
|
|
{
|
|
int32 body_id = PG_GETARG_INT32(0);
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
|
int64 ts = PG_GETARG_INT64(2);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
errmsg("planet_next_rise_refracted: body_id %d must be 1-8 (Mercury-Neptune)",
|
|
body_id)));
|
|
|
|
if (body_id == BODY_EARTH)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("cannot observe Earth from Earth")));
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_PLANET, body_id, obs,
|
|
start_jd, stop_jd,
|
|
REFRACTION_ONLY_HORIZON_RAD, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* planet_next_set_refracted(body_id, observer, timestamptz) -> timestamptz
|
|
*
|
|
* Refracted planet set is later than geometric.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
planet_next_set_refracted(PG_FUNCTION_ARGS)
|
|
{
|
|
int32 body_id = PG_GETARG_INT32(0);
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
|
int64 ts = PG_GETARG_INT64(2);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
errmsg("planet_next_set_refracted: body_id %d must be 1-8 (Mercury-Neptune)",
|
|
body_id)));
|
|
|
|
if (body_id == BODY_EARTH)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("cannot observe Earth from Earth")));
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_PLANET, body_id, obs,
|
|
start_jd, stop_jd,
|
|
REFRACTION_ONLY_HORIZON_RAD, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* moon_next_rise_refracted(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Uses -0.833 degree threshold (same as Sun: 0.569 deg refraction +
|
|
* 0.264 deg mean lunar semidiameter). Moon semidiameter varies
|
|
* 14.7'-16.7'; mean value error is ~1 arcmin → ~15 seconds in time.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
moon_next_rise_refracted(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_MOON, 0, obs,
|
|
start_jd, stop_jd,
|
|
SUN_MOON_REFRACTED_HORIZON_RAD, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* moon_next_set_refracted(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Refracted moonset is later than geometric.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
moon_next_set_refracted(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_MOON, 0, obs,
|
|
start_jd, stop_jd,
|
|
SUN_MOON_REFRACTED_HORIZON_RAD, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_rise_set_status(observer, timestamptz) -> text
|
|
*
|
|
* Returns 'rises_and_sets', 'circumpolar', or 'never_rises'.
|
|
* Call this when sun_next_rise/set returns NULL to find out why.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_rise_set_status(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd;
|
|
const char *status;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
status = classify_rise_set(BTYPE_SUN, 0, obs, start_jd);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text(status));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* moon_rise_set_status(observer, timestamptz) -> text
|
|
*
|
|
* Returns 'rises_and_sets', 'circumpolar', or 'never_rises'.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
moon_rise_set_status(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd;
|
|
const char *status;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
status = classify_rise_set(BTYPE_MOON, 0, obs, start_jd);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text(status));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* planet_rise_set_status(body_id, observer, timestamptz) -> text
|
|
*
|
|
* Returns 'rises_and_sets', 'circumpolar', or 'never_rises'.
|
|
* Body IDs: 1=Mercury, ..., 8=Neptune (not Sun, Earth, or Moon).
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
planet_rise_set_status(PG_FUNCTION_ARGS)
|
|
{
|
|
int32 body_id = PG_GETARG_INT32(0);
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
|
int64 ts = PG_GETARG_INT64(2);
|
|
double start_jd;
|
|
const char *status;
|
|
|
|
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
errmsg("planet_rise_set_status: body_id %d must be 1-8 (Mercury-Neptune)",
|
|
body_id)));
|
|
|
|
if (body_id == BODY_EARTH)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("cannot observe Earth from Earth")));
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
status = classify_rise_set(BTYPE_PLANET, body_id, obs, start_jd);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text(status));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_civil_dawn(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Returns the next time civil twilight begins (Sun crosses -6 deg
|
|
* heading upward). Civil twilight = enough light for outdoor
|
|
* activities without artificial lighting.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_civil_dawn(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
CIVIL_TWILIGHT_RAD, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_civil_dusk(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Returns the next time civil twilight ends (Sun crosses -6 deg
|
|
* heading downward). After civil dusk, outdoor activities require
|
|
* artificial lighting.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_civil_dusk(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
CIVIL_TWILIGHT_RAD, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_nautical_dawn(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Returns the next time nautical twilight begins (Sun crosses -12 deg
|
|
* heading upward). At nautical dawn the horizon becomes visible at
|
|
* sea and bright stars are still visible for navigation.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_nautical_dawn(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
NAUTICAL_TWILIGHT_RAD, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_nautical_dusk(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Returns the next time nautical twilight ends (Sun crosses -12 deg
|
|
* heading downward). After nautical dusk the horizon is no longer
|
|
* visible at sea; bright stars remain visible.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_nautical_dusk(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
NAUTICAL_TWILIGHT_RAD, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_astronomical_dawn(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Returns the next time astronomical twilight begins (Sun crosses
|
|
* -18 deg heading upward). Before astronomical dawn the sky is
|
|
* fully dark — faintest objects are observable.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_astronomical_dawn(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
ASTRONOMICAL_TWILIGHT_RAD, true);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
* sun_astronomical_dusk(observer, timestamptz) -> timestamptz
|
|
*
|
|
* Returns the next time astronomical twilight ends (Sun crosses
|
|
* -18 deg heading downward). After astronomical dusk the sky is
|
|
* fully dark — faintest objects become observable.
|
|
* ================================================================
|
|
*/
|
|
Datum
|
|
sun_astronomical_dusk(PG_FUNCTION_ARGS)
|
|
{
|
|
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
|
int64 ts = PG_GETARG_INT64(1);
|
|
double start_jd, stop_jd, result_jd;
|
|
|
|
start_jd = timestamptz_to_jd(ts);
|
|
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
|
|
|
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
|
start_jd, stop_jd,
|
|
ASTRONOMICAL_TWILIGHT_RAD, false);
|
|
|
|
if (result_jd < 0.0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
|
}
|