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