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().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 * <GeoLocation>
517 * <LocationName>Lakewood, NJ</LocationName>
518 * <Latitude>40.0828&deg</Latitude>
519 * <Longitude>-74.2094&deg</Longitude>
520 * <Elevation>0 Meters</Elevation>
521 * <TimezoneName>America/New_York</TimezoneName>
522 * <TimeZoneDisplayName>Eastern Standard Time</TimeZoneDisplayName>
523 * <TimezoneGMTOffset>-5</TimezoneGMTOffset>
524 * <TimezoneDSTOffset>1</TimezoneDSTOffset>
525 * </GeoLocation>
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("°").append(
536 "</Latitude>\n");
537 sb.append("\t<Longitude>").append(getLongitude()).append("°")
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(latitude) == Double
566 .doubleToLongBits(geo.latitude)
567 && Double.doubleToLongBits(longitude) == Double
568 .doubleToLongBits(geo.longitude)
569 && elevation == geo.elevation
570 && (locationName == null ? geo.locationName == null
571 : locationName.equals(geo.locationName))
572 && (timeZone == null ? geo.timeZone == null : 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(latitude);
583 long lonLong = Double.doubleToLongBits(longitude);
584 long elevLong = Double.doubleToLongBits(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 + (locationName == null ? 0 : locationName.hashCode());
594 result += 37 * result + (timeZone == null ? 0 : 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("°");
605 sb.append("\nLongitude:\t\t\t").append(getLongitude()).append("°");
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 }