Skip to content

Commit f18e6b3

Browse files
authored
Start using java.time classes
Breaking changes
1 parent 4bef262 commit f18e6b3

1 file changed

Lines changed: 111 additions & 53 deletions

File tree

src/main/java/com/kosherjava/zmanim/util/GeoLocation.java

Lines changed: 111 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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)
@@ -15,16 +15,20 @@
1515
*/
1616
package com.kosherjava.zmanim.util;
1717

18+
import java.util.Locale;
1819
import 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
*/
2933
public 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&deg; 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
"\nLatitude:\t\t\t" + getLatitude() + "\u00B0" +
625687
"\nLongitude:\t\t\t" + getLongitude() + "\u00B0" +
626688
"\nElevation:\t\t\t" + getElevation() + " Meters" +
627-
"\nTimezone ID:\t\t\t" + getTimeZone().getID() +
628-
"\nTimezone Display Name:\t\t" + getTimeZone().getDisplayName() +
629-
" (" + getTimeZone().getDisplayName(false, TimeZone.SHORT) + ")" +
630-
"\nTimezone GMT Offset:\t\t" + getTimeZone().getRawOffset() / HOUR_MILLIS +
631-
"\nTimezone DST Offset:\t\t" + getTimeZone().getDSTSavings() / HOUR_MILLIS;
689+
"\nTimezone ID:\t\t\t" + getZoneId().getId() +
690+
"\nTimezone 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

Comments
 (0)