001    /*
002     * Zmanim Java API
003     * Copyright (C) 2004-2008 Eliyahu Hershfeld
004     *
005     * This program is free software; you can redistribute it and/or modify it under the terms of the
006     * GNU General Public License as published by the Free Software Foundation; either version 2 of the
007     * License, or (at your option) any later version.
008     *
009     * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
010     * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
011     * General Public License for more details.
012     *
013     * You should have received a copy of the GNU General Public License along with this program; if
014     * not, write to the Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA
015     * 02111-1307, USA or connect to: http://www.fsf.org/copyleft/gpl.html
016     */
017    package net.sourceforge.zmanim.util;
018    
019    import java.util.TimeZone;
020    
021    /**
022     * A class that contains location information such as latitude and longitude
023     * required for astronomical calculations. The elevation field is not used by
024     * most calculation engines and would be ignored if set. Check the documentation
025     * for specific implementations of the {@link AstronomicalCalculator} to see if
026     * elevation is calculated as part o the algorithm.
027     * 
028     * @author © Eliyahu Hershfeld 2004 - 2008
029     * @version 1.1
030     */
031    public class GeoLocation implements Cloneable {
032            private double latitude;
033            private double longitude;
034            private String locationName;
035            private TimeZone timeZone;
036            private double elevation;
037            
038            /** constant for milliseconds in a minute (60,000) */
039            private static final long MINUTE_MILLIS = 60 * 1000;
040            
041            /** constant for milliseconds in an hour (3,600,000) */
042            private static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
043            
044            /**
045             * Method to get the elevation in Meters.
046             * 
047             * @return Returns the elevation in Meters.
048             */
049            public double getElevation() {
050                    return elevation;
051            }
052    
053            /**
054             * Method to set the elevation in Meters <b>above </b> sea level.
055             * 
056             * @param elevation
057             *            The elevation to set in Meters. An IllegalArgumentException
058             *            will be thrown if the value is a negative.
059             */
060            public void setElevation(double elevation) {
061                    if (elevation < 0) {
062                            throw new IllegalArgumentException("Elevation cannot be negative");
063                    }
064                    this.elevation = elevation;
065            }
066    
067            /**
068             * GeoLocation constructor with parameters for all required fields.
069             * 
070             * @param name
071             *            The location name for display use such as &quot;Lakewood,
072             *            NJ&quot;
073             * @param latitude
074             *            the latitude in a double format such as 40.095965 for
075             *            Lakewood, NJ
076             * @param longitude
077             *            double the longitude in a double format such as -74.222130 for
078             *            Lakewood, NJ. <br/> <b>Note: </b> For longitudes east of the
079             *            <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
080             *            Meridian </a> (Greenwich), a negative value should be used.
081             * @param timeZone
082             *            the <code>TimeZone</code> for the location.
083             */
084            public GeoLocation(String name, double latitude, double longitude,
085                            TimeZone timeZone) {
086                    this(name, latitude, longitude, 0, timeZone);
087                    this.setLocationName(name);
088                    this.setLatitude(latitude);
089                    this.setLongitude(longitude);
090                    this.setTimeZone(timeZone);
091            }
092    
093            /**
094             * GeoLocation constructor with parameters for all required fields.
095             * 
096             * @param name
097             *            The location name for display use such as &quot;Lakewood,
098             *            NJ&quot;
099             * @param latitude
100             *            the latitude in a double format such as 40.095965 for
101             *            Lakewood, NJ
102             * @param longitude
103             *            double the longitude in a double format such as -74.222130 for
104             *            Lakewood, NJ. <br/> <b>Note: </b> For longitudes east of the
105             *            <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
106             *            Meridian </a> (Greenwich), a negative value should be used.
107             * @param elevation
108             *            the elevation above sea level in Meters. Elevation is not used
109             *            in most algorithms used for calculating sunrise and set.
110             * @param timeZone
111             *            the <code>TimeZone</code> for the location.
112             */
113            public GeoLocation(String name, double latitude, double longitude,
114                            double elevation, TimeZone timeZone) {
115                    setLocationName(name);
116                    setLatitude(latitude);
117                    setLongitude(longitude);
118                    setElevation(elevation);
119                    setTimeZone(timeZone);
120            }
121    
122            /**
123             * Default GeoLocation constructor will set location to the Prime Meridian
124             * at Greenwich, England and a TimeZone of GMT. The longitude will be set to
125             * 0 and the latitude will be 51.4772 to match the location of the <a
126             * href="http://www.rog.nmm.ac.uk">Royal Observatory, Greenwich </a>. No
127             * daylight savings time will be used.
128             */
129            public GeoLocation() {
130                    setLocationName("Greenwich, England");
131                    setLongitude(0); // added for clarity
132                    setLatitude(51.4772);
133                    setTimeZone(TimeZone.getTimeZone("GMT"));
134            }
135    
136            /**
137             * Method to set the latitude in a double format.
138             * 
139             * @param latitude
140             *            The degrees of latitude to set in a double format between
141             *            -90&deg; and 90&deg;. An IllegalArgumentException will be
142             *            thrown if the value exceeds the limit. For example 40.095965
143             *            would be used for Lakewood, NJ.
144             */
145            public void setLatitude(double latitude) {
146                    if (latitude > 90 || latitude < -90) {
147                            throw new IllegalArgumentException(
148                                            "Latitude must be between -90 and  90");
149                    }
150                    this.latitude = latitude;
151            }
152    
153            /**
154             * Method to set the latitude in degrees, minutes and seconds.
155             * 
156             * @param degrees
157             *            The degrees of latitude to set between -90 and 90. An
158             *            IllegalArgumentException will be thrown if the value exceeds
159             *            the limit. For example 40 would be used for Lakewood, NJ.
160             * @param minutes
161             * @param seconds
162             * @param direction
163             *            N for north and S for south. An IllegalArgumentException will
164             *            be thrown if the value is not S or N.
165             */
166            public void setLatitude(int degrees, int minutes, double seconds,
167                            String direction) {
168                    double tempLat = degrees + ((minutes + (seconds / 60.0)) / 60.0);
169                    if (tempLat > 90 || tempLat < 0) {
170                            throw new IllegalArgumentException(
171                                            "Latitude must be between 0 and  90. Use direction of S instead of negative.");
172                    }
173                    if (direction.equals("S")) {
174                            tempLat *= -1;
175                    } else if (!direction.equals("N")) {
176                            throw new IllegalArgumentException(
177                                            "Latitude direction must be N or S");
178                    }
179                    this.latitude = tempLat;
180            }
181    
182            /**
183             * @return Returns the latitude.
184             */
185            public double getLatitude() {
186                    return latitude;
187            }
188    
189            /**
190             * Method to set the longitude in a double format.
191             * 
192             * @param longitude
193             *            The degrees of longitude to set in a double format between
194             *            -180&deg; and 180&deg;. An IllegalArgumentException will be
195             *            thrown if the value exceeds the limit. For example -74.2094
196             *            would be used for Lakewood, NJ. Note: for longitudes east of
197             *            the <a
198             *            href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
199             *            Meridian</a> (Greenwich) a negative value should be used.
200             */
201            public void setLongitude(double longitude) {
202                    if (longitude > 180 || longitude < -180) {
203                            throw new IllegalArgumentException(
204                                            "Longitude must be between -180 and  180");
205                    }
206                    this.longitude = longitude;
207            }
208    
209            /**
210             * Method to set the longitude in degrees, minutes and seconds.
211             * 
212             * @param degrees
213             *            The degrees of longitude to set between -180 and 180. An
214             *            IllegalArgumentException will be thrown if the value exceeds
215             *            the limit. For example -74 would be used for Lakewood, NJ.
216             *            Note: for longitudes east of the <a
217             *            href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
218             *            Meridian </a> (Greenwich) a negative value should be used.
219             * @param minutes
220             * @param seconds
221             * @param direction
222             *            E for east of the Prime Meridian or W for west of it. An
223             *            IllegalArgumentException will be thrown if the value is not E
224             *            or W.
225             */
226            public void setLongitude(int degrees, int minutes, double seconds,
227                            String direction) {
228                    double longTemp = degrees + ((minutes + (seconds / 60.0)) / 60.0);
229                    if (longTemp > 180 || longitude < 0) {
230                            throw new IllegalArgumentException(
231                                            "Longitude must be between 0 and  180. Use the ");
232                    }
233                    if (direction.equals("W")) {
234                            longTemp *= -1;
235                    } else if (!direction.equals("E")) {
236                            throw new IllegalArgumentException(
237                                            "Longitude direction must be E or W");
238                    }
239                    this.longitude = longTemp;
240            }
241    
242            /**
243             * @return Returns the longitude.
244             */
245            public double getLongitude() {
246                    return longitude;
247            }
248    
249            /**
250             * @return Returns the location name.
251             */
252            public String getLocationName() {
253                    return locationName;
254            }
255    
256            /**
257             * @param name
258             *            The setter method for the display name.
259             */
260            public void setLocationName(String name) {
261                    this.locationName = name;
262            }
263    
264            /**
265             * @return Returns the timeZone.
266             */
267            public TimeZone getTimeZone() {
268                    return timeZone;
269            }
270    
271            /**
272             * Method to set the TimeZone. If this is ever set after the GeoLocation is
273             * set in the {@link net.sourceforge.zmanim.AstronomicalCalendar}, it is
274             * critical that
275             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.{@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)}
276             * be called in order for the AstronomicalCalendar to output times in the
277             * expected offset. This situation will arise if the AstronomicalCalendar is
278             * ever {@link net.sourceforge.zmanim.AstronomicalCalendar#clone() cloned}.
279             * 
280             * @param timeZone
281             *            The timeZone to set.
282             */
283            public void setTimeZone(TimeZone timeZone) {
284                    this.timeZone = timeZone;
285            }
286    
287            /**
288             * A method that will return the location's local mean time offset in
289             * milliseconds from local standard time. The globe is split into 360&deg;,
290             * with 15&deg; per hour of the day. For a local that is at a longitude that
291             * is evenly divisible by 15 (longitude % 15 == 0), at solar
292             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getSunTransit() noon}
293             * (with adjustment for the <a
294             * href="http://en.wikipedia.org/wiki/Equation_of_time">equation of time</a>)
295             * the sun should be directly overhead, so a user who is 1&deg; west of this
296             * will have noon at 4 minutes after standard time noon, and conversely, a
297             * user who is 1&deg; east of the 15&deg longitude will have noon at 11:56
298             * AM. The offset returned does not account for the <a
299             * href="http://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving
300             * time</a> offset since this class is unaware of dates.
301             * 
302             * @return the offset in milliseconds not accounting for Daylight saving
303             *         time. A positive value will be returned East of the timezone
304             *         line, and a negative value West of it.
305             * @since 1.1
306             */
307            public long getLocalMeanTimeOffset() {
308                    return (long) ( getLongitude() * 4 * MINUTE_MILLIS - getTimeZone().getRawOffset());
309            }
310            
311            /**
312             * A method that returns an XML formatted <code>String</code> representing
313             * the serialized <code>Object</code>. Very similar to the toString
314             * method but the return value is in an xml format. The format currently
315             * used (subject to change) is:
316             * 
317             * <pre>
318             *   &lt;GeoLocation&gt;
319             *       &lt;LocationName&gt;Lakewood, NJ&lt;/LocationName&gt;
320             *       &lt;Latitude&gt;40.0828&amp;deg&lt;/Latitude&gt;
321             *       &lt;Longitude&gt;-74.2094&amp;deg&lt;/Longitude&gt;
322             *       &lt;Elevation&gt;0 Meters&lt;/Elevation&gt;
323             *       &lt;TimezoneName&gt;America/New_York&lt;/TimezoneName&gt;
324             *       &lt;TimeZoneDisplayName&gt;Eastern Standard Time&lt;/TimeZoneDisplayName&gt;
325             *       &lt;TimezoneGMTOffset&gt;-5&lt;/TimezoneGMTOffset&gt;
326             *       &lt;TimezoneDSTOffset&gt;1&lt;/TimezoneDSTOffset&gt;
327             *   &lt;/GeoLocation&gt;
328             * </pre>
329             * 
330             * @return The XML formatted <code>String</code>.
331             */
332            public String toXML() {
333                    StringBuffer sb = new StringBuffer();
334                    sb.append("<GeoLocation>\n");
335                    sb.append("\t<LocationName>").append(getLocationName()).append(
336                                    "</LocationName>\n");
337                    sb.append("\t<Latitude>").append(getLatitude()).append("&deg;").append(
338                                    "</Latitude>\n");
339                    sb.append("\t<Longitude>").append(getLongitude()).append("&deg;")
340                                    .append("</Longitude>\n");
341                    sb.append("\t<Elevation>").append(getElevation()).append(" Meters")
342                                    .append("</Elevation>\n");
343                    sb.append("\t<TimezoneName>").append(getTimeZone().getID()).append(
344                                    "</TimezoneName>\n");
345                    sb.append("\t<TimeZoneDisplayName>").append(
346                                    getTimeZone().getDisplayName()).append(
347                                    "</TimeZoneDisplayName>\n");
348                    sb.append("\t<TimezoneGMTOffset>").append(
349                                    getTimeZone().getRawOffset() / HOUR_MILLIS).append(
350                                    "</TimezoneGMTOffset>\n");
351                    sb.append("\t<TimezoneDSTOffset>").append(
352                                    getTimeZone().getDSTSavings() / HOUR_MILLIS).append(
353                                    "</TimezoneDSTOffset>\n");
354                    sb.append("</GeoLocation>");
355                    return sb.toString();
356            }
357    
358            /**
359             * @see java.lang.Object#equals(Object)
360             */
361            public boolean equals(Object object) {
362                    if (this == object)
363                            return true;
364                    if (!(object instanceof GeoLocation))
365                            return false;
366                    GeoLocation geo = (GeoLocation) object;
367                    return Double.doubleToLongBits(latitude) == Double
368                                    .doubleToLongBits(geo.latitude)
369                                    && Double.doubleToLongBits(longitude) == Double
370                                                    .doubleToLongBits(geo.longitude)
371                                    && elevation == geo.elevation
372                                    && (locationName == null ? geo.locationName == null
373                                                    : locationName.equals(geo.locationName))
374                                    && (timeZone == null ? geo.timeZone == null : timeZone
375                                                    .equals(geo.timeZone));
376            }
377    
378            /**
379             * @see java.lang.Object#hashCode()
380             */
381            public int hashCode() {
382    
383                    int result = 17;
384                    long latLong = Double.doubleToLongBits(latitude);
385                    long lonLong = Double.doubleToLongBits(longitude);
386                    long elevLong = Double.doubleToLongBits(elevation);
387                    int latInt = (int) (latLong ^ (latLong >>> 32));
388                    int lonInt = (int) (lonLong ^ (lonLong >>> 32));
389                    int elevInt = (int) (elevLong ^ (elevLong >>> 32));
390                    result = 37 * result + getClass().hashCode();
391                    result += 37 * result + latInt;
392                    result += 37 * result + lonInt;
393                    result += 37 * result + elevInt;
394                    result += 37 * result
395                                    + (locationName == null ? 0 : locationName.hashCode());
396                    result += 37 * result + (timeZone == null ? 0 : timeZone.hashCode());
397                    return result;
398            }
399    
400            /**
401             * @see java.lang.Object#toString()
402             */
403            public String toString() {
404                    StringBuffer sb = new StringBuffer();
405                    sb.append("\nLocation Name:\t\t\t").append(getLocationName());
406                    sb.append("\nLatitude:\t\t\t").append(getLatitude()).append("&deg;");
407                    sb.append("\nLongitude:\t\t\t").append(getLongitude()).append("&deg;");
408                    sb.append("\nElevation:\t\t\t").append(getElevation())
409                                    .append(" Meters");
410                    sb.append("\nTimezone Name:\t\t\t").append(getTimeZone().getID());
411                    /*
412                     * sb.append("\nTimezone Display Name:\t\t").append(
413                     * getTimeZone().getDisplayName());
414                     */
415                    sb.append("\nTimezone GMT Offset:\t\t").append(
416                                    getTimeZone().getRawOffset() / HOUR_MILLIS);
417                    sb.append("\nTimezone DST Offset:\t\t").append(
418                                    getTimeZone().getDSTSavings() / HOUR_MILLIS);
419                    return sb.toString();
420            }
421    
422            /**
423             * An implementation of the {@link java.lang.Object#clone()} method that
424             * creates a <a
425             * href="http://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a>
426             * of the object. <br/><b>Note:</b> If the {@link java.util.TimeZone} in
427             * the clone will be changed from the original, it is critical that
428             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.{@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)}
429             * is called after cloning in order for the AstronomicalCalendar to output
430             * times in the expected offset.
431             * 
432             * @see java.lang.Object#clone()
433             * @since 1.1
434             */
435            public Object clone() {
436                    GeoLocation clone = null;
437                    try {
438                            clone = (GeoLocation) super.clone();
439                    } catch (CloneNotSupportedException cnse) {
440                            System.out
441                                            .print("Required by the compiler. Should never be reached since we implement clone()");
442                    }
443                    clone.timeZone = (TimeZone) getTimeZone().clone();
444                    clone.locationName = (String) getLocationName();
445                    return clone;
446            }
447    }