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            private int DISTANCE = 0;
038            private int INITIAL_BEARING = 1;
039            private int FINAL_BEARING = 2;
040    
041            /** constant for milliseconds in a minute (60,000) */
042            private static final long MINUTE_MILLIS = 60 * 1000;
043    
044            /** constant for milliseconds in an hour (3,600,000) */
045            private static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
046    
047            /**
048             * Method to get the elevation in Meters.
049             * 
050             * @return Returns the elevation in Meters.
051             */
052            public double getElevation() {
053                    return elevation;
054            }
055    
056            /**
057             * Method to set the elevation in Meters <b>above </b> sea level.
058             * 
059             * @param elevation
060             *            The elevation to set in Meters. An IllegalArgumentException
061             *            will be thrown if the value is a negative.
062             */
063            public void setElevation(double elevation) {
064                    if (elevation < 0) {
065                            throw new IllegalArgumentException("Elevation cannot be negative");
066                    }
067                    this.elevation = elevation;
068            }
069    
070            /**
071             * GeoLocation constructor with parameters for all required fields.
072             * 
073             * @param name
074             *            The location name for display use such as &quot;Lakewood,
075             *            NJ&quot;
076             * @param latitude
077             *            the latitude in a double format such as 40.095965 for
078             *            Lakewood, NJ <br/> <b>Note: </b> For latitudes south of the
079             *            equator, a negative value should be used.
080             * @param longitude
081             *            double the longitude in a double format such as -74.222130 for
082             *            Lakewood, NJ. <br/> <b>Note: </b> For longitudes east of the
083             *            <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
084             *            Meridian </a> (Greenwich), a negative value should be used.
085             * @param timeZone
086             *            the <code>TimeZone</code> for the location.
087             */
088            public GeoLocation(String name, double latitude, double longitude,
089                            TimeZone timeZone) {
090                    this(name, latitude, longitude, 0, 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 <br/> <b>Note: </b> For latitudes south of the
102             *            equator, a negative value should be used.
103             * @param longitude
104             *            double the longitude in a double format such as -74.222130 for
105             *            Lakewood, NJ. <br/> <b>Note: </b> For longitudes east of the
106             *            <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
107             *            Meridian </a> (Greenwich), a negative value should be used.
108             * @param elevation
109             *            the elevation above sea level in Meters. Elevation is not used
110             *            in most algorithms used for calculating sunrise and set.
111             * @param timeZone
112             *            the <code>TimeZone</code> for the location.
113             */
114            public GeoLocation(String name, double latitude, double longitude,
115                            double elevation, TimeZone timeZone) {
116                    setLocationName(name);
117                    setLatitude(latitude);
118                    setLongitude(longitude);
119                    setElevation(elevation);
120                    setTimeZone(timeZone);
121            }
122    
123            /**
124             * Default GeoLocation constructor will set location to the Prime Meridian
125             * at Greenwich, England and a TimeZone of GMT. The longitude will be set to
126             * 0 and the latitude will be 51.4772 to match the location of the <a
127             * href="http://www.rog.nmm.ac.uk">Royal Observatory, Greenwich </a>. No
128             * daylight savings time will be used.
129             */
130            public GeoLocation() {
131                    setLocationName("Greenwich, England");
132                    setLongitude(0); // added for clarity
133                    setLatitude(51.4772);
134                    setTimeZone(TimeZone.getTimeZone("GMT"));
135            }
136    
137            /**
138             * Method to set the latitude.
139             * 
140             * @param latitude
141             *            The degrees of latitude to set. The values should be between
142             *            -90&deg; and 90&deg;. An IllegalArgumentException will be
143             *            thrown if the value exceeds the limit. For example 40.095965
144             *            would be used for Lakewood, NJ. <b>Note: </b> For latitudes south of the
145             *            equator, a negative value should be used.
146             */
147            public void setLatitude(double latitude) {
148                    if (latitude > 90 || latitude < -90) {
149                            throw new IllegalArgumentException(
150                                            "Latitude must be between -90 and  90");
151                    }
152                    this.latitude = latitude;
153            }
154    
155            /**
156             * Method to set the latitude in degrees, minutes and seconds.
157             * 
158             * @param degrees
159             *            The degrees of latitude to set between -90 and 90. An
160             *            IllegalArgumentException will be thrown if the value exceeds
161             *            the limit. For example 40 would be used for Lakewood, NJ.
162             * @param minutes <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">minutes of arc</a>
163             * @param seconds <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">seconds of arc</a>
164             * @param direction
165             *            N for north and S for south. An IllegalArgumentException will
166             *            be thrown if the value is not S or N.
167             */
168            public void setLatitude(int degrees, int minutes, double seconds,
169                            String direction) {
170                    double tempLat = degrees + ((minutes + (seconds / 60.0)) / 60.0);
171                    if (tempLat > 90 || tempLat < 0) {
172                            throw new IllegalArgumentException(
173                                            "Latitude must be between 0 and  90. Use direction of S instead of negative.");
174                    }
175                    if (direction.equals("S")) {
176                            tempLat *= -1;
177                    } else if (!direction.equals("N")) {
178                            throw new IllegalArgumentException(
179                                            "Latitude direction must be N or S");
180                    }
181                    this.latitude = tempLat;
182            }
183    
184            /**
185             * @return Returns the latitude.
186             */
187            public double getLatitude() {
188                    return latitude;
189            }
190    
191            /**
192             * Method to set the longitude in a double format.
193             * 
194             * @param longitude
195             *            The degrees of longitude to set in a double format between
196             *            -180&deg; and 180&deg;. An IllegalArgumentException will be
197             *            thrown if the value exceeds the limit. For example -74.2094
198             *            would be used for Lakewood, NJ. Note: for longitudes east of
199             *            the <a
200             *            href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
201             *            Meridian</a> (Greenwich) a negative value should be used.
202             */
203            public void setLongitude(double longitude) {
204                    if (longitude > 180 || longitude < -180) {
205                            throw new IllegalArgumentException(
206                                            "Longitude must be between -180 and  180");
207                    }
208                    this.longitude = longitude;
209            }
210    
211            /**
212             * Method to set the longitude in degrees, minutes and seconds.
213             * 
214             * @param degrees
215             *            The degrees of longitude to set between -180 and 180. An
216             *            IllegalArgumentException will be thrown if the value exceeds
217             *            the limit. For example -74 would be used for Lakewood, NJ.
218             *            Note: for longitudes east of the <a
219             *            href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
220             *            Meridian </a> (Greenwich) a negative value should be used.
221             * @param minutes <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">minutes of arc</a>
222             * @param seconds <a href="http://en.wikipedia.org/wiki/Minute_of_arc#Cartography">seconds of arc</a>
223             * @param direction
224             *            E for east of the Prime Meridian or W for west of it. An
225             *            IllegalArgumentException will be thrown if the value is not E
226             *            or W.
227             */
228            public void setLongitude(int degrees, int minutes, double seconds,
229                            String direction) {
230                    double longTemp = degrees + ((minutes + (seconds / 60.0)) / 60.0);
231                    if (longTemp > 180 || longitude < 0) {
232                            throw new IllegalArgumentException(
233                                            "Longitude must be between 0 and  180. Use the ");
234                    }
235                    if (direction.equals("W")) {
236                            longTemp *= -1;
237                    } else if (!direction.equals("E")) {
238                            throw new IllegalArgumentException(
239                                            "Longitude direction must be E or W");
240                    }
241                    this.longitude = longTemp;
242            }
243    
244            /**
245             * @return Returns the longitude.
246             */
247            public double getLongitude() {
248                    return longitude;
249            }
250    
251            /**
252             * @return Returns the location name.
253             */
254            public String getLocationName() {
255                    return locationName;
256            }
257    
258            /**
259             * @param name
260             *            The setter method for the display name.
261             */
262            public void setLocationName(String name) {
263                    this.locationName = name;
264            }
265    
266            /**
267             * @return Returns the timeZone.
268             */
269            public TimeZone getTimeZone() {
270                    return timeZone;
271            }
272    
273            /**
274             * Method to set the TimeZone. If this is ever set after the GeoLocation is
275             * set in the {@link net.sourceforge.zmanim.AstronomicalCalendar}, it is
276             * critical that
277             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.{@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)}
278             * be called in order for the AstronomicalCalendar to output times in the
279             * expected offset. This situation will arise if the AstronomicalCalendar is
280             * ever {@link net.sourceforge.zmanim.AstronomicalCalendar#clone() cloned}.
281             * 
282             * @param timeZone
283             *            The timeZone to set.
284             */
285            public void setTimeZone(TimeZone timeZone) {
286                    this.timeZone = timeZone;
287            }
288    
289            /**
290             * A method that will return the location's local mean time offset in
291             * milliseconds from local standard time. The globe is split into 360&deg;,
292             * with 15&deg; per hour of the day. For a local that is at a longitude that
293             * is evenly divisible by 15 (longitude % 15 == 0), at solar
294             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getSunTransit() noon}
295             * (with adjustment for the <a
296             * href="http://en.wikipedia.org/wiki/Equation_of_time">equation of time</a>)
297             * the sun should be directly overhead, so a user who is 1&deg; west of this
298             * will have noon at 4 minutes after standard time noon, and conversely, a
299             * user who is 1&deg; east of the 15&deg longitude will have noon at 11:56
300             * AM. The offset returned does not account for the <a
301             * href="http://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving
302             * time</a> offset since this class is unaware of dates.
303             * 
304             * @return the offset in milliseconds not accounting for Daylight saving
305             *         time. A positive value will be returned East of the timezone
306             *         line, and a negative value West of it.
307             * @since 1.1
308             */
309            public long getLocalMeanTimeOffset() {
310                    return (long) (getLongitude() * 4 * MINUTE_MILLIS - getTimeZone()
311                                    .getRawOffset());
312            }
313    
314            /**
315             * Calculate the initial <a
316             * href="http://en.wikipedia.org/wiki/Great_circle">geodesic</a> bearing
317             * between this Object and a second Object passed to this method using <a
318             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
319             * inverse formula See T Vincenty, "<a
320             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
321             * Solutions of Geodesics on the Ellipsoid with application of nested
322             * equations</a>", Survey Review, vol XXII no 176, 1975
323             * 
324             * @param location
325             *            the destination location
326             */
327            public double getGeodesicInitialBearing(GeoLocation location) {
328                    return vincentyFormula(location, INITIAL_BEARING);
329            }
330    
331            /**
332             * Calculate the final <a
333             * href="http://en.wikipedia.org/wiki/Great_circle">geodesic</a> bearing
334             * between this Object and a second Object passed to this method using <a
335             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
336             * inverse formula See T Vincenty, "<a
337             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
338             * Solutions of Geodesics on the Ellipsoid with application of nested
339             * equations</a>", Survey Review, vol XXII no 176, 1975
340             * 
341             * @param location
342             *            the destination location
343             */
344            public double getGeodesicFinalBearing(GeoLocation location) {
345                    return vincentyFormula(location, FINAL_BEARING);
346            }
347    
348            /**
349             * Calculate <a
350             * href="http://en.wikipedia.org/wiki/Great-circle_distance">geodesic
351             * distance</a> in Meters between this Object and a second Object passed to
352             * this method using <a
353             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
354             * inverse formula See T Vincenty, "<a
355             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
356             * Solutions of Geodesics on the Ellipsoid with application of nested
357             * equations</a>", Survey Review, vol XXII no 176, 1975
358             * 
359             * @param location
360             *            the destination location
361             */
362            public double getGeodesicDistance(GeoLocation location) {
363                    return vincentyFormula(location, DISTANCE);
364            }
365    
366            /**
367             * Calculate <a
368             * href="http://en.wikipedia.org/wiki/Great-circle_distance">geodesic
369             * distance</a> in Meters between this Object and a second Object passed to
370             * this method using <a
371             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
372             * inverse formula See T Vincenty, "<a
373             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
374             * Solutions of Geodesics on the Ellipsoid with application of nested
375             * equations</a>", Survey Review, vol XXII no 176, 1975
376             * 
377             * @param location
378             *            the destination location
379             * @param formula
380             *            This formula calculates initial bearing ({@link #INITIAL_BEARING}),
381             *            final bearing ({@link #FINAL_BEARING}) and distance ({@link #DISTANCE}).
382             */
383            private double vincentyFormula(GeoLocation location, int formula) {
384                    double a = 6378137;
385                    double b = 6356752.3142;
386                    double f = 1 / 298.257223563; // WGS-84 ellipsiod
387                    double L = Math.toRadians(location.getLongitude() - getLongitude());
388                    double U1 = Math
389                                    .atan((1 - f) * Math.tan(Math.toRadians(getLatitude())));
390                    double U2 = Math.atan((1 - f)
391                                    * Math.tan(Math.toRadians(location.getLatitude())));
392                    double sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
393                    double sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
394    
395                    double lambda = L;
396                    double lambdaP = 2 * Math.PI;
397                    double iterLimit = 20;
398                    double sinLambda = 0;
399                    double cosLambda = 0;
400                    double sinSigma = 0;
401                    double cosSigma = 0;
402                    double sigma = 0;
403                    double sinAlpha = 0;
404                    double cosSqAlpha = 0;
405                    double cos2SigmaM = 0;
406                    double C;
407                    while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0) {
408                            sinLambda = Math.sin(lambda);
409                            cosLambda = Math.cos(lambda);
410                            sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda)
411                                            + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)
412                                            * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));
413                            if (sinSigma == 0)
414                                    return 0; // co-incident points
415                            cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
416                            sigma = Math.atan2(sinSigma, cosSigma);
417                            sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
418                            cosSqAlpha = 1 - sinAlpha * sinAlpha;
419                            cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
420                            if (Double.isNaN(cos2SigmaM))
421                                    cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6)
422                            C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));
423                            lambdaP = lambda;
424                            lambda = L
425                                            + (1 - C)
426                                            * f
427                                            * sinAlpha
428                                            * (sigma + C
429                                                            * sinSigma
430                                                            * (cos2SigmaM + C * cosSigma
431                                                                            * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
432                    }
433                    if (iterLimit == 0)
434                            return Double.NaN; // formula failed to converge
435    
436                    double uSq = cosSqAlpha * (a * a - b * b) / (b * b);
437                    double A = 1 + uSq / 16384
438                                    * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
439                    double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
440                    double deltaSigma = B
441                                    * sinSigma
442                                    * (cos2SigmaM + B
443                                                    / 4
444                                                    * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B
445                                                                    / 6 * cos2SigmaM
446                                                                    * (-3 + 4 * sinSigma * sinSigma)
447                                                                    * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
448                    double distance = b * A * (sigma - deltaSigma);
449    
450                    // initial bearing
451                    double fwdAz = Math.toDegrees(Math.atan2(cosU2 * sinLambda, cosU1
452                                    * sinU2 - sinU1 * cosU2 * cosLambda));
453                    // final bearing
454                    double revAz = Math.toDegrees(Math.atan2(cosU1 * sinLambda, -sinU1
455                                    * cosU2 + cosU1 * sinU2 * cosLambda));
456                    if (formula == DISTANCE) {
457                            return distance;
458                    } else if (formula == INITIAL_BEARING) {
459                            return fwdAz;
460                    } else if (formula == FINAL_BEARING) {
461                            return revAz;
462                    } else { // should never happpen
463                            return Double.NaN;
464                    }
465            }
466    
467            /**
468             * Returns the <a href="http://en.wikipedia.org/wiki/Rhumb_line">rhumb line</a>
469             * bearing from the current location to the GeoLocation passed in.
470             * 
471             * @param location
472             *            destination location
473             * @return the bearing in degrees
474             */
475            public double getRhumbLineBearing(GeoLocation location) {
476                    double dLon = Math.toRadians(location.getLongitude() - getLongitude());
477                    double dPhi = Math.log(Math.tan(Math.toRadians(location.getLatitude())
478                                    / 2 + Math.PI / 4)
479                                    / Math.tan(Math.toRadians(getLatitude()) / 2 + Math.PI / 4));
480                    if (Math.abs(dLon) > Math.PI)
481                            dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon);
482                    return Math.toDegrees(Math.atan2(dLon, dPhi));
483            }
484    
485            /**
486             * Returns the <a href="http://en.wikipedia.org/wiki/Rhumb_line">rhumb line</a>
487             * distance from the current location to the GeoLocation passed in.
488             * 
489             * @param location
490             *            the destination location
491             * @return the distance in Meters
492             */
493            public double getRhumbLineDistance(GeoLocation location) {
494                    double R = 6371; // earth's mean radius in km
495                    double dLat = Math.toRadians(location.getLatitude() - getLatitude());
496                    double dLon = Math.toRadians(Math.abs(location.getLongitude()
497                                    - getLongitude()));
498                    double dPhi = Math.log(Math.tan(Math.toRadians(location.getLongitude())
499                                    / 2 + Math.PI / 4)
500                                    / Math.tan(Math.toRadians(getLatitude()) / 2 + Math.PI / 4));
501                    double q = (Math.abs(dLat) > 1e-10) ? dLat / dPhi : Math.cos(Math
502                                    .toRadians(getLatitude()));
503                    // if dLon over 180° take shorter rhumb across 180° meridian:
504                    if (dLon > Math.PI)
505                            dLon = 2 * Math.PI - dLon;
506                    double d = Math.sqrt(dLat * dLat + q * q * dLon * dLon);
507                    return d * R;
508            }
509    
510            /**
511             * A method that returns an XML formatted <code>String</code> representing
512             * the serialized <code>Object</code>. Very similar to the toString
513             * method but the return value is in an xml format. The format currently
514             * used (subject to change) is:
515             * 
516             * <pre>
517             *   &lt;GeoLocation&gt;
518             *       &lt;LocationName&gt;Lakewood, NJ&lt;/LocationName&gt;
519             *       &lt;Latitude&gt;40.0828&amp;deg&lt;/Latitude&gt;
520             *       &lt;Longitude&gt;-74.2094&amp;deg&lt;/Longitude&gt;
521             *       &lt;Elevation&gt;0 Meters&lt;/Elevation&gt;
522             *       &lt;TimezoneName&gt;America/New_York&lt;/TimezoneName&gt;
523             *       &lt;TimeZoneDisplayName&gt;Eastern Standard Time&lt;/TimeZoneDisplayName&gt;
524             *       &lt;TimezoneGMTOffset&gt;-5&lt;/TimezoneGMTOffset&gt;
525             *       &lt;TimezoneDSTOffset&gt;1&lt;/TimezoneDSTOffset&gt;
526             *   &lt;/GeoLocation&gt;
527             * </pre>
528             * 
529             * @return The XML formatted <code>String</code>.
530             */
531            public String toXML() {
532                    StringBuffer sb = new StringBuffer();
533                    sb.append("<GeoLocation>\n");
534                    sb.append("\t<LocationName>").append(getLocationName()).append(
535                                    "</LocationName>\n");
536                    sb.append("\t<Latitude>").append(getLatitude()).append("&deg;").append(
537                                    "</Latitude>\n");
538                    sb.append("\t<Longitude>").append(getLongitude()).append("&deg;")
539                                    .append("</Longitude>\n");
540                    sb.append("\t<Elevation>").append(getElevation()).append(" Meters")
541                                    .append("</Elevation>\n");
542                    sb.append("\t<TimezoneName>").append(getTimeZone().getID()).append(
543                                    "</TimezoneName>\n");
544                    sb.append("\t<TimeZoneDisplayName>").append(
545                                    getTimeZone().getDisplayName()).append(
546                                    "</TimeZoneDisplayName>\n");
547                    sb.append("\t<TimezoneGMTOffset>").append(
548                                    getTimeZone().getRawOffset() / HOUR_MILLIS).append(
549                                    "</TimezoneGMTOffset>\n");
550                    sb.append("\t<TimezoneDSTOffset>").append(
551                                    getTimeZone().getDSTSavings() / HOUR_MILLIS).append(
552                                    "</TimezoneDSTOffset>\n");
553                    sb.append("</GeoLocation>");
554                    return sb.toString();
555            }
556    
557            /**
558             * @see java.lang.Object#equals(Object)
559             */
560            public boolean equals(Object object) {
561                    if (this == object)
562                            return true;
563                    if (!(object instanceof GeoLocation))
564                            return false;
565                    GeoLocation geo = (GeoLocation) object;
566                    return Double.doubleToLongBits(latitude) == Double
567                                    .doubleToLongBits(geo.latitude)
568                                    && Double.doubleToLongBits(longitude) == Double
569                                                    .doubleToLongBits(geo.longitude)
570                                    && elevation == geo.elevation
571                                    && (locationName == null ? geo.locationName == null
572                                                    : locationName.equals(geo.locationName))
573                                    && (timeZone == null ? geo.timeZone == null : timeZone
574                                                    .equals(geo.timeZone));
575            }
576    
577            /**
578             * @see java.lang.Object#hashCode()
579             */
580            public int hashCode() {
581    
582                    int result = 17;
583                    long latLong = Double.doubleToLongBits(latitude);
584                    long lonLong = Double.doubleToLongBits(longitude);
585                    long elevLong = Double.doubleToLongBits(elevation);
586                    int latInt = (int) (latLong ^ (latLong >>> 32));
587                    int lonInt = (int) (lonLong ^ (lonLong >>> 32));
588                    int elevInt = (int) (elevLong ^ (elevLong >>> 32));
589                    result = 37 * result + getClass().hashCode();
590                    result += 37 * result + latInt;
591                    result += 37 * result + lonInt;
592                    result += 37 * result + elevInt;
593                    result += 37 * result
594                                    + (locationName == null ? 0 : locationName.hashCode());
595                    result += 37 * result + (timeZone == null ? 0 : timeZone.hashCode());
596                    return result;
597            }
598    
599            /**
600             * @see java.lang.Object#toString()
601             */
602            public String toString() {
603                    StringBuffer sb = new StringBuffer();
604                    sb.append("\nLocation Name:\t\t\t").append(getLocationName());
605                    sb.append("\nLatitude:\t\t\t").append(getLatitude()).append("&deg;");
606                    sb.append("\nLongitude:\t\t\t").append(getLongitude()).append("&deg;");
607                    sb.append("\nElevation:\t\t\t").append(getElevation())
608                                    .append(" Meters");
609                    sb.append("\nTimezone Name:\t\t\t").append(getTimeZone().getID());
610                    /*
611                     * sb.append("\nTimezone Display Name:\t\t").append(
612                     * getTimeZone().getDisplayName());
613                     */
614                    sb.append("\nTimezone GMT Offset:\t\t").append(
615                                    getTimeZone().getRawOffset() / HOUR_MILLIS);
616                    sb.append("\nTimezone DST Offset:\t\t").append(
617                                    getTimeZone().getDSTSavings() / HOUR_MILLIS);
618                    return sb.toString();
619            }
620    
621            /**
622             * An implementation of the {@link java.lang.Object#clone()} method that
623             * creates a <a
624             * href="http://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a>
625             * of the object. <br/><b>Note:</b> If the {@link java.util.TimeZone} in
626             * the clone will be changed from the original, it is critical that
627             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.{@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)}
628             * is called after cloning in order for the AstronomicalCalendar to output
629             * times in the expected offset.
630             * 
631             * @see java.lang.Object#clone()
632             * @since 1.1
633             */
634            public Object clone() {
635                    GeoLocation clone = null;
636                    try {
637                            clone = (GeoLocation) super.clone();
638                    } catch (CloneNotSupportedException cnse) {
639                            System.out
640                                            .print("Required by the compiler. Should never be reached since we implement clone()");
641                    }
642                    clone.timeZone = (TimeZone) getTimeZone().clone();
643                    clone.locationName = (String) getLocationName();
644                    return clone;
645            }
646    }