1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Based on sunrisesunsetlib-java:
5 * Copyright 2008-2009 Mike Reedell / LuckyCatLabs.
6 *
7 * Original project and source can be found at:
8 * http://mikereedell.github.com/sunrisesunsetlib-java/
9 *
10 * Licensed under the Apache License, Version 2.0 (the "License");
11 * you may not use this file except in compliance with the License.
12 * You may obtain a copy of the License at
13 *
14 *      http://www.apache.org/licenses/LICENSE-2.0
15 *
16 * Unless required by applicable law or agreed to in writing, software
17 * distributed under the License is distributed on an "AS IS" BASIS,
18 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 * See the License for the specific language governing permissions and
20 * limitations under the License.
21 */
22
23package com.android.wallpaper.grass;
24
25import java.util.Calendar;
26import java.util.TimeZone;
27
28import android.location.Location;
29
30class SunCalculator {
31    /** Astronomical sunrise/set is when the sun is 18 degrees below the horizon. */
32    static final double ZENITH_ASTRONOMICAL = 108;
33
34    /** Nautical sunrise/set is when the sun is 12 degrees below the horizon. */
35    static final double ZENITH_NAUTICAL = 102;
36
37    /** Civil sunrise/set (dawn/dusk) is when the sun is 6 degrees below the horizon. */
38    static final double ZENITH_CIVIL = 96;
39
40    /** Official sunrise/set is when the sun is 50' below the horizon. */
41    static final double ZENITH_OFFICIAL = 90.8333;
42
43    private Location mLocation;
44    private TimeZone mTimeZone;
45
46    SunCalculator(Location location, String timeZoneIdentifier) {
47        mLocation = location;
48        mTimeZone = TimeZone.getTimeZone(timeZoneIdentifier);
49    }
50
51    public void setLocation(Location location) {
52        mLocation = location;
53    }
54
55    /**
56     * Computes the sunrise time for the given zenith at the given date.
57     *
58     * @param solarZenith <code>Zenith</code> enum corresponding to the type
59     *        of sunrise to compute.
60     * @param date <code>Calendar</code> object representing the date to
61     *        compute the sunrise for.
62     * @return the sunrise time
63     */
64    public double computeSunriseTime(double solarZenith, Calendar date) {
65        return computeSolarEventTime(solarZenith, date, true);
66    }
67
68    /**
69     * Computes the sunset time for the given zenith at the given date.
70     *
71     * @param solarZenith <code>Zenith</code> enum corresponding to the type of
72     *        sunset to compute.
73     * @param date <code>Calendar</code> object representing the date to compute
74     *        the sunset for.
75     * @return the sunset time
76     */
77    public double computeSunsetTime(double solarZenith, Calendar date) {
78        return computeSolarEventTime(solarZenith, date, false);
79    }
80
81    public static int timeToHours(double time) {
82        int hour = (int) Math.floor(time);
83        int minute = (int) Math.round((time - hour) * 60);
84        if (minute == 60) {
85            hour++;
86        }
87        return hour;
88    }
89
90    public static int timeToMinutes(double time) {
91        int hour = (int) Math.floor(time);
92        int minute = (int) Math.round((time - hour) * 60);
93        if (minute == 60) {
94            minute = 0;
95        }
96        return minute;
97    }
98
99    public static float timeToDayFraction(double time) {
100        int hour = (int) Math.floor(time);
101        int minute = (int) Math.round((time - hour) * 60);
102        if (minute == 60) {
103            minute = 0;
104            hour++;
105        }
106        return (hour * 60 + minute) / 1440.0f;
107    }
108
109    public static String timeToString(double time) {
110        StringBuffer buffer = new StringBuffer();
111        int hour = (int) Math.floor(time);
112        int minute = (int) Math.round((time - hour) * 60);
113        if (minute == 60) {
114            minute = 0;
115            hour++;
116        }
117        buffer.append(hour).append(':').append(minute < 10 ? "0" + minute : minute);
118        return buffer.toString();
119    }
120
121    private double computeSolarEventTime(double solarZenith, Calendar date, boolean isSunrise) {
122        date.setTimeZone(mTimeZone);
123        double longitudeHour = getLongitudeHour(date, isSunrise);
124        double meanAnomaly = getMeanAnomaly(longitudeHour);
125        double sunTrueLong = getSunTrueLongitude(meanAnomaly);
126        double cosineSunLocalHour = getCosineSunLocalHour(sunTrueLong, solarZenith);
127        if ((cosineSunLocalHour < -1.0) || (cosineSunLocalHour > 1.0)) {
128            return 0;
129        }
130
131        double sunLocalHour = getSunLocalHour(cosineSunLocalHour, isSunrise);
132        double localMeanTime = getLocalMeanTime(sunTrueLong, longitudeHour, sunLocalHour);
133        return getLocalTime(localMeanTime, date);
134    }
135
136    /**
137     * Computes the base longitude hour, lngHour in the algorithm.
138     *
139     * @return the longitude of the location of the solar event divided by 15 (deg/hour), in
140     *         <code>double</code> form.
141     */
142    private double getBaseLongitudeHour() {
143        return mLocation.getLongitude() / 15.0;
144    }
145
146    /**
147     * Computes the longitude time, t in the algorithm.
148     *
149     * @return longitudinal time in <code>double</code> form.
150     */
151    private double getLongitudeHour(Calendar date, Boolean isSunrise) {
152        int offset = 18;
153        if (isSunrise) {
154            offset = 6;
155        }
156        double dividend = offset - getBaseLongitudeHour();
157        double addend = dividend / 24.0;
158        return getDayOfYear(date) + addend;
159    }
160
161    /**
162     * Computes the mean anomaly of the Sun, M in the algorithm.
163     *
164     * @return the suns mean anomaly, M, in <code>double</code> form.
165     */
166    private static double getMeanAnomaly(double longitudeHour) {
167        return 0.9856 * longitudeHour - 3.289;
168    }
169
170    /**
171     * Computes the true longitude of the sun, L in the algorithm, at the given
172     * location, adjusted to fit in the range [0-360].
173     *
174     * @param meanAnomaly the suns mean anomaly.
175     * @return the suns true longitude, in <code>double</code> form.
176     */
177    private static double getSunTrueLongitude(double meanAnomaly) {
178        final double meanRadians = Math.toRadians(meanAnomaly);
179        double sinMeanAnomaly = Math.sin(meanRadians);
180        double sinDoubleMeanAnomaly = Math.sin((meanRadians * 2.0));
181
182        double firstPart = meanAnomaly + sinMeanAnomaly * 1.916;
183        double secondPart = sinDoubleMeanAnomaly * 0.020 + 282.634;
184        double trueLongitude = firstPart + secondPart;
185
186        if (trueLongitude > 360) {
187            trueLongitude = trueLongitude - 360.0;
188        }
189        return trueLongitude;
190    }
191
192    /**
193     * Computes the suns right ascension, RA in the algorithm, adjusting for
194     * the quadrant of L and turning it into degree-hours. Will be in the
195     * range [0,360].
196     *
197     * @param sunTrueLong Suns true longitude, in <code>double</code>
198     * @return suns right ascension in degree-hours, in <code>double</code> form.
199     */
200    private static double getRightAscension(double sunTrueLong) {
201        double tanL = Math.tan(Math.toRadians(sunTrueLong));
202
203        double innerParens = Math.toDegrees(tanL) * 0.91764;
204        double rightAscension = Math.atan(Math.toRadians(innerParens));
205        rightAscension = Math.toDegrees(rightAscension);
206
207        if (rightAscension < 0.0) {
208            rightAscension = rightAscension + 360.0;
209        } else if (rightAscension > 360.0) {
210            rightAscension = rightAscension - 360.0;
211        }
212
213        double ninety = 90.0;
214        double longitudeQuadrant = (int) (sunTrueLong / ninety);
215        longitudeQuadrant = longitudeQuadrant * ninety;
216
217        double rightAscensionQuadrant = (int) (rightAscension / ninety);
218        rightAscensionQuadrant = rightAscensionQuadrant * ninety;
219
220        double augend = longitudeQuadrant - rightAscensionQuadrant;
221        return (rightAscension + augend) / 15.0;
222    }
223
224    private double getCosineSunLocalHour(double sunTrueLong, double zenith) {
225        double sinSunDeclination = getSinOfSunDeclination(sunTrueLong);
226        double cosineSunDeclination = getCosineOfSunDeclination(sinSunDeclination);
227
228        final double zenithInRads = Math.toRadians(zenith);
229        final double latitude = Math.toRadians(mLocation.getLatitude());
230
231        double cosineZenith = Math.cos(zenithInRads);
232        double sinLatitude = Math.sin(latitude);
233        double cosLatitude = Math.cos(latitude);
234
235        double sinDeclinationTimesSinLat = sinSunDeclination * sinLatitude;
236        double dividend = cosineZenith - sinDeclinationTimesSinLat;
237        double divisor = cosineSunDeclination * cosLatitude;
238
239        return dividend / divisor;
240    }
241
242    private static double getSinOfSunDeclination(double sunTrueLong) {
243        double sinTrueLongitude = Math.sin(Math.toRadians(sunTrueLong));
244        return sinTrueLongitude * 0.39782;
245    }
246
247    private static double getCosineOfSunDeclination(double sinSunDeclination) {
248        double arcSinOfSinDeclination = Math.asin(sinSunDeclination);
249        return Math.cos(arcSinOfSinDeclination);
250    }
251
252    private static double getSunLocalHour(double cosineSunLocalHour, Boolean isSunrise) {
253        double arcCosineOfCosineHourAngle = Math.acos(cosineSunLocalHour);
254        double localHour = Math.toDegrees(arcCosineOfCosineHourAngle);
255        if (isSunrise) {
256            localHour = 360.0 - localHour;
257        }
258        return localHour / 15.0;
259    }
260
261    private static double getLocalMeanTime(double sunTrueLong, double longitudeHour,
262            double sunLocalHour) {
263
264        double rightAscension = getRightAscension(sunTrueLong);
265        double innerParens = longitudeHour * 0.06571;
266        double localMeanTime = sunLocalHour + rightAscension - innerParens;
267        localMeanTime = localMeanTime - 6.622;
268
269        if (localMeanTime < 0.0) {
270            localMeanTime = localMeanTime + 24.0;
271        } else if (localMeanTime > 24.0) {
272            localMeanTime = localMeanTime - 24.0;
273        }
274        return localMeanTime;
275    }
276
277    private double getLocalTime(double localMeanTime, Calendar date) {
278        double utcTime = localMeanTime - getBaseLongitudeHour();
279        double utcOffSet = getUTCOffSet(date);
280        double utcOffSetTime = utcTime + utcOffSet;
281        return adjustForDST(utcOffSetTime, date);
282    }
283
284    private double adjustForDST(double localMeanTime, Calendar date) {
285        double localTime = localMeanTime;
286        if (mTimeZone.inDaylightTime(date.getTime())) {
287            localTime++;
288        }
289        if (localTime > 24.0) {
290            localTime = localTime - 24.0;
291        }
292        return localTime;
293    }
294
295    /**
296     * ****** UTILITY METHODS (Should probably go somewhere else. *****************
297     */
298
299    private static double getDayOfYear(Calendar date) {
300        return date.get(Calendar.DAY_OF_YEAR);
301    }
302
303    private static double getUTCOffSet(Calendar date) {
304        int offSetInMillis = date.get(Calendar.ZONE_OFFSET);
305        return offSetInMillis / 3600000;
306    }
307}