|
16 | 16 |
|
17 | 17 | import java.time.Instant; |
18 | 18 | import java.time.LocalDate; |
| 19 | +import java.util.concurrent.ConcurrentHashMap; |
19 | 20 | import java.util.Objects; |
20 | 21 |
|
21 | 22 | /** |
|
27 | 28 | * @author © Eliyahu Hershfeld 2004 - 2026 |
28 | 29 | */ |
29 | 30 | public abstract class AstronomicalCalculator implements Cloneable { |
| 31 | + |
| 32 | + /** |
| 33 | + * Whether this calculator's rise / set / noon / midnight results may be cached (see {@link #isResultCacheable()}). On by default; |
| 34 | + * see {@link #setUseResultCache(boolean)}. |
| 35 | + */ |
| 36 | + private boolean useResultCache = true; |
| 37 | + |
| 38 | + /** |
| 39 | + * The cached results for the most recently used dates, newest first, at most {@link #RESULT_CACHE_DATES}. The array is never |
| 40 | + * modified after it is published; a new date is added by publishing a new array. Each date's values are held in a concurrent |
| 41 | + * map, so a calculator shared between threads stays correct (at most a value is computed twice). Cleared by every setter that |
| 42 | + * can change a result, so a cached value is only ever returned for identical inputs and settings. Not part of {@link |
| 43 | + * #equals(Object)} or {@link #hashCode()}. |
| 44 | + */ |
| 45 | + private volatile ResultCache[] resultCaches; |
| 46 | + |
| 47 | + /** Number of dates kept in the result cache. A day's zmanim use one date (and rarely a neighbouring one). */ |
| 48 | + private static final int RESULT_CACHE_DATES = 3; |
| 49 | + |
| 50 | + /** The calculator method a cached result came from. A rise and a set at the same zenith are different times. */ |
| 51 | + private enum ResultEvent { SUNRISE, SUNSET, NOON, MIDNIGHT } |
| 52 | + |
| 53 | + /** The cache key: every location input a calculator can use, the zenith, the elevation adjustment and the event. */ |
| 54 | + private static final class ResultKey { |
| 55 | + private final double latitude; |
| 56 | + private final double longitude; |
| 57 | + private final double elevation; |
| 58 | + private final double horizonElevationAtSunrise; |
| 59 | + private final double horizonElevationAtSunset; |
| 60 | + private final double zenith; |
| 61 | + private final boolean adjustForElevation; |
| 62 | + private final ResultEvent event; |
| 63 | + |
| 64 | + ResultKey(GeoLocation geoLocation, double zenith, boolean adjustForElevation, ResultEvent event) { |
| 65 | + this.latitude = geoLocation.getLatitude(); |
| 66 | + this.longitude = geoLocation.getLongitude(); |
| 67 | + this.elevation = geoLocation.getElevation(); |
| 68 | + this.horizonElevationAtSunrise = geoLocation.getHorizonElevationAtSunrise(); |
| 69 | + this.horizonElevationAtSunset = geoLocation.getHorizonElevationAtSunset(); |
| 70 | + this.zenith = zenith; |
| 71 | + this.adjustForElevation = adjustForElevation; |
| 72 | + this.event = event; |
| 73 | + } |
| 74 | + |
| 75 | + @Override |
| 76 | + public boolean equals(Object object) { |
| 77 | + if (this == object) { |
| 78 | + return true; |
| 79 | + } |
| 80 | + if (!(object instanceof ResultKey)) { |
| 81 | + return false; |
| 82 | + } |
| 83 | + ResultKey other = (ResultKey) object; |
| 84 | + return Double.compare(latitude, other.latitude) == 0 && Double.compare(longitude, other.longitude) == 0 |
| 85 | + && Double.compare(elevation, other.elevation) == 0 |
| 86 | + && Double.compare(horizonElevationAtSunrise, other.horizonElevationAtSunrise) == 0 |
| 87 | + && Double.compare(horizonElevationAtSunset, other.horizonElevationAtSunset) == 0 |
| 88 | + && Double.compare(zenith, other.zenith) == 0 && adjustForElevation == other.adjustForElevation && event == other.event; |
| 89 | + } |
| 90 | + |
| 91 | + @Override |
| 92 | + public int hashCode() { |
| 93 | + return Objects.hash(latitude, longitude, elevation, horizonElevationAtSunrise, horizonElevationAtSunset, zenith, |
| 94 | + adjustForElevation, event); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + /** The cached results of one date. */ |
| 99 | + private static final class ResultCache { |
| 100 | + private final LocalDate date; |
| 101 | + private final ConcurrentHashMap<ResultKey, Double> values = new ConcurrentHashMap<ResultKey, Double>(); |
| 102 | + |
| 103 | + ResultCache(LocalDate date) { |
| 104 | + this.date = date; |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + * Whether this calculator's results depend only on the method arguments and on settings whose setters call {@link |
| 110 | + * #clearResultCache()}, so they can be cached. Defaults to {@code false}, so a calculator written without the cache in mind is |
| 111 | + * never cached. A subclass that returns {@code true} must call {@link #clearResultCache()} from every setter that can change a |
| 112 | + * rise, set, noon or midnight result. |
| 113 | + * @return whether results may be cached |
| 114 | + */ |
| 115 | + protected boolean isResultCacheable() { |
| 116 | + return false; |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Discards all cached results. Called by every setter that can change a result. |
| 121 | + */ |
| 122 | + protected void clearResultCache() { |
| 123 | + resultCaches = null; |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * Whether rise / set / noon / midnight results are cached (when the calculator supports it, see {@link #isResultCacheable()}). |
| 128 | + * Caching never changes a result. A full day of <em>zmanim</em> makes several hundred calculator calls that resolve to a few |
| 129 | + * dozen distinct solutions, so the cache removes most of the astronomical work. |
| 130 | + * @return whether results are cached. Defaults to {@code true}. |
| 131 | + */ |
| 132 | + public boolean isUseResultCache() { |
| 133 | + return useResultCache; |
| 134 | + } |
| 135 | + |
| 136 | + /** |
| 137 | + * Sets whether rise / set / noon / midnight results are cached. Caching never changes a result; turning it off is only useful for |
| 138 | + * benchmarking, or as a safeguard if a subclass's results depend on state the cache can not see. |
| 139 | + * @param useResultCache whether to cache results |
| 140 | + */ |
| 141 | + public void setUseResultCache(boolean useResultCache) { |
| 142 | + this.useResultCache = useResultCache; |
| 143 | + clearResultCache(); |
| 144 | + } |
| 145 | + |
| 146 | + private ConcurrentHashMap<ResultKey, Double> getResultCache(LocalDate localDate) { |
| 147 | + ResultCache[] caches = resultCaches; |
| 148 | + if (caches != null) { |
| 149 | + for (ResultCache cache : caches) { |
| 150 | + if (cache.date.equals(localDate)) { |
| 151 | + return cache.values; |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + ResultCache cache = new ResultCache(localDate); |
| 156 | + int keep = caches == null ? 0 : Math.min(caches.length, RESULT_CACHE_DATES - 1); |
| 157 | + ResultCache[] updated = new ResultCache[keep + 1]; |
| 158 | + updated[0] = cache; |
| 159 | + if (keep > 0) { |
| 160 | + System.arraycopy(caches, 0, updated, 1, keep); |
| 161 | + } |
| 162 | + resultCaches = updated; |
| 163 | + return cache.values; |
| 164 | + } |
| 165 | + |
| 166 | + private boolean isCaching() { |
| 167 | + return useResultCache && isResultCacheable(); |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * {@link #getUTCSunrise(LocalDate, GeoLocation, double, boolean)} with result caching (see {@link #isUseResultCache()}). Used by |
| 172 | + * {@link com.kosherjava.zmanim.AstronomicalCalendar}; the result is always identical to the uncached method. |
| 173 | + * @param localDate the date |
| 174 | + * @param geoLocation the location |
| 175 | + * @param zenith the zenith |
| 176 | + * @param adjustForElevation whether to adjust for elevation |
| 177 | + * @return as {@link #getUTCSunrise(LocalDate, GeoLocation, double, boolean)} |
| 178 | + */ |
| 179 | + public final double getCachedUTCSunrise(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation) { |
| 180 | + if (!isCaching()) { |
| 181 | + return getUTCSunrise(localDate, geoLocation, zenith, adjustForElevation); |
| 182 | + } |
| 183 | + ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate); |
| 184 | + ResultKey key = new ResultKey(geoLocation, zenith, adjustForElevation, ResultEvent.SUNRISE); |
| 185 | + Double value = cache.get(key); |
| 186 | + if (value == null) { |
| 187 | + value = getUTCSunrise(localDate, geoLocation, zenith, adjustForElevation); |
| 188 | + cache.putIfAbsent(key, value); |
| 189 | + } |
| 190 | + return value; |
| 191 | + } |
| 192 | + |
| 193 | + /** |
| 194 | + * {@link #getUTCSunset(LocalDate, GeoLocation, double, boolean)} with result caching (see {@link #isUseResultCache()}). |
| 195 | + * @param localDate the date |
| 196 | + * @param geoLocation the location |
| 197 | + * @param zenith the zenith |
| 198 | + * @param adjustForElevation whether to adjust for elevation |
| 199 | + * @return as {@link #getUTCSunset(LocalDate, GeoLocation, double, boolean)} |
| 200 | + */ |
| 201 | + public final double getCachedUTCSunset(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation) { |
| 202 | + if (!isCaching()) { |
| 203 | + return getUTCSunset(localDate, geoLocation, zenith, adjustForElevation); |
| 204 | + } |
| 205 | + ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate); |
| 206 | + ResultKey key = new ResultKey(geoLocation, zenith, adjustForElevation, ResultEvent.SUNSET); |
| 207 | + Double value = cache.get(key); |
| 208 | + if (value == null) { |
| 209 | + value = getUTCSunset(localDate, geoLocation, zenith, adjustForElevation); |
| 210 | + cache.putIfAbsent(key, value); |
| 211 | + } |
| 212 | + return value; |
| 213 | + } |
| 214 | + |
| 215 | + /** |
| 216 | + * {@link #getUTCNoon(LocalDate, GeoLocation)} with result caching (see {@link #isUseResultCache()}). |
| 217 | + * @param localDate the date |
| 218 | + * @param geoLocation the location |
| 219 | + * @return as {@link #getUTCNoon(LocalDate, GeoLocation)} |
| 220 | + */ |
| 221 | + public final double getCachedUTCNoon(LocalDate localDate, GeoLocation geoLocation) { |
| 222 | + if (!isCaching()) { |
| 223 | + return getUTCNoon(localDate, geoLocation); |
| 224 | + } |
| 225 | + ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate); |
| 226 | + ResultKey key = new ResultKey(geoLocation, 0, false, ResultEvent.NOON); |
| 227 | + Double value = cache.get(key); |
| 228 | + if (value == null) { |
| 229 | + value = getUTCNoon(localDate, geoLocation); |
| 230 | + cache.putIfAbsent(key, value); |
| 231 | + } |
| 232 | + return value; |
| 233 | + } |
| 234 | + |
| 235 | + /** |
| 236 | + * {@link #getUTCMidnight(LocalDate, GeoLocation)} with result caching (see {@link #isUseResultCache()}). |
| 237 | + * @param localDate the date |
| 238 | + * @param geoLocation the location |
| 239 | + * @return as {@link #getUTCMidnight(LocalDate, GeoLocation)} |
| 240 | + */ |
| 241 | + public final double getCachedUTCMidnight(LocalDate localDate, GeoLocation geoLocation) { |
| 242 | + if (!isCaching()) { |
| 243 | + return getUTCMidnight(localDate, geoLocation); |
| 244 | + } |
| 245 | + ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate); |
| 246 | + ResultKey key = new ResultKey(geoLocation, 0, false, ResultEvent.MIDNIGHT); |
| 247 | + Double value = cache.get(key); |
| 248 | + if (value == null) { |
| 249 | + value = getUTCMidnight(localDate, geoLocation); |
| 250 | + cache.putIfAbsent(key, value); |
| 251 | + } |
| 252 | + return value; |
| 253 | + } |
30 | 254 | /** |
31 | 255 | * The commonly used average solar refraction in degrees that defaults to 34′, or 0.5666°. |
32 | 256 | * <a href="https://www.cs.tau.ac.il//~nachum/calendar-book/index.shtml">Calendrical Calculations</a> lists a more accurate |
@@ -76,6 +300,7 @@ public boolean isUseApparentSolarRadius() { |
76 | 300 | */ |
77 | 301 | public void setUseApparentSolarRadius(boolean useApparentSolarRadius) { |
78 | 302 | this.useApparentSolarRadius = useApparentSolarRadius; |
| 303 | + clearResultCache(); |
79 | 304 | } |
80 | 305 |
|
81 | 306 | /** |
@@ -114,6 +339,7 @@ public double getEarthRadius() { |
114 | 339 | */ |
115 | 340 | public void setEarthRadius(double earthRadius) { |
116 | 341 | this.earthRadius = earthRadius; |
| 342 | + clearResultCache(); |
117 | 343 | } |
118 | 344 |
|
119 | 345 | /** |
@@ -347,6 +573,7 @@ public double getRefraction() { |
347 | 573 | */ |
348 | 574 | public void setRefraction(double refraction) { |
349 | 575 | this.refraction = refraction; |
| 576 | + clearResultCache(); |
350 | 577 | } |
351 | 578 |
|
352 | 579 | /** |
@@ -399,6 +626,7 @@ public void setSolarRadius(double solarRadius) { |
399 | 626 | } |
400 | 627 | this.solarRadius = solarRadius; |
401 | 628 | this.useApparentSolarRadius = false; |
| 629 | + clearResultCache(); |
402 | 630 | } |
403 | 631 |
|
404 | 632 | /** |
|
0 commit comments