From 5e588fcf285a4a026f0bd1164b5449a8ab7e938f Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Wed, 21 Oct 2026 12:57:30 -0400 Subject: [PATCH 1/2] Enhance GeoLocation and related calculators to support eastern and western horizon elevations for accurate sunrise and sunset calculations --- .../kosherjava/zmanim/util/GeoLocation.java | 135 +++++++++++++++++- .../zmanim/util/MeeusCalculator.java | 9 +- .../zmanim/util/NOAACalculator.java | 9 +- .../kosherjava/zmanim/util/SPACalculator.java | 9 +- .../zmanim/util/SunTimesCalculator.java | 4 +- 5 files changed, 160 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java b/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java index b894f1ab..cc012587 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 in the direction of sunrise. The sun's rising point moves along + * the horizon through the year, so this describes the skyline at that day's sunrise azimuth and is expected to change from + * day to day. Defaults to 0, a sea-level horizon. + * @see #getHorizonElevationAtSunrise() + * @see #setHorizonElevationAtSunrise(double) + * @see #getSunriseElevation() + */ + private double horizonElevationAtSunrise = 0; + + /** + * The elevation in Meters above sea level of the horizon in the direction of sunset. The sun's setting point moves along + * the horizon through the year, so this describes the skyline at that day's sunset azimuth and is expected to change from + * day to day. Defaults to 0, a sea-level horizon. + * @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 sunrise azimuth. + * + * @return Returns 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 sunrise azimuth. Sunrise is calculated with the + * observer's height above this horizon instead of above sea level. Since the sunrise azimuth moves through the year, this is + * a per-day value; a caller tracking a varied skyline should set it for the date being calculated. + * @param horizonElevationAtSunrise The horizon elevation at sunrise to set 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 sunset azimuth. + * + * @return Returns 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 sunset azimuth. Sunset is calculated with the + * observer's height above this horizon instead of above sea level. Since the sunset azimuth moves through the year, this is + * a per-day value; a caller tracking a varied skyline should set it for the date being calculated. + * @param horizonElevationAtSunset The horizon elevation at sunset to set 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() horizon at the sunrise azimuth}, used for sunrise. + * 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() horizon at the sunset azimuth}, used for sunset. + * 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,31 @@ 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 sunrise azimuth. The sun + * rises at a different point on the horizon each day, so this is the skyline height for the date being calculated + * and is expected to change from day to day. + * @param horizonElevationAtSunset the elevation above sea level in Meters of the horizon at the sunset azimuth. Like the + * sunrise horizon, this is a per-day value that follows the sun's setting point along the horizon. + * @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 +637,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 +648,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 +661,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); } From e8907918e16eaf0670ee1bc580f3851c38e7c0e6 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Wed, 21 Oct 2026 13:17:08 -0400 Subject: [PATCH 2/2] update docs --- .../kosherjava/zmanim/util/GeoLocation.java | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java b/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java index cc012587..22d56420 100644 --- a/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java +++ b/src/main/java/com/kosherjava/zmanim/util/GeoLocation.java @@ -66,9 +66,9 @@ public class GeoLocation implements Cloneable { private double elevation; /** - * The elevation in Meters above sea level of the horizon in the direction of sunrise. The sun's rising point moves along - * the horizon through the year, so this describes the skyline at that day's sunrise azimuth and is expected to change from - * day to day. Defaults to 0, a sea-level horizon. + * 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() @@ -76,9 +76,9 @@ public class GeoLocation implements Cloneable { private double horizonElevationAtSunrise = 0; /** - * The elevation in Meters above sea level of the horizon in the direction of sunset. The sun's setting point moves along - * the horizon through the year, so this describes the skyline at that day's sunset azimuth and is expected to change from - * day to day. Defaults to 0, a sea-level horizon. + * 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() @@ -139,9 +139,9 @@ public void setElevation(double elevation) { } /** - * Method to return the elevation in Meters above sea level of the horizon at the sunrise azimuth. + * Method to return the elevation in Meters above sea level of the horizon at the point where the sun rises that day. * - * @return Returns the horizon elevation at sunrise in Meters. + * @return the horizon elevation at sunrise in Meters. * @see #setHorizonElevationAtSunrise(double) */ public double getHorizonElevationAtSunrise() { @@ -149,10 +149,10 @@ public double getHorizonElevationAtSunrise() { } /** - * Method to set the elevation in Meters above sea level of the horizon at the sunrise azimuth. Sunrise is calculated with the - * observer's height above this horizon instead of above sea level. Since the sunrise azimuth moves through the year, this is - * a per-day value; a caller tracking a varied skyline should set it for the date being calculated. - * @param horizonElevationAtSunrise The horizon elevation at sunrise to set in Meters. An IllegalArgumentException will be thrown + * 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() */ @@ -162,9 +162,9 @@ public void setHorizonElevationAtSunrise(double horizonElevationAtSunrise) { } /** - * Method to return the elevation in Meters above sea level of the horizon at the sunset azimuth. + * Method to return the elevation in Meters above sea level of the horizon at the point where the sun sets that day. * - * @return Returns the horizon elevation at sunset in Meters. + * @return the horizon elevation at sunset in Meters. * @see #setHorizonElevationAtSunset(double) */ public double getHorizonElevationAtSunset() { @@ -172,10 +172,10 @@ public double getHorizonElevationAtSunset() { } /** - * Method to set the elevation in Meters above sea level of the horizon at the sunset azimuth. Sunset is calculated with the - * observer's height above this horizon instead of above sea level. Since the sunset azimuth moves through the year, this is - * a per-day value; a caller tracking a varied skyline should set it for the date being calculated. - * @param horizonElevationAtSunset The horizon elevation at sunset to set in Meters. An IllegalArgumentException will be thrown + * 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() */ @@ -198,7 +198,7 @@ private static void validateHorizonElevation(double horizonElevation) { } /** - * The height in Meters of the observer above the {@link #getHorizonElevationAtSunrise() horizon at the sunrise azimuth}, used for sunrise. + * 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 @@ -209,7 +209,7 @@ public double getSunriseElevation() { } /** - * The height in Meters of the observer above the {@link #getHorizonElevationAtSunset() horizon at the sunset azimuth}, used for sunset. + * 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 @@ -264,11 +264,10 @@ public GeoLocation(String name, double latitude, double longitude, double elevat * 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 sunrise azimuth. The sun - * rises at a different point on the horizon each day, so this is the skyline height for the date being calculated - * and is expected to change from day to day. - * @param horizonElevationAtSunset the elevation above sea level in Meters of the horizon at the sunset azimuth. Like the - * sunrise horizon, this is a per-day value that follows the sun's setting point along the horizon. + * @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) */