11/*
22 * Zmanim Java API
3- * Copyright (C) 2004-2025 Eliyahu Hershfeld
3+ * Copyright (C) 2004-2026 Eliyahu Hershfeld
44 *
55 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
66 * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option)
1515 */
1616package com .kosherjava .zmanim .util ;
1717
18+ import java .util .Locale ;
1819import java .util .Objects ;
19- import java .util .TimeZone ;
20+ import java .time .Instant ;
21+ import java .time .ZoneId ;
22+ import java .time .ZonedDateTime ;
23+ import java .time .format .TextStyle ;
2024
2125/**
2226 * A class that contains location information such as latitude and longitude required for astronomical calculations. The
2327 * elevation field may not be used by some calculation engines and would be ignored if set. Check the documentation for
2428 * specific implementations of the {@link AstronomicalCalculator} to see if elevation is calculated as part of the
2529 * algorithm.
2630 *
27- * @author © Eliyahu Hershfeld 2004 - 2025
31+ * @author © Eliyahu Hershfeld 2004 - 2026
2832 */
2933public class GeoLocation implements Cloneable {
3034 /**
@@ -51,11 +55,11 @@ public class GeoLocation implements Cloneable {
5155 private String locationName ;
5256
5357 /**
54- * The location's time zone.
55- * @see #getTimeZone ()
56- * @see #setTimeZone(TimeZone )
58+ * The location's zoneId
59+ * @see #getZoneId ()
60+ * @see #setZoneId(ZoneId )
5761 */
58- private TimeZone timeZone ;
62+ private ZoneId zoneId ;
5963
6064 /**
6165 * The elevation in Meters <b>above</b> sea level.
@@ -127,11 +131,11 @@ public void setElevation(double elevation) {
127131 * the longitude as a <code>double</code>, for example -74.222 for Lakewood, NJ. <b>Note:</b> For longitudes
128132 * east of the <a href="https://en.wikipedia.org/wiki/Prime_Meridian">Prime Meridian</a> (Greenwich),
129133 * a negative value should be used.
130- * @param timeZone
131- * the <code>TimeZone </code> for the location.
134+ * @param zoneId
135+ * the <code>ZoneId </code> for the location.
132136 */
133- public GeoLocation (String name , double latitude , double longitude , TimeZone timeZone ) {
134- this (name , latitude , longitude , 0 , timeZone );
137+ public GeoLocation (String name , double latitude , double longitude , ZoneId zoneId ) {
138+ this (name , latitude , longitude , 0 , zoneId );
135139 }
136140
137141 /**
@@ -148,27 +152,27 @@ public GeoLocation(String name, double latitude, double longitude, TimeZone time
148152 * Meridian</a> (Greenwich), a negative value should be used.
149153 * @param elevation
150154 * the elevation above sea level in Meters.
151- * @param timeZone
152- * the <code>TimeZone </code> for the location.
155+ * @param zoneId
156+ * the <code>ZoneId </code> for the location.
153157 */
154- public GeoLocation (String name , double latitude , double longitude , double elevation , TimeZone timeZone ) {
158+ public GeoLocation (String name , double latitude , double longitude , double elevation , ZoneId zoneId ) {
155159 setLocationName (name );
156160 setLatitude (latitude );
157161 setLongitude (longitude );
158162 setElevation (elevation );
159- setTimeZone ( timeZone );
163+ this . setZoneId ( zoneId );
160164 }
161165
162166 /**
163- * Default GeoLocation constructor will set location to the Prime Meridian at Greenwich, England and a TimeZone of
164- * GMT. The longitude will be set to 0 and the latitude will be 51.4772 to match the location of the <a
167+ * Default GeoLocation constructor will set location to the Prime Meridian at Greenwich, England and a <code>ZoneId</code>
168+ * of GMT. The longitude will be set to 0 and the latitude will be 51.4772 to match the location of the <a
165169 * href="https://www.rmg.co.uk/royal-observatory">Royal Observatory, Greenwich</a>. No daylight savings time will be used.
166170 */
167171 public GeoLocation () {
168172 setLocationName ("Greenwich, England" );
169173 setLongitude (0 ); // added for clarity
170174 setLatitude (51.4772 );
171- setTimeZone ( TimeZone . getTimeZone ("GMT" ));
175+ setZoneId ( ZoneId . of ("GMT" ));
172176 }
173177
174178 /**
@@ -296,28 +300,27 @@ public String getLocationName() {
296300 public void setLocationName (String name ) {
297301 this .locationName = name ;
298302 }
299-
303+
300304 /**
301- * Method to return the time zone .
302- * @return Returns the timeZone .
305+ * Method to return the <code>ZoneId</code> .
306+ * @return Returns the zoneId .
303307 */
304- public TimeZone getTimeZone () {
305- return timeZone ;
308+ public ZoneId getZoneId () {
309+ return zoneId ;
306310 }
307-
311+
308312 /**
309- * Method to set the TimeZone . If this is ever set after the GeoLocation is set in the
313+ * Method to set the zoneId . If this is ever set after the GeoLocation is set in the
310314 * {@link com.kosherjava.zmanim.AstronomicalCalendar}, it is critical that
311- * {@link com.kosherjava.zmanim.AstronomicalCalendar#getCalendar()}.
312- * {@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)} be called in order for the
315+ * {@link java.time.ZonedDateTime #setZoneId(ZoneId) setZoneId(ZoneId)} be called in order for the
313316 * AstronomicalCalendar to output times in the expected offset. This situation will arise if the
314317 * AstronomicalCalendar is ever {@link com.kosherjava.zmanim.AstronomicalCalendar#clone() cloned}.
315318 *
316- * @param timeZone
317- * The timeZone to set.
319+ * @param zoneId
320+ * The zoneId to set.
318321 */
319- public void setTimeZone ( TimeZone timeZone ) {
320- this .timeZone = timeZone ;
322+ public void setZoneId ( ZoneId zoneId ) {
323+ this .zoneId = zoneId ;
321324 }
322325
323326 /**
@@ -332,12 +335,15 @@ public void setTimeZone(TimeZone timeZone) {
332335 * and 10 seconds earlier than standard time. The offset returned does not account for the <a
333336 * href="https://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving time</a> offset since this class is
334337 * unaware of dates.
335- *
338+ * @param instant
339+ * the <code>Instant</code> used to claculate the local mean offset for the date in question.
336340 * @return the offset in milliseconds not accounting for Daylight saving time. A positive value will be returned
337341 * East of the 15° timezone line, and a negative value West of it.
338342 */
339- public long getLocalMeanTimeOffset () {
340- return (long ) (getLongitude () * 4 * MINUTE_MILLIS - getTimeZone ().getRawOffset ());
343+ public long getLocalMeanTimeOffset (Instant instant ) {
344+ ZonedDateTime zonedDateTime = ZonedDateTime .ofInstant (instant , zoneId );
345+ long timezoneOffsetMillis = zonedDateTime .getOffset ().getTotalSeconds () * 1000 ;
346+ return (long ) (getLongitude () * 4 * MINUTE_MILLIS - timezoneOffsetMillis );
341347 }
342348
343349 /**
@@ -354,11 +360,13 @@ public long getLocalMeanTimeOffset() {
354360 * 2018-02-03, the calculator should operate using 2018-02-02 since the expected zone is -11. After determining the
355361 * UTC time, the local DST offset of <a href="https://en.wikipedia.org/wiki/UTC%2B14:00">UTC+14:00</a> should be applied
356362 * to bring the date back to 2018-02-03.
363+ * @param instant
364+ * the <code>Instant</code> required for the local mean time offset calculation
357365 *
358366 * @return the number of days to adjust the date This will typically be 0 unless the date crosses the antimeridian
359367 */
360- public int getAntimeridianAdjustment () {
361- double localHoursOffset = getLocalMeanTimeOffset () / (double )HOUR_MILLIS ;
368+ public int getAntimeridianAdjustment (Instant instant ) {
369+ double localHoursOffset = getLocalMeanTimeOffset (instant ) / (double )HOUR_MILLIS ;
362370
363371 if (localHoursOffset >= 20 ){// if the offset is 20 hours or more in the future (never expected anywhere other
364372 // than a location using a timezone across the antimeridian to the east such as Samoa)
@@ -415,6 +423,61 @@ public double getGeodesicFinalBearing(GeoLocation location) {
415423 public double getGeodesicDistance (GeoLocation location ) {
416424 return vincentyInverseFormula (location , DISTANCE );
417425 }
426+
427+ /**
428+ * Calculate the destination point based on an initial bearing and distance in meters from the current location using
429+ * <a href="https://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a> direct formula. See T Vincenty, "<a
430+ * href="https://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse Solutions of Geodesics on the Ellipsoid
431+ * with application of nested equations</a>", Survey Review, vol XXII no 176, 1975.
432+ *
433+ * @param initialBearing the initialBearing
434+ * @param distance the distance in meters.
435+ * @return the GeoLocation containing the destination point. The ZoneId is set to the origin point (current object).
436+ */
437+ private GeoLocation vincentyDirectFormulaDestination (double initialBearing , double distance ) {
438+ double major_semi_axis = 6378137 ;
439+ double minor_semi_axis = 6356752.3142 ;
440+ double flattening = 1 / 298.257223563 ; // WGS-84 ellipsoid
441+ double initial_bearing_radians = Math .toRadians (initialBearing );
442+ double sinAzimuth1 = Math .sin (initial_bearing_radians );
443+ double cosAzimuth1 = Math .cos (initial_bearing_radians );
444+ double tanU1 = (1 - flattening ) * Math .tan (Math .toRadians (getLatitude ()));
445+ double cosU1 = 1 / Math .sqrt ((1 + tanU1 *tanU1 ));
446+ double sinU1 = tanU1 * cosU1 ;
447+ double eq_p1_ang_dist = Math .atan2 (tanU1 , cosAzimuth1 ); // eq_p1_ang_dist = angular distance on the sphere from the equator to P1
448+ double sinAzimuth = cosU1 * sinAzimuth1 ; //azimuth of the geodesic at the equator
449+ double cosSqAzimuth = 1 - sinAzimuth *sinAzimuth ;
450+ double uSq = cosSqAzimuth * (Math .pow (major_semi_axis , 2 ) - Math .pow (minor_semi_axis , 2 ) / Math .pow (minor_semi_axis , 2 ));
451+ double a = 1 + uSq /16384 *(4096 + uSq *(-768 + uSq *(320 - 175 * uSq )));
452+ double b = uSq / 1024 * (256 + uSq *(-128 + uSq * (74 -47 * uSq )));
453+ double p1_p2_ang_dist = distance / (minor_semi_axis * a ); //p1_p2_ang_dist = angular distance P1 P2 on the sphere
454+ double sinSigma = Double .NaN ;
455+ double cosSigma = Double .NaN ;
456+ double cos2_eq_mid_ang_distance = Double .NaN ; // # eq_mid_ang_distance = angular distance on the sphere from the equator to the midpoint of the line
457+ double a_prime = Double .NaN ;
458+ int iterations = 0 ;
459+
460+ do {
461+ cos2_eq_mid_ang_distance = Math .cos (2 *eq_p1_ang_dist + p1_p2_ang_dist );
462+ sinSigma = Math .sin (p1_p2_ang_dist );
463+ cosSigma = Math .cos (p1_p2_ang_dist );
464+ double delta_ang_distance = b * sinSigma * (cos2_eq_mid_ang_distance + b / 4 *
465+ (cosSigma * (-1 + 2 * cos2_eq_mid_ang_distance * cos2_eq_mid_ang_distance ) - b / 6 * cos2_eq_mid_ang_distance *
466+ (-3 +4 *sinSigma *sinSigma )*(-3 +4 *cos2_eq_mid_ang_distance *cos2_eq_mid_ang_distance )));
467+ a_prime = p1_p2_ang_dist ;
468+ p1_p2_ang_dist = distance / (minor_semi_axis * a ) + delta_ang_distance ;
469+ } while (Math .abs (p1_p2_ang_dist -a_prime ) > 1e-12 && ++iterations < 100 ); // iterate until negligible change in lambda (about 0.006mm)
470+
471+ double x = sinU1 * sinSigma - cosU1 * cosSigma * cosAzimuth1 ;
472+ double other_latitude = Math .toDegrees (Math .atan2 (sinU1 * cosSigma + cosU1 * sinSigma * cosAzimuth1 , (1 - flattening ) * Math .sqrt (sinAzimuth *sinAzimuth + x * x )));
473+ double lambda = Math .atan2 (sinSigma *sinAzimuth1 , cosU1 *cosSigma - sinU1 *sinSigma *cosAzimuth1 );
474+ double c = flattening /16 *cosSqAzimuth *(4 +flattening *(4 -3 *cosSqAzimuth ));
475+ double l = lambda - (1 -c ) * flattening * sinAzimuth *
476+ (p1_p2_ang_dist + c * sinSigma * (cos2_eq_mid_ang_distance + c * cosSigma * (-1 + 2 * cos2_eq_mid_ang_distance * cos2_eq_mid_ang_distance )));
477+ double other_longitude = longitude + Math .toDegrees (l );
478+
479+ return new GeoLocation ("Destination" , other_latitude , other_longitude , getZoneId ()); //ToDo - we can easily return final_bearing, it just needs some minor refactoring
480+ }
418481
419482 /**
420483 * Calculate <a href="https://en.wikipedia.org/wiki/Great-circle_distance">geodesic distance</a> in Meters between
@@ -570,12 +633,11 @@ public String toXML() {
570633 "\t <Latitude>" + getLatitude () + "</Latitude>\n " +
571634 "\t <Longitude>" + getLongitude () + "</Longitude>\n " +
572635 "\t <Elevation>" + getElevation () + " Meters" + "</Elevation>\n " +
573- "\t <TimezoneName>" + getTimeZone ().getID () + "</TimezoneName>\n " +
574- "\t <TimeZoneDisplayName>" + getTimeZone ().getDisplayName () + "</TimeZoneDisplayName>\n " +
575- "\t <TimezoneGMTOffset>" + getTimeZone ().getRawOffset () / HOUR_MILLIS +
576- "</TimezoneGMTOffset>\n " +
636+ "\t <TimezoneName>" + getZoneId ().getId () + "</TimezoneName>\n " +
637+ "\t <TimeZoneDisplayName>" + getZoneId ().getDisplayName (TextStyle .FULL , Locale .ENGLISH ) + "</TimeZoneDisplayName>\n " +
638+ /*"</TimezoneGMTOffset>\n" +
577639 "\t<TimezoneDSTOffset>" + getTimeZone().getDSTSavings() / HOUR_MILLIS +
578- "</TimezoneDSTOffset>\n " +
640+ "</TimezoneDSTOffset>\n" +*/ // FIXME
579641 "</GeoLocation>" ;
580642 }
581643
@@ -592,7 +654,7 @@ public boolean equals(Object object) {
592654 && Double .doubleToLongBits (this .longitude ) == Double .doubleToLongBits (geo .longitude )
593655 && this .elevation == geo .elevation
594656 && (Objects .equals (this .locationName , geo .locationName ))
595- && (Objects .equals (this .timeZone , geo .timeZone ));
657+ && (Objects .equals (this .zoneId , geo .zoneId ));
596658 }
597659
598660 /**
@@ -612,7 +674,7 @@ public int hashCode() {
612674 result += 37 * result + lonInt ;
613675 result += 37 * result + elevInt ;
614676 result += 37 * result + (this .locationName == null ? 0 : this .locationName .hashCode ());
615- result += 37 * result + (this .timeZone == null ? 0 : this .timeZone .hashCode ());
677+ result += 37 * result + (this .zoneId == null ? 0 : this .zoneId .hashCode ());
616678 return result ;
617679 }
618680
@@ -624,20 +686,16 @@ public String toString() {
624686 "\n Latitude:\t \t \t " + getLatitude () + "\u00B0 " +
625687 "\n Longitude:\t \t \t " + getLongitude () + "\u00B0 " +
626688 "\n Elevation:\t \t \t " + getElevation () + " Meters" +
627- "\n Timezone ID:\t \t \t " + getTimeZone ().getID () +
628- "\n Timezone Display Name:\t \t " + getTimeZone ().getDisplayName () +
629- " (" + getTimeZone ().getDisplayName (false , TimeZone .SHORT ) + ")" +
630- "\n Timezone GMT Offset:\t \t " + getTimeZone ().getRawOffset () / HOUR_MILLIS +
631- "\n Timezone DST Offset:\t \t " + getTimeZone ().getDSTSavings () / HOUR_MILLIS ;
689+ "\n Timezone ID:\t \t \t " + getZoneId ().getId () +
690+ "\n Timezone Display Name:\t \t " + getZoneId ().getDisplayName (TextStyle .FULL , Locale .ENGLISH );// +
691+ //"\nTimezone DST Offset:\t\t" + getZoneId().getDSTSavings() / HOUR_MILLIS; // FIXME
632692 }
633693
634694 /**
635695 * An implementation of the {@link java.lang.Object#clone()} method that creates a <a
636696 * href="https://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a> of the object.
637- * <b>Note:</b> If the {@link java.util.TimeZone} in the clone will be changed from the original, it is critical
638- * that {@link com.kosherjava.zmanim.AstronomicalCalendar#getCalendar()}.
639- * {@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)} is called after cloning in order for the
640- * AstronomicalCalendar to output times in the expected offset.
697+ * <b>Note:</b> If the {@link java.time.ZoneId} in the clone will be changed from the original, it is critical
698+ * that {@link com.kosherjava.zmanim.AstronomicalCalendar#getZonedDateTime()}.
641699 *
642700 * @see java.lang.Object#clone()
643701 */
@@ -649,7 +707,7 @@ public Object clone() {
649707 //Required by the compiler. Should never be reached since we implement clone()
650708 }
651709 if (clone != null ) {
652- clone .timeZone = (TimeZone ) getTimeZone (). clone ();
710+ clone .zoneId = (ZoneId ) getZoneId ();
653711 clone .locationName = getLocationName ();
654712 }
655713 return clone ;
0 commit comments