001    /*
002     * Zmanim Java API
003     * Copyright (C) 2004-2011 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 - 2011
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 static final int DISTANCE = 0;
038            private static final int INITIAL_BEARING = 1;
039            private static final 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 this.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 this.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 || this.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 this.longitude;
249            }
250    
251            /**
252             * @return Returns the location name.
253             */
254            public String getLocationName() {
255                    return this.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 this.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().getRawOffset());
311            }
312    
313            /**
314             * Calculate the initial <a
315             * href="http://en.wikipedia.org/wiki/Great_circle">geodesic</a> bearing
316             * between this Object and a second Object passed to this method using <a
317             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
318             * inverse formula See T Vincenty, "<a
319             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
320             * Solutions of Geodesics on the Ellipsoid with application of nested
321             * equations</a>", Survey Review, vol XXII no 176, 1975
322             * 
323             * @param location
324             *            the destination location
325             */
326            public double getGeodesicInitialBearing(GeoLocation location) {
327                    return vincentyFormula(location, INITIAL_BEARING);
328            }
329    
330            /**
331             * Calculate the final <a
332             * href="http://en.wikipedia.org/wiki/Great_circle">geodesic</a> bearing
333             * between this Object and a second Object passed to this method using <a
334             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
335             * inverse formula See T Vincenty, "<a
336             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
337             * Solutions of Geodesics on the Ellipsoid with application of nested
338             * equations</a>", Survey Review, vol XXII no 176, 1975
339             * 
340             * @param location
341             *            the destination location
342             */
343            public double getGeodesicFinalBearing(GeoLocation location) {
344                    return vincentyFormula(location, FINAL_BEARING);
345            }
346    
347            /**
348             * Calculate <a
349             * href="http://en.wikipedia.org/wiki/Great-circle_distance">geodesic
350             * distance</a> in Meters between this Object and a second Object passed to
351             * this method using <a
352             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
353             * inverse formula See T Vincenty, "<a
354             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
355             * Solutions of Geodesics on the Ellipsoid with application of nested
356             * equations</a>", Survey Review, vol XXII no 176, 1975
357             * 
358             * @param location
359             *            the destination location
360             */
361            public double getGeodesicDistance(GeoLocation location) {
362                    return vincentyFormula(location, DISTANCE);
363            }
364    
365            /**
366             * Calculate <a
367             * href="http://en.wikipedia.org/wiki/Great-circle_distance">geodesic
368             * distance</a> in Meters between this Object and a second Object passed to
369             * this method using <a
370             * href="http://en.wikipedia.org/wiki/Thaddeus_Vincenty">Thaddeus Vincenty's</a>
371             * inverse formula See T Vincenty, "<a
372             * href="http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf">Direct and Inverse
373             * Solutions of Geodesics on the Ellipsoid with application of nested
374             * equations</a>", Survey Review, vol XXII no 176, 1975
375             * 
376             * @param location
377             *            the destination location
378             * @param formula
379             *            This formula calculates initial bearing ({@link #INITIAL_BEARING}),
380             *            final bearing ({@link #FINAL_BEARING}) and distance ({@link #DISTANCE}).
381             */
382            private double vincentyFormula(GeoLocation location, int formula) {
383                    double a = 6378137;
384                    double b = 6356752.3142;
385                    double f = 1 / 298.257223563; // WGS-84 ellipsiod
386                    double L = Math.toRadians(location.getLongitude() - getLongitude());
387                    double U1 = Math
388                                    .atan((1 - f) * Math.tan(Math.toRadians(getLatitude())));
389                    double U2 = Math.atan((1 - f)
390                                    * Math.tan(Math.toRadians(location.getLatitude())));
391                    double sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
392                    double sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
393    
394                    double lambda = L;
395                    double lambdaP = 2 * Math.PI;
396                    double iterLimit = 20;
397                    double sinLambda = 0;
398                    double cosLambda = 0;
399                    double sinSigma = 0;
400                    double cosSigma = 0;
401                    double sigma = 0;
402                    double sinAlpha = 0;
403                    double cosSqAlpha = 0;
404                    double cos2SigmaM = 0;
405                    double C;
406                    while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0) {
407                            sinLambda = Math.sin(lambda);
408                            cosLambda = Math.cos(lambda);
409                            sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda)
410                                            + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)
411                                            * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));
412                            if (sinSigma == 0)
413                                    return 0; // co-incident points
414                            cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
415                            sigma = Math.atan2(sinSigma, cosSigma);
416                            sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
417                            cosSqAlpha = 1 - sinAlpha * sinAlpha;
418                            cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
419                            if (Double.isNaN(cos2SigmaM))
420                                    cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6)
421                            C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));
422                            lambdaP = lambda;
423                            lambda = L
424                                            + (1 - C)
425                                            * f
426                                            * sinAlpha
427                                            * (sigma + C
428                                                            * sinSigma
429                                                            * (cos2SigmaM + C * cosSigma
430                                                                            * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
431                    }
432                    if (iterLimit == 0)
433                            return Double.NaN; // formula failed to converge
434    
435                    double uSq = cosSqAlpha * (a * a - b * b) / (b * b);
436                    double A = 1 + uSq / 16384
437                                    * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
438                    double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
439                    double deltaSigma = B
440                                    * sinSigma
441                                    * (cos2SigmaM + B
442                                                    / 4
443                                                    * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B
444                                                                    / 6 * cos2SigmaM
445                                                                    * (-3 + 4 * sinSigma * sinSigma)
446                                                                    * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
447                    double distance = b * A * (sigma - deltaSigma);
448    
449                    // initial bearing
450                    double fwdAz = Math.toDegrees(Math.atan2(cosU2 * sinLambda, cosU1
451                                    * sinU2 - sinU1 * cosU2 * cosLambda));
452                    // final bearing
453                    double revAz = Math.toDegrees(Math.atan2(cosU1 * sinLambda, -sinU1
454                                    * cosU2 + cosU1 * sinU2 * cosLambda));
455                    if (formula == DISTANCE) {
456                            return distance;
457                    } else if (formula == INITIAL_BEARING) {
458                            return fwdAz;
459                    } else if (formula == FINAL_BEARING) {
460                            return revAz;
461                    } else { // should never happpen
462                            return Double.NaN;
463                    }
464            }
465    
466            /**
467             * Returns the <a href="http://en.wikipedia.org/wiki/Rhumb_line">rhumb line</a>
468             * bearing from the current location to the GeoLocation passed in.
469             * 
470             * @param location
471             *            destination location
472             * @return the bearing in degrees
473             */
474            public double getRhumbLineBearing(GeoLocation location) {
475                    double dLon = Math.toRadians(location.getLongitude() - getLongitude());
476                    double dPhi = Math.log(Math.tan(Math.toRadians(location.getLatitude())
477                                    / 2 + Math.PI / 4)
478                                    / Math.tan(Math.toRadians(getLatitude()) / 2 + Math.PI / 4));
479                    if (Math.abs(dLon) > Math.PI)
480                            dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon);
481                    return Math.toDegrees(Math.atan2(dLon, dPhi));
482            }
483    
484            /**
485             * Returns the <a href="http://en.wikipedia.org/wiki/Rhumb_line">rhumb line</a>
486             * distance from the current location to the GeoLocation passed in.
487             * 
488             * @param location
489             *            the destination location
490             * @return the distance in Meters
491             */
492            public double getRhumbLineDistance(GeoLocation location) {
493                    double R = 6371; // earth's mean radius in km
494                    double dLat = Math.toRadians(location.getLatitude() - getLatitude());
495                    double dLon = Math.toRadians(Math.abs(location.getLongitude()
496                                    - getLongitude()));
497                    double dPhi = Math.log(Math.tan(Math.toRadians(location.getLongitude())
498                                    / 2 + Math.PI / 4)
499                                    / Math.tan(Math.toRadians(getLatitude()) / 2 + Math.PI / 4));
500                    double q = (Math.abs(dLat) > 1e-10) ? dLat / dPhi : Math.cos(Math
501                                    .toRadians(getLatitude()));
502                    // if dLon over 180° take shorter rhumb across 180° meridian:
503                    if (dLon > Math.PI)
504                            dLon = 2 * Math.PI - dLon;
505                    double d = Math.sqrt(dLat * dLat + q * q * dLon * dLon);
506                    return d * R;
507            }
508    
509            /**
510             * A method that returns an XML formatted <code>String</code> representing
511             * the serialized <code>Object</code>. Very similar to the toString
512             * method but the return value is in an xml format. The format currently
513             * used (subject to change) is:
514             * 
515             * <pre>
516             *   &lt;GeoLocation&gt;
517             *       &lt;LocationName&gt;Lakewood, NJ&lt;/LocationName&gt;
518             *       &lt;Latitude&gt;40.0828&amp;deg&lt;/Latitude&gt;
519             *       &lt;Longitude&gt;-74.2094&amp;deg&lt;/Longitude&gt;
520             *       &lt;Elevation&gt;0 Meters&lt;/Elevation&gt;
521             *       &lt;TimezoneName&gt;America/New_York&lt;/TimezoneName&gt;
522             *       &lt;TimeZoneDisplayName&gt;Eastern Standard Time&lt;/TimeZoneDisplayName&gt;
523             *       &lt;TimezoneGMTOffset&gt;-5&lt;/TimezoneGMTOffset&gt;
524             *       &lt;TimezoneDSTOffset&gt;1&lt;/TimezoneDSTOffset&gt;
525             *   &lt;/GeoLocation&gt;
526             * </pre>
527             * 
528             * @return The XML formatted <code>String</code>.
529             */
530            public String toXML() {
531                    StringBuffer sb = new StringBuffer();
532                    sb.append("<GeoLocation>\n");
533                    sb.append("\t<LocationName>").append(getLocationName()).append(
534                                    "</LocationName>\n");
535                    sb.append("\t<Latitude>").append(getLatitude()).append("&deg;").append(
536                                    "</Latitude>\n");
537                    sb.append("\t<Longitude>").append(getLongitude()).append("&deg;")
538                                    .append("</Longitude>\n");
539                    sb.append("\t<Elevation>").append(getElevation()).append(" Meters")
540                                    .append("</Elevation>\n");
541                    sb.append("\t<TimezoneName>").append(getTimeZone().getID()).append(
542                                    "</TimezoneName>\n");
543                    sb.append("\t<TimeZoneDisplayName>").append(
544                                    getTimeZone().getDisplayName()).append(
545                                    "</TimeZoneDisplayName>\n");
546                    sb.append("\t<TimezoneGMTOffset>").append(
547                                    getTimeZone().getRawOffset() / HOUR_MILLIS).append(
548                                    "</TimezoneGMTOffset>\n");
549                    sb.append("\t<TimezoneDSTOffset>").append(
550                                    getTimeZone().getDSTSavings() / HOUR_MILLIS).append(
551                                    "</TimezoneDSTOffset>\n");
552                    sb.append("</GeoLocation>");
553                    return sb.toString();
554            }
555    
556            /**
557             * @see java.lang.Object#equals(Object)
558             */
559            public boolean equals(Object object) {
560                    if (this == object)
561                            return true;
562                    if (!(object instanceof GeoLocation))
563                            return false;
564                    GeoLocation geo = (GeoLocation) object;
565                    return Double.doubleToLongBits(this.latitude) == Double
566                                    .doubleToLongBits(geo.latitude)
567                                    && Double.doubleToLongBits(this.longitude) == Double
568                                                    .doubleToLongBits(geo.longitude)
569                                    && this.elevation == geo.elevation
570                                    && (this.locationName == null ? geo.locationName == null
571                                                    : this.locationName.equals(geo.locationName))
572                                    && (this.timeZone == null ? geo.timeZone == null : this.timeZone
573                                                    .equals(geo.timeZone));
574            }
575    
576            /**
577             * @see java.lang.Object#hashCode()
578             */
579            public int hashCode() {
580    
581                    int result = 17;
582                    long latLong = Double.doubleToLongBits(this.latitude);
583                    long lonLong = Double.doubleToLongBits(this.longitude);
584                    long elevLong = Double.doubleToLongBits(this.elevation);
585                    int latInt = (int) (latLong ^ (latLong >>> 32));
586                    int lonInt = (int) (lonLong ^ (lonLong >>> 32));
587                    int elevInt = (int) (elevLong ^ (elevLong >>> 32));
588                    result = 37 * result + getClass().hashCode();
589                    result += 37 * result + latInt;
590                    result += 37 * result + lonInt;
591                    result += 37 * result + elevInt;
592                    result += 37 * result
593                                    + (this.locationName == null ? 0 : this.locationName.hashCode());
594                    result += 37 * result + (this.timeZone == null ? 0 : this.timeZone.hashCode());
595                    return result;
596            }
597    
598            /**
599             * @see java.lang.Object#toString()
600             */
601            public String toString() {
602                    StringBuffer sb = new StringBuffer();
603                    sb.append("\nLocation Name:\t\t\t").append(getLocationName());
604                    sb.append("\nLatitude:\t\t\t").append(getLatitude()).append("&deg;");
605                    sb.append("\nLongitude:\t\t\t").append(getLongitude()).append("&deg;");
606                    sb.append("\nElevation:\t\t\t").append(getElevation())
607                                    .append(" Meters");
608                    sb.append("\nTimezone Name:\t\t\t").append(getTimeZone().getID());
609                    /*
610                     * sb.append("\nTimezone Display Name:\t\t").append(
611                     * getTimeZone().getDisplayName());
612                     */
613                    sb.append("\nTimezone GMT Offset:\t\t").append(
614                                    getTimeZone().getRawOffset() / HOUR_MILLIS);
615                    sb.append("\nTimezone DST Offset:\t\t").append(
616                                    getTimeZone().getDSTSavings() / HOUR_MILLIS);
617                    return sb.toString();
618            }
619    
620            /**
621             * An implementation of the {@link java.lang.Object#clone()} method that
622             * creates a <a
623             * href="http://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a>
624             * of the object. <br/><b>Note:</b> If the {@link java.util.TimeZone} in
625             * the clone will be changed from the original, it is critical that
626             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.{@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)}
627             * is called after cloning in order for the AstronomicalCalendar to output
628             * times in the expected offset.
629             * 
630             * @see java.lang.Object#clone()
631             * @since 1.1
632             */
633            public Object clone() {
634                    GeoLocation clone = null;
635                    try {
636                            clone = (GeoLocation) super.clone();
637                    } catch (CloneNotSupportedException cnse) {
638                            System.out
639                                            .print("Required by the compiler. Should never be reached since we implement clone()");
640                    }
641                    clone.timeZone = (TimeZone) getTimeZone().clone();
642                    clone.locationName = (String) getLocationName();
643                    return clone;
644            }
645    }