org.shredzone.commons.suncalc.MoonPhase Maven / Gradle / Ivy
Show all versions of commons-suncalc Show documentation
/*
* Shredzone Commons - suncalc
*
* Copyright (C) 2018 Richard "Shred" Körber
* http://commons.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.commons.suncalc;
import static java.lang.Math.PI;
import static java.lang.Math.toRadians;
import static org.shredzone.commons.suncalc.util.ExtendedMath.PI2;
import java.time.ZonedDateTime;
import org.shredzone.commons.suncalc.param.Builder;
import org.shredzone.commons.suncalc.param.GenericParameter;
import org.shredzone.commons.suncalc.param.TimeParameter;
import org.shredzone.commons.suncalc.util.BaseBuilder;
import org.shredzone.commons.suncalc.util.JulianDate;
import org.shredzone.commons.suncalc.util.Moon;
import org.shredzone.commons.suncalc.util.Pegasus;
import org.shredzone.commons.suncalc.util.Sun;
import org.shredzone.commons.suncalc.util.Vector;
/**
* Calculates the date and time when the moon reaches the desired phase.
*
* Note: Due to the simplified formulas used in suncalc, the returned time can have an
* error of several minutes.
*/
public class MoonPhase {
private final ZonedDateTime time;
private final double distance;
private MoonPhase(ZonedDateTime time, double distance) {
this.time = time;
this.distance = distance;
}
/**
* Starts the computation of {@link MoonPhase}.
*
* @return {@link Parameters} to set.
*/
public static Parameters compute() {
return new MoonPhaseBuilder();
}
/**
* Collects all parameters for {@link MoonPhase}.
*/
public interface Parameters extends
GenericParameter,
TimeParameter,
Builder {
/**
* Sets the desired {@link Phase}.
*
* Defaults to {@link Phase#NEW_MOON}.
*
* @param phase
* {@link Phase} to be used.
* @return itself
*/
Parameters phase(Phase phase);
/**
* Sets a free phase to be used.
*
* @param phase
* Desired phase, in degrees. 0 = new moon, 90 = first quarter, 180 =
* full moon, 270 = third quarter.
* @return itself
*/
Parameters phase(double phase);
}
/**
* Enumeration of moon phases.
*/
public enum Phase {
/**
* New moon.
*/
NEW_MOON(0.0),
/**
* Waxing crescent moon.
*
* @since 3.5
*/
WAXING_CRESCENT(45.0),
/**
* Waxing half moon.
*/
FIRST_QUARTER(90.0),
/**
* Waxing gibbous moon.
*
* @since 3.5
*/
WAXING_GIBBOUS(135.0),
/**
* Full moon.
*/
FULL_MOON(180.0),
/**
* Waning gibbous moon.
*
* @since 3.5
*/
WANING_GIBBOUS(225.0),
/**
* Waning half moon.
*/
LAST_QUARTER(270.0),
/**
* Waning crescent moon.
*
* @since 3.5
*/
WANING_CRESCENT(315.0);
/**
* Converts an angle to the closest matching moon phase.
*
* @param angle
* Moon phase angle, in degrees. 0 = New Moon, 180 = Full Moon. Angles
* outside the [0,360) range are normalized into that range.
* @return Closest Phase that is matching that angle.
* @since 3.5
*/
public static Phase toPhase(double angle) {
// bring into range 0.0 .. 360.0
double normalized = angle % 360.0;
if (normalized < 0.0) {
normalized += 360.0;
}
if (normalized < 22.5) {
return NEW_MOON;
}
if (normalized < 67.5) {
return WAXING_CRESCENT;
}
if (normalized < 112.5) {
return FIRST_QUARTER;
}
if (normalized < 157.5) {
return WAXING_GIBBOUS;
}
if (normalized < 202.5) {
return FULL_MOON;
}
if (normalized < 247.5) {
return WANING_GIBBOUS;
}
if (normalized < 292.5) {
return LAST_QUARTER;
}
if (normalized < 337.5) {
return WANING_CRESCENT;
}
return NEW_MOON;
}
private final double angle;
private final double angleRad;
Phase(double angle) {
this.angle = angle;
this.angleRad = toRadians(angle);
}
/**
* Returns the moons's angle in reference to the sun, in degrees.
*/
public double getAngle() {
return angle;
}
/**
* Returns the moons's angle in reference to the sun, in radians.
*/
public double getAngleRad() {
return angleRad;
}
}
/**
* Builder for {@link MoonPhase}. Performs the computations based on the parameters,
* and creates a {@link MoonPhase} object that holds the result.
*/
private static class MoonPhaseBuilder extends BaseBuilder implements Parameters {
private static final double SUN_LIGHT_TIME_TAU = 8.32 / (1440.0 * 36525.0);
private double phase = Phase.NEW_MOON.getAngleRad();
@Override
public Parameters phase(Phase phase) {
this.phase = phase.getAngleRad();
return this;
}
@Override
public Parameters phase(double phase) {
this.phase = toRadians(phase);
return this;
}
@Override
public MoonPhase execute() {
final JulianDate jd = getJulianDate();
double dT = 7.0 / 36525.0; // step rate: 1 week
double accuracy = (0.5 / 1440.0) / 36525.0; // accuracy: 30 seconds
double t0 = jd.getJulianCentury();
double t1 = t0 + dT;
double d0 = moonphase(jd, t0);
double d1 = moonphase(jd, t1);
while (d0 * d1 > 0.0 || d1 < d0) {
t0 = t1;
d0 = d1;
t1 += dT;
d1 = moonphase(jd, t1);
}
double tphase = Pegasus.calculate(t0, t1, accuracy, x -> moonphase(jd, x));
JulianDate tjd = jd.atJulianCentury(tphase);
return new MoonPhase(tjd.getDateTime(), Moon.positionEquatorial(tjd).getR());
}
/**
* Calculates the position of the moon at the given phase.
*
* @param jd
* Base Julian date
* @param t
* Ephemeris time
* @return difference angle of the sun's and moon's position
*/
private double moonphase(JulianDate jd, double t) {
Vector sun = Sun.positionEquatorial(jd.atJulianCentury(t - SUN_LIGHT_TIME_TAU));
Vector moon = Moon.positionEquatorial(jd.atJulianCentury(t));
double diff = moon.getPhi() - sun.getPhi() - phase; //NOSONAR: false positive
while (diff < 0.0) {
diff += PI2;
}
return ((diff + PI) % PI2) - PI;
}
}
/**
* Date and time of the desired moon phase. The time is rounded to full minutes.
*/
public ZonedDateTime getTime() {
return time;
}
/**
* Geocentric distance of the moon at the given phase, in kilometers.
*
* @since 3.4
*/
public double getDistance() { return distance; }
/**
* Checks if the moon is in a SuperMoon position.
*
* Note that there is no official definition of supermoon. Suncalc will assume a
* supermoon if the center of the moon is closer than 360,000 km to the center of
* Earth. Usually only full moons or new moons are candidates for supermoons.
*
* @since 3.4
*/
public boolean isSuperMoon() {
return distance < 360000.0;
}
/**
* Checks if the moon is in a MicroMoon position.
*
* Note that there is no official definition of micromoon. Suncalc will assume a
* micromoon if the center of the moon is farther than 405,000 km from the center of
* Earth. Usually only full moons or new moons are candidates for micromoons.
*
* @since 3.4
*/
public boolean isMicroMoon() {
return distance > 405000.0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("MoonPhase[time=").append(time);
sb.append(", distance=").append(distance);
sb.append(" km]");
return sb.toString();
}
}