@@ -41,35 +41,42 @@ public abstract class AstronomicalCalculator implements Cloneable {
4141 * The commonly used average solar radius that is about 16′ or 0.2666°.
4242 *
4343 * @see #getSolarRadius()
44- * @see #getSolarRadius (LocalDate)
44+ * @see #getApparentSolarRadius (LocalDate)
4545 */
4646 private double solarRadius = 16 / 60d ;
4747
4848 /**
49- * Should the calculator use the {@link #isUseAstronomicApparentSolarRadius()} (defaults to {@code true}).
50- * @see #setUseAstronomicApparentSolarRadius(boolean)
51- * @see #setUseAstronomicApparentSolarRadius(boolean)
52- * */
53- private boolean useAstronomicApparentSolarRadius = true ;
49+ * Whether sunrise and sunset should use the date-based apparent solar radius ({@link #getApparentSolarRadius(LocalDate)}),
50+ * which varies slightly through the year as the Earth-Sun distance changes, rather than the fixed {@link #getSolarRadius()
51+ * solar radius}. Defaults to {@code true}. When {@code false}, the fixed {@link #getSolarRadius()} value (default 16′)
52+ * is used instead, which is appropriate for matching other implementations that assume a constant solar radius. Note that
53+ * calling {@link #setSolarRadius(double)} automatically sets this to {@code false}.
54+ *
55+ * @see #isUseApparentSolarRadius()
56+ * @see #setUseApparentSolarRadius(boolean)
57+ * @see #getApparentSolarRadius(LocalDate)
58+ * @see #getSolarRadius()
59+ */
60+ private boolean useApparentSolarRadius = true ;
5461
5562 /**
5663 * Returns if useAstronomicApparentSolarRadius is true (the default) or false.
5764 * @return if useAstronomicApparentSolarRadius is true or false.
58- * @see #getSolarRadius (LocalDate)
65+ * @see #getApparentSolarRadius (LocalDate)
5966 * @see #getSolarRadius()
6067 */
61- public boolean isUseAstronomicApparentSolarRadius () {
62- return useAstronomicApparentSolarRadius ;
68+ public boolean isUseApparentSolarRadius () {
69+ return useApparentSolarRadius ;
6370 }
6471
6572 /**
6673 * Sets if useAstronomicApparentSolarRadius should be true (the default) or false.
67- * @param useAstronomicalApparentSolarRadius should astronomic apparent solar radius be used (default is true).
68- * @see #getSolarRadius (LocalDate)
74+ * @param useApparentSolarRadius should apparent solar radius be used (default is true).
75+ * @see #getApparentSolarRadius (LocalDate)
6976 * @see #getSolarRadius()
7077 */
71- public void setUseAstronomicApparentSolarRadius (boolean useAstronomicalApparentSolarRadius ) {
72- this .useAstronomicApparentSolarRadius = useAstronomicalApparentSolarRadius ;
78+ public void setUseApparentSolarRadius (boolean useApparentSolarRadius ) {
79+ this .useApparentSolarRadius = useApparentSolarRadius ;
7380 }
7481
7582 /**
@@ -297,7 +304,7 @@ public abstract double getUTCSunset(LocalDate localDate, GeoLocation geoLocation
297304 * com.kosherjava.zmanim.AstronomicalCalendar#getEndNauticalTwilight()} that passes {@link
298305 * com.kosherjava.zmanim.AstronomicalCalendar#NAUTICAL_ZENITH} to this method.
299306 * @param elevation elevation in Meters.
300- * @param localDate the date to use for the solar radius. See {@link #getSolarRadius (LocalDate)}.
307+ * @param localDate the date to use for the solar radius. See {@link #getApparentSolarRadius (LocalDate)}.
301308 * @return The zenith adjusted to include the {@link #getSolarRadius sun's radius}, {@link #getRefraction
302309 * refraction} and {@link #getElevationAdjustment elevation} adjustment. This will only be adjusted for
303310 * sunrise and sunset (if the zenith == 90°)
@@ -306,8 +313,8 @@ public abstract double getUTCSunset(LocalDate localDate, GeoLocation geoLocation
306313 double adjustZenith (double zenith , double elevation , LocalDate localDate ) {
307314 double adjustedZenith = zenith ;
308315 if (zenith == GEOMETRIC_ZENITH ) { // only adjust if it is exactly sunrise or sunset
309- if (isUseAstronomicApparentSolarRadius () && localDate != null ) {
310- adjustedZenith = zenith + (getSolarRadius (localDate ) + getRefraction () + getElevationAdjustment (elevation ));
316+ if (isUseApparentSolarRadius () && localDate != null ) {
317+ adjustedZenith = zenith + (getApparentSolarRadius (localDate ) + getRefraction () + getElevationAdjustment (elevation ));
311318 } else {
312319 adjustedZenith = zenith + (getSolarRadius () + getRefraction () + getElevationAdjustment (elevation ));
313320 }
@@ -341,34 +348,55 @@ public void setRefraction(double refraction) {
341348 }
342349
343350 /**
344- * Method to get the sun's radius. The default value is 16 arcminutes. The sun's radius as it appears from earth is almost
345- * universally given as 16 arcminutes but in fact it differs by the time of the year. At the <a
346- * href="https://en.wikipedia.org/wiki/Perihelion">perihelion</a> it has an apparent radius of 16.293′ (0.2710°), while at the
347- * <a href="https://en.wikipedia.org/wiki/Aphelion">aphelion</a> it has an apparent radius of 15.755′ (0.2622°). There is little
348- * affect for most location, but at high and low latitudes the difference becomes more apparent. My Calculations for
349- * the difference at the location of the <a href="https://www.rmg.co.uk/royal-observatory">Royal Observatory, Greenwich</a>
350- * shows only a 4.494-second difference between the perihelion and aphelion radii, but moving into the arctic circle the
351- * difference becomes more noticeable. Tests for Tromso, Norway (latitude 69.672312, longitude 19.049787) show that
352- * on May 17, the rise of the midnight sun, a 2 minute and 23 second difference is observed between the perihelion and
353- * aphelion radii using the USNO algorithm, but only 1 minute and 6 seconds difference using the NOAA algorithm.
354- * Areas farther north show an even greater difference. Note that these test are not real valid test cases because
355- * they show the extreme difference on days that are not the perihelion or aphelion, but are shown for illustrative
356- * purposes only.
357- *
358- * @return The sun's radius in degrees.
359- */
351+ * Method to get the fixed sun's radius. The default value is 16 arcminutes. The sun's radius as it appears from earth is almost
352+ * universally given as 16 arcminutes but in fact it differs by the time of the year. At the <a href=
353+ * "https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">perihelion</a> it has an apparent radius of 16.293′ (0.2710°),
354+ * while at the <a href="https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">aphelion</a> it has an apparent radius of
355+ * 15.755′ (0.2622°). There is little effect for most locations, but at high and low latitudes the difference becomes more
356+ * apparent. Calculations for the difference at the location of the <a href="https://www.rmg.co.uk/royal-observatory">Royal
357+ * Observatory, Greenwich</a> show only a 4.494-second difference between the perihelion and aphelion radii, but moving into the
358+ * arctic circle the difference becomes more noticeable. Tests for Tromso, Norway (latitude 69.67°, longitude 19.05°) show that on
359+ * May 17, the rise of the midnight sun, a 2 minute and 23 second difference is observed between the perihelion and aphelion radii
360+ * using the USNO algorithm, but only 1 minute and 6 seconds difference using the NOAA algorithm. Areas farther north show an even
361+ * greater difference. Note that these are not real-world tests. It simply compared the min and max solar radius at different
362+ * locations. Real world examples of comparing the actual apperant solar radius would yield less significant differences.
363+ * Regardless, this is exactly the error that the date-based apparent solar radius eliminates: by default ({@link
364+ * #isUseApparentSolarRadius()} is {@code true}) sunrise and sunset use {@link #getApparentSolarRadius(LocalDate)}
365+ * instead, and the fixed value returned here only applies when that setting is {@code false} (typically to match other
366+ * implementations that assume a constant 16′ radius; see {@link #setSolarRadius(double)}, which switches that setting off
367+ * automatically).
368+ *
369+ * @return The fixed sun's radius in degrees, used only when {@link #isUseApparentSolarRadius()} is {@code false}.
370+ * @see #getApparentSolarRadius(LocalDate)
371+ * @see #isUseApparentSolarRadius()
372+ */
360373 public double getSolarRadius () {
361374 return this .solarRadius ;
362375 }
363376
364377 /**
365- * Method to set the sun's radius.
366- *
367- * @param solarRadius The sun's radius in degrees.
378+ * Sets the Sun's radius as a fixed, date-independent value in degrees, for example {@code 16.0 / 60.0} for the
379+ * conventional 16′. This is typically used to match another implementation or reference that assumes a constant
380+ * solar radius.
381+ * <p>
382+ * A fixed radius has no effect while {@link #isUseApparentSolarRadius()} is {@code true} (the default), since
383+ * in that mode the date-based {@link #getApparentSolarRadius(LocalDate) apparent solar radius} is used instead. So that
384+ * a value set here actually takes effect, this method also disables that mode (equivalent to calling
385+ * {@link #setUseApparentSolarRadius(boolean) setUseAstronomicApparentSolarRadius(false)}). To return to the
386+ * date-based radius afterward, pass {@code true} to {@link #setUseApparentSolarRadius(boolean)}.
387+ *
388+ * @param solarRadius the Sun's radius in degrees. Must be a non-negative number.
389+ * @throws IllegalArgumentException if {@code solarRadius} is < 0 or is {@link Double#NaN}.
368390 * @see #getSolarRadius()
391+ * @see #isUseApparentSolarRadius()
392+ * @see #getApparentSolarRadius(LocalDate)
369393 */
370394 public void setSolarRadius (double solarRadius ) {
395+ if (solarRadius < 0 || Double .isNaN (solarRadius )) {
396+ throw new IllegalArgumentException ("Solar radius must be a non-negative number" );
397+ }
371398 this .solarRadius = solarRadius ;
399+ this .useApparentSolarRadius = false ;
372400 }
373401
374402 /**
@@ -392,7 +420,8 @@ public boolean equals(Object object) {
392420
393421 return Double .compare (this .getEarthRadius (), calculator .getEarthRadius ()) == 0
394422 && Double .compare (this .getRefraction (), calculator .getRefraction ()) == 0
395- && Double .compare (this .getSolarRadius (), calculator .getSolarRadius ()) == 0 ;
423+ && Double .compare (this .getSolarRadius (), calculator .getSolarRadius ()) == 0
424+ && this .isUseApparentSolarRadius () == calculator .isUseApparentSolarRadius ();
396425 }
397426
398427 /**
@@ -405,7 +434,7 @@ public boolean equals(Object object) {
405434 */
406435 @ Override
407436 public int hashCode () {
408- return Objects .hash (getEarthRadius (), getRefraction (), getSolarRadius ());
437+ return Objects .hash (getEarthRadius (), getRefraction (), getSolarRadius (), useApparentSolarRadius );
409438 }
410439
411440 /**
@@ -468,15 +497,26 @@ protected static double asinDegrees(double angle) {
468497 }
469498
470499 /**
471- * The Sun's apparent angular semi-diameter ("solar radius") in degrees for each day of the year, precomputed from the
472- * <a href="https://en.wikipedia.org/wiki/VSOP_model">VSOP87</a> Earth-Sun distance for reference year 2050 (chosen as the
473- * midpoint of 2000-2100 to keep the century drift symmetric and small; 2050 is a common year, so there are 365 entries and
474- * February 29 reuses the February 28 value - see {@link #getSolarRadius(LocalDate)}). The Sun's apparent size varies through
475- * the year as the Earth-Sun distance changes between perihelion (~Jan 3, about 16.27′ or 0.2710°) and aphelion (~Jul 5, about
476- * 15.73′ or 0.2622°), with a mean of the conventional 16′. Indexed by day-of-year (1-365).
477- * <p>A fixed table is used instead of evaluating the series at run time because the value for a given calendar date
478- * drifts by less than ~0.4′ across the whole of 2000-2100 (perihelion advances ~1 day per 57 years), which
479- * at latitude 60° is at most ~50 ms of sunrise / sunset time, basically negligible.
500+ * The Sun's apparent angular semi-diameter (the "solar radius") in degrees for each day of the year, precomputed from
501+ * the <a href="https://en.wikipedia.org/wiki/VSOP_(planets)">VSOP87</a> Earth-Sun distance for reference year 2050.
502+ * The Sun's apparent size varies through the year as the Earth-Sun distance changes between the perihelion (~Jan 3,
503+ * about 16.27′ or 0.2710°) and the aphelion (~Jul 5, about 15.73′ or 0.2622°), with a mean of the conventional 16′.
504+ * The table has 365 entries indexed by day-of-year (1-365); 2050 is a common year, so February 29 reuses the
505+ * February 28 value (see {@link #getApparentSolarRadius(LocalDate)}).
506+ * <p>
507+ * <b>Valid epoch.</b> Reference year 2050 was chosen as the midpoint of the years 2000-2100 so that the small drift in
508+ * the value for a fixed calendar date stays symmetric and minimal across that span. The date of the perihelion advances
509+ * through the calendar by roughly one day every 57 years, so the apparent radius for a given calendar date is not
510+ * perfectly constant from year to year. Across 2000-2100 this table is within about 0.4″ of the exact value (at most
511+ * roughly 50 ms of sunrise / sunset time at latitude 60°, and less elsewhere) - negligible for any practical purpose.
512+ * Outside 2000-2100 the table is still returned but its accuracy degrades gradually as the perihelion drifts further;
513+ * for use centuries away from 2050 the table should be regenerated against a closer reference year.
514+ * <p>
515+ * A fixed table is used in preference to evaluating the VSOP87 series at run time because, given the negligible drift
516+ * above, the lookup is both simpler and faster while remaining far more accurate than any sunrise / sunset calculation
517+ * requires.
518+ *
519+ * @see #getApparentSolarRadius(LocalDate)
480520 */
481521 private static final double [] SOLAR_RADIUS_BY_DAY_OF_YEAR = {
482522 0.27108024 , 0.27108486 , 0.27108790 , 0.27108930 , 0.27108899 , 0.27108695 , 0.27108316 , 0.27107762 , 0.27107033 ,
@@ -522,26 +562,38 @@ protected static double asinDegrees(double angle) {
522562 0.27105266 , 0.27106312 , 0.27107182 , 0.27107876 , 0.27108399 };
523563
524564 /**
525- * Return the Sun's apparent angular semi-diameter ("solar radius") for the given date, in degrees, suitable for passing to
526- * {@link #setSolarRadius(double)} for slightly more accurate sunrise / sunset times. The value comes from a precomputed table
527- * ({@link #SOLAR_RADIUS_BY_DAY_OF_YEAR}) keyed by calendar day. The date's month/day is mapped onto the (common-year) reference
528- * 2050 via {@code withYear(2050)}; because 2050 is not a leap year, a February 29 input is automatically resolved to February 28
529- * and assigned that day's value.
530- *
531- * <p>Example: //FIXME once integrated
532- * <pre>
533- * calculator.setSolarRadius(calculator.getSolarRadius(localDate));
534- * </pre>
565+ * Returns the Sun's apparent angular semi-diameter (the "solar radius") for the given date, in degrees. The Sun's
566+ * apparent size changes over the year as the Earth-Sun distance varies between the <a href=
567+ * "https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">perihelion</a> (~Jan 3, when the Sun is largest)
568+ * and the <a href="https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">aphelion</a> (~Jul 5, when it is
569+ * smallest). The value is read from a precomputed table ({@link #SOLAR_RADIUS_BY_DAY_OF_YEAR}) keyed by calendar
570+ * day; the date's month and day are mapped onto the (common-year) reference year 2050 via {@code withYear(2050)},
571+ * so a February 29 input is automatically resolved to February 28 and assigned that day's value.
572+ * <p>
573+ * This is the value the calculator applies <b>automatically</b> for sunrise and sunset while
574+ * {@link #isUseApparentSolarRadius()} is {@code true} (the default). It is recomputed from the date on
575+ * every call and is never stored, so it is always correct for the date being calculated. The method is exposed only
576+ * for inspection and comparison.
577+ * <p>
578+ * <b>Do not pass the result to {@link #setSolarRadius(double)}.</b> {@link #setSolarRadius(double)} is meant for a
579+ * fixed, date-independent radius (for example to match another implementation that uses a constant 16′). Storing
580+ * a single day's apparent radius there would freeze that one day's value onto this instance and apply it incorrectly
581+ * to every other date subsequently calculated (and would also switch off {@link #isUseApparentSolarRadius()
582+ * apparent-radius mode}). To use the date-based radius, simply leave {@link #isUseApparentSolarRadius()}
583+ * enabled and let the calculator call this method itself.
535584 *
536- * @param date the date for which to return the solar semi-diameter.
537- * @return the Sun's apparent semi-diameter in degrees (about 0.2711° near the
538- * <a href="https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">perihelion</a>, 0.2622° near the <a href=
539- * "https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">aphelion</a>, and about 0.2666° - the conventional 16′
540- * near the <a href="https://en.wikipedia.org/wiki/Equinox">equinoxes</a>).
541- * @see #setSolarRadius(double)
542- */
543- public double getSolarRadius (LocalDate date ) {
544- return SOLAR_RADIUS_BY_DAY_OF_YEAR [date .withYear (2050 ).getDayOfYear () - 1 ];
585+ * @param localDate the date for which to return the Sun's apparent semi-diameter. If a {@code null} is passed, the
586+ * default solar radius of 16/60 will be returned.
587+ *
588+ * @return the Sun's apparent semi-diameter in degrees: about 0.2711° near the <a href=
589+ * "https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">perihelion</a>, about 0.2622° near the
590+ * <a href="https://en.wikipedia.org/wiki/Apsis#Perihelion_and_aphelion">aphelion</a>, and about 0.2666°
591+ * - the conventional 16′ - near the <a href="https://en.wikipedia.org/wiki/Equinox">equinoxes</a>.
592+ * @see #isUseApparentSolarRadius()
593+ * @see #getSolarRadius()
594+ */
595+ public double getApparentSolarRadius (LocalDate localDate ) {
596+ return localDate == null ? 16 / 60d : SOLAR_RADIUS_BY_DAY_OF_YEAR [localDate .withYear (2050 ).getDayOfYear () - 1 ];
545597 }
546598
547599}
0 commit comments