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 org.shredzone.commons.suncalc.util.ExtendedMath.apparentRefraction;
017import static org.shredzone.commons.suncalc.util.ExtendedMath.parallax;
018
019import java.util.Date;
020
021import edu.umd.cs.findbugs.annotations.Nullable;
022import org.shredzone.commons.suncalc.param.Builder;
023import org.shredzone.commons.suncalc.param.GenericParameter;
024import org.shredzone.commons.suncalc.param.LocationParameter;
025import org.shredzone.commons.suncalc.param.TimeParameter;
026import org.shredzone.commons.suncalc.param.TimeResultParameter;
027import org.shredzone.commons.suncalc.util.BaseBuilder;
028import org.shredzone.commons.suncalc.util.JulianDate;
029import org.shredzone.commons.suncalc.util.Moon;
030import org.shredzone.commons.suncalc.util.QuadraticInterpolation;
031import org.shredzone.commons.suncalc.util.Vector;
032
033/**
034 * Calculates the times of the moon.
035 */
036public final class MoonTimes {
037
038    private final @Nullable Date rise;
039    private final @Nullable Date set;
040    private final boolean alwaysUp;
041    private final boolean alwaysDown;
042
043    private MoonTimes(@Nullable Date rise, @Nullable Date set, boolean alwaysUp,
044                      boolean alwaysDown) {
045        this.rise = rise;
046        this.set = set;
047        this.alwaysUp = alwaysUp;
048        this.alwaysDown = alwaysDown;
049    }
050
051    /**
052     * Starts the computation of {@link MoonTimes}.
053     *
054     * @return {@link Parameters} to set.
055     */
056    public static Parameters compute() {
057        return new MoonTimesBuilder();
058    }
059
060    /**
061     * Collects all parameters for {@link MoonTimes}.
062     */
063    public static interface Parameters extends
064            GenericParameter<Parameters>,
065            LocationParameter<Parameters>,
066            TimeParameter<Parameters>,
067            TimeResultParameter<Parameters>,
068            Builder<MoonTimes> {
069
070        /**
071         * Checks only the next 24 hours. Rise or set times can be {@code null} if the
072         * moon never reaches the point during one day.
073         * <p>
074         * This is the default.
075         *
076         * @return itself
077         */
078        Parameters oneDay();
079
080        /**
081         * Computes until rise and set times are found, even if the moon needs more than a
082         * day for it. This can increase computation time.
083         *
084         * @return itself
085         */
086        Parameters fullCycle();
087    }
088
089    /**
090     * Builder for {@link MoonTimes}. Performs the computations based on the parameters,
091     * and creates a {@link MoonTimes} object that holds the result.
092     */
093    private static class MoonTimesBuilder extends BaseBuilder<Parameters> implements Parameters {
094        private boolean fullCycle = false;
095        private double refraction = apparentRefraction(0.0);
096
097        @Override
098        public Parameters oneDay() {
099            this.fullCycle = false;
100            return this;
101        }
102
103        @Override
104        public Parameters fullCycle() {
105            this.fullCycle = true;
106            return this;
107        }
108
109        @Override
110        public MoonTimes execute() {
111            JulianDate jd = getJulianDate();
112
113            Double rise = null;
114            Double set = null;
115            boolean alwaysUp = false;
116            boolean alwaysDown = false;
117
118            int hour = 0;
119            int maxHours = fullCycle ? 365 * 24 : 24;
120
121            double y_minus = correctedMoonHeight(jd.atHour(hour - 1.0));
122            double y_0 = correctedMoonHeight(jd.atHour(hour));
123            double y_plus = correctedMoonHeight(jd.atHour(hour + 1.0));
124
125            while (hour <= maxHours) {
126                QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus);
127                double ye = qi.getYe();
128
129                if (qi.getNumberOfRoots() == 1) {
130                    double rt = qi.getRoot1() + hour;
131                    if (y_minus < 0.0) {
132                        if (rise == null && rt >= 0.0) {
133                            rise = rt;
134                        }
135                    } else {
136                        if (set == null && rt >= 0.0) {
137                            set = rt;
138                        }
139                    }
140                } else if (qi.getNumberOfRoots() == 2) {
141                    if (rise == null) {
142                        double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1());
143                        if (rt >= 0.0) {
144                            rise = rt;
145                        }
146                    }
147                    if (set == null) {
148                        double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2());
149                        if (rt >= 0.0) {
150                            set = rt;
151                        }
152                    }
153                }
154
155                if (hour == 23 && rise == null && set == null) {
156                    alwaysUp = ye >= 0.0;
157                    alwaysDown = ye < 0.0;
158                }
159
160                if (rise != null && set != null) {
161                    break;
162                }
163
164                hour++;
165                y_minus = y_0;
166                y_0 = y_plus;
167                y_plus = correctedMoonHeight(jd.atHour(hour + 1.0));
168            }
169
170            if (!fullCycle) {
171                if (rise != null && rise >= 24.0) {
172                    rise = null;
173                }
174                if (set != null && set >= 24.0) {
175                    set = null;
176                }
177            }
178
179            return new MoonTimes(
180                    rise != null ? jd.atHour(rise).getDateTruncated(getTruncatedTo()) : null,
181                    set != null ? jd.atHour(set).getDateTruncated(getTruncatedTo()) : null,
182                    alwaysUp,
183                    alwaysDown);
184        }
185
186        /**
187         * Computes the moon height at the given date and position.
188         *
189         * @param jd {@link JulianDate} to use
190         * @return height, in radians
191         */
192        private double correctedMoonHeight(JulianDate jd) {
193            Vector pos = Moon.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad());
194            double hc = parallax(getHeight(), pos.getR())
195                            - refraction
196                            - Moon.angularRadius(pos.getR());
197            return pos.getTheta() - hc;
198        }
199    }
200
201    /**
202     * Moonrise time. {@code null} if the moon does not rise that day.
203     */
204    @Nullable
205    public Date getRise() {
206        return rise != null ? new Date(rise.getTime()) : null;
207    }
208
209    /**
210     * Moonset time. {@code null} if the moon does not set that day.
211     */
212    @Nullable
213    public Date getSet() {
214        return set != null ? new Date(set.getTime()) : null;
215    }
216
217    /**
218     * {@code true} if the moon never rises/sets, but is always above the horizon within
219     * the next 24 hours.
220     * <p>
221     * Note that {@link Parameters#fullCycle()} does not affect this result.
222     */
223    public boolean isAlwaysUp() {
224        return alwaysUp;
225    }
226
227    /**
228     * {@code true} if the moon never rises/sets, but is always below the horizon within
229     * the next 24 hours.
230     * <p>
231     * Note that {@link Parameters#fullCycle()} does not affect this result.
232     */
233    public boolean isAlwaysDown() {
234        return alwaysDown;
235    }
236
237    @Override
238    public String toString() {
239        StringBuilder sb = new StringBuilder();
240        sb.append("MoonTimes[rise=").append(rise);
241        sb.append(", set=").append(set);
242        sb.append(", alwaysUp=").append(alwaysUp);
243        sb.append(", alwaysDown=").append(alwaysDown);
244        sb.append(']');
245        return sb.toString();
246    }
247
248}