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}