├── .gitignore ├── README.md ├── docs └── suncalc.net paris lookup from east coast.png ├── pom.xml └── src ├── main └── java │ └── com │ └── florianmski │ └── suncalc │ ├── SunCalc.java │ ├── models │ ├── EquatorialCoordinates.java │ ├── GeocentricCoordinates.java │ ├── MoonPosition.java │ ├── SunPhase.java │ └── SunPosition.java │ └── utils │ ├── Constants.java │ ├── DateUtils.java │ ├── MoonUtils.java │ ├── PositionUtils.java │ ├── SunUtils.java │ ├── TimeUtils.java │ └── package-info.java └── test └── groovy └── com └── florianmski └── suncalc └── SunCalcSpec.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | # IDE files # 9 | .idea/ 10 | *.iml 11 | 12 | # Maven Files # 13 | target -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SunCalc-Java 2 | ============ 3 | 4 | A Java port (with some tweaks) of the awesome [SunCalc JS lib](https://github.com/mourner/suncalc). 5 | 6 | How to use 7 | ========== 8 | 9 | ``` 10 | // now 11 | Calendar d = Calendar.getInstance(); 12 | // Paris coordinates 13 | double LAT = 48.818684; 14 | double LON = 2.323096; 15 | 16 | // get a list of phases at a given location & day 17 | List sunPhases = SunCalc.getPhases(d, LAT, LON); 18 | for(SunPhase e : SunCalc.getPhases(d, LAT, LON)) 19 | { 20 | System.out.println("Phase : " + e.getName()); 21 | System.out.println("start at : " + e.getStartDate().getTime()); 22 | System.out.println("end at : " + e.getEndDate().getTime()); 23 | System.out.println("==========================================="); 24 | } 25 | 26 | // get the sun position (azimuth and elevation) at a given location & time 27 | SunPosition sp = SunCalc.getSunPosition(d, LAT, LON); 28 | ``` 29 | 30 | Gotchas 31 | ======= 32 | 33 | Currently supported sun phases are: 34 | 35 | * Night (Morning) 36 | * Twilight Astronomical (Morning) 37 | * Twilight Nautical (Morning) 38 | * Twilight Civil (Morning) 39 | * Sunrise (Morning) 40 | * Golden Hour (Morning) 41 | * Daylight 42 | * Golden Hour (Evening) 43 | * Sunset (Evening) 44 | * Twilight Civil (Evening) 45 | * Twilight Nautical (Evening) 46 | * Twilight Astronomical (Evening) 47 | * Night (Evening) 48 | 49 | *** 50 | 51 | There is an implementation of the moon phases but it has not been really tested, use it at your own risks. 52 | 53 | *** 54 | 55 | If you try to get sun phases at extremes location (such as poles) you could get invalid dates (such as the famous January 1970) 56 | 57 | Written By 58 | ============ 59 | 60 | * Florian Mierzejewski - 61 | 62 | 63 | License 64 | ======= 65 | 66 | "THE BEER-WARE LICENSE" (Revision 42): 67 | You can do whatever you want with this stuff. 68 | If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. -------------------------------------------------------------------------------- /docs/suncalc.net paris lookup from east coast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florianmski/SunCalc-Java/ad0e18eec4f0f877aad61323bf33659800a28030/docs/suncalc.net paris lookup from east coast.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.florianmski 8 | suncalc 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | org.spockframework 14 | spock-core 15 | 1.0-groovy-2.4 16 | test 17 | 18 | 19 | info.solidsoft.spock 20 | spock-global-unroll 21 | 0.5.1 22 | test 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-compiler-plugin 31 | 3.6.0 32 | 33 | 1.8 34 | 1.8 35 | 36 | 37 | 38 | org.codehaus.mojo 39 | build-helper-maven-plugin 40 | 1.12 41 | 42 | 43 | add-test-source 44 | generate-test-sources 45 | 46 | add-test-source 47 | 48 | 49 | 50 | src/test/groovy 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.codehaus.gmavenplus 58 | gmavenplus-plugin 59 | 1.5 60 | 61 | 62 | 63 | testCompile 64 | 65 | 66 | 67 | 68 | 69 | 70 | ${project.basedir}/src/test/groovy 71 | 72 | **/*.groovy 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-surefire-plugin 81 | 2.19.1 82 | 83 | methods 84 | 5 85 | 86 | **/*Test.* 87 | **/*Spec.* 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/SunCalc.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc; 2 | 3 | 4 | import com.florianmski.suncalc.models.*; 5 | import com.florianmski.suncalc.utils.*; 6 | 7 | import java.util.Calendar; 8 | import java.util.List; 9 | import java.util.TimeZone; 10 | 11 | /** 12 | * Calculations for the sun and moon relative to earth 13 | */ 14 | public class SunCalc 15 | { 16 | /** 17 | * 18 | * Calculates the sun's position at a particular location and moment 19 | * 20 | * @param date the day, time and timezone to calculate for 21 | * @param lat measured from North, in degrees 22 | * @param lng measured from East, in degrees 23 | * @return the sun's position in the sky relative to the location 24 | */ 25 | public static SunPosition getSunPosition(Calendar date, double lat, double lng) 26 | { 27 | double lw = Constants.TO_RAD * -lng; 28 | double phi = Constants.TO_RAD * lat; 29 | double d = DateUtils.toDays(date); 30 | 31 | EquatorialCoordinates c = SunUtils.getSunCoords(d); 32 | double H = PositionUtils.getSiderealTime(d, lw) - c.getRightAscension(); 33 | 34 | return new SunPosition( 35 | PositionUtils.getAzimuth(H, phi, c.getDeclination()), 36 | PositionUtils.getAltitude(H, phi, c.getDeclination())); 37 | } 38 | 39 | /** 40 | * 41 | * Calculates the moon's position at a particular location and moment 42 | * 43 | * @param date the day, time and timezone to calculate for 44 | * @param lat measured from North, in degrees 45 | * @param lng measured from East, in degrees 46 | * @return the moon's position in the sky relative to the location 47 | */ 48 | public static MoonPosition getMoonPosition(Calendar date, double lat, double lng) 49 | { 50 | double lw = Constants.TO_RAD * -lng; 51 | double phi = Constants.TO_RAD * lat; 52 | double d = DateUtils.toDays(date); 53 | 54 | GeocentricCoordinates c = MoonUtils.getMoonCoords(d); 55 | double H = PositionUtils.getSiderealTime(d, lw) - c.getRightAscension(); 56 | double h = PositionUtils.getAltitude(H, phi, c.getDeclination()); 57 | 58 | // altitude correction for refraction 59 | h = h + Constants.TO_RAD * 0.017 / Math.tan(h + Constants.TO_RAD * 10.26 / (h + Constants.TO_RAD * 5.10)); 60 | 61 | return new MoonPosition(PositionUtils.getAzimuth(H, phi, c.getDeclination()), h, c.getDistance()); 62 | } 63 | 64 | /** 65 | * Calculates moon illumination for a particular day and time. 66 | * Location is not needed because percentage will be the same for 67 | * both Northern and Southern hemisphere. 68 | * 69 | * @param date the day, time and timezone to calculate for 70 | * @return fraction of moon's illuminated limb and phase 71 | */ 72 | public static double getMoonFraction(Calendar date) 73 | { 74 | double d = DateUtils.toDays(date); 75 | EquatorialCoordinates s = SunUtils.getSunCoords(d); 76 | GeocentricCoordinates m = MoonUtils.getMoonCoords(d); 77 | 78 | int sdist = 149598000; // distance from Earth to Sun in km 79 | 80 | double phi = Math.acos(Math.sin(s.getDeclination()) * Math.sin(m.getDeclination()) + Math.cos(s.getDeclination()) * Math.cos(m.getDeclination()) * Math.cos(s.getRightAscension() - m.getRightAscension())); 81 | double inc = Math.atan2(sdist * Math.sin(phi), m.getDistance() - sdist * Math.cos(phi)); 82 | 83 | return (1 + Math.cos(inc)) / 2; 84 | } 85 | 86 | /** 87 | * Calculates phases of the sun for a single day 88 | * 89 | * @param date the day and timezone to calculate sun positions for, time is ignored 90 | * @param lat measured from North, in degrees 91 | * @param lng measured from East, in degrees 92 | * @return phases by name, with their start/end angles and start/end times 93 | */ 94 | public static List getPhases(Calendar date, double lat, double lng) 95 | { 96 | double lw = Constants.TO_RAD * -lng; 97 | double phi = Constants.TO_RAD * lat; 98 | double d = DateUtils.toDays(date); 99 | 100 | double n = TimeUtils.getJulianCycle(d, lw); 101 | double ds = TimeUtils.getApproxTransit(0, lw, n); 102 | 103 | double M = SunUtils.getSolarMeanAnomaly(ds); 104 | double C = SunUtils.getEquationOfCenter(M); 105 | double L = SunUtils.getEclipticLongitude(M, C); 106 | 107 | double dec = PositionUtils.getDeclination(L, 0); 108 | 109 | double jnoon = TimeUtils.getSolarTransitJ(ds, M, L); 110 | 111 | List results = SunPhase.all(); 112 | 113 | TimeZone originalTimeZone = date.getTimeZone(); 114 | for(SunPhase sunPhase : results) 115 | { 116 | // not pretty, this is to have correct timezones 117 | Calendar startDate = getPhaseDate(sunPhase.getStartAngle(), sunPhase.isStartRise(), jnoon, phi, dec, lw, n, M, L); 118 | startDate.setTimeZone(originalTimeZone); 119 | sunPhase.setStartDate(startDate); 120 | Calendar endDate = getPhaseDate(sunPhase.getEndAngle(), sunPhase.isEndRise(), jnoon, phi, dec, lw, n, M, L); 121 | endDate.setTimeZone(originalTimeZone); 122 | sunPhase.setEndDate(endDate); 123 | } 124 | 125 | // not pretty, this is to have correct dates 126 | Calendar nightMorningStartDate = (Calendar) date.clone(); 127 | nightMorningStartDate.set(Calendar.HOUR_OF_DAY, 0); 128 | nightMorningStartDate.set(Calendar.MINUTE, 0); 129 | nightMorningStartDate.set(Calendar.SECOND, 0); 130 | nightMorningStartDate.set(Calendar.MILLISECOND, 0); 131 | results.get(0).setStartDate(nightMorningStartDate); 132 | 133 | Calendar nightEveningEndDate = (Calendar) date.clone(); 134 | nightEveningEndDate.set(Calendar.HOUR_OF_DAY, 23); 135 | nightEveningEndDate.set(Calendar.MINUTE, 59); 136 | nightEveningEndDate.set(Calendar.SECOND, 59); 137 | nightEveningEndDate.set(Calendar.MILLISECOND, 999); 138 | results.get(results.size()-1).setEndDate(nightEveningEndDate); 139 | 140 | return results; 141 | } 142 | 143 | private static Calendar getPhaseDate(double angle, boolean rising, double jnoon, double phi, double dec, double lw, double n, double M, double L) 144 | { 145 | // short circuit at inflection points 146 | if (angle == Constants.SunAngles.SOLAR_NOON) { 147 | return DateUtils.fromJulian(jnoon); 148 | } else if (angle == Constants.SunAngles.NADIR) { 149 | return DateUtils.fromJulian(jnoon - 0.5); 150 | } 151 | 152 | double h = angle * Constants.TO_RAD; 153 | double w = TimeUtils.getHourAngle(h, phi, dec); 154 | double a = TimeUtils.getApproxTransit(w, lw, n); 155 | 156 | // set time for the given sun altitude 157 | double jset = TimeUtils.getSolarTransitJ(a, M, L); 158 | 159 | if(rising) 160 | { 161 | double jrise = jnoon - (jset - jnoon); 162 | return DateUtils.fromJulian(jrise); 163 | } 164 | else 165 | return DateUtils.fromJulian(jset); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/models/EquatorialCoordinates.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.models; 2 | 3 | /** 4 | * Container class for the equatorial coordinate system. See 5 | * 6 | * https://en.wikipedia.org/wiki/Equatorial_coordinate_system#Geocentric_equatorial_coordinates for more info. 7 | */ 8 | public class GeocentricCoordinates 9 | { 10 | private double rightAscension, declination, distance; 11 | 12 | 13 | /** 14 | * @param rightAscension the angular distance measured eastward, in radians 15 | * @param declination in radians 16 | * @param distance in km, to the celestial body in question 17 | */ 18 | public GeocentricCoordinates(double rightAscension, double declination, double distance) 19 | { 20 | this.rightAscension = rightAscension; 21 | this.declination = declination; 22 | this.distance = distance; 23 | } 24 | 25 | /** 26 | * @return the angular distance measured eastward, in radians 27 | */ 28 | public double getRightAscension() 29 | { 30 | return rightAscension; 31 | } 32 | 33 | /** 34 | * 35 | * @param rightAscension the angular distance measured eastward, in radians 36 | */ 37 | public void setRightAscension(double rightAscension) 38 | { 39 | this.rightAscension = rightAscension; 40 | } 41 | 42 | /** 43 | * @return in radians 44 | */ 45 | public double getDeclination() 46 | { 47 | return declination; 48 | } 49 | 50 | /** 51 | * @param declination in radians 52 | */ 53 | public void setDeclination(double declination) 54 | { 55 | this.declination = declination; 56 | } 57 | 58 | /** 59 | * @return in km, to the celestial body in question 60 | */ 61 | public double getDistance() 62 | { 63 | return distance; 64 | } 65 | 66 | /** 67 | * @param distance in km, to the celestial body in question 68 | */ 69 | public void setDistance(double distance) 70 | { 71 | this.distance = distance; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/models/MoonPosition.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.models; 2 | 3 | public class MoonPosition 4 | { 5 | private double azimuth, altitude, distance; 6 | 7 | public MoonPosition(double azimuth, double altitude, double distance) 8 | { 9 | this.azimuth = azimuth; 10 | this.altitude = altitude; 11 | this.distance = distance; 12 | } 13 | 14 | public double getAzimuth() 15 | { 16 | return azimuth; 17 | } 18 | 19 | public void setAzimuth(double azimuth) 20 | { 21 | this.azimuth = azimuth; 22 | } 23 | 24 | public double getAltitude() 25 | { 26 | return altitude; 27 | } 28 | 29 | public void setAltitude(double altitude) 30 | { 31 | this.altitude = altitude; 32 | } 33 | 34 | public double getDistance() 35 | { 36 | return distance; 37 | } 38 | 39 | public void setDistance(double distance) 40 | { 41 | this.distance = distance; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/models/SunPhase.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.models; 2 | 3 | import com.florianmski.suncalc.utils.Constants.SunAngles; 4 | 5 | import java.security.InvalidParameterException; 6 | import java.util.*; 7 | 8 | /** 9 | * Phases of the Sun for a particular day 10 | */ 11 | public class SunPhase 12 | { 13 | public enum Name 14 | { 15 | NIGHT_MORNING("Night Morning"), 16 | TWILIGHT_ASTRONOMICAL_MORNING("Twilight Astronomical Morning"), 17 | TWILIGHT_NAUTICAL_MORNING("Twilight Nautical Morning"), 18 | TWILIGHT_CIVIL_MORNING("Twilight Civil Morning"), 19 | SUNRISE("Sunrise"), 20 | GOLDEN_HOUR_MORNING("Golden Hour Morning"), 21 | DAYLIGHT("Daylight"), 22 | DAYLIGHT_RISING("Daylight Rising"), 23 | DAYLIGHT_SETTING("Daylight Setting"), 24 | GOLDEN_HOUR_EVENING("Golden Hour Evening"), 25 | SUNSET("Sunset"), 26 | TWILIGHT_CIVIL_EVENING("Twilight Civil Evening"), 27 | TWILIGHT_NAUTICAL_EVENING("Twilight Nautical Evening"), 28 | TWILIGHT_ASTRONOMICAL_EVENING("Twilight Astronomical Evening"), 29 | NIGHT_EVENING("Night Evening"), 30 | NIGHT_SETTING("Night Setting"), 31 | NIGHT_RISING("Night Rising"); 32 | 33 | private final String value; 34 | Name(String value) 35 | { 36 | this.value = value; 37 | } 38 | 39 | @Override 40 | public String toString() 41 | { 42 | return this.value; 43 | } 44 | 45 | private static final Map STRING_MAPPING = new HashMap(); 46 | 47 | static 48 | { 49 | for (Name via : Name.values()) 50 | { 51 | STRING_MAPPING.put(via.toString().toUpperCase(), via); 52 | } 53 | } 54 | 55 | public static Name fromValue(String value) 56 | { 57 | return STRING_MAPPING.get(value.toUpperCase()); 58 | } 59 | 60 | } 61 | 62 | private Name name; 63 | private double startAngle, endAngle; 64 | private boolean startRise, endRise; 65 | private Calendar startDate, endDate; 66 | 67 | /** 68 | * Describes the sun's position between two angles 69 | * 70 | * @param name common name 71 | * @param startAngle zenith angle in degrees at the start. See {@link SunAngles} for details 72 | * @param startRise is the sun rising during the starting angle? (direction second derivative) 73 | * @param endAngle zenith angle in degrees at the end. See {@link SunAngles} for details 74 | * @param endRise is the sun rising during the starting angle? (direction of second derivative) 75 | */ 76 | private SunPhase(Name name, double startAngle, boolean startRise, double endAngle, boolean endRise) 77 | { 78 | this.name = name; 79 | this.startAngle = startAngle; 80 | this.startRise = startRise; 81 | this.endAngle = endAngle; 82 | this.endRise = endRise; 83 | } 84 | 85 | /** 86 | * Retrieves the interval of a sun phase 87 | * 88 | * @param name the name of the phase 89 | * @return the phase itself, containing its starting and ending zenith angles 90 | */ 91 | public static SunPhase get(Name name) 92 | { 93 | switch(name) 94 | { 95 | case SUNRISE: 96 | return new SunPhase(name, SunAngles.SUNRISE_START, true, SunAngles.SUNRISE_END, true); 97 | case GOLDEN_HOUR_MORNING: 98 | return new SunPhase(name, SunAngles.GOLDEN_HOUR_MORNING_START, true, SunAngles.GOLDEN_HOUR_MORNING_END, true); 99 | 100 | case DAYLIGHT: 101 | return new SunPhase(name, SunAngles.DAYLIGHT_START, true, SunAngles.DAYLIGHT_END, false); 102 | case DAYLIGHT_RISING: 103 | return new SunPhase(name, SunAngles.DAYLIGHT_START, true, SunAngles.SOLAR_NOON, true); 104 | case DAYLIGHT_SETTING: 105 | return new SunPhase(name, SunAngles.SOLAR_NOON, false, SunAngles.DAYLIGHT_END, false); 106 | 107 | case GOLDEN_HOUR_EVENING: 108 | return new SunPhase(name, SunAngles.GOLDEN_HOUR_EVENING_START, false, SunAngles.GOLDEN_HOUR_EVENING_END, false); 109 | case SUNSET: 110 | return new SunPhase(name, SunAngles.SUNSET_START, false, SunAngles.SUNSET_END, false); 111 | case TWILIGHT_ASTRONOMICAL_EVENING: 112 | return new SunPhase(name, SunAngles.TWILIGHT_ASTRONOMICAL_EVENING_START, false, SunAngles.TWILIGHT_ASTRONOMICAL_EVENING_END, false); 113 | case TWILIGHT_NAUTICAL_EVENING: 114 | return new SunPhase(name, SunAngles.TWILIGHT_NAUTICAL_EVENING_START, false, SunAngles.TWILIGHT_NAUTICAL_EVENING_END, false); 115 | case TWILIGHT_CIVIL_EVENING: 116 | return new SunPhase(name, SunAngles.TWILIGHT_CIVIL_EVENING_START, false, SunAngles.TWILIGHT_CIVIL_EVENING_END, false); 117 | 118 | case NIGHT_EVENING: 119 | return new SunPhase(name, SunAngles.NIGHT_START, false, SunAngles.NIGHT_END, true); 120 | case NIGHT_MORNING: 121 | return new SunPhase(name, SunAngles.NIGHT_START, false, SunAngles.NIGHT_END, true); 122 | case NIGHT_SETTING: 123 | return new SunPhase(name, SunAngles.NIGHT_START, false, SunAngles.NADIR, false); 124 | case NIGHT_RISING: 125 | return new SunPhase(name, SunAngles.NADIR, true, SunAngles.NIGHT_END, true); 126 | 127 | case TWILIGHT_CIVIL_MORNING: 128 | return new SunPhase(name, SunAngles.TWILIGHT_CIVIL_MORNING_START, true, SunAngles.TWILIGHT_CIVIL_MORNING_END, true); 129 | case TWILIGHT_NAUTICAL_MORNING: 130 | return new SunPhase(name, SunAngles.TWILIGHT_NAUTICAL_MORNING_START, true, SunAngles.TWILIGHT_NAUTICAL_MORNING_END, true); 131 | case TWILIGHT_ASTRONOMICAL_MORNING: 132 | return new SunPhase(name, SunAngles.TWILIGHT_ASTRONOMICAL_MORNING_START, true, SunAngles.TWILIGHT_ASTRONOMICAL_MORNING_END, true); 133 | default: 134 | throw new InvalidParameterException(name.value + " is not supported"); 135 | } 136 | } 137 | 138 | public static List all() 139 | { 140 | List results = new ArrayList(); 141 | for(Name n : Name.values()) 142 | results.add(get(n)); 143 | return results; 144 | } 145 | 146 | public Name getName() 147 | { 148 | return name; 149 | } 150 | 151 | /** 152 | * @return is the sun rising during the starting angle? (direction second derivative) 153 | */ 154 | public boolean isStartRise() 155 | { 156 | return startRise; 157 | } 158 | 159 | /** 160 | * @return is the sun rising during the starting angle? (direction of second derivative) 161 | */ 162 | public boolean isEndRise() 163 | { 164 | return endRise; 165 | } 166 | 167 | /** 168 | * @return zenith angle in degrees at the start of the phase. See {@link SunAngles} for details 169 | */ 170 | public double getStartAngle() 171 | { 172 | return startAngle; 173 | } 174 | 175 | /** 176 | * @return zenith angle in degrees at the end of the phase. See {@link SunAngles} for details 177 | */ 178 | public double getEndAngle() 179 | { 180 | return endAngle; 181 | } 182 | 183 | public Calendar getStartDate() 184 | { 185 | return startDate; 186 | } 187 | 188 | public void setStartDate(Calendar startDate) 189 | { 190 | this.startDate = startDate; 191 | } 192 | 193 | public Calendar getEndDate() 194 | { 195 | return endDate; 196 | } 197 | 198 | public void setEndDate(Calendar endDate) 199 | { 200 | this.endDate = endDate; 201 | } 202 | 203 | @Override 204 | public String toString() { 205 | return "SunPhase{" + 206 | "name=" + name + 207 | ", startAngle=" + startAngle + 208 | ", endAngle=" + endAngle + 209 | ", startRise=" + startRise + 210 | ", endRise=" + endRise + 211 | ", startDate=" + startDate + 212 | ", endDate=" + endDate + 213 | '}'; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/models/SunPosition.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.models; 2 | 3 | public class SunPosition 4 | { 5 | private double azimuth, altitude; 6 | 7 | public SunPosition(double azimuth, double altitude) 8 | { 9 | this.azimuth = azimuth; 10 | this.altitude = altitude; 11 | } 12 | 13 | public double getAzimuth() 14 | { 15 | return azimuth; 16 | } 17 | 18 | public void setAzimuth(double azimuth) 19 | { 20 | this.azimuth = azimuth; 21 | } 22 | 23 | public double getAltitude() 24 | { 25 | return altitude; 26 | } 27 | 28 | public void setAltitude(double altitude) 29 | { 30 | this.altitude = altitude; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/utils/Constants.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.utils; 2 | 3 | public class Constants 4 | { 5 | /** Multiplier to convert from degrees to radians */ 6 | public final static double TO_RAD = Math.PI / 180.0; 7 | 8 | /** Measure of the tilt of the earth, in radians */ 9 | public final static double EARTH_OBLIQUITY = TO_RAD * 23.4397; 10 | 11 | /** 12 | *

