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 "Lakewood,
075 * NJ"
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 "Lakewood,
098 * NJ"
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° and 90°. 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° and 180°. 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°,
292 * with 15° 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° west of this
298 * will have noon at 4 minutes after standard time noon, and conversely, a
299 * user who is 1° east of the 15° 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 * <GeoLocation>
518 * <LocationName>Lakewood, NJ</LocationName>
519 * <Latitude>40.0828&deg</Latitude>
520 * <Longitude>-74.2094&deg</Longitude>
521 * <Elevation>0 Meters</Elevation>
522 * <TimezoneName>America/New_York</TimezoneName>
523 * <TimeZoneDisplayName>Eastern Standard Time</TimeZoneDisplayName>
524 * <TimezoneGMTOffset>-5</TimezoneGMTOffset>
525 * <TimezoneDSTOffset>1</TimezoneDSTOffset>
526 * </GeoLocation>
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("°").append(
537 "</Latitude>\n");
538 sb.append("\t<Longitude>").append(getLongitude()).append("°")
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("°");
606 sb.append("\nLongitude:\t\t\t").append(getLongitude()).append("°");
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 }