diff --git a/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java b/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java index b894f1ab..22d56420 100644 --- a/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java +++ b/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java @@ -64,6 +64,26 @@ public class GeoLocation implements Cloneable { * @see #setElevation(double) */ private double elevation; + + /** + * The elevation in Meters above sea level of the horizon at the point where the sun rises that day. + * This point is not simply due east and shifts through the year as the sunrise azimuth changes with the season. + * Defaults to 0. + * @see #getHorizonElevationAtSunrise() + * @see #setHorizonElevationAtSunrise(double) + * @see #getSunriseElevation() + */ + private double horizonElevationAtSunrise = 0; + + /** + * The elevation in Meters above sea level of the horizon at the point where the sun sets that day. + * This point is not simply due west and shifts through the year as the sunset azimuth changes with the season. + * Defaults to 0. + * @see #getHorizonElevationAtSunset() + * @see #setHorizonElevationAtSunset(double) + * @see #getSunsetElevation() + */ + private double horizonElevationAtSunset = 0; /** * Constant for a distance type calculation. @@ -118,6 +138,87 @@ public void setElevation(double elevation) { this.elevation = elevation; } + /** + * Method to return the elevation in Meters above sea level of the horizon at the point where the sun rises that day. + * + * @return the horizon elevation at sunrise in Meters. + * @see #setHorizonElevationAtSunrise(double) + */ + public double getHorizonElevationAtSunrise() { + return horizonElevationAtSunrise; + } + + /** + * Method to set the elevation in Meters above sea level of the horizon at the point where the sun rises that day. + * This point is not simply due east and shifts through the year as the sunrise azimuth changes with the season. + * Sunrise is calculated with the observer's height above this horizon instead of above sea level. + * @param horizonElevationAtSunrise the horizon elevation at sunrise in Meters. An IllegalArgumentException will be thrown + * if the value is a negative, {@link java.lang.Double#isNaN(double)} or {@link java.lang.Double#isInfinite(double)}. + * @see #getSunriseElevation() + */ + public void setHorizonElevationAtSunrise(double horizonElevationAtSunrise) { + validateHorizonElevation(horizonElevationAtSunrise); + this.horizonElevationAtSunrise = horizonElevationAtSunrise; + } + + /** + * Method to return the elevation in Meters above sea level of the horizon at the point where the sun sets that day. + * + * @return the horizon elevation at sunset in Meters. + * @see #setHorizonElevationAtSunset(double) + */ + public double getHorizonElevationAtSunset() { + return horizonElevationAtSunset; + } + + /** + * Method to set the elevation in Meters above sea level of the horizon at the point where the sun sets that day. + * This point is not simply due west and shifts through the year as the sunset azimuth changes with the season. + * Sunset is calculated with the observer's height above this horizon instead of above sea level. + * @param horizonElevationAtSunset the horizon elevation at sunset in Meters. An IllegalArgumentException will be thrown + * if the value is a negative, {@link java.lang.Double#isNaN(double)} or {@link java.lang.Double#isInfinite(double)}. + * @see #getSunsetElevation() + */ + public void setHorizonElevationAtSunset(double horizonElevationAtSunset) { + validateHorizonElevation(horizonElevationAtSunset); + this.horizonElevationAtSunset = horizonElevationAtSunset; + } + + /** + * Throws if a horizon elevation is not a usable height in Meters above sea level. + * @param horizonElevation the value to check. + */ + private static void validateHorizonElevation(double horizonElevation) { + if (horizonElevation < 0) { + throw new IllegalArgumentException("Horizon elevation cannot be negative"); + } + if (Double.isNaN(horizonElevation) || Double.isInfinite(horizonElevation)) { + throw new IllegalArgumentException("Horizon elevation cannot be NaN or infinite"); + } + } + + /** + * The height in Meters of the observer above the {@link #getHorizonElevationAtSunrise() sunrise horizon}. + * A higher horizon returns 0, since halachically land blocking the sun does not delay sunrise. + * + * @return the observer's height above the sunrise horizon in Meters, never negative. With the default sea-level horizon + * this is the same as {@link #getElevation()}. + */ + public double getSunriseElevation() { + return Math.max(elevation - horizonElevationAtSunrise, 0); + } + + /** + * The height in Meters of the observer above the {@link #getHorizonElevationAtSunset() sunset horizon}. + * A higher horizon returns 0, since halachically land blocking the sun does not make sunset earlier. + * + * @return the observer's height above the sunset horizon in Meters, never negative. With the default sea-level horizon + * this is the same as {@link #getElevation()}. + */ + public double getSunsetElevation() { + return Math.max(elevation - horizonElevationAtSunset, 0); + } + /** * GeoLocation constructor with parameters for all required fields. * @@ -153,6 +254,30 @@ public GeoLocation(String name, double latitude, double longitude, double elevat setZoneId(zoneId); } + /** + * GeoLocation constructor for a location whose horizon at sunrise or sunset is not at sea level. + * + * @param name The location name for display, for example "Lakewood, NJ". + * @param latitude the latitude as a {@code double}, for example 40.096 for Lakewood, NJ. Note: For latitudes south of + * the equator, a negative value should be used. + * @param longitude the longitude as a {@code double}, for example -74.222 for Lakewood, NJ. Note: For longitudes west + * of the Prime Meridian (Greenwich), a negative value should + * be used. + * @param elevation the elevation above sea level in Meters. + * @param horizonElevationAtSunrise the elevation above sea level in Meters of the horizon at the point where the sun rises + * that day. + * @param horizonElevationAtSunset the elevation above sea level in Meters of the horizon at the point where the sun sets + * that day. + * @param zoneId the {@code ZoneId} for the location. + * @see #setHorizonElevationAtSunrise(double) + */ + public GeoLocation(String name, double latitude, double longitude, double elevation, double horizonElevationAtSunrise, + double horizonElevationAtSunset, ZoneId zoneId) { + this(name, latitude, longitude, elevation, zoneId); + setHorizonElevationAtSunrise(horizonElevationAtSunrise); + setHorizonElevationAtSunset(horizonElevationAtSunset); + } + /** * Default GeoLocation constructor will set location to the Prime Meridian at Greenwich, England and a {@code ZoneId} * of GMT. The longitude will be set to 0 and the latitude will be 51.4772 to match the location of the " + getLatitude() + "\n" + "\t" + getLongitude() + "\n" + "\t" + getElevation() + " Meters" + "\n" + + "\t" + getHorizonElevationAtSunrise() + " Meters" + "\n" + + "\t" + getHorizonElevationAtSunset() + " Meters" + "\n" + "\t" + getZoneId().getId() + "\n" + "\t" + getZoneId().getDisplayName(TextStyle.FULL, Locale.ENGLISH) + "\n" + ""; @@ -509,6 +636,8 @@ public boolean equals(Object object) { return Double.compare(this.latitude, geo.latitude) == 0 && Double.compare(this.longitude, geo.longitude) == 0 && Double.compare(this.elevation, geo.elevation) == 0 + && Double.compare(this.horizonElevationAtSunrise, geo.horizonElevationAtSunrise) == 0 + && Double.compare(this.horizonElevationAtSunset, geo.horizonElevationAtSunset) == 0 && Objects.equals(this.locationName, geo.locationName) && Objects.equals(this.zoneId, geo.zoneId); } @@ -518,7 +647,8 @@ public boolean equals(Object object) { */ @Override public int hashCode() { - return Objects.hash(getClass(), this.latitude, this.longitude, this.elevation, this.locationName, this.zoneId); + return Objects.hash(getClass(), this.latitude, this.longitude, this.elevation, this.horizonElevationAtSunrise, + this.horizonElevationAtSunset, this.locationName, this.zoneId); } /** @@ -530,6 +660,8 @@ public String toString() { "\nLatitude:\t\t\t" + getLatitude() + "\u00B0" + "\nLongitude:\t\t\t" + getLongitude() + "\u00B0" + "\nElevation:\t\t\t" + getElevation() + " Meters" + + "\nHorizon Elevation At Sunrise:\t" + getHorizonElevationAtSunrise() + " Meters" + + "\nHorizon Elevation At Sunset:\t" + getHorizonElevationAtSunset() + " Meters" + "\nTimezone ID:\t\t\t" + getZoneId().getId() + "\nTimezone Display Name:\t\t" + getZoneId().getDisplayName(TextStyle.FULL, Locale.ENGLISH); } diff --git a/src/main/java/com/kosherjava/zmanim/util/MeeusCalculator.java b/src/main/java/com/kosherjava/zmanim/util/MeeusCalculator.java index 046afaed..e4187a63 100644 --- a/src/main/java/com/kosherjava/zmanim/util/MeeusCalculator.java +++ b/src/main/java/com/kosherjava/zmanim/util/MeeusCalculator.java @@ -150,7 +150,14 @@ public double getUTCSunset(LocalDate dt, GeoLocation geoLocation, double zenith, */ private double getUTCSunRiseSet(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation, SolarEvent solarEvent) { - double elevation = adjustForElevation ? geoLocation.getElevation() : 0; + double elevation = 0; + if (adjustForElevation) { + if (solarEvent == SolarEvent.SUNRISE) { + elevation = geoLocation.getSunriseElevation(); + } else { + elevation = geoLocation.getSunsetElevation(); + } + } double adjustedZenith = adjustZenith(zenith, elevation, localDate); double riseSet = getSunRiseSetUTC(localDate, geoLocation.getLatitude(), -geoLocation.getLongitude(), adjustedZenith, solarEvent); diff --git a/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java b/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java index 633ae38b..416e2f74 100644 --- a/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java +++ b/src/main/java/com/kosherjava/zmanim/util/NOAACalculator.java @@ -97,7 +97,14 @@ public double getUTCSunset(LocalDate dt, GeoLocation geoLocation, double zenith, */ private double getUTCSunRiseSet(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation, SolarEvent solarEvent) { - double elevation = adjustForElevation ? geoLocation.getElevation() : 0; + double elevation = 0; + if (adjustForElevation) { + if (solarEvent == SolarEvent.SUNRISE) { + elevation = geoLocation.getSunriseElevation(); + } else { + elevation = geoLocation.getSunsetElevation(); + } + } double adjustedZenith = adjustZenith(zenith, elevation, localDate); double riseSet = getSunRiseSetUTC(localDate, geoLocation.getLatitude(), -geoLocation.getLongitude(), adjustedZenith, solarEvent); diff --git a/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java b/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java index 0717069b..100457ea 100644 --- a/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java +++ b/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java @@ -207,7 +207,14 @@ public double getUTCSunset(LocalDate dt, GeoLocation geoLocation, double zenith, */ private double getUTCSunRiseSet(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation, SolarEvent solarEvent) { - double elevation = adjustForElevation ? geoLocation.getElevation() : 0; + double elevation = 0; + if (adjustForElevation) { + if (solarEvent == SolarEvent.SUNRISE) { + elevation = geoLocation.getSunriseElevation(); + } else { + elevation = geoLocation.getSunsetElevation(); + } + } double adjustedZenith = adjustZenith(zenith, elevation, localDate); double riseSet = solveRiseSet(localDate, geoLocation, adjustedZenith, solarEvent); if (Double.isNaN(riseSet)) { diff --git a/src/main/java/com/kosherjava/zmanim/util/SunTimesCalculator.java b/src/main/java/com/kosherjava/zmanim/util/SunTimesCalculator.java index 4b36ae54..5c341e46 100644 --- a/src/main/java/com/kosherjava/zmanim/util/SunTimesCalculator.java +++ b/src/main/java/com/kosherjava/zmanim/util/SunTimesCalculator.java @@ -66,14 +66,14 @@ public String getCalculatorName() { @Override public double getUTCSunrise(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation) { - double elevation = adjustForElevation ? geoLocation.getElevation() : 0; + double elevation = adjustForElevation ? geoLocation.getSunriseElevation() : 0; double adjustedZenith = adjustZenith(zenith, elevation, localDate); return getTimeUTC(localDate, geoLocation, adjustedZenith, true); } @Override public double getUTCSunset(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation) { - double elevation = adjustForElevation ? geoLocation.getElevation() : 0; + double elevation = adjustForElevation ? geoLocation.getSunsetElevation() : 0; double adjustedZenith = adjustZenith(zenith, elevation, localDate); return getTimeUTC(localDate, geoLocation, adjustedZenith, false); }