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