001/* 002 * Shredzone Commons - suncalc 003 * 004 * Copyright (C) 2017 Richard "Shred" Körber 005 * http://commons.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.commons.suncalc; 015 016import static java.lang.Math.*; 017import static org.shredzone.commons.suncalc.util.ExtendedMath.*; 018 019import java.time.Duration; 020import java.time.ZonedDateTime; 021 022import edu.umd.cs.findbugs.annotations.Nullable; 023import org.shredzone.commons.suncalc.param.Builder; 024import org.shredzone.commons.suncalc.param.GenericParameter; 025import org.shredzone.commons.suncalc.param.LocationParameter; 026import org.shredzone.commons.suncalc.param.TimeParameter; 027import org.shredzone.commons.suncalc.util.BaseBuilder; 028import org.shredzone.commons.suncalc.util.JulianDate; 029import org.shredzone.commons.suncalc.util.QuadraticInterpolation; 030import org.shredzone.commons.suncalc.util.Sun; 031import org.shredzone.commons.suncalc.util.Vector; 032 033/** 034 * Calculates the rise and set times of the sun. 035 */ 036public class SunTimes { 037 038 private final @Nullable ZonedDateTime rise; 039 private final @Nullable ZonedDateTime set; 040 private final @Nullable ZonedDateTime noon; 041 private final @Nullable ZonedDateTime nadir; 042 private final boolean alwaysUp; 043 private final boolean alwaysDown; 044 045 private SunTimes(@Nullable ZonedDateTime rise, @Nullable ZonedDateTime set, 046 @Nullable ZonedDateTime noon, @Nullable ZonedDateTime nadir, 047 boolean alwaysUp, boolean alwaysDown) { 048 this.rise = rise; 049 this.set = set; 050 this.noon = noon; 051 this.nadir = nadir; 052 this.alwaysUp = alwaysUp; 053 this.alwaysDown = alwaysDown; 054 } 055 056 /** 057 * Starts the computation of {@link SunTimes}. 058 * 059 * @return {@link Parameters} to set. 060 */ 061 public static Parameters compute() { 062 return new SunTimesBuilder(); 063 } 064 065 /** 066 * Collects all parameters for {@link SunTimes}. 067 */ 068 public interface Parameters extends 069 GenericParameter<Parameters>, 070 LocationParameter<Parameters>, 071 TimeParameter<Parameters>, 072 Builder<SunTimes> { 073 074 /** 075 * Sets the {@link Twilight} mode. 076 * <p> 077 * Defaults to {@link Twilight#VISUAL}. 078 * 079 * @param twilight 080 * {@link Twilight} mode to be used. 081 * @return itself 082 */ 083 Parameters twilight(Twilight twilight); 084 085 /** 086 * Sets the desired elevation angle of the sun. The sunrise and sunset times are 087 * referring to the moment when the center of the sun passes this angle. 088 * 089 * @param angle 090 * Geocentric elevation angle, in degrees. 091 * @return itself 092 */ 093 Parameters twilight(double angle); 094 095 /** 096 * Limits the calculation window to the given {@link Duration}. 097 * 098 * @param duration 099 * Duration of the calculation window. Must be positive. 100 * @return itself 101 * @since 3.1 102 */ 103 Parameters limit(Duration duration); 104 105 /** 106 * Limits the time window to the next 24 hours. 107 * 108 * @return itself 109 */ 110 default Parameters oneDay() { 111 return limit(Duration.ofDays(1L)); 112 } 113 114 /** 115 * Computes until all rise, set, noon, and nadir times are found. 116 * <p> 117 * This is the default. 118 * 119 * @return itself 120 */ 121 default Parameters fullCycle() { 122 return limit(Duration.ofDays(365L)); 123 } 124 } 125 126 /** 127 * Enumeration of predefined twilights. 128 * <p> 129 * The twilight angles use a geocentric reference, by definition. However, 130 * {@link #VISUAL} and {@link #VISUAL_LOWER} are topocentric, and take the spectator's 131 * elevation and the atmospheric refraction into account. 132 * 133 * @see <a href="https://en.wikipedia.org/wiki/Twilight">Wikipedia: Twilight</a> 134 */ 135 public enum Twilight { 136 137 /** 138 * The moment when the visual upper edge of the sun crosses the horizon. This is 139 * commonly referred to as "sunrise" and "sunset". Atmospheric refraction is taken 140 * into account. 141 * <p> 142 * This is the default. 143 */ 144 VISUAL(0.0, 1.0), 145 146 /** 147 * The moment when the visual lower edge of the sun crosses the horizon. This is 148 * the ending of the sunrise and the starting of the sunset. Atmospheric 149 * refraction is taken into account. 150 */ 151 VISUAL_LOWER(0.0, -1.0), 152 153 /** 154 * The moment when the center of the sun crosses the horizon (0°). 155 */ 156 HORIZON(0.0), 157 158 /** 159 * Civil twilight (-6°). 160 */ 161 CIVIL(-6.0), 162 163 /** 164 * Nautical twilight (-12°). 165 */ 166 NAUTICAL(-12.0), 167 168 /** 169 * Astronomical twilight (-18°). 170 */ 171 ASTRONOMICAL(-18.0), 172 173 /** 174 * Golden hour (6°). The Golden hour is between {@link #GOLDEN_HOUR} and 175 * {@link #BLUE_HOUR}. The Magic hour is between {@link #GOLDEN_HOUR} and 176 * {@link #CIVIL}. 177 * 178 * @see <a href= 179 * "https://en.wikipedia.org/wiki/Golden_hour_(photography)">Wikipedia: 180 * Golden hour</a> 181 */ 182 GOLDEN_HOUR(6.0), 183 184 /** 185 * Blue hour (-4°). The Blue hour is between {@link #NIGHT_HOUR} and 186 * {@link #BLUE_HOUR}. 187 * 188 * @see <a href="https://en.wikipedia.org/wiki/Blue_hour">Wikipedia: Blue hour</a> 189 */ 190 BLUE_HOUR(-4.0), 191 192 /** 193 * End of Blue hour (-8°). 194 * <p> 195 * "Night Hour" is not an official term, but just a name that is marking the 196 * beginning/end of the Blue hour. 197 */ 198 NIGHT_HOUR(-8.0); 199 200 private final double angle; 201 private final double angleRad; 202 private final @Nullable Double position; 203 204 Twilight(double angle) { 205 this(angle, null); 206 } 207 208 Twilight(double angle, @Nullable Double position) { 209 this.angle = angle; 210 this.angleRad = toRadians(angle); 211 this.position = position; 212 } 213 214 /** 215 * Returns the sun's angle at the twilight position, in degrees. 216 */ 217 public double getAngle() { 218 return angle; 219 } 220 221 /** 222 * Returns the sun's angle at the twilight position, in radians. 223 */ 224 public double getAngleRad() { 225 return angleRad; 226 } 227 228 /** 229 * Returns {@code true} if this twilight position is topocentric. Then the 230 * parallax and the atmospheric refraction is taken into account. 231 */ 232 public boolean isTopocentric() { 233 return position != null; 234 } 235 236 /** 237 * Returns the angular position. {@code 0.0} means center of the sun. {@code 1.0} 238 * means upper edge of the sun. {@code -1.0} means lower edge of the sun. 239 * {@code null} means the angular position is not topocentric. 240 */ 241 @Nullable 242 private Double getAngularPosition() { 243 return position; 244 } 245 } 246 247 /** 248 * Builder for {@link SunTimes}. Performs the computations based on the parameters, 249 * and creates a {@link SunTimes} object that holds the result. 250 */ 251 private static class SunTimesBuilder extends BaseBuilder<Parameters> implements Parameters { 252 private double angle = Twilight.VISUAL.getAngleRad(); 253 private @Nullable Double position = Twilight.VISUAL.getAngularPosition(); 254 private Duration limit = Duration.ofDays(365L); 255 256 @Override 257 public Parameters twilight(Twilight twilight) { 258 this.angle = twilight.getAngleRad(); 259 this.position = twilight.getAngularPosition(); 260 return this; 261 } 262 263 @Override 264 public Parameters twilight(double angle) { 265 this.angle = toRadians(angle); 266 this.position = null; 267 return this; 268 } 269 270 @Override 271 public Parameters limit(Duration duration) { 272 if (duration == null || duration.isNegative()) { 273 throw new IllegalArgumentException("duration must be positive"); 274 } 275 limit = duration; 276 return this; 277 } 278 279 @Override 280 public SunTimes execute() { 281 if (!hasLocation()) { 282 throw new IllegalArgumentException("Geolocation is missing."); 283 } 284 285 JulianDate jd = getJulianDate(); 286 287 Double rise = null; 288 Double set = null; 289 Double noon = null; 290 Double nadir = null; 291 boolean alwaysUp = false; 292 boolean alwaysDown = false; 293 double ye; 294 295 int hour = 0; 296 double limitHours = limit.toMillis() / (60 * 60 * 1000.0); 297 int maxHours = (int) ceil(limitHours); 298 299 double y_minus = correctedSunHeight(jd.atHour(hour - 1.0)); 300 double y_0 = correctedSunHeight(jd.atHour(hour)); 301 double y_plus = correctedSunHeight(jd.atHour(hour + 1.0)); 302 303 if (y_0 > 0.0) { 304 alwaysUp = true; 305 } else { 306 alwaysDown = true; 307 } 308 309 while (hour <= maxHours) { 310 QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus); 311 ye = qi.getYe(); 312 313 if (qi.getNumberOfRoots() == 1) { 314 double rt = qi.getRoot1() + hour; 315 if (y_minus < 0.0) { 316 if (rise == null && rt >= 0.0 && rt < limitHours) { 317 rise = rt; 318 alwaysDown = false; 319 } 320 } else { 321 if (set == null && rt >= 0.0 && rt < limitHours) { 322 set = rt; 323 alwaysUp = false; 324 } 325 } 326 } else if (qi.getNumberOfRoots() == 2) { 327 if (rise == null) { 328 double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1()); 329 if (rt >= 0.0 && rt < limitHours) { 330 rise = rt; 331 alwaysDown = false; 332 } 333 } 334 if (set == null) { 335 double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2()); 336 if (rt >= 0.0 && rt < limitHours) { 337 set = rt; 338 alwaysUp = false; 339 } 340 } 341 } 342 343 double xeAbs = abs(qi.getXe()); 344 if (xeAbs <= 1.0) { 345 double xeHour = qi.getXe() + hour; 346 if (xeHour >= 0.0) { 347 if (qi.isMaximum()) { 348 if (noon == null) { 349 noon = xeHour; 350 } 351 } else { 352 if (nadir == null) { 353 nadir = xeHour; 354 } 355 } 356 } 357 } 358 359 if (rise != null && set != null && noon != null && nadir != null) { 360 break; 361 } 362 363 hour++; 364 y_minus = y_0; 365 y_0 = y_plus; 366 y_plus = correctedSunHeight(jd.atHour(hour + 1.0)); 367 } 368 369 if (noon != null) { 370 noon = readjustMax(noon, 2.0, 14, t -> correctedSunHeight(jd.atHour(t))); 371 if (noon < 0.0 || noon >= limitHours) { 372 noon = null; 373 } 374 } 375 376 if (nadir != null) { 377 nadir = readjustMin(nadir, 2.0, 14, t -> correctedSunHeight(jd.atHour(t))); 378 if (nadir < 0.0 || nadir >= limitHours) { 379 nadir = null; 380 } 381 } 382 383 return new SunTimes( 384 rise != null ? jd.atHour(rise).getDateTime() : null, 385 set != null ? jd.atHour(set).getDateTime() : null, 386 noon != null ? jd.atHour(noon).getDateTime() : null, 387 nadir != null ? jd.atHour(nadir).getDateTime() : null, 388 alwaysUp, 389 alwaysDown 390 ); 391 } 392 393 /** 394 * Computes the sun height at the given date and position. 395 * 396 * @param jd {@link JulianDate} to use 397 * @return height, in radians 398 */ 399 private double correctedSunHeight(JulianDate jd) { 400 Vector pos = Sun.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad()); 401 402 double hc = angle; 403 if (position != null) { 404 hc -= apparentRefraction(hc); 405 hc += parallax(getElevation(), pos.getR()); 406 hc -= position * Sun.angularRadius(pos.getR()); 407 } 408 409 return pos.getTheta() - hc; 410 } 411 } 412 413 /** 414 * Sunrise time. {@code null} if the sun does not rise that day. 415 * <p> 416 * Always returns a sunrise time if {@link Parameters#fullCycle()} was set. 417 */ 418 @Nullable 419 public ZonedDateTime getRise() { 420 return rise; 421 } 422 423 /** 424 * Sunset time. {@code null} if the sun does not set that day. 425 * <p> 426 * Always returns a sunset time if {@link Parameters#fullCycle()} was set. 427 */ 428 @Nullable 429 public ZonedDateTime getSet() { 430 return set; 431 } 432 433 /** 434 * The time when the sun reaches its highest point. 435 * <p> 436 * Use {@link #isAlwaysDown()} to find out if the highest point is still below the 437 * twilight angle. 438 */ 439 @Nullable 440 public ZonedDateTime getNoon() { 441 return noon; 442 } 443 444 /** 445 * The time when the sun reaches its lowest point. 446 * <p> 447 * Use {@link #isAlwaysUp()} to find out if the lowest point is still above the 448 * twilight angle. 449 */ 450 @Nullable 451 public ZonedDateTime getNadir() { 452 return nadir; 453 } 454 455 /** 456 * {@code true} if the sun never rises/sets, but is always above the twilight angle. 457 */ 458 public boolean isAlwaysUp() { 459 return alwaysUp; 460 } 461 462 /** 463 * {@code true} if the sun never rises/sets, but is always below the twilight angle. 464 */ 465 public boolean isAlwaysDown() { 466 return alwaysDown; 467 } 468 469 @Override 470 public String toString() { 471 StringBuilder sb = new StringBuilder(); 472 sb.append("SunTimes[rise=").append(rise); 473 sb.append(", set=").append(set); 474 sb.append(", noon=").append(noon); 475 sb.append(", nadir=").append(nadir); 476 sb.append(", alwaysUp=").append(alwaysUp); 477 sb.append(", alwaysDown=").append(alwaysDown); 478 sb.append(']'); 479 return sb.toString(); 480 } 481 482}