001/*
002 * Zmanim Java API
003 * Copyright (C) 2004-2023 Eliyahu Hershfeld
004 *
005 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
006 * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option)
007 * any later version.
008 *
009 * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied
010 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
011 * details.
012 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to
013 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA,
014 * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
015 */
016package com.kosherjava.zmanim.util;
017
018import java.util.Calendar;
019
020/**
021 * An abstract class that all sun time calculating classes extend. This allows the algorithm used to be changed at
022 * runtime, easily allowing comparison the results of using different algorithms.
023 * @todo Consider methods that would allow atmospheric modeling. This can currently be adjusted by {@link
024 * #setRefraction(double) setting the refraction}.
025 * 
026 * @author © Eliyahu Hershfeld 2004 - 2023
027 */
028public abstract class AstronomicalCalculator implements Cloneable {
029        /**
030         * The commonly used average solar refraction. Calendrical Calculations lists a more accurate global average of
031         * 34.478885263888294
032         * 
033         * @see #getRefraction()
034         */
035        private double refraction = 34 / 60d;
036
037        /**
038         * The commonly used average solar radius in minutes of a degree.
039         * 
040         * @see #getSolarRadius()
041         */
042        private double solarRadius = 16 / 60d;
043
044        /**
045         * The commonly used average earth radius in KM. At this time, this only affects elevation adjustment and not the
046         * sunrise and sunset calculations. The value currently defaults to 6356.9 KM.
047         * 
048         * @see #getEarthRadius()
049         * @see #setEarthRadius(double)
050         */
051        private double earthRadius = 6356.9; // in KM
052
053        /**
054         * A method that returns the earth radius in KM. The value currently defaults to 6356.9 KM if not set.
055         * 
056         * @return the earthRadius the earth radius in KM.
057         */
058        public double getEarthRadius() {
059                return earthRadius;
060        }
061
062        /**
063         * A method that allows setting the earth's radius.
064         * 
065         * @param earthRadius
066         *            the earthRadius to set in KM
067         */
068        public void setEarthRadius(double earthRadius) {
069                this.earthRadius = earthRadius;
070        }
071
072        /**
073         * The zenith of astronomical sunrise and sunset. The sun is 90° from the vertical 0°
074         */
075        private static final double GEOMETRIC_ZENITH = 90;
076
077        /**
078         * Returns the default class for calculating sunrise and sunset. This is currently the {@link NOAACalculator},
079         * but this may change.
080         * 
081         * @return AstronomicalCalculator the default class for calculating sunrise and sunset. In the current
082         *         implementation the default calculator returned is the {@link NOAACalculator}.
083         */
084        public static AstronomicalCalculator getDefault() {
085                return new NOAACalculator();
086        }
087
088        /**
089         * Returns the name of the algorithm.
090         * 
091         * @return the descriptive name of the algorithm.
092         */
093        public abstract String getCalculatorName();
094
095        /**
096         * Setter method for the descriptive name of the calculator. This will typically not have to be set
097         * 
098         * @param calculatorName
099         *            descriptive name of the algorithm.
100         */
101
102        /**
103         * A method that calculates UTC sunrise as well as any time based on an angle above or below sunrise. This abstract
104         * method is implemented by the classes that extend this class.
105         * 
106         * @param calendar
107         *            Used to calculate day of year.
108         * @param geoLocation
109         *            The location information used for astronomical calculating sun times.
110         * @param zenith
111         *            the azimuth below the vertical zenith of 90 degrees. for sunrise typically the {@link #adjustZenith
112         *            zenith} used for the calculation uses geometric zenith of 90° and {@link #adjustZenith adjusts}
113         *            this slightly to account for solar refraction and the sun's radius. Another example would be
114         *            {@link com.kosherjava.zmanim.AstronomicalCalendar#getBeginNauticalTwilight()} that passes
115         *            {@link com.kosherjava.zmanim.AstronomicalCalendar#NAUTICAL_ZENITH} to this method.
116         * @param adjustForElevation
117         *            Should the time be adjusted for elevation
118         * @return The UTC time of sunrise in 24 hour format. 5:45:00 AM will return 5.75.0. If an error was encountered in
119         *         the calculation (expected behavior for some locations such as near the poles,
120         *         {@link java.lang.Double#NaN} will be returned.
121         * @see #getElevationAdjustment(double)
122         */
123        public abstract double getUTCSunrise(Calendar calendar, GeoLocation geoLocation, double zenith,
124                        boolean adjustForElevation);
125
126        /**
127         * A method that calculates UTC sunset as well as any time based on an angle above or below sunset. This abstract
128         * method is implemented by the classes that extend this class.
129         * 
130         * @param calendar
131         *            Used to calculate day of year.
132         * @param geoLocation
133         *            The location information used for astronomical calculating sun times.
134         * @param zenith
135         *            the azimuth below the vertical zenith of 90°. For sunset typically the {@link #adjustZenith
136         *            zenith} used for the calculation uses geometric zenith of 90° and {@link #adjustZenith adjusts}
137         *            this slightly to account for solar refraction and the sun's radius. Another example would be
138         *            {@link com.kosherjava.zmanim.AstronomicalCalendar#getEndNauticalTwilight()} that passes
139         *            {@link com.kosherjava.zmanim.AstronomicalCalendar#NAUTICAL_ZENITH} to this method.
140         * @param adjustForElevation
141         *            Should the time be adjusted for elevation
142         * @return The UTC time of sunset in 24 hour format. 5:45:00 AM will return 5.75.0. If an error was encountered in
143         *         the calculation (expected behavior for some locations such as near the poles,
144         *         {@link java.lang.Double#NaN} will be returned.
145         * @see #getElevationAdjustment(double)
146         */
147        public abstract double getUTCSunset(Calendar calendar, GeoLocation geoLocation, double zenith,
148                        boolean adjustForElevation);
149        
150        
151        /**
152         * Return <a href="https://en.wikipedia.org/wiki/Noon#Solar_noon">solar noon</a> (UTC) for the given day at the
153         * given location on earth. The the {@link com.kosherjava.zmanim.util.NOAACalculator} implementation calculates
154         * true solar noon, while the {@link com.kosherjava.zmanim.util.SunTimesCalculator} approximates it, calculating
155         * the time as halfway between sunrise and sunset.
156         * 
157         * @param calendar
158         *            Used to calculate day of year.
159         * @param geoLocation
160         *            The location information used for astronomical calculating sun times.         
161         * 
162         * @return the time in minutes from zero UTC
163         */
164        public abstract double getUTCNoon(Calendar calendar, GeoLocation geoLocation);
165
166        /**
167         * Method to return the adjustment to the zenith required to account for the elevation. Since a person at a higher
168         * elevation can see farther below the horizon, the calculation for sunrise / sunset is calculated below the horizon
169         * used at sea level. This is only used for sunrise and sunset and not times before or after it such as
170         * {@link com.kosherjava.zmanim.AstronomicalCalendar#getBeginNauticalTwilight() nautical twilight} since those
171         * calculations are based on the level of available light at the given dip below the horizon, something that is not
172         * affected by elevation, the adjustment should only made if the zenith == 90&deg; {@link #adjustZenith adjusted}
173         * for refraction and solar radius. The algorithm used is
174         * 
175         * <pre>
176         * elevationAdjustment = Math.toDegrees(Math.acos(earthRadiusInMeters / (earthRadiusInMeters + elevationMeters)));
177         * </pre>
178         * 
179         * The source of this algorithm is <a href="http://www.calendarists.com">Calendrical Calculations</a> by Edward M.
180         * Reingold and Nachum Dershowitz. An alternate algorithm that produces an almost identical (but not accurate)
181         * result found in Ma'aglay Tzedek by Moishe Kosower and other sources is:
182         * 
183         * <pre>
184         * elevationAdjustment = 0.0347 * Math.sqrt(elevationMeters);
185         * </pre>
186         * 
187         * @param elevation
188         *            elevation in Meters.
189         * @return the adjusted zenith
190         */
191        double getElevationAdjustment(double elevation) {
192                double elevationAdjustment = Math.toDegrees(Math.acos(earthRadius / (earthRadius + (elevation / 1000))));
193                return elevationAdjustment;
194        }
195
196        /**
197         * Adjusts the zenith of astronomical sunrise and sunset to account for solar refraction, solar radius and
198         * elevation. The value for Sun's zenith and true rise/set Zenith (used in this class and subclasses) is the angle
199         * that the center of the Sun makes to a line perpendicular to the Earth's surface. If the Sun were a point and the
200         * Earth were without an atmosphere, true sunset and sunrise would correspond to a 90&deg; zenith. Because the Sun
201         * is not a point, and because the atmosphere refracts light, this 90&deg; zenith does not, in fact, correspond to
202         * true sunset or sunrise, instead the center of the Sun's disk must lie just below the horizon for the upper edge
203         * to be obscured. This means that a zenith of just above 90&deg; must be used. The Sun subtends an angle of 16
204         * minutes of arc (this can be changed via the {@link #setSolarRadius(double)} method , and atmospheric refraction
205         * accounts for 34 minutes or so (this can be changed via the {@link #setRefraction(double)} method), giving a total
206         * of 50 arcminutes. The total value for ZENITH is 90+(5/6) or 90.8333333&deg; for true sunrise/sunset. Since a
207         * person at an elevation can see blow the horizon of a person at sea level, this will also adjust the zenith to
208         * account for elevation if available. Note that this will only adjust the value if the zenith is exactly 90 degrees.
209         * For values below and above this no correction is done. As an example, astronomical twilight is when the sun is
210         * 18&deg; below the horizon or {@link com.kosherjava.zmanim.AstronomicalCalendar#ASTRONOMICAL_ZENITH 108&deg;
211         * below the zenith}. This is traditionally calculated with none of the above mentioned adjustments. The same goes
212         * for various <em>tzais</em> and <em>alos</em> times such as the
213         * {@link com.kosherjava.zmanim.ZmanimCalendar#ZENITH_16_POINT_1 16.1&deg;} dip used in
214         * {@link com.kosherjava.zmanim.ComplexZmanimCalendar#getAlos16Point1Degrees()}.
215         * 
216         * @param zenith
217         *            the azimuth below the vertical zenith of 90&deg;. For sunset typically the {@link #adjustZenith
218         *            zenith} used for the calculation uses geometric zenith of 90&deg; and {@link #adjustZenith adjusts}
219         *            this slightly to account for solar refraction and the sun's radius. Another example would be
220         *            {@link com.kosherjava.zmanim.AstronomicalCalendar#getEndNauticalTwilight()} that passes
221         *            {@link com.kosherjava.zmanim.AstronomicalCalendar#NAUTICAL_ZENITH} to this method.
222         * @param elevation
223         *            elevation in Meters.
224         * @return The zenith adjusted to include the {@link #getSolarRadius sun's radius}, {@link #getRefraction
225         *         refraction} and {@link #getElevationAdjustment elevation} adjustment. This will only be adjusted for
226         *         sunrise and sunset (if the zenith == 90&deg;)
227         * @see #getElevationAdjustment(double)
228         */
229        double adjustZenith(double zenith, double elevation) {
230                double adjustedZenith = zenith;
231                if (zenith == GEOMETRIC_ZENITH) { // only adjust if it is exactly sunrise or sunset
232                        adjustedZenith = zenith + (getSolarRadius() + getRefraction() + getElevationAdjustment(elevation));
233                }
234                return adjustedZenith;
235        }
236
237        /**
238         * Method to get the refraction value to be used when calculating sunrise and sunset. The default value is 34 arc
239         * minutes. The <a href=
240         * "https://web.archive.org/web/20150915094635/http://emr.cs.iit.edu/home/reingold/calendar-book/second-edition/errata.pdf"
241         * >Errata and Notes for Calendrical Calculations: The Millennium Edition</a> by Edward M. Reingold and Nachum Dershowitz
242         * lists the actual average refraction value as 34.478885263888294 or approximately 34' 29". The refraction value as well
243         * as the solarRadius and elevation adjustment are added to the zenith used to calculate sunrise and sunset.
244         * 
245         * @return The refraction in arc minutes.
246         */
247        public double getRefraction() {
248                return this.refraction;
249        }
250
251        /**
252         * A method to allow overriding the default refraction of the calculator.
253         * @todo At some point in the future, an AtmosphericModel or Refraction object that models the atmosphere of different
254         * locations might be used for increased accuracy.
255         * 
256         * @param refraction
257         *            The refraction in arc minutes.
258         * @see #getRefraction()
259         */
260        public void setRefraction(double refraction) {
261                this.refraction = refraction;
262        }
263
264        /**
265         * Method to get the sun's radius. The default value is 16 arc minutes. The sun's radius as it appears from earth is
266         * almost universally given as 16 arc minutes but in fact it differs by the time of the year. At the <a
267         * href="https://en.wikipedia.org/wiki/Perihelion">perihelion</a> it has an apparent radius of 16.293, while at the
268         * <a href="https://en.wikipedia.org/wiki/Aphelion">aphelion</a> it has an apparent radius of 15.755. There is little
269         * affect for most location, but at high and low latitudes the difference becomes more apparent. My Calculations for
270         * the difference at the location of the <a href="https://www.rmg.co.uk/royal-observatory">Royal Observatory, Greenwich</a>
271         * shows only a 4.494 second difference between the perihelion and aphelion radii, but moving into the arctic circle the
272         * difference becomes more noticeable. Tests for Tromso, Norway (latitude 69.672312, longitude 19.049787) show that
273         * on May 17, the rise of the midnight sun, a 2 minute 23 second difference is observed between the perihelion and
274         * aphelion radii using the USNO algorithm, but only 1 minute and 6 seconds difference using the NOAA algorithm.
275         * Areas farther north show an even greater difference. Note that these test are not real valid test cases because
276         * they show the extreme difference on days that are not the perihelion or aphelion, but are shown for illustrative
277         * purposes only.
278         * 
279         * @return The sun's radius in arc minutes.
280         */
281        public double getSolarRadius() {
282                return this.solarRadius;
283        }
284
285        /**
286         * Method to set the sun's radius.
287         * 
288         * @param solarRadius
289         *            The sun's radius in arc minutes.
290         * @see #getSolarRadius()
291         */
292        public void setSolarRadius(double solarRadius) {
293                this.solarRadius = solarRadius;
294        }
295
296        /**
297         * @see java.lang.Object#clone()
298         * @since 1.1
299         */
300        public Object clone() {
301                AstronomicalCalculator clone = null;
302                try {
303                        clone = (AstronomicalCalculator) super.clone();
304                } catch (CloneNotSupportedException cnse) {
305                        System.out.print("Required by the compiler. Should never be reached since we implement clone()");
306                }
307                return clone;
308        }
309}