13 | * Solar elevation angles in degrees, i.e. the angle of the sun from the horizon. 14 | * Defined as 90 deg - solar zenith angle. 15 | *

16 | *

17 | * See 18 | * https://en.wikipedia.org/wiki/Solar_zenith_angle and 19 | * https://en.wikipedia.org/wiki/Twilight 20 | * for more info. 21 | *

22 | */ 23 | public class SunAngles 24 | { 25 | /** sunrise (top edge of the sun appears on the horizon) */ 26 | public final static double SUNRISE_START = -0.833; 27 | /** soft, warm light as sun is rising (best time for photography) */ 28 | public final static double GOLDEN_HOUR_MORNING_START = -0.3; 29 | /** daylight starts */ 30 | public final static double DAYLIGHT_START = 6.0; 31 | /** soft, warm light as sun is setting (best time for photography) */ 32 | public final static double GOLDEN_HOUR_EVENING_START = DAYLIGHT_START; 33 | /** sunset starts (bottom edge of the sun touches the horizon) */ 34 | public final static double SUNSET_START = GOLDEN_HOUR_MORNING_START; 35 | /** evening civil twilight starts (sun disappears below the horizon) */ 36 | public final static double TWILIGHT_CIVIL_EVENING_START = SUNRISE_START; 37 | /** evening nautical twilight starts (many brighter stars start appearing, horizon faintly visible) */ 38 | public final static double TWILIGHT_NAUTICAL_EVENING_START = -6.0; 39 | /** evening astronomical twilight starts (fainter stars start appearing) */ 40 | public final static double TWILIGHT_ASTRONOMICAL_EVENING_START = -12.0; 41 | /** night starts (dark enough for astronomical observations) */ 42 | public final static double NIGHT_START = -18.0; 43 | /** astronomical dawn (fainter stars start disappearing) */ 44 | public final static double TWILIGHT_ASTRONOMICAL_MORNING_START = NIGHT_START; 45 | /** nautical dawn (brighter stars start disappearing) */ 46 | public final static double TWILIGHT_NAUTICAL_MORNING_START = TWILIGHT_ASTRONOMICAL_EVENING_START; 47 | /** dawn (atmosphere begins to scatter light) */ 48 | public final static double TWILIGHT_CIVIL_MORNING_START = TWILIGHT_NAUTICAL_EVENING_START; 49 | 50 | /** sunrise ends (bottom edge of the sun touches the horizon) */ 51 | public final static double SUNRISE_END = GOLDEN_HOUR_MORNING_START; 52 | /** morning golden hour (soft light, best time for photography) ends */ 53 | public final static double GOLDEN_HOUR_MORNING_END = DAYLIGHT_START; 54 | /** daylight ends, evening golden hour starts */ 55 | public final static double DAYLIGHT_END = GOLDEN_HOUR_EVENING_START; 56 | /** sunset starts (bottom edge of the sun touches the horizon) */ 57 | public final static double GOLDEN_HOUR_EVENING_END = SUNSET_START; 58 | /** sunset ends (sun disappears below the horizon) */ 59 | public final static double SUNSET_END = TWILIGHT_CIVIL_EVENING_START; 60 | /** dusk (many brighter stars start appearing, horizon faintly visible) */ 61 | public final static double TWILIGHT_CIVIL_EVENING_END = TWILIGHT_NAUTICAL_EVENING_START; 62 | /** nautical dusk (fainter stars start appearing) */ 63 | public final static double TWILIGHT_NAUTICAL_EVENING_END = TWILIGHT_ASTRONOMICAL_EVENING_START; 64 | /** night starts, astronomical dusk (dark enough for astronomical observations) */ 65 | public final static double TWILIGHT_ASTRONOMICAL_EVENING_END = NIGHT_START; 66 | /** night ends (fainter stars start disappearing) */ 67 | public final static double NIGHT_END = TWILIGHT_ASTRONOMICAL_MORNING_START; 68 | /** morning astronomical twilight ends (brighter stars start disappearing) */ 69 | public final static double TWILIGHT_ASTRONOMICAL_MORNING_END = TWILIGHT_NAUTICAL_MORNING_START; 70 | /** morning nautical twilight ends (atmosphere begins to scatter light) */ 71 | public final static double TWILIGHT_NAUTICAL_MORNING_END = TWILIGHT_CIVIL_MORNING_START; 72 | /** morning civil twilight ends (top edge of the sun appears on the horizon) */ 73 | public final static double TWILIGHT_CIVIL_MORNING_END = SUNRISE_START; 74 | 75 | /** sun is directly overhead */ 76 | public final static double SOLAR_NOON = 90; 77 | /** sun is directly below */ 78 | public final static double NADIR = -90; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/utils/DateUtils.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.utils; 2 | 3 | import java.util.Calendar; 4 | 5 | /** 6 | * Julian date utilities. 7 | */ 8 | public class DateUtils 9 | { 10 | /** Number of millis in one day */ 11 | public final static int DAY_MS = 1000 * 60 * 60 * 24; 12 | /** The Julian day of the POSIX epoch, i.e. Jan 1, 1970 */ 13 | public final static int J1970 = 2440588; 14 | /** The Julian day of Jan 1, 2000 */ 15 | public final static int J2000 = 2451545; 16 | 17 | /** 18 | * Converts a datetime into its Julian date 19 | * 20 | * @param date datetime with timezone information 21 | * @return the Julian date 22 | */ 23 | public static double toJulian(Calendar date) 24 | { 25 | // offset to add depending on user timezone 26 | // I'm not sure about that, it's not used in the JS lib but: 27 | // - I'm in France and between 00:00 and 01:00 the sunphases were still calculated for the day before 28 | // - I've tested the app with and without offset, with seems to be more accurate regarding the azimuth 29 | long offset = date.getTimeZone().getOffset(date.getTimeInMillis()); 30 | return ((double)date.getTimeInMillis() + offset) / DAY_MS - 0.5 + J1970; 31 | } 32 | 33 | /** 34 | * Converts a Julian date to a datetime ASSUMING the timezone of the current JVM 35 | * 36 | * @param j the Julian date 37 | * @return datetime using the current timezone system's timezone 38 | */ 39 | public static Calendar fromJulian(double j) 40 | { 41 | Calendar date = Calendar.getInstance(); 42 | date.setTimeInMillis((long) (((j + 0.5 - J1970) * DAY_MS))); 43 | return date; 44 | } 45 | 46 | /** 47 | * Number of Julian days since Jan 1, 2000. Often used in astronomical calculations 48 | * 49 | * @param date the current datetime with timezone info 50 | * @return number of Julian days 51 | */ 52 | public static double toDays(Calendar date) 53 | { 54 | return toJulian(date) - J2000; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/utils/MoonUtils.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.utils; 2 | 3 | import com.florianmski.suncalc.models.GeocentricCoordinates; 4 | 5 | public class MoonUtils 6 | { 7 | public static GeocentricCoordinates getMoonCoords(double d) 8 | { 9 | // geocentric ecliptic coordinates of the moon 10 | 11 | double L = Constants.TO_RAD * (218.316 + 13.176396 * d); // ecliptic longitude 12 | double M = Constants.TO_RAD * (134.963 + 13.064993 * d); // mean anomaly 13 | double F = Constants.TO_RAD * (93.272 + 13.229350 * d); // mean distance 14 | 15 | double l = L + Constants.TO_RAD * 6.289 * Math.sin(M); // longitude 16 | double b = Constants.TO_RAD * 5.128 * Math.sin(F); // latitude 17 | double dt = 385001 - 20905 * Math.cos(M); // distance to the moon in km 18 | 19 | return new GeocentricCoordinates( 20 | PositionUtils.getRightAscension(l, b), 21 | PositionUtils.getDeclination(l, b), 22 | dt); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/utils/PositionUtils.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.utils; 2 | 3 | /** 4 | * Calculations for the position of the celestial bodies. 5 | */ 6 | public class PositionUtils 7 | { 8 | 9 | /** Sidereal time from Earth, in degrees at longitude 0 degrees at the instant of J2000 (table 5) */ 10 | private static final double THETA_0 = 280.16; 11 | 12 | /** Rate of change of sidereal time from Earth, in degrees/day (table 5) */ 13 | private static final double THETA_1 = 360.9856235; 14 | 15 | public static double getRightAscension(double l, double b) 16 | { 17 | return Math.atan2(Math.sin(l) * Math.cos(Constants.EARTH_OBLIQUITY) - Math.tan(b) * Math.sin(Constants.EARTH_OBLIQUITY), Math.cos(l)); 18 | } 19 | 20 | /** 21 | * Declination of a celestial body, with respect to an observer on the Earth (eq. 12) 22 | * 23 | * @param l ecliptic longitude of the celestial body, in radians 24 | * @param b ecliptic latitude of the sun for the celestial body 25 | * @return declination for an Earth observer, in radians 26 | */ 27 | public static double getDeclination(double l, double b) 28 | { 29 | return Math.asin(Math.sin(b) * Math.cos(Constants.EARTH_OBLIQUITY) + Math.cos(b) * Math.sin(Constants.EARTH_OBLIQUITY) * Math.sin(l)); 30 | } 31 | 32 | /** 33 | * The azimuth angle of the celestial body (eq. 25) 34 | *

35 | * Note this returns the angle taking NORTH as zero, unlike the original SunCalcJS. See https://github.com/mourner/suncalc/issues/6 for more info 37 | *

38 | * @param H the hour angle, measured in radians 39 | * @param phi latitude, in radians 40 | * @param dec declination, in radians 41 | * @return the azimuth in radians, with NORTH as zero 42 | */ 43 | public static double getAzimuth(double H, double phi, double dec) 44 | { 45 | return Math.PI + Math.atan2(Math.sin(H), Math.cos(H) * Math.sin(phi) - Math.tan(dec) * Math.cos(phi)); 46 | // return Math.atan2(Math.sin(H), Math.cos(H) * Math.sin(phi) - Math.tan(dec) * Math.cos(phi)); 47 | } 48 | 49 | /** 50 | * The altitude above the horizon of the celestial body (eq. 23) 51 | * 52 | * @param H the hour angle, measured in radians 53 | * @param phi latitude, in radians 54 | * @param dec declination, in radians 55 | * @return the altitude in radians 56 | */ 57 | public static double getAltitude(double H, double phi, double dec) 58 | { 59 | return Math.asin(Math.sin(phi) * Math.sin(dec) + Math.cos(phi) * Math.cos(dec) * Math.cos(H)); 60 | } 61 | 62 | /** 63 | * Sidereal time, from the perspective of the Earth (eq. 20) 64 | * 65 | * @param d number of Julian days since Jan 1, 2000 66 | * @param lw West longitude, in radians 67 | * @return the sidereal time, in radians 68 | */ 69 | public static double getSiderealTime(double d, double lw) 70 | { 71 | return Constants.TO_RAD * (THETA_0 + THETA_1 * d) - lw; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/utils/SunUtils.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.utils; 2 | 3 | import com.florianmski.suncalc.models.EquatorialCoordinates; 4 | 5 | /** 6 | * Calculations for the Sun. 7 | */ 8 | public class SunUtils 9 | { 10 | 11 | /** Perihelion of the Earth, measured in degrees (table 3) */ 12 | private static final double PERIHELION = 102.9372; 13 | 14 | /** Kepler's coefficient for earth orbit, 1st order in degrees (table 2) */ 15 | private static final double C1 = 1.9148; 16 | /** Kepler's coefficient for earth orbit, 2nd order in degrees (table 2) */ 17 | private static final double C2 = 0.02; 18 | /** Kepler's coefficient for earth orbit, 3rd order in degrees (table 2) */ 19 | private static final double C3 = 0.0003; 20 | 21 | /** Earth mean anomaly constant, in degrees (table 1) */ 22 | private static final double M0 = 357.5291; 23 | /** Earth mean anomaly coefficient, in degrees/julian days (table 1) */ 24 | private static final double M1 = 0.98560028; 25 | 26 | /** 27 | * 28 | * The solar mean anomaly (eq. 1) 29 | * 30 | * @param d the number of Julian days since Jan 1, 2000 31 | * @return anomaly, in radians 32 | */ 33 | public static double getSolarMeanAnomaly(double d) 34 | { 35 | return Constants.TO_RAD * (M0 + M1 * d); 36 | } 37 | 38 | /** 39 | * Equation of the Center (eq. 4), used to correct the mean anomaly 40 | * 41 | * @param M the mean anomaly, in radians 42 | * @return equation of center, in radians, to three orders 43 | */ 44 | public static double getEquationOfCenter(double M) 45 | { 46 | return Constants.TO_RAD * (C1 * Math.sin(M) + C2 * Math.sin(2 * M) + C3 * Math.sin(3 * M)); 47 | } 48 | 49 | /** 50 | * Ecliptic longitude of the Sun (eq. 8), as seen from another planet 51 | * 52 | * @param M the mean anomaly, in radians 53 | * @param C equation of center, in radians 54 | * @return in radians 55 | */ 56 | public static double getEclipticLongitude(double M, double C) 57 | { 58 | double P = Constants.TO_RAD * PERIHELION; 59 | return M + C + P + Math.PI; 60 | } 61 | 62 | /** 63 | * Helpful g for retuning the position of the sun for a given day 64 | * 65 | * @param d Julian days since Jan 1, 2000 66 | * @return equatorial coordinates of the sun 67 | */ 68 | public static EquatorialCoordinates getSunCoords(double d) 69 | { 70 | double M = getSolarMeanAnomaly(d); 71 | double C = getEquationOfCenter(M); 72 | double L = getEclipticLongitude(M, C); 73 | 74 | return new EquatorialCoordinates( 75 | PositionUtils.getRightAscension(L, 0), 76 | PositionUtils.getDeclination(L, 0)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/utils/TimeUtils.java: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc.utils; 2 | 3 | /** 4 | * Time calculations for solar transit. 5 | */ 6 | public class TimeUtils 7 | { 8 | /** 9 | * Provides the date and time of a transit of the Sun (eq. 32) 10 | */ 11 | private final static double J0 = 0.0009; 12 | 13 | /** 14 | * Describes how much the time of transit can vary because of the eccentricity of the Earth's orbit (eq. 32) 15 | */ 16 | private static final double J1 = 0.0053; 17 | 18 | /** 19 | * Calculates the Julian cycle (eq. 33) for a given day 20 | * 21 | *

22 | * This is not exactly equal to the days since Jan 1, 2000 since, 23 | * depending on longitude, it may be a different number. See 24 | * 25 | * http://users.electromagnetic.net/bu/astro/sunrise-set.php 26 | *

27 | * 28 | * @param d the day to calculate for, number Julian days since Jan 1, 2000 29 | * @param lw West longitude, in radians 30 | * @return the rounded julian cycle 31 | */ 32 | public static double getJulianCycle(double d, double lw) 33 | { 34 | return Math.round(d - J0 - lw / (2 * Math.PI)); 35 | } 36 | 37 | /** 38 | * First order estimate for approximate solar transit (eq. 34) 39 | * 40 | * @param Ht 41 | * @param lw West longitude, in radians 42 | * @param n the julian cycle, see {@link #getJulianCycle(double, double)} 43 | * @return reasonable estimate for the date and time of the transit 44 | */ 45 | public static double getApproxTransit(double Ht, double lw, double n) 46 | { 47 | return J0 + (Ht + lw) / (2 * Math.PI) + n; 48 | } 49 | 50 | /** 51 | * Better estimate for solar transit, with eccentricity and obliquity corrections (eq. 35) 52 | * 53 | * @param ds approximate first order solar transit, in Julian days 54 | * @param M Earth's mean anomaly, in radians, recalculated for the Julian day in question using eq. 33 and eq. 34 55 | * @param L ecliptic longitude of the Sun, in radians (eq. 8) 56 | * @return solar transit, in Julian days 57 | */ 58 | public static double getSolarTransitJ(double ds, double M, double L) 59 | { 60 | return DateUtils.J2000 + ds + J1 * Math.sin(M) - 0.0069 * Math.sin(2 * L); 61 | } 62 | 63 | /** 64 | * The hour angle (eq. 24). Indicates how long ago the celestial body has passed through the celestial meridian 65 | * 66 | * @param h altitude above the horizon, measured in radians. The altitude is zero at the horizon, PI/2 at the zenith 67 | * (straight above your head) and -PI/2 at nadir (straight down) 68 | * @param phi latitude from the North (beginning of section 7) 69 | * @param d declination from Earth, measured in radians 70 | * @return hour angle, in radians 71 | */ 72 | public static double getHourAngle(double h, double phi, double d) 73 | { 74 | return Math.acos((Math.sin(h) - Math.sin(phi) * Math.sin(d)) / (Math.cos(phi) * Math.cos(d))); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/florianmski/suncalc/utils/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | *

3 | * The names of the utility functions here are camel cased getters that directly 4 | * map back to the original Javascript API's function names. 5 | *

6 | *

7 | * Equations and tables referenced in this package are from the 8 | * 9 | * Astronomy Answers: Position of the Sun page 10 | *

11 | */ 12 | package com.florianmski.suncalc.utils; -------------------------------------------------------------------------------- /src/test/groovy/com/florianmski/suncalc/SunCalcSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.florianmski.suncalc 2 | 3 | import com.florianmski.suncalc.models.SunPhase 4 | import com.florianmski.suncalc.models.SunPosition 5 | import spock.lang.Shared 6 | 7 | import java.text.DateFormat 8 | import java.text.SimpleDateFormat 9 | 10 | import static com.florianmski.suncalc.models.SunPhase.Name.* 11 | 12 | /** 13 | * Unit tests for sun and moon calculations 14 | */ 15 | class SunCalcSpec extends spock.lang.Specification { 16 | 17 | // ------------------------ SUN CALCULATION TESTS ------------------------------- 18 | 19 | def "should calculate correct sun position for #location.description"() { 20 | 21 | when: 22 | SunPosition sunPosition = SunCalc.getSunPosition(location.d, location.lat, location.lon) 23 | 24 | then: 25 | sunPosition.azimuth == azimuth 26 | sunPosition.altitude == altitude 27 | 28 | where: 29 | location || azimuth | altitude 30 | TestData.PARIS || 0.18313767314092333 | -1.0930109733694826 31 | 32 | } 33 | 34 | /** 35 | *

36 | * This test was written from the eastern timezone, using suncalc.net. The reason it looks 37 | * funny is because although the region is in Paris, the times returned by the web API are always in the person's 38 | * default timezone [perhaps by design]. A screenshot is attached in 39 | *

40 | *

41 | * Depsite its quirks, this parameterized test serves as a solid regression of all calculated phases of the Java 42 | * API. 43 | *

44 | */ 45 | def "should calculate correct phase #phase for #location.description"() { 46 | 47 | when: 48 | List sunPhases = SunCalc.getPhases(location.d, location.lat, location.lon) 49 | 50 | then: "collect a single result" 51 | List filteredResults = sunPhases.findAll { it.name == phase } 52 | filteredResults.size() == 1 53 | SunPhase sunPhase = filteredResults[0] 54 | 55 | and: 56 | sunPhase.startDate.time.toString() == start_datetime 57 | sunPhase.startAngle == start_angle 58 | sunPhase.endDate.time.toString() == end_datetime 59 | sunPhase.endAngle == end_angle 60 | 61 | where: 62 | location | phase | start_datetime | start_angle | end_datetime | end_angle 63 | TestData.PARIS | NIGHT_MORNING | 'Sun Dec 01 00:00:00 EST 2013' | -18.0 | 'Sun Dec 01 00:30:22 EST 2013' | -18.0 64 | TestData.PARIS | TWILIGHT_ASTRONOMICAL_MORNING | 'Sun Dec 01 00:30:22 EST 2013' | -18.0 | 'Sun Dec 01 01:08:20 EST 2013' | -12.0 65 | TestData.PARIS | TWILIGHT_NAUTICAL_MORNING | 'Sun Dec 01 01:08:20 EST 2013' | -12.0 | 'Sun Dec 01 01:47:59 EST 2013' | -6.0 66 | TestData.PARIS | TWILIGHT_CIVIL_MORNING | 'Sun Dec 01 01:47:59 EST 2013' | -6.0 | 'Sun Dec 01 02:24:12 EST 2013' | -0.833 67 | TestData.PARIS | SUNRISE | 'Sun Dec 01 02:24:12 EST 2013' | -0.833 | 'Sun Dec 01 02:28:06 EST 2013' | -0.3 68 | TestData.PARIS | GOLDEN_HOUR_MORNING | 'Sun Dec 01 02:28:06 EST 2013' | -0.3 | 'Sun Dec 01 03:17:10 EST 2013' | 6.0 69 | TestData.PARIS | DAYLIGHT | 'Sun Dec 01 03:17:10 EST 2013' | 6.0 | 'Sun Dec 01 10:05:23 EST 2013' | 6.0 70 | TestData.PARIS | GOLDEN_HOUR_EVENING | 'Sun Dec 01 10:05:23 EST 2013' | 6.0 | 'Sun Dec 01 10:54:28 EST 2013' | -0.3 71 | TestData.PARIS | SUNSET | 'Sun Dec 01 10:54:28 EST 2013' | -0.3 | 'Sun Dec 01 10:58:21 EST 2013' | -0.833 72 | TestData.PARIS | TWILIGHT_CIVIL_EVENING | 'Sun Dec 01 10:58:21 EST 2013' | -0.833 | 'Sun Dec 01 11:34:35 EST 2013' | -6.0 73 | TestData.PARIS | TWILIGHT_NAUTICAL_EVENING | 'Sun Dec 01 11:34:35 EST 2013' | -6.0 | 'Sun Dec 01 12:14:14 EST 2013' | -12.0 74 | TestData.PARIS | TWILIGHT_ASTRONOMICAL_EVENING | 'Sun Dec 01 12:14:14 EST 2013' | -12.0 | 'Sun Dec 01 12:52:12 EST 2013' | -18.0 75 | TestData.PARIS | NIGHT_EVENING | 'Sun Dec 01 12:52:12 EST 2013' | -18.0 | 'Sun Dec 01 00:30:22 EST 2013' | -18.0 76 | 77 | } 78 | 79 | enum TestData { 80 | 81 | PARIS("Paris", 48.818684, 2.323096, [2013, 11, 1, 0, 1, 0]) 82 | 83 | TestData(String description, double latitude, double longitude, def datetime) { 84 | this.description = description 85 | this.lat = latitude 86 | this.lon = longitude 87 | this.d = datetime as GregorianCalendar 88 | } 89 | 90 | String description 91 | double lat 92 | double lon 93 | Calendar d 94 | } 95 | 96 | /** 97 | * port of original SunCalc-JS test 98 | * https://github.com/mourner/suncalc/blob/master/test.js 99 | */ 100 | def "getPosition returns azimuth and altitude for the given time and location"() { 101 | 102 | given: 103 | // zero-based month 104 | Calendar d = new GregorianCalendar(2013, 2, 5, 0, 0, 0); 105 | d.setTimeZone(TimeZone.getTimeZone("UTC")) 106 | 107 | when: 108 | SunPosition actual = SunCalc.getSunPosition(d, 50.5, 30.5) 109 | 110 | then: 111 | // azimuth angle convention is 180 off, see https://github.com/mourner/suncalc/issues/6 112 | near(actual.azimuth, -2.5003175907168385 + Math.PI) 113 | near(actual.altitude, -0.7000406838781611) 114 | } 115 | 116 | /** 117 | * port of original SunCalc-JS test 118 | * https://github.com/mourner/suncalc/blob/master/test.js 119 | */ 120 | def "getTimes returns sun phases for the given date and location: #description"() { 121 | 122 | given: 123 | double lat = 50.5 124 | double lng = 30.5 125 | DateFormat dtmFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") 126 | DateFormat timeFormat = new SimpleDateFormat("HH:mm:ss") 127 | TimeZone UTC = TimeZone.getTimeZone("UTC") 128 | dtmFormat.setTimeZone(UTC) 129 | timeFormat.setTimeZone(UTC) 130 | 131 | and: 132 | Date date = dtmFormat.parse(startdatetime) 133 | Calendar d = Calendar.getInstance(UTC) 134 | d.setTime(date) 135 | 136 | when: 137 | List results = SunCalc.getPhases(d, lat, lng) 138 | 139 | then: 140 | List actuals = results.findAll { it.name == sunPhaseName } 141 | // sanity check 142 | actuals.size == 1 143 | SunPhase actual = actuals[0] 144 | 145 | expect: 146 | timeFormat.format(actual.startDate.time) == timeFormat.format(date) 147 | 148 | where: 149 | sunPhaseName | description | startdatetime 150 | SunPhase.Name.NIGHT_RISING | 'nadir' | '2013-03-04T22:10:57Z' 151 | SunPhase.Name.TWILIGHT_ASTRONOMICAL_MORNING | 'nightEnd' | '2013-03-05T02:46:17Z' 152 | SunPhase.Name.TWILIGHT_NAUTICAL_MORNING | 'nauticalDawn' | '2013-03-05T03:24:31Z' 153 | SunPhase.Name.TWILIGHT_CIVIL_MORNING | 'dawn' | '2013-03-05T04:02:17Z' 154 | SunPhase.Name.SUNRISE | 'sunrise' | '2013-03-05T04:34:56Z' 155 | SunPhase.Name.GOLDEN_HOUR_MORNING | 'sunriseEnd' | '2013-03-05T04:38:19Z' 156 | SunPhase.Name.DAYLIGHT | 'goldenHourEnd' | '2013-03-05T05:19:01Z' 157 | SunPhase.Name.DAYLIGHT_SETTING | 'solarNoon' | '2013-03-05T10:10:57Z' 158 | SunPhase.Name.GOLDEN_HOUR_EVENING | 'goldenHour' | '2013-03-05T15:02:52Z' 159 | SunPhase.Name.SUNSET | 'sunsetStart' | '2013-03-05T15:43:34Z' 160 | SunPhase.Name.TWILIGHT_CIVIL_EVENING | 'sunset' | '2013-03-05T15:46:57Z' 161 | SunPhase.Name.TWILIGHT_NAUTICAL_EVENING | 'dusk' | '2013-03-05T16:19:36Z' 162 | SunPhase.Name.TWILIGHT_ASTRONOMICAL_EVENING | 'nauticalDusk' | '2013-03-05T16:57:22Z' 163 | SunPhase.Name.NIGHT_EVENING | 'night' | '2013-03-05T17:35:36Z' 164 | 165 | 166 | } 167 | 168 | def "calculations should return same timezone as original: #timeZone"() { 169 | 170 | given: 171 | // Mumbai, India 172 | double lat = 19.08 173 | double lng = 72.88 174 | TimeZone originalTimeZone = TimeZone.getTimeZone(timeZone) 175 | Calendar d = Calendar.getInstance(originalTimeZone) 176 | d.setTime(new Date()) 177 | 178 | when: 179 | List phases = SunCalc.getPhases(d, lat, lng) 180 | 181 | then: 182 | phases.forEach { 183 | assert it.startDate.timeZone == originalTimeZone 184 | assert it.endDate.timeZone == originalTimeZone 185 | } 186 | 187 | where: 188 | // have at least two time zones for the test, in case one of them is the tester's native one 189 | timeZone | _ 190 | 'GMT+5:50' | _ 191 | 'GMT-8:00' | _ 192 | } 193 | 194 | // ----------------------- MOON CALCULATION TESTS ------------------------------- 195 | 196 | @Shared 197 | double MOON_CALC_ACCURACY = 0.05 198 | 199 | def "calculate moon phase illumination for #description using Naval Observatory Data"() { 200 | given: 201 | Calendar d = new GregorianCalendar(year, month, day, hour, min, 0) 202 | d.setTimeZone(TimeZone.getTimeZone("UTC")) 203 | 204 | expect: 205 | near(SunCalc.getMoonFraction(d), expectedIllumination, MOON_CALC_ACCURACY) 206 | 207 | // expected data from US Naval Observatory 208 | // http://aa.usno.navy.mil/cgi-bin/aa_phases.pl?year=2013&month=11&day=1&nump=50&format=p 209 | where: 210 | year | month | day | hour | min | expectedIllumination | description 211 | 2013 | Calendar.NOVEMBER | 3 | 0 | 22 | 0.00 | 'new moon on Nov 3, 2013' 212 | 2013 | Calendar.NOVEMBER | 10 | 15 | 12 | 0.50 | 'first quarter on Nov 10, 2013' 213 | 2013 | Calendar.NOVEMBER | 17 | 9 | 28 | 1.00 | 'full moon on Nov 17, 2013' 214 | 2013 | Calendar.NOVEMBER | 25 | 13 | 48 | 0.50 | 'last quarter on Nov 25, 2013' 215 | 216 | } 217 | 218 | private static double DEFAULT_MARGIN = Math.pow(10, -15) 219 | 220 | private static boolean near(double val1, double val2) { 221 | return near(val1, val2, DEFAULT_MARGIN) 222 | } 223 | 224 | private static boolean near(double val1, double val2, double margin) { 225 | return Math.abs(val1 - val2) < margin 226 | } 227 | 228 | 229 | } 230 | --------------------------------------------------------------------------------