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