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}