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