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