001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.util;
029
030// J2SE dependencies
031import java.awt.geom.Point2D;
032import java.text.DateFormat;
033import java.text.ParseException;
034import java.util.Date;
035import java.util.TimeZone;
036import visad.Data;
037import visad.FlatField;
038
039
040/**
041 * Calcule la position du soleil relativement à la position de l'observateur.
042 * Cette classe reçoit en entrés les coordonnées spatio-temporelles de
043 * l'observateur, soit:
044 *
045 * <TABLE border='0'><TR><TD valign="top">
046 * &nbsp;<BR>
047 * <UL>
048 *   <LI>La longitude (en degrées) de l'observateur;</LI>
049 *   <LI>La latitude (en degrées) de l'observateur;</LI>
050 *   <LI>La date et heure en heure universelle (GMT).</LI>
051 * </UL>
052 *
053 * La position du soleil calculée en sortie comprend les valeurs suivantes:
054 *
055 * <UL>
056 *   <LI>L'azimuth du soleil (en degrés dans le sens des aiguilles d'une montre depuis le nord);</LI>
057 *   <LI>L'élévation du soleil (en degrés par rapport a l'horizon).</LI>
058 * </UL>
059 * </TD>
060 *
061 * <TD><img src="doc-files/CelestialSphere.png"></TD>
062 * </TR></TABLE>
063 *
064 * Les algorithmes utilisés dans cette classe sont des adaptations des algorithmes
065 * en javascript écrit par le "National Oceanic and Atmospheric Administration,
066 * Surface Radiation Research Branch". L'application original est le
067 *
068 * <a href="http://www.srrb.noaa.gov/highlights/sunrise/azel.html">Solar Position Calculator</a>.
069 *
070 * <p>
071 * The approximations used in these programs are very good for years between
072 * 1800 and 2100. Results should still be sufficiently accurate for the range
073 * from -1000 to 3000. Outside of this range, results will be given, but the
074 * potential for error is higher.
075 *
076 * @since 2.1
077 *
078 *
079 * @source $URL$
080 * @version $Id$
081 * @author Remi Eve
082 * @author Martin Desruisseaux (IRD)
083 */
084public class SunRelativePosition {
085    /**
086     * Number of milliseconds in a day.
087     */
088    private static final int DAY_MILLIS = 24*60*60*1000;
089
090    /**
091     * Valeur affectée lorsque un resultat n'est pas calculable du
092     * fait de la nuit. Cette valeur concerne les valeurs de sorties
093     * {@link #elevation} et {@link #azimuth}.
094     */
095    private static final double DARK = Double.NaN;
096
097    /**
098     * {@linkplain #getElevation Elevation angle} of astronomical twilight, in degrees.
099     * Astronomical twilight is the time of morning or evening when the sun is 18° below
100     * the horizon (solar elevation angle of -18°).
101     */
102    public static final double ASTRONOMICAL_TWILIGHT = -18;
103
104    /**
105     * {@linkplain #getElevation Elevation angle} of nautical twilight, in degrees.
106     * Nautical twilight is the time of morning or evening when the sun is 12° below
107     * the horizon (solar elevation angle of -12°).
108     */
109    public static final double NAUTICAL_TWILIGHT = -12;
110
111    /**
112     * {@linkplain #getElevation Elevation angle} of civil twilight, in degrees. Civil
113     * twilight is the time of morning or evening when the sun is 6° below the horizon
114     * (solar elevation angle of -6°).
115     */
116    public static final double CIVIL_TWILIGHT = -6;
117
118    /**
119     * Sun's {@linkplain #getElevation elevation angle} at twilight, in degrees.
120     * Common values are defined for the
121     * {@linkplain #ASTRONOMICAL_TWILIGHT astronomical twilight} (-18°),
122     * {@linkplain #NAUTICAL_TWILIGHT nautical twilight} (-12°) and
123     * {@linkplain #CIVIL_TWILIGHT civil twilight} (-6°).
124     * If no twilight are defined, then this value is {@linkplain Double#NaN NaN}.
125     * The {@linkplain #getElevation elevation} and {@linkplain #getAzimuth azimuth} are
126     * set to {@linkplain Double#NaN NaN} when the sun elevation is below the twilight
127     * value (i.e. during night). The default value is {@link #CIVIL_TWILIGHT}.
128     */
129    private double twilight = CIVIL_TWILIGHT;
130
131    /**
132     * Heure à laquelle le soleil est au plus haut dans la journée en millisecondes
133     * écoulées depuis le 1er janvier 1970.
134     */
135    private long noonTime;
136    
137    /**
138     * Azimuth du soleil, en degrés dans le sens des
139     * aiguilles d'une montre depuis le nord.
140     */
141    private double azimuth;
142
143    /**
144     * Elévation du soleil, en degrés par rapport a l'horizon.
145     */
146    private double elevation;
147
148    /**
149     * Geographic coordinate where current elevation and azimuth were computed.
150     * Value are in degrees of longitude or latitude.
151     */
152    private double latitude, longitude;
153
154    /**
155     * Date and time when the current elevation and azimuth were computed.
156     * Value is in milliseconds ellapsed since midnight UTC, January 1st, 1970.
157     */
158    private long time = System.currentTimeMillis();
159    
160    private double julianDay = Double.NaN;
161    
162    private double jddate;
163    
164    double solarDec;
165    double eqTime;
166    
167    private boolean timeUpdated = false;
168
169    /**
170     * {@code true} is the elevation and azimuth are computed, or {@code false}
171     * if they need to be computed.  This flag is set to {@code false} when the date
172     * and/or the coordinate change.
173     */
174    private boolean updated;
175
176    /**
177     * Calculate the equation of center for the sun. This value is a correction
178     * to add to the geometric mean longitude in order to get the "true" longitude
179     * of the sun.
180     *
181     * @param  t number of Julian centuries since J2000.
182     * @return Equation of center in degrees.
183     */
184    private static double sunEquationOfCenter(final double t) {
185        final double m = Math.toRadians(sunGeometricMeanAnomaly(t));
186        return Math.sin(1*m) * (1.914602 - t*(0.004817 + 0.000014*t)) +
187               Math.sin(2*m) * (0.019993 - t*(0.000101             )) +
188               Math.sin(3*m) * (0.000289);
189    }
190
191    /**
192     * Calculate the Geometric Mean Longitude of the Sun.
193     * This value is close to 0° at the spring equinox,
194     * 90° at the summer solstice, 180° at the automne equinox
195     * and 270° at the winter solstice.
196     *
197     * @param  t number of Julian centuries since J2000.
198     * @return Geometric Mean Longitude of the Sun in degrees,
199     *         in the range 0° (inclusive) to 360° (exclusive).
200     */
201    private static double sunGeometricMeanLongitude(final double t) {
202        double L0 = 280.46646 + t*(36000.76983 + 0.0003032*t);
203        L0 = L0 - 360*Math.floor(L0/360);
204        return L0;
205    }
206
207    /**
208     * Calculate the true longitude of the sun. This the geometric mean
209     * longitude plus a correction factor ("equation of center" for the
210     * sun).
211     *
212     * @param  t number of Julian centuries since J2000.
213     * @return Sun's true longitude in degrees.
214     */
215    private static double sunTrueLongitude(final double t) {
216        return sunGeometricMeanLongitude(t) + sunEquationOfCenter(t);
217    }
218
219    /**
220     * Calculate the apparent longitude of the sun.
221     *
222     * @param  t number of Julian centuries since J2000.
223     * @return Sun's apparent longitude in degrees.
224     */
225    private static double sunApparentLongitude(final double t) {
226        final double omega = Math.toRadians(125.04 - 1934.136 * t);
227        return sunTrueLongitude(t) - 0.00569 - 0.00478 * Math.sin(omega);
228    }
229
230    /**
231     * Calculate the Geometric Mean Anomaly of the Sun.
232     *
233     * @param  t number of Julian centuries since J2000.
234     * @return Geometric Mean Anomaly of the Sun in degrees.
235     */
236    private static double sunGeometricMeanAnomaly(final double t) {
237        return 357.52911 + t * (35999.05029 - 0.0001537*t);
238    }
239
240    /**
241     * Calculate the true anamoly of the sun.
242     *
243     * @param  t number of Julian centuries since J2000.
244     * @return Sun's true anamoly in degrees.
245     */
246    private static double sunTrueAnomaly(final double t) {
247        return sunGeometricMeanAnomaly(t) + sunEquationOfCenter(t);
248    }
249
250    /**
251     * Calculate the eccentricity of earth's orbit. This is the ratio
252     * {@code (a-b)/a} where <var>a</var> is the semi-major axis
253     * length and <var>b</var> is the semi-minor axis length.   Value
254     * is 0 for a circular orbit.
255     *
256     * @param  t number of Julian centuries since J2000.
257     * @return The unitless eccentricity.
258     */
259    private static double eccentricityEarthOrbit(final double t) {
260        return 0.016708634 - t*(0.000042037 + 0.0000001267*t);
261    }
262
263    /**
264     * Calculate the distance to the sun in Astronomical Units (AU).
265     *
266     * @param  t number of Julian centuries since J2000.
267     * @return Sun radius vector in AUs.
268     */
269    private static double sunRadiusVector(final double t) {
270        final double v = Math.toRadians(sunTrueAnomaly(t));
271        final double e = eccentricityEarthOrbit(t);
272        return (1.000001018 * (1 - e*e)) / (1 + e*Math.cos(v));
273    }
274
275    /**
276     * Calculate the mean obliquity of the ecliptic.
277     *
278     * @param  t number of Julian centuries since J2000.
279     * @return Mean obliquity in degrees.
280     */
281    private static double meanObliquityOfEcliptic(final double t) {
282        final double seconds = 21.448 - t*(46.8150 + t*(0.00059 - t*(0.001813)));
283        return 23.0 + (26.0 + (seconds/60.0))/60.0;
284    }
285
286
287    /**
288     * Calculate the corrected obliquity of the ecliptic.
289     *
290     * @param  t number of Julian centuries since J2000.
291     * @return Corrected obliquity in degrees.
292     */
293    private static double obliquityCorrected(final double t) {
294        final double e0 = meanObliquityOfEcliptic(t);
295        final double omega = Math.toRadians(125.04 - 1934.136*t);
296        return e0 + 0.00256 * Math.cos(omega);
297    }
298
299    /**
300     * Calculate the right ascension of the sun. Similar to the angular system
301     * used to define latitude and longitude on Earth's surface, right ascension
302     * is roughly analogous to longitude, and defines an angular offset from the
303     * meridian of the vernal equinox.
304     *
305     * <P align="center"><img src="doc-files/CelestialSphere.png"></P>
306     *
307     * @param t number of Julian centuries since J2000.
308     * @return Sun's right ascension in degrees.
309     */
310    private static double sunRightAscension(final double t) {
311        final double e = Math.toRadians(obliquityCorrected(t));
312        final double b = Math.toRadians(sunApparentLongitude(t));
313        final double y = Math.sin(b) * Math.cos(e);
314        final double x = Math.cos(b);
315        final double alpha = Math.atan2(y, x);
316        return Math.toDegrees(alpha);
317    }
318
319    /**
320     * Calculate the declination of the sun. Declination is analogous to latitude
321     * on Earth's surface, and measures an angular displacement north or south
322     * from the projection of Earth's equator on the celestial sphere to the
323     * location of a celestial body.
324     *
325     * @param t number of Julian centuries since J2000.
326     * @return Sun's declination in degrees.
327     */
328    private static double sunDeclination(final double t) {
329        final double e = Math.toRadians(obliquityCorrected(t));
330        final double b = Math.toRadians(sunApparentLongitude(t));
331        final double sint = Math.sin(e) * Math.sin(b);
332        final double theta = Math.asin(sint);
333        return Math.toDegrees(theta);
334    }
335
336   /**
337    * Calculate the Universal Coordinated Time (UTC) of solar noon for the given day
338    * at the given location on earth.
339    *
340    * @param  lon       longitude of observer in degrees.                               
341    * @param  eqTime    Equation of time.
342    * @return Time in minutes from beginnning of day in UTC.
343    */
344    private static double solarNoonTime(final double lon, final double eqTime) { 
345        return 720.0 + (lon * 4.0) - eqTime;
346    }
347
348    /**
349     * Calculate the difference between true solar time and mean. The "equation
350     * of time" is a term accounting for changes in the time of solar noon for
351     * a given location over the course of a year. Earth's elliptical orbit and
352     * Kepler's law of equal areas in equal times are the culprits behind this
353     * phenomenon. See the
354     * <A HREF="http://www.analemma.com/Pages/framesPage.html">Analemma page</A>.
355     * Below is a plot of the equation of time versus the day of the year.
356     *
357     * <P align="center"><img src="doc-files/EquationOfTime.png"></P>
358     *
359     * @param  t number of Julian centuries since J2000.
360     * @return Equation of time in minutes of time.
361     */
362    private static double equationOfTime(final double t) {
363        double eps = Math.toRadians(obliquityCorrected(t));
364        double l0  = Math.toRadians(sunGeometricMeanLongitude(t));
365        double m   = Math.toRadians(sunGeometricMeanAnomaly(t));
366        double e   = eccentricityEarthOrbit(t);
367        double y   = Math.tan(eps/2);
368        y *= y;
369
370        double sin2l0 = Math.sin(2 * l0);
371        double cos2l0 = Math.cos(2 * l0);
372        double sin4l0 = Math.sin(4 * l0);
373        double sin1m  = Math.sin(m);
374        double sin2m  = Math.sin(2 * m);
375
376        double etime = y*sin2l0 - 2*e*sin1m + 4*e*y*sin1m*cos2l0
377                       - 0.5*y*y*sin4l0 - 1.25*e*e*sin2m;
378
379        return Math.toDegrees(etime)*4.0;
380    }
381
382    /**
383     * Computes the refraction correction angle.
384     * The effects of the atmosphere vary with atmospheric pressure, humidity
385     * and other variables. Therefore the calculation is approximate. Errors
386     * can be expected to increase the further away you are from the equator,
387     * because the sun rises and sets at a very shallow angle. Small variations
388     * in the atmosphere can have a larger effect.
389     *
390     * @param  zenith The sun zenith angle in degrees.
391     * @return The refraction correction in degrees.
392     */
393    private static double refractionCorrection(final double zenith) {
394        final double exoatmElevation = 90 - zenith;
395        if (exoatmElevation > 85) {
396            return 0;
397        }
398        final double refractionCorrection; // In minute of degrees
399        final double te = Math.tan(Math.toRadians(exoatmElevation));
400        if (exoatmElevation > 5.0) {
401            refractionCorrection = 58.1/te - 0.07/(te*te*te) + 0.000086/(te*te*te*te*te);
402        } else {
403            if (exoatmElevation > -0.575) {
404                refractionCorrection =  1735.0 + exoatmElevation *
405                                       (-518.2 + exoatmElevation *
406                                       ( 103.4 + exoatmElevation *
407                                       (-12.79 + exoatmElevation *
408                                         0.711)));
409            } else {
410                refractionCorrection = -20.774 / te;
411            }
412        }
413        return refractionCorrection / 3600;
414    }
415
416    /**
417     * Constructs a sun relative position calculator.
418     */
419    public SunRelativePosition() {
420    }
421
422    /**
423     * Constructs a sun relative position calculator with the specified value
424     * for the {@linkplain #setTwilight sun elevation at twilight}.
425     *
426     * @param twilight The new sun elevation at twilight, or {@link Double#NaN}
427     *                 if no twilight value should be taken in account.
428     * @throws IllegalArgumentException if the twilight value is illegal.
429     */
430    public SunRelativePosition(final double twilight) throws IllegalArgumentException {
431        setTwilight(twilight);
432    }
433
434    /**
435     * Calculates solar position for the current date, time and location.
436     * Results are reported in azimuth and elevation in degrees.
437     */
438    private void compute() {
439        double latitude  = this.latitude;
440        double longitude = this.longitude;
441
442        // NOAA convention use positive longitude west, and negative east.
443        // Inverse the sign, in order to be closer to OpenGIS convention.
444        longitude = -longitude;
445
446        // Compute: 1) Julian day (days ellapsed since January 1, 4723 BC at 12:00 GMT).
447        //          2) Time as the centuries ellapsed since January 1, 2000 at 12:00 GMT.
448        
449        if (!timeUpdated) {
450           julianDay = Calendar.julianDay(this.time);
451           final double time      = (julianDay-2451545)/36525;
452
453           solarDec = sunDeclination(time);
454           eqTime   = equationOfTime(time);
455           timeUpdated = true;
456        }
457        
458        
459        this.noonTime   = Math.round(solarNoonTime(longitude, eqTime) * (60*1000)) +
460                          (this.time/DAY_MILLIS)*DAY_MILLIS;
461
462        // Formula below use longitude in degrees. Steps are:
463        //   1) Extract the time part of the date, in minutes.
464        //   2) Apply a correction for longitude and equation of time.
465        //   3) Clamp in a 24 hours range (24 hours == 1440 minutes).
466        double trueSolarTime = ((julianDay+0.5) - Math.floor(julianDay+0.5)) * 1440;
467        trueSolarTime += (eqTime - 4.0*longitude); // Correction in minutes.
468        trueSolarTime -= 1440*Math.floor(trueSolarTime/1440);
469
470        // Convert all angles to radians.  From this point until
471        // the end of this method, local variables are always in
472        // radians. Output variables ('azimuth' and 'elevation')
473        // will still computed in degrees.
474        longitude = Math.toRadians(longitude);
475        latitude  = Math.toRadians(latitude );
476        solarDec  = Math.toRadians(solarDec );
477
478        double csz = Math.sin(latitude) *
479                     Math.sin(solarDec) +
480                     Math.cos(latitude) *
481                     Math.cos(solarDec) *
482                     Math.cos(Math.toRadians(trueSolarTime/4 - 180));
483        if (csz > +1) csz = +1;
484        if (csz < -1) csz = -1;
485
486        final double zenith  = Math.acos(csz);
487        final double azDenom = Math.cos(latitude) * Math.sin(zenith);
488
489        //////////////////////////////////////////
490        ////    Compute azimuth in degrees    ////
491        //////////////////////////////////////////
492        if (Math.abs(azDenom) > 0.001) {
493            double azRad = ((Math.sin(latitude)*Math.cos(zenith)) - Math.sin(solarDec)) / azDenom;
494            if (azRad > +1) azRad = +1;
495            if (azRad < -1) azRad = -1;
496
497            azimuth = 180 - Math.toDegrees(Math.acos(azRad));
498            if (trueSolarTime > 720) { // 720 minutes == 12 hours
499                azimuth = -azimuth;
500            }
501        } else {
502            azimuth = (latitude>0) ? 180 : 0;
503        }
504        azimuth -= 360*Math.floor(azimuth/360);
505
506        ////////////////////////////////////////////
507        ////    Compute elevation in degrees    ////
508        ////////////////////////////////////////////
509        final double refractionCorrection = refractionCorrection(Math.toDegrees(zenith));
510        final double solarZen = Math.toDegrees(zenith) - refractionCorrection;
511
512        elevation = 90 - solarZen;
513        if (elevation < twilight) {
514            // do not report azimuth & elevation after twilight
515            azimuth   = DARK;
516            elevation = DARK;
517        }
518        updated = true;
519    }
520
521    /**
522     * Set the geographic coordinate where to compute the {@linkplain #getElevation elevation}
523     * and {@linkplain #getAzimuth azimuth}.
524     *
525     * @param longitude The longitude in degrees. Positive values are East; negative values are West.
526     * @param latitude  The latitude in degrees. Positive values are North, negative values are South.
527     */
528    public void setCoordinate(double longitude, double latitude) {
529        if (latitude > +89.8) latitude = +89.8;
530        if (latitude < -89.8) latitude = -89.8;
531        if (latitude != this.latitude || longitude != this.longitude) {
532            this.latitude  = latitude;
533            this.longitude = longitude;
534            this.updated   = false;
535        }
536    }
537
538    /**
539     * Set the geographic coordinate where to compute the {@linkplain #getElevation elevation}
540     * and {@linkplain #getAzimuth azimuth}.
541     *
542     * @param point The geographic coordinates in degrees of longitude and latitude.
543     */
544    public void setCoordinate(final Point2D point) {
545        setCoordinate(point.getX(), point.getY());
546    }
547
548    /**
549     * Returns the coordinate used for {@linkplain #getElevation elevation} and
550     * {@linkplain #getAzimuth azimuth} computation. This is the coordinate
551     * specified during the last call to a {@link #setCoordinate(double,double)
552     * setCoordinate(...)} method.
553     */
554    public Point2D getCoordinate() {
555        return new Point2D.Double(longitude, latitude);
556    }
557
558    /**
559     * Set the date and time when to compute the {@linkplain #getElevation elevation}
560     * and {@linkplain #getAzimuth azimuth}.
561     *
562     * @param date The date and time.
563     */
564    public void setDate(final Date date) {
565        final long time = date.getTime();
566        if (time != this.time) {
567            this.time = time;
568            this.updated = false;
569            this.timeUpdated = false;
570        }
571    }
572
573    /**
574     * Returns the date used for {@linkplain #getElevation elevation} and
575     * {@linkplain #getAzimuth azimuth} computation. This is the date specified
576     * during the last call to {@link #setDate}.
577     */
578    public Date getDate() {
579        return new Date(time);
580    }
581
582    /**
583     * Set the sun's {@linkplain #getElevation elevation angle} at twilight, in degrees.
584     * Common values are defined for the
585     * {@linkplain #ASTRONOMICAL_TWILIGHT astronomical twilight} (-18°),
586     * {@linkplain #NAUTICAL_TWILIGHT nautical twilight} (-12°) and
587     * {@linkplain #CIVIL_TWILIGHT civil twilight} (-6°).
588     * The {@linkplain #getElevation elevation} and {@linkplain #getAzimuth azimuth} are
589     * set to {@linkplain Double#NaN NaN} when the sun elevation is below the twilight
590     * value (i.e. during night). The default value is {@link #CIVIL_TWILIGHT}.
591     *
592     * @param twilight The new sun elevation at twilight, or {@link Double#NaN}
593     *                 if no twilight value should be taken in account.
594     * @throws IllegalArgumentException if the twilight value is illegal.
595     */
596    public void setTwilight(final double twilight) throws IllegalArgumentException {
597        if (twilight<-90 || twilight>-90) {
598            // TODO: provides a better (localized) message.
599            throw new IllegalArgumentException(String.valueOf(twilight));
600        }
601        this.twilight = twilight;
602        this.updated = false;
603    }
604
605    /**
606     * Returns the sun's {@linkplain #getElevation elevation angle} at twilight, in degrees.
607     * This is the value set during the last call to {@link #setTwilight}.
608     */
609    public double getTwilight() {
610        return twilight;
611    }
612
613    /**
614     * Retourne l'azimuth en degrés.
615     *
616     * @return L'azimuth en degrés.
617     */
618    public double getAzimuth() {
619        if (!updated) {
620            compute();
621        }
622        return azimuth;
623    }
624
625    /**
626     * Retourne l'élévation en degrés.
627     *
628     * @return L'élévation en degrés.
629     */
630    public double getElevation() {
631        if (!updated) {
632            compute();
633        }
634        return elevation;
635    }
636    
637    /**
638     * 
639     * @return solar zenith angle in degrees 
640     */
641    public double getSolarZenith() {
642       if (!updated) {
643          compute();
644       }
645       return 90.0 - elevation;
646    }
647
648    /**
649     * Retourne l'heure à laquelle le soleil est au plus haut. L'heure est
650     * retournée en nombre de millisecondes écoulées depuis le debut de la
651     * journée (minuit) en heure UTC.
652     */
653    public long getNoonTime() {
654        if (!updated) {
655            compute();
656        }
657        return noonTime % DAY_MILLIS;
658    }
659
660    /**
661     * Retourne la date à laquelle le soleil est au plus haut dans la journée.
662     * Cette méthode est équivalente à {@link #getNoonTime} mais inclue le jour
663     * de la date qui avait été spécifiée à la méthode {@link #compute}.
664     */
665    public Date getNoonDate() {
666        if (!updated) {
667            compute();
668        }
669        return new Date(noonTime);
670    }
671    
672    /**
673     * @param longitudes float[] of degrees
674     * @param latitudes float[] of degrees
675     * @param dateTime java.util.Date
676     * @param output FlatField of which computed angles (degrees) will be written into the range
677     * @throws Exception 
678     */    
679    public static void getCosSolarZenith(float[] longitudes, float[] latitudes, Date dateTime, FlatField output) throws Exception {
680       float[][] values = output.getFloats(false);
681       getSolarZenith(longitudes, latitudes, dateTime, values[0]);
682       for (int i=0; i<values[0].length; i++) {
683          values[0][i] = (float) Math.cos(values[0][i]*Data.DEGREES_TO_RADIANS);
684       }       
685    }
686    
687    /**
688     * @param longitudes float[] of degrees
689     * @param latitudes float[] of degrees
690     * @param dateTime java.util.Date
691     * @param output FlatField of which computed angles (degrees) will be written into the range
692     * @throws Exception 
693     */    
694    public static void getSolarZenith(float[] longitudes, float[] latitudes, Date dateTime, FlatField output) throws Exception {
695       float[][] values = output.getFloats(false);
696       getSolarZenith(longitudes, latitudes, dateTime, values[0]);       
697    }
698    
699    /**
700     * @param longitudes float[] of degrees
701     * @param latitudes float[] of degrees
702     * @param dateTime java.util.Date
703     * @param solzen float[] of computed solar zenith angle (degrees). If null, array will be allocated.
704     * @return the compute values
705     * @throws Exception 
706     */
707    public static float[] getSolarZenith(float[] longitudes, float[] latitudes, Date dateTime, float[] solzen) throws Exception {
708       SunRelativePosition calculator = new SunRelativePosition();
709       
710       int numPts = longitudes.length;
711       if (numPts != latitudes.length) {
712          throw new Exception("number of longitudes and latitudes must match");
713       }
714       
715       if (solzen == null) {
716          solzen = new float[numPts];
717       }
718       
719       calculator.setDate(dateTime);
720       for (int k=0; k<numPts; k++) {
721          calculator.setCoordinate(longitudes[k], latitudes[k]);
722          solzen[k] = (float) calculator.getSolarZenith();
723       }
724       
725       return solzen;
726    }
727    
728    /**
729     * Affiche la position du soleil à la date et coordonnées spécifiée.
730     * Cette application peut être lancée avec la syntaxe suivante:
731     *
732     * <pre>SunRelativePosition <var>[longitude]</var> <var>[latitude]</var> <var>[date]</var></pre>
733     *
734     * où <var>date</var> est un argument optionel spécifiant la date et l'heure.
735     * Si cet argument est omis, la date et heure actuelles seront utilisées.
736     */
737    public static void main(final String[] args) throws ParseException {
738        final DateFormat format=DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
739        format.setTimeZone(TimeZone.getTimeZone("UTC"));
740        double longitude = 0;
741        double latitude  = 0;
742        Date time = new Date();
743        switch (args.length) {
744            case 3: time      = format.parse      (args[2]); // fall through
745            case 2: latitude  = Double.parseDouble(args[1]); // fall through
746            case 1: longitude = Double.parseDouble(args[0]); // fall through
747        }
748        final SunRelativePosition calculator = new SunRelativePosition();
749        calculator.setDate(time);
750        calculator.setCoordinate(longitude, latitude);
751        System.out.print("Date (UTC): "); System.out.println(format.format(time));
752        System.out.print("Longitude:  "); System.out.println(longitude);
753        System.out.print("Latitude:   "); System.out.println(latitude);
754        System.out.print("Elevation:  "); System.out.println(calculator.getElevation());
755        System.out.print("Azimuth:    "); System.out.println(calculator.getAzimuth());
756        System.out.print("Noon date:  "); System.out.println(format.format(calculator.getNoonDate()));
757    }
758}