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}