001/*
002 * Zmanim Java API
003 * Copyright (C) 2004-2011 Eliyahu Hershfeld
004 *
005 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
006 * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option)
007 * any later version.
008 *
009 * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied
010 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
011 * details.
012 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to
013 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA,
014 * or connect to: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
015 */
016package net.sourceforge.zmanim.util;
017
018import java.util.TimeZone;
019
020/**
021 * A class that contains location information such as latitude and longitude required for astronomical calculations. The
022 * elevation field may not be used by some calculation engines and would be ignored if set. Check the documentation for
023 * specific implementations of the {@link AstronomicalCalculator} to see if elevation is calculated as part of the
024 * algorithm.
025 * 
026 * @author © Eliyahu Hershfeld 2004 - 2011
027 * @version 1.1
028 */
029public class GeoLocation implements Cloneable {
030        private double latitude;
031        private double longitude;
032        private String locationName;
033        private TimeZone timeZone;
034        private double elevation;
035        private static final int DISTANCE = 0;
036        private static final int INITIAL_BEARING = 1;
037        private static final int FINAL_BEARING = 2;
038
039        /** constant for milliseconds in a minute (60,000) */
040        private static final long MINUTE_MILLIS = 60 * 1000;
041
042        /** constant for milliseconds in an hour (3,600,000) */
043        private static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
044
045        /**
046         * Method to get the elevation in Meters.
047         * 
048         * @return Returns the elevation in Meters.
049         */
050        public double getElevation() {
051                return elevation;
052        }
053
054        /**
055         * Method to set the elevation in Meters <b>above </b> sea level.
056         * 
057         * @param elevation
058         *            The elevation to set in Meters. An IllegalArgumentException 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, NJ&quot;
072         * @param latitude
073         *            the latitude in a double format such as 40.095965 for Lakewood, NJ <br/>
074         *            <b>Note: </b> For latitudes south of the equator, a negative value should be used.
075         * @param longitude
076         *            double the longitude in a double format such as -74.222130 for Lakewood, NJ. <br/>
077         *            <b>Note: </b> For longitudes east of the <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
078         *            Meridian </a> (Greenwich), a negative value should be used.
079         * @param timeZone
080         *            the <code>TimeZone</code> for the location.
081         */
082        public GeoLocation(String name, double latitude, double longitude, TimeZone timeZone) {
083                this(name, latitude, longitude, 0, timeZone);
084        }
085
086        /**
087         * GeoLocation constructor with parameters for all required fields.
088         * 
089         * @param name
090         *            The location name for display use such as &quot;Lakewood, NJ&quot;
091         * @param latitude
092         *            the latitude in a double format such as 40.095965 for Lakewood, NJ <br/>
093         *            <b>Note: </b> For latitudes south of the equator, a negative value should be used.
094         * @param longitude
095         *            double the longitude in a double format such as -74.222130 for Lakewood, NJ. <br/>
096         *            <b>Note: </b> For longitudes east of the <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
097         *            Meridian </a> (Greenwich), a negative value should be used.
098         * @param elevation
099         *            the elevation above sea level in Meters. Elevation is not used in most algorithms used for calculating
100         *            sunrise and set.
101         * @param timeZone
102         *            the <code>TimeZone</code> for the location.
103         */
104        public GeoLocation(String name, double latitude, double longitude, double elevation, TimeZone timeZone) {
105                setLocationName(name);
106                setLatitude(latitude);
107                setLongitude(longitude);
108                setElevation(elevation);
109                setTimeZone(timeZone);
110        }
111
112        /**
113         * Default GeoLocation constructor will set location to the Prime Meridian at Greenwich, England and a TimeZone of
114         * GMT. The longitude will be set to 0 and the latitude will be 51.4772 to match the location of the <a
115         * href="http://www.rog.nmm.ac.uk">Royal Observatory, Greenwich </a>. No daylight savings time will be used.
116         */
117        public GeoLocation() {
118                setLocationName("Greenwich, England");
119                setLongitude(0); // added for clarity
120                setLatitude(51.4772);
121                setTimeZone(TimeZone.getTimeZone("GMT"));
122        }
123
124        /**
125         * Method to set the latitude.
126         * 
127         * @param latitude
128         *            The degrees of latitude to set. The values should be between -90&deg; and 90&deg;. An
129         *            IllegalArgumentException will be thrown if the value exceeds the limit. For example 40.095965 would be
130         *            used for Lakewood, NJ. <b>Note: </b> For latitudes south of the equator, a negative value should be
131         *            used.
132         */
133        public void setLatitude(double latitude) {
134                if (latitude > 90 || latitude < -90) {
135                        throw new IllegalArgumentException("Latitude must be between -90 and  90");
136                }
137                this.latitude = latitude;
138        }
139
140        /**
141         * Method to set the latitude in degrees, minutes and seconds.
142         * 
143         * @param degrees
144         *            The degrees of latitude to set between -90 and 90. An IllegalArgumentException will be thrown if the
145         *            value exceeds the limit. For example 40 would be used for Lakewood, NJ.
146         * @param minutes
147         *            <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">minutes of arc</a>
148         * @param seconds
149         *            <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">seconds of arc</a>
150         * @param direction
151         *            N for north and S for south. An IllegalArgumentException will be thrown if the value is not S or N.
152         */
153        public void setLatitude(int degrees, int minutes, double seconds, String direction) {
154                double tempLat = degrees + ((minutes + (seconds / 60.0)) / 60.0);
155                if (tempLat > 90 || tempLat < 0) {
156                        throw new IllegalArgumentException(
157                                        "Latitude must be between 0 and  90. Use direction of S instead of negative.");
158                }
159                if (direction.equals("S")) {
160                        tempLat *= -1;
161                } else if (!direction.equals("N")) {
162                        throw new IllegalArgumentException("Latitude direction must be N or S");
163                }
164                this.latitude = tempLat;
165        }
166
167        /**
168         * @return Returns the latitude.
169         */
170        public double getLatitude() {
171                return latitude;
172        }
173
174        /**
175         * Method to set the longitude in a double format.
176         * 
177         * @param longitude
178         *            The degrees of longitude to set in a double format between -180&deg; and 180&deg;. An
179         *            IllegalArgumentException will be thrown if the value exceeds the limit. For example -74.2094 would be
180         *            used for Lakewood, NJ. Note: for longitudes east of the <a
181         *            href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime Meridian</a> (Greenwich) a negative value
182         *            should be used.
183         */
184        public void setLongitude(double longitude) {
185                if (longitude > 180 || longitude < -180) {
186                        throw new IllegalArgumentException("Longitude must be between -180 and  180");
187                }
188                this.longitude = longitude;
189        }
190
191        /**
192         * Method to set the longitude in degrees, minutes and seconds.
193         * 
194         * @param degrees
195         *            The degrees of longitude to set between -180 and 180. An IllegalArgumentException will be thrown if
196         *            the value exceeds the limit. For example -74 would be used for Lakewood, NJ. Note: for longitudes east
197         *            of the <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime Meridian </a> (Greenwich) a
198         *            negative value should be used.
199         * @param minutes
200         *            <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">minutes of arc</a>
201         * @param seconds
202         *            <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">seconds of arc</a>
203         * @param direction
204         *            E for east of the Prime Meridian or W for west of it. An IllegalArgumentException will be thrown if
205         *            the value is not E or W.
206         */
207        public void setLongitude(int degrees, int minutes, double seconds, String direction) {
208                double longTemp = degrees + ((minutes + (seconds / 60.0)) / 60.0);
209                if (longTemp > 180 || this.longitude < 0) {
210                        throw new IllegalArgumentException("Longitude must be between 0 and  180. Use the ");
211                }
212                if (direction.equals("W")) {
213                        longTemp *= -1;
214                } else if (!direction.equals("E")) {
215                        throw new IllegalArgumentException("Longitude direction must be E or W");
216                }
217                this.longitude = longTemp;
218        }
219
220        /**
221         * @return Returns the longitude.
222         */
223        public double getLongitude() {
224                return longitude;
225        }
226
227        /**
228         * @return Returns the location name.
229         */
230        public String getLocationName() {
231                return locationName;
232        }
233
234        /**
235         * @param name
236         *            The setter method for the display name.
237         */
238        public void setLocationName(String name) {
239                this.locationName = name;
240        }
241
242        /**
243         * @return Returns the timeZone.
244         */
245        public TimeZone getTimeZone() {
246                return timeZone;
247        }
248
249        /**
250         * Method to set the TimeZone. If this is ever set after the GeoLocation is set in the
251         * {@link net.sourceforge.zmanim.AstronomicalCalendar}, it is critical that
252         * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.
253         * {@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)} be called in order for the
254         * AstronomicalCalendar to output times in the expected offset. This situation will arise if the
255         * AstronomicalCalendar is ever {@link net.sourceforge.zmanim.AstronomicalCalendar#clone() cloned}.
256         * 
257         * @param timeZone
258         *            The timeZone to set.
259         */
260        public void setTimeZone(TimeZone timeZone) {
261                this.timeZone = timeZone;
262        }
263
264        /**
265         * A method that will return the location's local mean time offset in milliseconds from local <a
266         * href="http://en.wikipedia.org/wiki/Standard_time">standard time</a>. The globe is split into 360&deg;, with
267         * 15&deg; per hour of the day. For a local that is at a longitude that is evenly divisible by 15 (longitude % 15 ==
268         * 0), at solar {@link net.sourceforge.zmanim.AstronomicalCalendar#getSunTransit() noon} (with adjustment for the <a
269         * href="http://en.wikipedia.org/wiki/Equation_of_time">equation of time</a>) the sun should be directly overhead,
270         * so a user who is 1&deg; west of this will have noon at 4 minutes after standard time noon, and conversely, a user
271         * who is 1&deg; east of the 15&deg; longitude will have noon at 11:56 AM. Lakewood, N.J., whose longitude is
272         * -74.2094, is 0.7906 away from the closest multiple of 15 at -75&deg;. This is multiplied by 4 to yield 3 minutes
273         * and 10 seconds earlier than standard time. The offset returned does not account for the <a
274         * href="http://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving time</a> offset since this class is
275         * unaware of dates.
276         * 
277         * @return the offset in milliseconds not accounting for Daylight saving time. A positive value will be returned
278         *         East of the 15&deg; timezone line, and a negative value West of it.
279         * @since 1.1
280         */
281        public long getLocalMeanTimeOffset() {
282                return (long) (getLongitude() * 4 * MINUTE_MILLIS - getTimeZone().getRawOffset());
283        }
284
285        /**
286         * Calculate the initial <a href="http://en.wikipedia.org/wiki/Great_circle">geodesic</a> bearing between this
287         * Object and a second Object passed to this method using <a
288         * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a> inverse formula See T Vincenty, "<a
289         * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse Solutions of Geodesics on the Ellipsoid
290         * with application of nested equations</a>", Survey Review, vol XXII no 176, 1975
291         * 
292         * @param location
293         *            the destination location
294         */
295        public double getGeodesicInitialBearing(GeoLocation location) {
296                return vincentyFormula(location, INITIAL_BEARING);
297        }
298
299        /**
300         * Calculate the final <a href="http://en.wikipedia.org/wiki/Great_circle">geodesic</a> bearing between this Object
301         * and a second Object passed to this method using <a href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus
302         * Vincenty's</a> inverse formula See T Vincenty, "<a href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and
303         * Inverse Solutions of Geodesics on the Ellipsoid with application of nested equations</a>", Survey Review, vol
304         * XXII no 176, 1975
305         * 
306         * @param location
307         *            the destination location
308         */
309        public double getGeodesicFinalBearing(GeoLocation location) {
310                return vincentyFormula(location, FINAL_BEARING);
311        }
312
313        /**
314         * Calculate <a href="http://en.wikipedia.org/wiki/Great-circle_distance">geodesic distance</a> in Meters between
315         * this Object and a second Object passed to this method using <a
316         * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a> inverse formula See T Vincenty, "<a
317         * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse Solutions of Geodesics on the Ellipsoid
318         * with application of nested equations</a>", Survey Review, vol XXII no 176, 1975
319         * 
320         * @param location
321         *            the destination location
322         */
323        public double getGeodesicDistance(GeoLocation location) {
324                return vincentyFormula(location, DISTANCE);
325        }
326
327        /**
328         * Calculate <a href="http://en.wikipedia.org/wiki/Great-circle_distance">geodesic distance</a> in Meters between
329         * this Object and a second Object passed to this method using <a
330         * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a> inverse formula See T Vincenty, "<a
331         * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse Solutions of Geodesics on the Ellipsoid
332         * with application of nested equations</a>", Survey Review, vol XXII no 176, 1975
333         * 
334         * @param location
335         *            the destination location
336         * @param formula
337         *            This formula calculates initial bearing ({@link #INITIAL_BEARING}), final bearing (
338         *            {@link #FINAL_BEARING}) and distance ({@link #DISTANCE}).
339         */
340        private double vincentyFormula(GeoLocation location, int formula) {
341                double a = 6378137;
342                double b = 6356752.3142;
343                double f = 1 / 298.257223563; // WGS-84 ellipsiod
344                double L = Math.toRadians(location.getLongitude() - getLongitude());
345                double U1 = Math.atan((1 - f) * Math.tan(Math.toRadians(getLatitude())));
346                double U2 = Math.atan((1 - f) * Math.tan(Math.toRadians(location.getLatitude())));
347                double sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
348                double sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
349
350                double lambda = L;
351                double lambdaP = 2 * Math.PI;
352                double iterLimit = 20;
353                double sinLambda = 0;
354                double cosLambda = 0;
355                double sinSigma = 0;
356                double cosSigma = 0;
357                double sigma = 0;
358                double sinAlpha = 0;
359                double cosSqAlpha = 0;
360                double cos2SigmaM = 0;
361                double C;
362                while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0) {
363                        sinLambda = Math.sin(lambda);
364                        cosLambda = Math.cos(lambda);
365                        sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda)
366                                        + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));
367                        if (sinSigma == 0)
368                                return 0; // co-incident points
369                        cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
370                        sigma = Math.atan2(sinSigma, cosSigma);
371                        sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
372                        cosSqAlpha = 1 - sinAlpha * sinAlpha;
373                        cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
374                        if (Double.isNaN(cos2SigmaM))
375                                cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6)
376                        C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));
377                        lambdaP = lambda;
378                        lambda = L + (1 - C) * f * sinAlpha
379                                        * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
380                }
381                if (iterLimit == 0)
382                        return Double.NaN; // formula failed to converge
383
384                double uSq = cosSqAlpha * (a * a - b * b) / (b * b);
385                double A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
386                double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
387                double deltaSigma = B
388                                * sinSigma
389                                * (cos2SigmaM + B
390                                                / 4
391                                                * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM
392                                                                * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
393                double distance = b * A * (sigma - deltaSigma);
394
395                // initial bearing
396                double fwdAz = Math.toDegrees(Math.atan2(cosU2 * sinLambda, cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));
397                // final bearing
398                double revAz = Math.toDegrees(Math.atan2(cosU1 * sinLambda, -sinU1 * cosU2 + cosU1 * sinU2 * cosLambda));
399                if (formula == DISTANCE) {
400                        return distance;
401                } else if (formula == INITIAL_BEARING) {
402                        return fwdAz;
403                } else if (formula == FINAL_BEARING) {
404                        return revAz;
405                } else { // should never happpen
406                        return Double.NaN;
407                }
408        }
409
410        /**
411         * Returns the <a href="http://en.wikipedia.org/wiki/Rhumb_line">rhumb line</a> bearing from the current location to
412         * the GeoLocation passed in.
413         * 
414         * @param location
415         *            destination location
416         * @return the bearing in degrees
417         */
418        public double getRhumbLineBearing(GeoLocation location) {
419                double dLon = Math.toRadians(location.getLongitude() - getLongitude());
420                double dPhi = Math.log(Math.tan(Math.toRadians(location.getLatitude()) / 2 + Math.PI / 4)
421                                / Math.tan(Math.toRadians(getLatitude()) / 2 + Math.PI / 4));
422                if (Math.abs(dLon) > Math.PI)
423                        dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon);
424                return Math.toDegrees(Math.atan2(dLon, dPhi));
425        }
426
427        /**
428         * Returns the <a href="http://en.wikipedia.org/wiki/Rhumb_line">rhumb line</a> distance from the current location
429         * to the GeoLocation passed in.
430         * 
431         * @param location
432         *            the destination location
433         * @return the distance in Meters
434         */
435        public double getRhumbLineDistance(GeoLocation location) {
436                double R = 6371; // earth's mean radius in km
437                double dLat = Math.toRadians(location.getLatitude() - getLatitude());
438                double dLon = Math.toRadians(Math.abs(location.getLongitude() - getLongitude()));
439                double dPhi = Math.log(Math.tan(Math.toRadians(location.getLongitude()) / 2 + Math.PI / 4)
440                                / Math.tan(Math.toRadians(getLatitude()) / 2 + Math.PI / 4));
441                double q = (Math.abs(dLat) > 1e-10) ? dLat / dPhi : Math.cos(Math.toRadians(getLatitude()));
442                // if dLon over 180° take shorter rhumb across 180° meridian:
443                if (dLon > Math.PI)
444                        dLon = 2 * Math.PI - dLon;
445                double d = Math.sqrt(dLat * dLat + q * q * dLon * dLon);
446                return d * R;
447        }
448
449        /**
450         * A method that returns an XML formatted <code>String</code> representing the serialized <code>Object</code>. Very
451         * similar to the toString method but the return value is in an xml format. The format currently used (subject to
452         * change) is:
453         * 
454         * <pre>
455         *   &lt;GeoLocation&gt;
456         *       &lt;LocationName&gt;Lakewood, NJ&lt;/LocationName&gt;
457         *       &lt;Latitude&gt;40.0828&amp;deg&lt;/Latitude&gt;
458         *       &lt;Longitude&gt;-74.2094&amp;deg&lt;/Longitude&gt;
459         *       &lt;Elevation&gt;0 Meters&lt;/Elevation&gt;
460         *       &lt;TimezoneName&gt;America/New_York&lt;/TimezoneName&gt;
461         *       &lt;TimeZoneDisplayName&gt;Eastern Standard Time&lt;/TimeZoneDisplayName&gt;
462         *       &lt;TimezoneGMTOffset&gt;-5&lt;/TimezoneGMTOffset&gt;
463         *       &lt;TimezoneDSTOffset&gt;1&lt;/TimezoneDSTOffset&gt;
464         *   &lt;/GeoLocation&gt;
465         * </pre>
466         * 
467         * @return The XML formatted <code>String</code>.
468         */
469        public String toXML() {
470                StringBuffer sb = new StringBuffer();
471                sb.append("<GeoLocation>\n");
472                sb.append("\t<LocationName>").append(getLocationName()).append("</LocationName>\n");
473                sb.append("\t<Latitude>").append(getLatitude()).append("&deg;").append("</Latitude>\n");
474                sb.append("\t<Longitude>").append(getLongitude()).append("&deg;").append("</Longitude>\n");
475                sb.append("\t<Elevation>").append(getElevation()).append(" Meters").append("</Elevation>\n");
476                sb.append("\t<TimezoneName>").append(getTimeZone().getID()).append("</TimezoneName>\n");
477                sb.append("\t<TimeZoneDisplayName>").append(getTimeZone().getDisplayName()).append("</TimeZoneDisplayName>\n");
478                sb.append("\t<TimezoneGMTOffset>").append(getTimeZone().getRawOffset() / HOUR_MILLIS)
479                                .append("</TimezoneGMTOffset>\n");
480                sb.append("\t<TimezoneDSTOffset>").append(getTimeZone().getDSTSavings() / HOUR_MILLIS)
481                                .append("</TimezoneDSTOffset>\n");
482                sb.append("</GeoLocation>");
483                return sb.toString();
484        }
485
486        /**
487         * @see java.lang.Object#equals(Object)
488         */
489        public boolean equals(Object object) {
490                if (this == object)
491                        return true;
492                if (!(object instanceof GeoLocation))
493                        return false;
494                GeoLocation geo = (GeoLocation) object;
495                return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(geo.latitude)
496                                && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(geo.longitude)
497                                && this.elevation == geo.elevation
498                                && (this.locationName == null ? geo.locationName == null : this.locationName.equals(geo.locationName))
499                                && (this.timeZone == null ? geo.timeZone == null : this.timeZone.equals(geo.timeZone));
500        }
501
502        /**
503         * @see java.lang.Object#hashCode()
504         */
505        public int hashCode() {
506
507                int result = 17;
508                long latLong = Double.doubleToLongBits(this.latitude);
509                long lonLong = Double.doubleToLongBits(this.longitude);
510                long elevLong = Double.doubleToLongBits(this.elevation);
511                int latInt = (int) (latLong ^ (latLong >>> 32));
512                int lonInt = (int) (lonLong ^ (lonLong >>> 32));
513                int elevInt = (int) (elevLong ^ (elevLong >>> 32));
514                result = 37 * result + getClass().hashCode();
515                result += 37 * result + latInt;
516                result += 37 * result + lonInt;
517                result += 37 * result + elevInt;
518                result += 37 * result + (this.locationName == null ? 0 : this.locationName.hashCode());
519                result += 37 * result + (this.timeZone == null ? 0 : this.timeZone.hashCode());
520                return result;
521        }
522
523        /**
524         * @see java.lang.Object#toString()
525         */
526        public String toString() {
527                StringBuffer sb = new StringBuffer();
528                sb.append("\nLocation Name:\t\t\t").append(getLocationName());
529                sb.append("\nLatitude:\t\t\t").append(getLatitude()).append("&deg;");
530                sb.append("\nLongitude:\t\t\t").append(getLongitude()).append("&deg;");
531                sb.append("\nElevation:\t\t\t").append(getElevation()).append(" Meters");
532                sb.append("\nTimezone Name:\t\t\t").append(getTimeZone().getID());
533                /*
534                 * sb.append("\nTimezone Display Name:\t\t").append( getTimeZone().getDisplayName());
535                 */
536                sb.append("\nTimezone GMT Offset:\t\t").append(getTimeZone().getRawOffset() / HOUR_MILLIS);
537                sb.append("\nTimezone DST Offset:\t\t").append(getTimeZone().getDSTSavings() / HOUR_MILLIS);
538                return sb.toString();
539        }
540
541        /**
542         * An implementation of the {@link java.lang.Object#clone()} method that creates a <a
543         * href="http://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a> of the object. <br/>
544         * <b>Note:</b> If the {@link java.util.TimeZone} in the clone will be changed from the original, it is critical
545         * that {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.
546         * {@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)} is called after cloning in order for the
547         * AstronomicalCalendar to output times in the expected offset.
548         * 
549         * @see java.lang.Object#clone()
550         * @since 1.1
551         */
552        public Object clone() {
553                GeoLocation clone = null;
554                try {
555                        clone = (GeoLocation) super.clone();
556                } catch (CloneNotSupportedException cnse) {
557                        //Required by the compiler. Should never be reached since we implement clone()
558                }
559                clone.timeZone = (TimeZone) getTimeZone().clone();
560                clone.locationName = getLocationName();
561                return clone;
562        }
563}