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}