001/* 002 * Zmanim Java API 003 * Copyright (C) 2004-2024 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; 017 018import java.math.BigDecimal; 019import java.util.Calendar; 020import java.util.Date; 021import java.util.TimeZone; 022 023import com.kosherjava.zmanim.util.AstronomicalCalculator; 024import com.kosherjava.zmanim.util.GeoLocation; 025import com.kosherjava.zmanim.util.ZmanimFormatter; 026 027/** 028 * A Java calendar that calculates astronomical times such as {@link #getSunrise() sunrise}, {@link #getSunset() 029 * sunset} and twilight times. This class contains a {@link #getCalendar() Calendar} and can therefore use the standard 030 * Calendar functionality to change dates etc. The calculation engine used to calculate the astronomical times can be 031 * changed to a different implementation by implementing the abstract {@link AstronomicalCalculator} and setting it with 032 * the {@link #setAstronomicalCalculator(AstronomicalCalculator)}. A number of different calculation engine 033 * implementations are included in the util package. 034 * <b>Note:</b> There are times when the algorithms can't calculate proper values for sunrise, sunset and twilight. This 035 * is usually caused by trying to calculate times for areas either very far North or South, where sunrise / sunset never 036 * happen on that date. This is common when calculating twilight with a deep dip below the horizon for locations as far 037 * south of the North Pole as London, in the northern hemisphere. The sun never reaches this dip at certain times of the 038 * year. When the calculations encounter this condition a <code>null</code> will be returned when a 039 * <code>{@link java.util.Date}</code> is expected and {@link Long#MIN_VALUE} when a <code>long</code> is expected. The 040 * reason that <code>Exception</code>s are not thrown in these cases is because the lack of a rise/set or twilight is 041 * not an exception, but an expected condition in many parts of the world. 042 * <p> 043 * Here is a simple example of how to use the API to calculate sunrise. 044 * First create the Calendar for the location you would like to calculate sunrise or sunset times for: 045 * 046 * <pre> 047 * String locationName = "Lakewood, NJ"; 048 * double latitude = 40.0828; // Lakewood, NJ 049 * double longitude = -74.2094; // Lakewood, NJ 050 * double elevation = 20; // optional elevation correction in Meters 051 * // the String parameter in getTimeZone() has to be a valid timezone listed in 052 * // {@link java.util.TimeZone#getAvailableIDs()} 053 * TimeZone timeZone = TimeZone.getTimeZone("America/New_York"); 054 * GeoLocation location = new GeoLocation(locationName, latitude, longitude, elevation, timeZone); 055 * AstronomicalCalendar ac = new AstronomicalCalendar(location); 056 * </pre> 057 * 058 * To get the time of sunrise, first set the date you want (if not set, the date will default to today): 059 * 060 * <pre> 061 * ac.getCalendar().set(Calendar.MONTH, Calendar.FEBRUARY); 062 * ac.getCalendar().set(Calendar.DAY_OF_MONTH, 8); 063 * Date sunrise = ac.getSunrise(); 064 * </pre> 065 * 066 * @author © Eliyahu Hershfeld 2004 - 2024 067 */ 068public class AstronomicalCalendar implements Cloneable { 069 070 /** 071 * 90° below the vertical. Used as a basis for most calculations since the location of the sun is 90° below 072 * the horizon at sunrise and sunset. 073 * <b>Note </b>: it is important to note that for sunrise and sunset the {@link AstronomicalCalculator#adjustZenith 074 * adjusted zenith} is required to account for the radius of the sun and refraction. The adjusted zenith should not 075 * be used for calculations above or below 90° since they are usually calculated as an offset to 90°. 076 */ 077 public static final double GEOMETRIC_ZENITH = 90; 078 079 /** Sun's zenith at civil twilight (96°). */ 080 public static final double CIVIL_ZENITH = 96; 081 082 /** Sun's zenith at nautical twilight (102°). */ 083 public static final double NAUTICAL_ZENITH = 102; 084 085 /** Sun's zenith at astronomical twilight (108°). */ 086 public static final double ASTRONOMICAL_ZENITH = 108; 087 088 /** constant for milliseconds in a minute (60,000) */ 089 public static final long MINUTE_MILLIS = 60 * 1000; 090 091 /** constant for milliseconds in an hour (3,600,000) */ 092 public static final long HOUR_MILLIS = MINUTE_MILLIS * 60; 093 094 /** 095 * The Java Calendar encapsulated by this class to track the current date used by the class 096 */ 097 private Calendar calendar; 098 099 /** 100 * the {@link GeoLocation} used for calculations. 101 */ 102 private GeoLocation geoLocation; 103 104 /** 105 * the internal {@link AstronomicalCalculator} used for calculating solar based times. 106 */ 107 private AstronomicalCalculator astronomicalCalculator; 108 109 /** 110 * The getSunrise method returns a <code>Date</code> representing the 111 * {@link AstronomicalCalculator#getElevationAdjustment(double) elevation adjusted} sunrise time. The zenith used 112 * for the calculation uses {@link #GEOMETRIC_ZENITH geometric zenith} of 90° plus 113 * {@link AstronomicalCalculator#getElevationAdjustment(double)}. This is adjusted by the 114 * {@link AstronomicalCalculator} to add approximately 50/60 of a degree to account for 34 archminutes of refraction 115 * and 16 archminutes for the sun's radius for a total of {@link AstronomicalCalculator#adjustZenith 90.83333°}. 116 * See documentation for the specific implementation of the {@link AstronomicalCalculator} that you are using. 117 * 118 * @return the <code>Date</code> representing the exact sunrise time. If the calculation can't be computed such as 119 * in the Arctic Circle where there is at least one day a year where the sun does not rise, and one where it 120 * does not set, a <code>null</code> will be returned. See detailed explanation on top of the page. 121 * @see AstronomicalCalculator#adjustZenith 122 * @see #getSeaLevelSunrise() 123 * @see AstronomicalCalendar#getUTCSunrise 124 */ 125 public Date getSunrise() { 126 double sunrise = getUTCSunrise(GEOMETRIC_ZENITH); 127 if (Double.isNaN(sunrise)) { 128 return null; 129 } else { 130 return getDateFromTime(sunrise, SolarEvent.SUNRISE); 131 } 132 } 133 134 /** 135 * A method that returns the sunrise without {@link AstronomicalCalculator#getElevationAdjustment(double) elevation 136 * adjustment}. Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible light, 137 * something that is not affected by elevation. This method returns sunrise calculated at sea level. This forms the 138 * base for dawn calculations that are calculated as a dip below the horizon before sunrise. 139 * 140 * @return the <code>Date</code> representing the exact sea-level sunrise time. If the calculation can't be computed 141 * such as in the Arctic Circle where there is at least one day a year where the sun does not rise, and one 142 * where it does not set, a <code>null</code> will be returned. See detailed explanation on top of the page. 143 * @see AstronomicalCalendar#getSunrise 144 * @see AstronomicalCalendar#getUTCSeaLevelSunrise 145 * @see #getSeaLevelSunset() 146 */ 147 public Date getSeaLevelSunrise() { 148 double sunrise = getUTCSeaLevelSunrise(GEOMETRIC_ZENITH); 149 if (Double.isNaN(sunrise)) { 150 return null; 151 } else { 152 return getDateFromTime(sunrise, SolarEvent.SUNRISE); 153 } 154 } 155 156 /** 157 * A method that returns the beginning of <a href="https://en.wikipedia.org/wiki/Twilight#Civil_twilight">civil twilight</a> 158 * (dawn) using a zenith of {@link #CIVIL_ZENITH 96°}. 159 * 160 * @return The <code>Date</code> of the beginning of civil twilight using a zenith of 96°. If the calculation 161 * can't be computed, <code>null</code> will be returned. See detailed explanation on top of the page. 162 * @see #CIVIL_ZENITH 163 */ 164 public Date getBeginCivilTwilight() { 165 return getSunriseOffsetByDegrees(CIVIL_ZENITH); 166 } 167 168 /** 169 * A method that returns the beginning of <a href= 170 * "https://en.wikipedia.org/wiki/Twilight#Nautical_twilight">nautical twilight</a> using a zenith of {@link 171 * #NAUTICAL_ZENITH 102°}. 172 * 173 * @return The <code>Date</code> of the beginning of nautical twilight using a zenith of 102°. If the calculation 174 * can't be computed <code>null</code> will be returned. See detailed explanation on top of the page. 175 * @see #NAUTICAL_ZENITH 176 */ 177 public Date getBeginNauticalTwilight() { 178 return getSunriseOffsetByDegrees(NAUTICAL_ZENITH); 179 } 180 181 /** 182 * A method that returns the beginning of <a href= 183 * "https://en.wikipedia.org/wiki/Twilight#Astronomical_twilight">astronomical twilight</a> using a zenith of 184 * {@link #ASTRONOMICAL_ZENITH 108°}. 185 * 186 * @return The <code>Date</code> of the beginning of astronomical twilight using a zenith of 108°. If the calculation 187 * can't be computed, <code>null</code> will be returned. See detailed explanation on top of the page. 188 * @see #ASTRONOMICAL_ZENITH 189 */ 190 public Date getBeginAstronomicalTwilight() { 191 return getSunriseOffsetByDegrees(ASTRONOMICAL_ZENITH); 192 } 193 194 /** 195 * The getSunset method returns a <code>Date</code> representing the 196 * {@link AstronomicalCalculator#getElevationAdjustment(double) elevation adjusted} sunset time. The zenith used for 197 * the calculation uses {@link #GEOMETRIC_ZENITH geometric zenith} of 90° plus 198 * {@link AstronomicalCalculator#getElevationAdjustment(double)}. This is adjusted by the 199 * {@link AstronomicalCalculator} to add approximately 50/60 of a degree to account for 34 archminutes of refraction 200 * and 16 archminutes for the sun's radius for a total of {@link AstronomicalCalculator#adjustZenith 90.83333°}. 201 * See documentation for the specific implementation of the {@link AstronomicalCalculator} that you are using. Note: 202 * In certain cases the calculates sunset will occur before sunrise. This will typically happen when a timezone 203 * other than the local timezone is used (calculating Los Angeles sunset using a GMT timezone for example). In this 204 * case the sunset date will be incremented to the following date. 205 * 206 * @return the <code>Date</code> representing the exact sunset time. If the calculation can't be computed such as in 207 * the Arctic Circle where there is at least one day a year where the sun does not rise, and one where it 208 * does not set, a <code>null</code> will be returned. See detailed explanation on top of the page. 209 * @see AstronomicalCalculator#adjustZenith 210 * @see #getSeaLevelSunset() 211 * @see AstronomicalCalendar#getUTCSunset 212 */ 213 public Date getSunset() { 214 double sunset = getUTCSunset(GEOMETRIC_ZENITH); 215 if (Double.isNaN(sunset)) { 216 return null; 217 } else { 218 return getDateFromTime(sunset, SolarEvent.SUNSET); 219 } 220 } 221 222 /** 223 * A method that returns the sunset without {@link AstronomicalCalculator#getElevationAdjustment(double) elevation 224 * adjustment}. Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible light, 225 * something that is not affected by elevation. This method returns sunset calculated at sea level. This forms the 226 * base for dusk calculations that are calculated as a dip below the horizon after sunset. 227 * 228 * @return the <code>Date</code> representing the exact sea-level sunset time. If the calculation can't be computed 229 * such as in the Arctic Circle where there is at least one day a year where the sun does not rise, and one 230 * where it does not set, a <code>null</code> will be returned. See detailed explanation on top of the page. 231 * @see AstronomicalCalendar#getSunset 232 * @see AstronomicalCalendar#getUTCSeaLevelSunset 2see {@link #getSunset()} 233 */ 234 public Date getSeaLevelSunset() { 235 double sunset = getUTCSeaLevelSunset(GEOMETRIC_ZENITH); 236 if (Double.isNaN(sunset)) { 237 return null; 238 } else { 239 return getDateFromTime(sunset, SolarEvent.SUNSET); 240 } 241 } 242 243 /** 244 * A method that returns the end of <a href="https://en.wikipedia.org/wiki/Twilight#Civil_twilight">civil twilight</a> 245 * using a zenith of {@link #CIVIL_ZENITH 96°}. 246 * 247 * @return The <code>Date</code> of the end of civil twilight using a zenith of {@link #CIVIL_ZENITH 96°}. If the 248 * calculation can't be computed, <code>null</code> will be returned. See detailed explanation on top of the page. 249 * @see #CIVIL_ZENITH 250 */ 251 public Date getEndCivilTwilight() { 252 return getSunsetOffsetByDegrees(CIVIL_ZENITH); 253 } 254 255 /** 256 * A method that returns the end of nautical twilight using a zenith of {@link #NAUTICAL_ZENITH 102°}. 257 * 258 * @return The <code>Date</code> of the end of nautical twilight using a zenith of {@link #NAUTICAL_ZENITH 102°}. If 259 * the calculation can't be computed, <code>null</code> will be returned. See detailed explanation on top of the 260 * page. 261 * @see #NAUTICAL_ZENITH 262 */ 263 public Date getEndNauticalTwilight() { 264 return getSunsetOffsetByDegrees(NAUTICAL_ZENITH); 265 } 266 267 /** 268 * A method that returns the end of astronomical twilight using a zenith of {@link #ASTRONOMICAL_ZENITH 108°}. 269 * 270 * @return the <code>Date</code> of the end of astronomical twilight using a zenith of {@link #ASTRONOMICAL_ZENITH 271 * 108°}. If the calculation can't be computed, <code>null</code> will be returned. See detailed 272 * explanation on top of the page. 273 * @see #ASTRONOMICAL_ZENITH 274 */ 275 public Date getEndAstronomicalTwilight() { 276 return getSunsetOffsetByDegrees(ASTRONOMICAL_ZENITH); 277 } 278 279 /** 280 * A utility method that returns a date offset by the offset time passed in as a parameter. This method casts the 281 * offset as a <code>long</code> and calls {@link #getTimeOffset(Date, long)}. 282 * 283 * @param time 284 * the start time 285 * @param offset 286 * the offset in milliseconds to add to the time 287 * @return the {@link java.util.Date}with the offset added to it 288 */ 289 public static Date getTimeOffset(Date time, double offset) { 290 return getTimeOffset(time, (long) offset); 291 } 292 293 /** 294 * A utility method that returns a date offset by the offset time passed in. Please note that the level of light 295 * during twilight is not affected by elevation, so if this is being used to calculate an offset before sunrise or 296 * after sunset with the intent of getting a rough "level of light" calculation, the sunrise or sunset time passed 297 * to this method should be sea level sunrise and sunset. 298 * 299 * @param time 300 * the start time 301 * @param offset 302 * the offset in milliseconds to add to the time. 303 * @return the {@link java.util.Date} with the offset in milliseconds added to it 304 */ 305 public static Date getTimeOffset(Date time, long offset) { 306 if (time == null || offset == Long.MIN_VALUE) { 307 return null; 308 } 309 return new Date(time.getTime() + offset); 310 } 311 312 /** 313 * A utility method that returns the time of an offset by degrees below or above the horizon of 314 * {@link #getSunrise() sunrise}. Note that the degree offset is from the vertical, so for a calculation of 14° 315 * before sunrise, an offset of 14 + {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a parameter. 316 * 317 * @param offsetZenith 318 * the degrees before {@link #getSunrise()} to use in the calculation. For time after sunrise use 319 * negative numbers. Note that the degree offset is from the vertical, so for a calculation of 14° 320 * before sunrise, an offset of 14 + {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a 321 * parameter. 322 * @return The {@link java.util.Date} of the offset after (or before) {@link #getSunrise()}. If the calculation 323 * can't be computed such as in the Arctic Circle where there is at least one day a year where the sun does 324 * not rise, and one where it does not set, a <code>null</code> will be returned. See detailed explanation 325 * on top of the page. 326 */ 327 public Date getSunriseOffsetByDegrees(double offsetZenith) { 328 double dawn = getUTCSunrise(offsetZenith); 329 if (Double.isNaN(dawn)) { 330 return null; 331 } else { 332 return getDateFromTime(dawn, SolarEvent.SUNRISE); 333 } 334 } 335 336 /** 337 * A utility method that returns the time of an offset by degrees below or above the horizon of {@link #getSunset() 338 * sunset}. Note that the degree offset is from the vertical, so for a calculation of 14° after sunset, an 339 * offset of 14 + {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a parameter. 340 * 341 * @param offsetZenith 342 * the degrees after {@link #getSunset()} to use in the calculation. For time before sunset use negative 343 * numbers. Note that the degree offset is from the vertical, so for a calculation of 14° after 344 * sunset, an offset of 14 + {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a parameter. 345 * @return The {@link java.util.Date}of the offset after (or before) {@link #getSunset()}. If the calculation can't 346 * be computed such as in the Arctic Circle where there is at least one day a year where the sun does not 347 * rise, and one where it does not set, a <code>null</code> will be returned. See detailed explanation on 348 * top of the page. 349 */ 350 public Date getSunsetOffsetByDegrees(double offsetZenith) { 351 double sunset = getUTCSunset(offsetZenith); 352 if (Double.isNaN(sunset)) { 353 return null; 354 } else { 355 return getDateFromTime(sunset, SolarEvent.SUNSET); 356 } 357 } 358 359 /** 360 * Default constructor will set a default {@link GeoLocation#GeoLocation()}, a default 361 * {@link AstronomicalCalculator#getDefault() AstronomicalCalculator} and default the calendar to the current date. 362 */ 363 public AstronomicalCalendar() { 364 this(new GeoLocation()); 365 } 366 367 /** 368 * A constructor that takes in <a href="https://en.wikipedia.org/wiki/Geolocation">geolocation</a> information as a 369 * parameter. The default {@link AstronomicalCalculator#getDefault() AstronomicalCalculator} used for solar 370 * calculations is the more accurate {@link com.kosherjava.zmanim.util.NOAACalculator}. 371 * 372 * @param geoLocation 373 * The location information used for calculating astronomical sun times. 374 * 375 * @see #setAstronomicalCalculator(AstronomicalCalculator) for changing the calculator class. 376 */ 377 public AstronomicalCalendar(GeoLocation geoLocation) { 378 setCalendar(Calendar.getInstance(geoLocation.getTimeZone())); 379 setGeoLocation(geoLocation);// duplicate call 380 setAstronomicalCalculator(AstronomicalCalculator.getDefault()); 381 } 382 383 /** 384 * A method that returns the sunrise in UTC time without correction for time zone offset from GMT and without using 385 * daylight savings time. 386 * 387 * @param zenith 388 * the degrees below the horizon. For time after sunrise use negative numbers. 389 * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the 390 * Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does 391 * not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page. 392 */ 393 public double getUTCSunrise(double zenith) { 394 return getAstronomicalCalculator().getUTCSunrise(getAdjustedCalendar(), getGeoLocation(), zenith, true); 395 } 396 397 /** 398 * A method that returns the sunrise in UTC time without correction for time zone offset from GMT and without using 399 * daylight savings time. Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible 400 * light, something that is not affected by elevation. This method returns UTC sunrise calculated at sea level. This 401 * forms the base for dawn calculations that are calculated as a dip below the horizon before sunrise. 402 * 403 * @param zenith 404 * the degrees below the horizon. For time after sunrise use negative numbers. 405 * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the 406 * Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does 407 * not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page. 408 * @see AstronomicalCalendar#getUTCSunrise 409 * @see AstronomicalCalendar#getUTCSeaLevelSunset 410 */ 411 public double getUTCSeaLevelSunrise(double zenith) { 412 return getAstronomicalCalculator().getUTCSunrise(getAdjustedCalendar(), getGeoLocation(), zenith, false); 413 } 414 415 /** 416 * A method that returns the sunset in UTC time without correction for time zone offset from GMT and without using 417 * daylight savings time. 418 * 419 * @param zenith 420 * the degrees below the horizon. For time after sunset use negative numbers. 421 * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the 422 * Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does 423 * not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page. 424 * @see AstronomicalCalendar#getUTCSeaLevelSunset 425 */ 426 public double getUTCSunset(double zenith) { 427 return getAstronomicalCalculator().getUTCSunset(getAdjustedCalendar(), getGeoLocation(), zenith, true); 428 } 429 430 /** 431 * A method that returns the sunset in UTC time without correction for elevation, time zone offset from GMT and 432 * without using daylight savings time. Non-sunrise and sunset calculations such as dawn and dusk, depend on the 433 * amount of visible light, something that is not affected by elevation. This method returns UTC sunset calculated 434 * at sea level. This forms the base for dusk calculations that are calculated as a dip below the horizon after 435 * sunset. 436 * 437 * @param zenith 438 * the degrees below the horizon. For time before sunset use negative numbers. 439 * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the 440 * Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does 441 * not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page. 442 * @see AstronomicalCalendar#getUTCSunset 443 * @see AstronomicalCalendar#getUTCSeaLevelSunrise 444 */ 445 public double getUTCSeaLevelSunset(double zenith) { 446 return getAstronomicalCalculator().getUTCSunset(getAdjustedCalendar(), getGeoLocation(), zenith, false); 447 } 448 449 /** 450 * A method that returns an {@link AstronomicalCalculator#getElevationAdjustment(double) elevation adjusted} 451 * temporal (solar) hour. The day from {@link #getSunrise() sunrise} to {@link #getSunset() sunset} is split into 12 452 * equal parts with each one being a temporal hour. 453 * 454 * @see #getSunrise() 455 * @see #getSunset() 456 * @see #getTemporalHour(Date, Date) 457 * 458 * @return the <code>long</code> millisecond length of a temporal hour. If the calculation can't be computed, 459 * {@link Long#MIN_VALUE} will be returned. See detailed explanation on top of the page. 460 * 461 * @see #getTemporalHour(Date, Date) 462 */ 463 public long getTemporalHour() { 464 return getTemporalHour(getSeaLevelSunrise(), getSeaLevelSunset()); 465 } 466 467 /** 468 * A utility method that will allow the calculation of a temporal (solar) hour based on the sunrise and sunset 469 * passed as parameters to this method. An example of the use of this method would be the calculation of a 470 * non-elevation adjusted temporal hour by passing in {@link #getSeaLevelSunrise() sea level sunrise} and 471 * {@link #getSeaLevelSunset() sea level sunset} as parameters. 472 * 473 * @param startOfDay 474 * The start of the day. 475 * @param endOfDay 476 * The end of the day. 477 * 478 * @return the <code>long</code> millisecond length of the temporal hour. If the calculation can't be computed a 479 * {@link Long#MIN_VALUE} will be returned. See detailed explanation on top of the page. 480 * 481 * @see #getTemporalHour() 482 */ 483 public long getTemporalHour(Date startOfDay, Date endOfDay) { 484 if (startOfDay == null || endOfDay == null) { 485 return Long.MIN_VALUE; 486 } 487 return (endOfDay.getTime() - startOfDay.getTime()) / 12; 488 } 489 490 /** 491 * A method that returns sundial or solar noon. It occurs when the Sun is <a href= 492 * "https://en.wikipedia.org/wiki/Transit_%28astronomy%29">transiting</a> the <a 493 * href="https://en.wikipedia.org/wiki/Meridian_%28astronomy%29">celestial meridian</a>. The calculations used by 494 * this class depend on the {@link AstronomicalCalculator} used. If this calendar instance is {@link 495 * #setAstronomicalCalculator(AstronomicalCalculator) set} to use the {@link com.kosherjava.zmanim.util.NOAACalculator} 496 * (the default) it will calculate astronomical noon. If the calendar instance is to use the 497 * {@link com.kosherjava.zmanim.util.SunTimesCalculator}, that does not have code to calculate astronomical noon, the 498 * sun transit is calculated as halfway between sea level sunrise and sea level sunset, which can be slightly off the 499 * real transit time due to changes in declination (the lengthening or shortening day). See <a href= 500 * "https://kosherjava.com/2020/07/02/definition-of-chatzos/">The Definition of Chatzos</a> for details on the proper 501 * definition of solar noon / midday. 502 * 503 * @return the <code>Date</code> representing Sun's transit. If the calculation can't be computed such as when using 504 * the {@link com.kosherjava.zmanim.util.SunTimesCalculator USNO calculator} that does not support getting solar 505 * noon for the Arctic Circle (where there is at least one day a year where the sun does not rise, and one where 506 * it does not set), a <code>null</code> will be returned. See detailed explanation on top of the page. 507 * @see #getSunTransit(Date, Date) 508 * @see #getTemporalHour() 509 * @see com.kosherjava.zmanim.util.NOAACalculator#getUTCNoon(Calendar, GeoLocation) 510 * @see com.kosherjava.zmanim.util.SunTimesCalculator#getUTCNoon(Calendar, GeoLocation) 511 */ 512 public Date getSunTransit() { 513 double noon = getAstronomicalCalculator().getUTCNoon(getAdjustedCalendar(), getGeoLocation()); 514 return getDateFromTime(noon, SolarEvent.NOON); 515 } 516 517 /** 518 * A method that returns solar midnight. It occurs when the Sun is <a href= 519 * "https://en.wikipedia.org/wiki/Transit_%28astronomy%29">transiting</a> the lower <a 520 * href="https://en.wikipedia.org/wiki/Meridian_%28astronomy%29">celestial meridian</a>, or when the sun is at it's 521 * <a href="https://en.wikipedia.org/wiki/Nadir">nadir</a>. The calculations used by this class depend on the {@link 522 * AstronomicalCalculator} used. If this calendar instance is {@link #setAstronomicalCalculator(AstronomicalCalculator) 523 * set} to use the {@link com.kosherjava.zmanim.util.NOAACalculator} (the default) it will calculate astronomical 524 * midnight. If the calendar instance is to use the {@link com.kosherjava.zmanim.util.SunTimesCalculator}, that does not 525 * have code to calculate astronomical noon, midnight is calculated as halfway between sea level sunrise and sea level 526 * sunset on the other side of the world (180° awa)y, which can be slightly off the real transit time due to changes 527 * in declination (the lengthening or shortening day). See <a href= 528 * "https://kosherjava.com/2020/07/02/definition-of-chatzos/">The Definition of Chatzos</a> for details on the proper 529 * definition of solar noon / midday. 530 * 531 * @return the <code>Date</code> representing Sun's lower transit. If the calculation can't be computed such as when using 532 * the {@link com.kosherjava.zmanim.util.SunTimesCalculator USNO calculator} that does not support getting solar 533 * midnight for the Arctic Circle (where there is at least one day a year where the sun does not rise, and one 534 * where it does not set), a <code>null</code> will be returned. See detailed explanation on top of the page. 535 * 536 * @see #getSunTransit() 537 * @see com.kosherjava.zmanim.util.NOAACalculator#getUTCNoon(Calendar, GeoLocation) 538 * @see com.kosherjava.zmanim.util.SunTimesCalculator#getUTCNoon(Calendar, GeoLocation) 539 */ 540 public Date getSunLowerTransit() { 541 Calendar cal = getAdjustedCalendar(); 542 GeoLocation lowerGeoLocation = (GeoLocation) getGeoLocation().clone(); 543 double meridian = lowerGeoLocation.getLongitude(); 544 double lowerMeridian = meridian + 180; 545 if (lowerMeridian > 180){ 546 lowerMeridian = lowerMeridian - 360; 547 cal.add(Calendar.DAY_OF_MONTH, -1); 548 } 549 lowerGeoLocation.setLongitude(lowerMeridian); 550 double noon = getAstronomicalCalculator().getUTCNoon(cal, lowerGeoLocation); 551 return getDateFromTime(noon, SolarEvent.MIDNIGHT); 552 } 553 554 /** 555 * A method that returns "solar" midnight, or the time when the sun is at its <a 556 * href="https://en.wikipedia.org/wiki/Nadir">nadir</a>. The current calculation is halfway between today and 557 * tomorrow's {@link #getSunTransit() sun transit}. 558 * 559 * @return the <code>Date</code> of astronomical solar midnight. If the calculation can't be computed such as 560 * when using the {@link com.kosherjava.zmanim.util.SunTimesCalculator USNO calculator} that does not 561 * support getting solar noon for the Arctic Circle (where there is at least one day a year where the 562 * sun does not rise, and one where it does not set), a <code>null</code> will be returned. See 563 * detailed explanation on top of the page. 564 * @see com.kosherjava.zmanim.util.NOAACalculator#getUTCNoon(Calendar, GeoLocation) 565 * @see com.kosherjava.zmanim.util.SunTimesCalculator#getUTCNoon(Calendar, GeoLocation) 566 */ 567 public Date getSolarMidnight() { 568 AstronomicalCalendar clonedCal = (AstronomicalCalendar) clone(); 569 clonedCal.getCalendar().add(Calendar.DATE, 1); 570 return getTimeOffset(getSunTransit(), (clonedCal.getSunTransit().getTime() - getSunTransit().getTime()) / 2); 571 } 572 573 /** 574 * A method that returns sundial or solar noon. It occurs when the Sun is <a href 575 * ="https://en.wikipedia.org/wiki/Transit_%28astronomy%29">transiting</a> the <a 576 * href="https://en.wikipedia.org/wiki/Meridian_%28astronomy%29">celestial meridian</a>. In this class it is 577 * calculated as halfway between the sunrise and sunset passed to this method. This time can be slightly off the 578 * real transit time due to changes in declination (the lengthening or shortening day). 579 * 580 * @param startOfDay 581 * the start of day for calculating the sun's transit. This can be sea level sunrise, visual sunrise (or 582 * any arbitrary start of day) passed to this method. 583 * @param endOfDay 584 * the end of day for calculating the sun's transit. This can be sea level sunset, visual sunset (or any 585 * arbitrary end of day) passed to this method. 586 * 587 * @return the <code>Date</code> representing Sun's transit. If the calculation can't be computed such as in the 588 * Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does 589 * not set, <code>null</code> will be returned. See detailed explanation on top of the page. 590 */ 591 public Date getSunTransit(Date startOfDay, Date endOfDay) { 592 long temporalHour = getTemporalHour(startOfDay, endOfDay); 593 return getTimeOffset(startOfDay, temporalHour * 6); 594 } 595 596 /** 597 * An enum to indicate what type of solar event is being calculated. 598 */ 599 protected enum SolarEvent { 600 /**SUNRISE A solar event related to sunrise*/SUNRISE, /**SUNSET A solar event related to sunset*/SUNSET, 601 /**NOON A solar event related to noon*/NOON, /**MIDNIGHT A solar event related to midnight*/MIDNIGHT 602 } 603 /** 604 * A method that returns a <code>Date</code> from the time passed in as a parameter. 605 * 606 * @param time 607 * The time to be set as the time for the <code>Date</code>. The time expected is in the format: 18.75 608 * for 6:45:00 PM.time is sunrise and false if it is sunset 609 * @param solarEvent the type of {@link SolarEvent} 610 * @return The Date object representation of the time double 611 */ 612 protected Date getDateFromTime(double time, SolarEvent solarEvent) { 613 if (Double.isNaN(time)) { 614 return null; 615 } 616 double calculatedTime = time; 617 618 Calendar adjustedCalendar = getAdjustedCalendar(); 619 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 620 cal.clear();// clear all fields 621 cal.set(Calendar.YEAR, adjustedCalendar.get(Calendar.YEAR)); 622 cal.set(Calendar.MONTH, adjustedCalendar.get(Calendar.MONTH)); 623 cal.set(Calendar.DAY_OF_MONTH, adjustedCalendar.get(Calendar.DAY_OF_MONTH)); 624 625 int hours = (int) calculatedTime; // retain only the hours 626 calculatedTime -= hours; 627 int minutes = (int) (calculatedTime *= 60); // retain only the minutes 628 calculatedTime -= minutes; 629 int seconds = (int) (calculatedTime *= 60); // retain only the seconds 630 calculatedTime -= seconds; // remaining milliseconds 631 632 // Check if a date transition has occurred, or is about to occur - this indicates the date of the event is 633 // actually not the target date, but the day prior or after 634 int localTimeHours = (int)getGeoLocation().getLongitude() / 15; 635 if (solarEvent == SolarEvent.SUNRISE && localTimeHours + hours > 18) { 636 cal.add(Calendar.DAY_OF_MONTH, -1); 637 } else if (solarEvent == SolarEvent.SUNSET && localTimeHours + hours < 6) { 638 cal.add(Calendar.DAY_OF_MONTH, 1); 639 } else if (solarEvent == SolarEvent.MIDNIGHT && localTimeHours + hours > 12) { 640 cal.add(Calendar.DAY_OF_MONTH, -1); 641 } 642 643 cal.set(Calendar.HOUR_OF_DAY, hours); 644 cal.set(Calendar.MINUTE, minutes); 645 cal.set(Calendar.SECOND, seconds); 646 cal.set(Calendar.MILLISECOND, (int) (calculatedTime * 1000)); 647 return cal.getTime(); 648 } 649 650 /** 651 * Returns the dip below the horizon before sunrise that matches the offset minutes on passed in as a parameter. For 652 * example passing in 72 minutes for a calendar set to the equinox in Jerusalem returns a value close to 16.1° 653 * Please note that this method is very slow and inefficient and should NEVER be used in a loop. 654 * @todo Improve efficiency of this method by not brute forcing the calculation. 655 * 656 * @param minutes 657 * offset 658 * @return the degrees below the horizon before sunrise that match the offset in minutes passed it as a parameter. 659 * @see #getSunsetSolarDipFromOffset(double) 660 */ 661 public double getSunriseSolarDipFromOffset(double minutes) { 662 Date offsetByDegrees = getSeaLevelSunrise(); 663 Date offsetByTime = getTimeOffset(getSeaLevelSunrise(), -(minutes * MINUTE_MILLIS)); 664 665 BigDecimal degrees = new BigDecimal(0); 666 BigDecimal incrementor = new BigDecimal("0.0001"); 667 668 while (offsetByDegrees == null || ((minutes < 0.0 && offsetByDegrees.getTime() < offsetByTime.getTime()) || 669 (minutes > 0.0 && offsetByDegrees.getTime() > offsetByTime.getTime()))) { 670 if (minutes > 0.0) { 671 degrees = degrees.add(incrementor); 672 } else { 673 degrees = degrees.subtract(incrementor); 674 } 675 offsetByDegrees = getSunriseOffsetByDegrees(GEOMETRIC_ZENITH + degrees.doubleValue()); 676 } 677 return degrees.doubleValue(); 678 } 679 680 /** 681 * Returns the dip below the horizon after sunset that matches the offset minutes on passed in as a parameter. For 682 * example passing in 72 minutes for a calendar set to the equinox in Jerusalem returns a value close to 16.1° 683 * Please note that this method is very slow and inefficient and should NEVER be used in a loop. 684 * @todo Improve efficiency of this method by not brute forcing the calculation. 685 * 686 * @param minutes 687 * offset 688 * @return the degrees below the horizon after sunset that match the offset in minutes passed it as a parameter. 689 * @see #getSunriseSolarDipFromOffset(double) 690 */ 691 public double getSunsetSolarDipFromOffset(double minutes) { 692 Date offsetByDegrees = getSeaLevelSunset(); 693 Date offsetByTime = getTimeOffset(getSeaLevelSunset(), minutes * MINUTE_MILLIS); 694 BigDecimal degrees = new BigDecimal(0); 695 BigDecimal incrementor = new BigDecimal("0.001"); 696 while (offsetByDegrees == null || ((minutes > 0.0 && offsetByDegrees.getTime() < offsetByTime.getTime()) || 697 (minutes < 0.0 && offsetByDegrees.getTime() > offsetByTime.getTime()))) { 698 if (minutes > 0.0) { 699 degrees = degrees.add(incrementor); 700 } else { 701 degrees = degrees.subtract(incrementor); 702 } 703 offsetByDegrees = getSunsetOffsetByDegrees(GEOMETRIC_ZENITH + degrees.doubleValue()); 704 } 705 return degrees.doubleValue(); 706 } 707 708 /** 709 * A method that returns <a href="https://en.wikipedia.org/wiki/Local_mean_time">local mean time (LMT)</a> time 710 * converted to regular clock time for the number of hours (0.0 to 23.999...) passed to this method. This time is 711 * adjusted from standard time to account for the local latitude. The 360° of the globe divided by 24 calculates 712 * to 15° per hour with 4 minutes per degree, so at a longitude of 0 , 15, 30 etc... noon is at exactly 12:00pm. 713 * Lakewood, N.J., with a longitude of -74.222, is 0.7906 away from the closest multiple of 15 at -75°. This is 714 * multiplied by 4 clock minutes (per degree) to yield 3 minutes and 7 seconds for a noon time of 11:56:53am. This 715 * method is not tied to the theoretical 15° time zones, but will adjust to the actual time zone and <a href= 716 * "https://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving time</a> to return LMT. 717 * 718 * @param hours 719 * the hour (such as 12.0 for noon and 0.0 for midnight) to calculate as LMT. Valid values are in the range of 720 * 0.0 to 23.999.... An IllegalArgumentException will be thrown if the value does not fit in the expected range. 721 * @return the Date representing the local mean time (LMT) for the number of hours passed in. In Lakewood, NJ, passing 12 722 * (noon) will return 11:56:50am. 723 * @see GeoLocation#getLocalMeanTimeOffset() 724 */ 725 public Date getLocalMeanTime(double hours) { 726 if (hours < 0 || hours >= 24) { 727 throw new IllegalArgumentException("Hours must between 0 and 23.9999..."); 728 } 729 return getTimeOffset(getDateFromTime(hours - getGeoLocation().getTimeZone().getRawOffset() 730 / (double) HOUR_MILLIS, SolarEvent.SUNRISE), -getGeoLocation().getLocalMeanTimeOffset()); 731 } 732 733 /** 734 * Adjusts the <code>Calendar</code> to deal with edge cases where the location crosses the antimeridian. 735 * 736 * @see GeoLocation#getAntimeridianAdjustment() 737 * @return the adjusted Calendar 738 */ 739 private Calendar getAdjustedCalendar(){ 740 int offset = getGeoLocation().getAntimeridianAdjustment(); 741 if (offset == 0) { 742 return getCalendar(); 743 } 744 Calendar adjustedCalendar = (Calendar) getCalendar().clone(); 745 adjustedCalendar.add(Calendar.DAY_OF_MONTH, offset); 746 return adjustedCalendar; 747 } 748 749 /** 750 * @return an XML formatted representation of the class. It returns the default output of the 751 * {@link com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar) toXML} method. 752 * @see com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar) 753 * @see java.lang.Object#toString() 754 */ 755 public String toString() { 756 return ZmanimFormatter.toXML(this); 757 } 758 759 /** 760 * @return a JSON formatted representation of the class. It returns the default output of the 761 * {@link com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar) toJSON} method. 762 * @see com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar) 763 * @see java.lang.Object#toString() 764 */ 765 public String toJSON() { 766 return ZmanimFormatter.toJSON(this); 767 } 768 769 /** 770 * @see java.lang.Object#equals(Object) 771 */ 772 public boolean equals(Object object) { 773 if (this == object) { 774 return true; 775 } 776 if (!(object instanceof AstronomicalCalendar)) { 777 return false; 778 } 779 AstronomicalCalendar aCal = (AstronomicalCalendar) object; 780 return getCalendar().equals(aCal.getCalendar()) && getGeoLocation().equals(aCal.getGeoLocation()) 781 && getAstronomicalCalculator().equals(aCal.getAstronomicalCalculator()); 782 } 783 784 /** 785 * @see java.lang.Object#hashCode() 786 */ 787 public int hashCode() { 788 int result = 17; 789 result = 37 * result + getClass().hashCode(); // needed or this and subclasses will return identical hash 790 result += 37 * result + getCalendar().hashCode(); 791 result += 37 * result + getGeoLocation().hashCode(); 792 result += 37 * result + getAstronomicalCalculator().hashCode(); 793 return result; 794 } 795 796 /** 797 * A method that returns the currently set {@link GeoLocation} which contains location information used for the 798 * astronomical calculations. 799 * 800 * @return Returns the geoLocation. 801 */ 802 public GeoLocation getGeoLocation() { 803 return this.geoLocation; 804 } 805 806 /** 807 * Sets the {@link GeoLocation} <code>Object</code> to be used for astronomical calculations. 808 * 809 * @param geoLocation 810 * The geoLocation to set. 811 * @todo Possibly adjust for horizon elevation. It may be smart to just have the calculator check the GeoLocation 812 * though it doesn't really belong there. 813 */ 814 public void setGeoLocation(GeoLocation geoLocation) { 815 this.geoLocation = geoLocation; 816 getCalendar().setTimeZone(geoLocation.getTimeZone()); 817 } 818 819 /** 820 * A method that returns the currently set AstronomicalCalculator. 821 * 822 * @return Returns the astronomicalCalculator. 823 * @see #setAstronomicalCalculator(AstronomicalCalculator) 824 */ 825 public AstronomicalCalculator getAstronomicalCalculator() { 826 return this.astronomicalCalculator; 827 } 828 829 /** 830 * A method to set the {@link AstronomicalCalculator} used for astronomical calculations. The Zmanim package ships 831 * with a number of different implementations of the <code>abstract</code> {@link AstronomicalCalculator} based on 832 * different algorithms, including the default {@link com.kosherjava.zmanim.util.NOAACalculator} based on <a href= 833 * "https://noaa.gov">NOAA's</a> implementation of Jean Meeus's algorithms as well as {@link 834 * com.kosherjava.zmanim.util.SunTimesCalculator} based on the <a href = "https://www.cnmoc.usff.navy.mil/usno/">US 835 * Naval Observatory's</a> algorithm. This allows easy runtime switching and comparison of different algorithms. 836 * 837 * @param astronomicalCalculator 838 * The astronomicalCalculator to set. 839 */ 840 public void setAstronomicalCalculator(AstronomicalCalculator astronomicalCalculator) { 841 this.astronomicalCalculator = astronomicalCalculator; 842 } 843 844 /** 845 * returns the Calendar object encapsulated in this class. 846 * 847 * @return Returns the calendar. 848 */ 849 public Calendar getCalendar() { 850 return this.calendar; 851 } 852 853 /** 854 * @param calendar 855 * The calendar to set. 856 */ 857 public void setCalendar(Calendar calendar) { 858 this.calendar = calendar; 859 if (getGeoLocation() != null) {// if available set the Calendar's timezone to the GeoLocation TimeZone 860 getCalendar().setTimeZone(getGeoLocation().getTimeZone()); 861 } 862 } 863 864 /** 865 * A method that creates a <a href="https://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a> of the object. 866 * <b>Note:</b> If the {@link java.util.TimeZone} in the cloned {@link com.kosherjava.zmanim.util.GeoLocation} will 867 * be changed from the original, it is critical that 868 * {@link com.kosherjava.zmanim.AstronomicalCalendar#getCalendar()}. 869 * {@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)} be called in order for the 870 * AstronomicalCalendar to output times in the expected offset after being cloned. 871 * 872 * @see java.lang.Object#clone() 873 */ 874 public Object clone() { 875 AstronomicalCalendar clone = null; 876 try { 877 clone = (AstronomicalCalendar) super.clone(); 878 } catch (CloneNotSupportedException cnse) { 879 // Required by the compiler. Should never be reached since we implement clone() 880 } 881 if (clone != null) { 882 clone.setGeoLocation((GeoLocation) getGeoLocation().clone()); 883 clone.setCalendar((Calendar) getCalendar().clone()); 884 clone.setAstronomicalCalculator((AstronomicalCalculator) getAstronomicalCalculator().clone()); 885 } 886 return clone; 887 } 888}