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 if (!hasLocation()) { 121 throw new IllegalArgumentException("Geolocation is missing."); 122 } 123 124 JulianDate jd = getJulianDate(); 125 126 Double rise = null; 127 Double set = null; 128 boolean alwaysUp = false; 129 boolean alwaysDown = false; 130 double ye; 131 132 int hour = 0; 133 double limitHours = limit.toMillis() / (60 * 60 * 1000.0); 134 int maxHours = (int) ceil(limitHours); 135 136 double y_minus = correctedMoonHeight(jd.atHour(hour - 1.0)); 137 double y_0 = correctedMoonHeight(jd.atHour(hour)); 138 double y_plus = correctedMoonHeight(jd.atHour(hour + 1.0)); 139 140 if (y_0 > 0.0) { 141 alwaysUp = true; 142 } else { 143 alwaysDown = true; 144 } 145 146 while (hour <= maxHours) { 147 QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus); 148 ye = qi.getYe(); 149 150 if (qi.getNumberOfRoots() == 1) { 151 double rt = qi.getRoot1() + hour; 152 if (y_minus < 0.0) { 153 if (rise == null && rt >= 0.0 && rt < limitHours) { 154 rise = rt; 155 alwaysDown = false; 156 } 157 } else { 158 if (set == null && rt >= 0.0 && rt < limitHours) { 159 set = rt; 160 alwaysUp = false; 161 } 162 } 163 } else if (qi.getNumberOfRoots() == 2) { 164 if (rise == null) { 165 double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1()); 166 if (rt >= 0.0 && rt < limitHours) { 167 rise = rt; 168 alwaysDown = false; 169 } 170 } 171 if (set == null) { 172 double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2()); 173 if (rt >= 0.0 && rt < limitHours) { 174 set = rt; 175 alwaysUp = false; 176 } 177 } 178 } 179 180 if (rise != null && set != null) { 181 break; 182 } 183 184 hour++; 185 y_minus = y_0; 186 y_0 = y_plus; 187 y_plus = correctedMoonHeight(jd.atHour(hour + 1.0)); 188 } 189 190 return new MoonTimes( 191 rise != null ? jd.atHour(rise).getDateTime() : null, 192 set != null ? jd.atHour(set).getDateTime() : null, 193 alwaysUp, 194 alwaysDown); 195 } 196 197 /** 198 * Computes the moon height at the given date and position. 199 * 200 * @param jd {@link JulianDate} to use 201 * @return height, in radians 202 */ 203 private double correctedMoonHeight(JulianDate jd) { 204 Vector pos = Moon.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad()); 205 double hc = parallax(getElevation(), pos.getR()) 206 - refraction 207 - Moon.angularRadius(pos.getR()); 208 return pos.getTheta() - hc; 209 } 210 } 211 212 /** 213 * Moonrise time. {@code null} if the moon does not rise that day. 214 */ 215 @Nullable 216 public ZonedDateTime getRise() { 217 return rise; 218 } 219 220 /** 221 * Moonset time. {@code null} if the moon does not set that day. 222 */ 223 @Nullable 224 public ZonedDateTime getSet() { 225 return set; 226 } 227 228 /** 229 * {@code true} if the moon never rises/sets, but is always above the horizon. 230 */ 231 public boolean isAlwaysUp() { 232 return alwaysUp; 233 } 234 235 /** 236 * {@code true} if the moon never rises/sets, but is always below the horizon. 237 */ 238 public boolean isAlwaysDown() { 239 return alwaysDown; 240 } 241 242 @Override 243 public String toString() { 244 StringBuilder sb = new StringBuilder(); 245 sb.append("MoonTimes[rise=").append(rise); 246 sb.append(", set=").append(set); 247 sb.append(", alwaysUp=").append(alwaysUp); 248 sb.append(", alwaysDown=").append(alwaysDown); 249 sb.append(']'); 250 return sb.toString(); 251 } 252 253}