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