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