001/*
002 * Shredzone Commons - suncalc
003 *
004 * Copyright (C) 2018 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.PI;
017import static java.lang.Math.toRadians;
018import static org.shredzone.commons.suncalc.util.ExtendedMath.PI2;
019
020import java.time.ZonedDateTime;
021
022import org.shredzone.commons.suncalc.param.Builder;
023import org.shredzone.commons.suncalc.param.GenericParameter;
024import org.shredzone.commons.suncalc.param.TimeParameter;
025import org.shredzone.commons.suncalc.util.BaseBuilder;
026import org.shredzone.commons.suncalc.util.JulianDate;
027import org.shredzone.commons.suncalc.util.Moon;
028import org.shredzone.commons.suncalc.util.Pegasus;
029import org.shredzone.commons.suncalc.util.Sun;
030import org.shredzone.commons.suncalc.util.Vector;
031
032/**
033 * Calculates the date and time when the moon reaches the desired phase.
034 * <p>
035 * Note: Due to the simplified formulas used in suncalc, the returned time can have an
036 * error of several minutes.
037 */
038public class MoonPhase {
039
040    private final ZonedDateTime time;
041    private final double distance;
042
043    private MoonPhase(ZonedDateTime time, double distance) {
044        this.time = time;
045        this.distance = distance;
046    }
047
048    /**
049     * Starts the computation of {@link MoonPhase}.
050     *
051     * @return {@link Parameters} to set.
052     */
053    public static Parameters compute() {
054        return new MoonPhaseBuilder();
055    }
056
057    /**
058     * Collects all parameters for {@link MoonPhase}.
059     */
060    public interface Parameters extends
061            GenericParameter<Parameters>,
062            TimeParameter<Parameters>,
063            Builder<MoonPhase> {
064
065        /**
066         * Sets the desired {@link Phase}.
067         * <p>
068         * Defaults to {@link Phase#NEW_MOON}.
069         *
070         * @param phase
071         *            {@link Phase} to be used.
072         * @return itself
073         */
074        Parameters phase(Phase phase);
075
076        /**
077         * Sets a free phase to be used.
078         *
079         * @param phase
080         *            Desired phase, in degrees. 0 = new moon, 90 = first quarter, 180 =
081         *            full moon, 270 = third quarter.
082         * @return itself
083         */
084        Parameters phase(double phase);
085    }
086
087    /**
088     * Enumeration of moon phases.
089     */
090    public enum Phase {
091
092        /**
093         * New moon.
094         */
095        NEW_MOON(0.0),
096
097        /**
098         * Waxing crescent moon.
099         *
100         * @since 3.5
101         */
102        WAXING_CRESCENT(45.0),
103
104        /**
105         * Waxing half moon.
106         */
107        FIRST_QUARTER(90.0),
108
109        /**
110         * Waxing gibbous moon.
111         *
112         * @since 3.5
113         */
114        WAXING_GIBBOUS(135.0),
115
116        /**
117         * Full moon.
118         */
119        FULL_MOON(180.0),
120
121        /**
122         * Waning gibbous moon.
123         *
124         * @since 3.5
125         */
126        WANING_GIBBOUS(225.0),
127
128        /**
129         * Waning half moon.
130         */
131        LAST_QUARTER(270.0),
132
133        /**
134         * Waning crescent moon.
135         *
136         * @since 3.5
137         */
138        WANING_CRESCENT(315.0);
139
140        /**
141         * Converts an angle to the closest matching moon phase.
142         *
143         * @param angle
144         *         Moon phase angle, in degrees. 0 = New Moon, 180 = Full Moon. Angles
145         *         outside the [0,360) range are normalized into that range.
146         * @return Closest Phase that is matching that angle.
147         * @since 3.5
148         */
149        public static Phase toPhase(double angle) {
150            // bring into range 0.0 .. 360.0
151            double normalized = angle % 360.0;
152            if (normalized < 0.0) {
153                normalized += 360.0;
154            }
155
156            if (normalized < 22.5) {
157                return NEW_MOON;
158            }
159            if (normalized < 67.5) {
160                return WAXING_CRESCENT;
161            }
162            if (normalized < 112.5) {
163                return FIRST_QUARTER;
164            }
165            if (normalized < 157.5) {
166                return WAXING_GIBBOUS;
167            }
168            if (normalized < 202.5) {
169                return FULL_MOON;
170            }
171            if (normalized < 247.5) {
172                return WANING_GIBBOUS;
173            }
174            if (normalized < 292.5) {
175                return LAST_QUARTER;
176            }
177            if (normalized < 337.5) {
178                return WANING_CRESCENT;
179            }
180            return NEW_MOON;
181        }
182
183        private final double angle;
184        private final double angleRad;
185
186        Phase(double angle) {
187            this.angle = angle;
188            this.angleRad = toRadians(angle);
189        }
190
191        /**
192         * Returns the moons's angle in reference to the sun, in degrees.
193         */
194        public double getAngle() {
195            return angle;
196        }
197
198        /**
199         * Returns the moons's angle in reference to the sun, in radians.
200         */
201        public double getAngleRad() {
202            return angleRad;
203        }
204    }
205
206    /**
207     * Builder for {@link MoonPhase}. Performs the computations based on the parameters,
208     * and creates a {@link MoonPhase} object that holds the result.
209     */
210    private static class MoonPhaseBuilder extends BaseBuilder<Parameters> implements Parameters {
211        private static final double SUN_LIGHT_TIME_TAU = 8.32 / (1440.0 * 36525.0);
212
213        private double phase = Phase.NEW_MOON.getAngleRad();
214
215        @Override
216        public Parameters phase(Phase phase) {
217            this.phase = phase.getAngleRad();
218            return this;
219        }
220
221        @Override
222        public Parameters phase(double phase) {
223            this.phase = toRadians(phase);
224            return this;
225        }
226
227        @Override
228        public MoonPhase execute() {
229            final JulianDate jd = getJulianDate();
230
231            double dT = 7.0 / 36525.0;                      // step rate: 1 week
232            double accuracy = (0.5 / 1440.0) / 36525.0;     // accuracy: 30 seconds
233
234            double t0 = jd.getJulianCentury();
235            double t1 = t0 + dT;
236
237            double d0 = moonphase(jd, t0);
238            double d1 = moonphase(jd, t1);
239
240            while (d0 * d1 > 0.0 || d1 < d0) {
241                t0 = t1;
242                d0 = d1;
243                t1 += dT;
244                d1 = moonphase(jd, t1);
245            }
246
247            double tphase = Pegasus.calculate(t0, t1, accuracy, x -> moonphase(jd, x));
248            JulianDate tjd = jd.atJulianCentury(tphase);
249            return new MoonPhase(tjd.getDateTime(), Moon.positionEquatorial(tjd).getR());
250        }
251
252        /**
253         * Calculates the position of the moon at the given phase.
254         *
255         * @param jd
256         *            Base Julian date
257         * @param t
258         *            Ephemeris time
259         * @return difference angle of the sun's and moon's position
260         */
261        private double moonphase(JulianDate jd, double t) {
262            Vector sun = Sun.positionEquatorial(jd.atJulianCentury(t - SUN_LIGHT_TIME_TAU));
263            Vector moon = Moon.positionEquatorial(jd.atJulianCentury(t));
264            double diff = moon.getPhi() - sun.getPhi() - phase; //NOSONAR: false positive
265            while (diff < 0.0) {
266                diff += PI2;
267            }
268            return ((diff + PI) % PI2) - PI;
269        }
270
271    }
272
273    /**
274     * Date and time of the desired moon phase. The time is rounded to full minutes.
275     */
276    public ZonedDateTime getTime() {
277        return time;
278    }
279
280    /**
281     * Geocentric distance of the moon at the given phase, in kilometers.
282     *
283     * @since 3.4
284     */
285    public double getDistance() { return distance; }
286
287    /**
288     * Checks if the moon is in a SuperMoon position.
289     * <p>
290     * Note that there is no official definition of supermoon. Suncalc will assume a
291     * supermoon if the center of the moon is closer than 360,000 km to the center of
292     * Earth. Usually only full moons or new moons are candidates for supermoons.
293     *
294     * @since 3.4
295     */
296    public boolean isSuperMoon() {
297        return distance < 360000.0;
298    }
299
300    /**
301     * Checks if the moon is in a MicroMoon position.
302     * <p>
303     * Note that there is no official definition of micromoon. Suncalc will assume a
304     * micromoon if the center of the moon is farther than 405,000 km from the center of
305     * Earth. Usually only full moons or new moons are candidates for micromoons.
306     *
307     * @since 3.4
308     */
309    public boolean isMicroMoon() {
310        return distance > 405000.0;
311    }
312
313    @Override
314    public String toString() {
315        StringBuilder sb = new StringBuilder();
316        sb.append("MoonPhase[time=").append(time);
317        sb.append(", distance=").append(distance);
318        sb.append(" km]");
319        return sb.toString();
320    }
321
322}