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.param.WindowParameter; 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 ZonedDateTime rise; 040 private final @Nullable ZonedDateTime set; 041 private final @Nullable ZonedDateTime noon; 042 private final @Nullable ZonedDateTime nadir; 043 private final boolean alwaysUp; 044 private final boolean alwaysDown; 045 046 private SunTimes(@Nullable ZonedDateTime rise, @Nullable ZonedDateTime set, 047 @Nullable ZonedDateTime noon, @Nullable ZonedDateTime nadir, 048 boolean alwaysUp, 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 WindowParameter<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 /** 099 * Enumeration of predefined twilights. 100 * <p> 101 * The twilight angles use a geocentric reference, by definition. However, 102 * {@link #VISUAL} and {@link #VISUAL_LOWER} are topocentric, and take the spectator's 103 * elevation and the atmospheric refraction into account. 104 * 105 * @see <a href="https://en.wikipedia.org/wiki/Twilight">Wikipedia: Twilight</a> 106 */ 107 public enum Twilight { 108 109 /** 110 * The moment when the visual upper edge of the sun crosses the horizon. This is 111 * commonly referred to as "sunrise" and "sunset". Atmospheric refraction is taken 112 * into account. 113 * <p> 114 * This is the default. 115 */ 116 VISUAL(0.0, 1.0), 117 118 /** 119 * The moment when the visual lower edge of the sun crosses the horizon. This is 120 * the ending of the sunrise and the starting of the sunset. Atmospheric 121 * refraction is taken into account. 122 */ 123 VISUAL_LOWER(0.0, -1.0), 124 125 /** 126 * The moment when the center of the sun crosses the horizon (0°). 127 */ 128 HORIZON(0.0), 129 130 /** 131 * Civil twilight (-6°). 132 */ 133 CIVIL(-6.0), 134 135 /** 136 * Nautical twilight (-12°). 137 */ 138 NAUTICAL(-12.0), 139 140 /** 141 * Astronomical twilight (-18°). 142 */ 143 ASTRONOMICAL(-18.0), 144 145 /** 146 * Golden hour (6°). The Golden hour is between {@link #GOLDEN_HOUR} and 147 * {@link #BLUE_HOUR}. The Magic hour is between {@link #GOLDEN_HOUR} and 148 * {@link #CIVIL}. 149 * 150 * @see <a href= 151 * "https://en.wikipedia.org/wiki/Golden_hour_(photography)">Wikipedia: 152 * Golden hour</a> 153 */ 154 GOLDEN_HOUR(6.0), 155 156 /** 157 * Blue hour (-4°). The Blue hour is between {@link #NIGHT_HOUR} and 158 * {@link #BLUE_HOUR}. 159 * 160 * @see <a href="https://en.wikipedia.org/wiki/Blue_hour">Wikipedia: Blue hour</a> 161 */ 162 BLUE_HOUR(-4.0), 163 164 /** 165 * End of Blue hour (-8°). 166 * <p> 167 * "Night Hour" is not an official term, but just a name that is marking the 168 * beginning/end of the Blue hour. 169 */ 170 NIGHT_HOUR(-8.0); 171 172 private final double angle; 173 private final double angleRad; 174 private final @Nullable Double position; 175 176 Twilight(double angle) { 177 this(angle, null); 178 } 179 180 Twilight(double angle, @Nullable Double position) { 181 this.angle = angle; 182 this.angleRad = toRadians(angle); 183 this.position = position; 184 } 185 186 /** 187 * Returns the sun's angle at the twilight position, in degrees. 188 */ 189 public double getAngle() { 190 return angle; 191 } 192 193 /** 194 * Returns the sun's angle at the twilight position, in radians. 195 */ 196 public double getAngleRad() { 197 return angleRad; 198 } 199 200 /** 201 * Returns {@code true} if this twilight position is topocentric. Then the 202 * parallax and the atmospheric refraction is taken into account. 203 */ 204 public boolean isTopocentric() { 205 return position != null; 206 } 207 208 /** 209 * Returns the angular position. {@code 0.0} means center of the sun. {@code 1.0} 210 * means upper edge of the sun. {@code -1.0} means lower edge of the sun. 211 * {@code null} means the angular position is not topocentric. 212 */ 213 @Nullable 214 private Double getAngularPosition() { 215 return position; 216 } 217 } 218 219 /** 220 * Builder for {@link SunTimes}. Performs the computations based on the parameters, 221 * and creates a {@link SunTimes} object that holds the result. 222 */ 223 private static class SunTimesBuilder extends BaseBuilder<Parameters> implements Parameters { 224 private double angle = Twilight.VISUAL.getAngleRad(); 225 private @Nullable Double position = Twilight.VISUAL.getAngularPosition(); 226 227 @Override 228 public Parameters twilight(Twilight twilight) { 229 this.angle = twilight.getAngleRad(); 230 this.position = twilight.getAngularPosition(); 231 return this; 232 } 233 234 @Override 235 public Parameters twilight(double angle) { 236 this.angle = toRadians(angle); 237 this.position = null; 238 return this; 239 } 240 241 @Override 242 public SunTimes execute() { 243 if (!hasLocation()) { 244 throw new IllegalArgumentException("Geolocation is missing."); 245 } 246 247 JulianDate jd = getJulianDate(); 248 249 Double rise = null; 250 Double set = null; 251 Double noon = null; 252 Double nadir = null; 253 boolean alwaysUp = false; 254 boolean alwaysDown = false; 255 double ye; 256 257 int hourStep; 258 double lowerLimitHours, upperLimitHours; 259 if (getDuration().isNegative()) { 260 hourStep = -1; 261 lowerLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0); 262 upperLimitHours = 0.0; 263 } else { 264 hourStep = 1; 265 lowerLimitHours = 0.0; 266 upperLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0);; 267 } 268 269 int hour = 0; 270 int minHours = (int) floor(lowerLimitHours); 271 int maxHours = (int) ceil(upperLimitHours); 272 273 double y_minus = correctedSunHeight(jd.atHour(hour - 1.0)); 274 double y_0 = correctedSunHeight(jd.atHour(hour)); 275 double y_plus = correctedSunHeight(jd.atHour(hour + 1.0)); 276 277 if (y_0 > 0.0) { 278 alwaysUp = true; 279 } else { 280 alwaysDown = true; 281 } 282 283 while (hour <= maxHours && hour >= minHours) { 284 QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus); 285 ye = qi.getYe(); 286 287 if (qi.getNumberOfRoots() == 1) { 288 double rt = qi.getRoot1() + hour; 289 if (y_minus < 0.0) { 290 if (rise == null && rt >= lowerLimitHours && rt < upperLimitHours) { 291 rise = rt; 292 alwaysDown = false; 293 } 294 } else { 295 if (set == null && rt >= lowerLimitHours && rt < upperLimitHours) { 296 set = rt; 297 alwaysUp = false; 298 } 299 } 300 } else if (qi.getNumberOfRoots() == 2) { 301 if (rise == null) { 302 double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1()); 303 if (rt >= lowerLimitHours && rt < upperLimitHours) { 304 rise = rt; 305 alwaysDown = false; 306 } 307 } 308 if (set == null) { 309 double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2()); 310 if (rt >= lowerLimitHours && rt < upperLimitHours) { 311 set = rt; 312 alwaysUp = false; 313 } 314 } 315 } 316 317 double xeAbs = abs(qi.getXe()); 318 if (xeAbs <= 1.0) { 319 double xeHour = qi.getXe() + hour; 320 if (hourStep > 0 ? xeHour >= 0.0 : xeHour <= 0.0) { 321 if (qi.isMaximum()) { 322 if (noon == null) { 323 noon = xeHour; 324 } 325 } else { 326 if (nadir == null) { 327 nadir = xeHour; 328 } 329 } 330 } 331 } 332 333 if (rise != null && set != null && noon != null && nadir != null) { 334 break; 335 } 336 337 hour += hourStep; 338 if (hourStep > 0) { 339 y_minus = y_0; 340 y_0 = y_plus; 341 y_plus = correctedSunHeight(jd.atHour(hour + 1.0)); 342 } else { 343 y_plus = y_0; 344 y_0 = y_minus; 345 y_minus = correctedSunHeight(jd.atHour(hour - 1.0)); 346 } 347 } 348 349 if (noon != null) { 350 noon = readjustMax(noon, 2.0, 14, t -> correctedSunHeight(jd.atHour(t))); 351 if (noon < lowerLimitHours || noon >= upperLimitHours) { 352 noon = null; 353 } 354 } 355 356 if (nadir != null) { 357 nadir = readjustMin(nadir, 2.0, 14, t -> correctedSunHeight(jd.atHour(t))); 358 if (nadir < lowerLimitHours || nadir >= upperLimitHours) { 359 nadir = null; 360 } 361 } 362 363 return new SunTimes( 364 rise != null ? jd.atHour(rise).getDateTime() : null, 365 set != null ? jd.atHour(set).getDateTime() : null, 366 noon != null ? jd.atHour(noon).getDateTime() : null, 367 nadir != null ? jd.atHour(nadir).getDateTime() : null, 368 alwaysUp, 369 alwaysDown 370 ); 371 } 372 373 /** 374 * Computes the sun height at the given date and position. 375 * 376 * @param jd {@link JulianDate} to use 377 * @return height, in radians 378 */ 379 private double correctedSunHeight(JulianDate jd) { 380 Vector pos = Sun.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad()); 381 382 double hc = angle; 383 if (position != null) { 384 hc -= apparentRefraction(hc); 385 hc += parallax(getElevation(), pos.getR()); 386 hc -= position * Sun.angularRadius(pos.getR()); 387 } 388 389 return pos.getTheta() - hc; 390 } 391 } 392 393 /** 394 * Sunrise time. {@code null} if the sun does not rise that day. 395 * <p> 396 * Always returns a sunrise time if {@link Parameters#fullCycle()} was set. 397 */ 398 @Nullable 399 public ZonedDateTime getRise() { 400 return rise; 401 } 402 403 /** 404 * Sunset time. {@code null} if the sun does not set that day. 405 * <p> 406 * Always returns a sunset time if {@link Parameters#fullCycle()} was set. 407 */ 408 @Nullable 409 public ZonedDateTime getSet() { 410 return set; 411 } 412 413 /** 414 * The time when the sun reaches its highest point. 415 * <p> 416 * Use {@link #isAlwaysDown()} to find out if the highest point is still below the 417 * twilight angle. 418 */ 419 @Nullable 420 public ZonedDateTime getNoon() { 421 return noon; 422 } 423 424 /** 425 * The time when the sun reaches its lowest point. 426 * <p> 427 * Use {@link #isAlwaysUp()} to find out if the lowest point is still above the 428 * twilight angle. 429 */ 430 @Nullable 431 public ZonedDateTime getNadir() { 432 return nadir; 433 } 434 435 /** 436 * {@code true} if the sun never rises/sets, but is always above the twilight angle. 437 */ 438 public boolean isAlwaysUp() { 439 return alwaysUp; 440 } 441 442 /** 443 * {@code true} if the sun never rises/sets, but is always below the twilight angle. 444 */ 445 public boolean isAlwaysDown() { 446 return alwaysDown; 447 } 448 449 @Override 450 public String toString() { 451 StringBuilder sb = new StringBuilder(); 452 sb.append("SunTimes[rise=").append(rise); 453 sb.append(", set=").append(set); 454 sb.append(", noon=").append(noon); 455 sb.append(", nadir=").append(nadir); 456 sb.append(", alwaysUp=").append(alwaysUp); 457 sb.append(", alwaysDown=").append(alwaysDown); 458 sb.append(']'); 459 return sb.toString(); 460 } 461 462}