pg_orrery/src/rise_set_funcs.c
Ryan Malloy 46c8a30575 Add v0.16.0: twilight dawn/dusk, lunar phase, planet apparent magnitude
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.
2026-02-26 12:42:01 -07:00

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));
}