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
038 /** constant for milliseconds in a minute (60,000) */
039 private static final long MINUTE_MILLIS = 60 * 1000;
040
041 /** constant for milliseconds in an hour (3,600,000) */
042 private static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
043
044 /**
045 * Method to get the elevation in Meters.
046 *
047 * @return Returns the elevation in Meters.
048 */
049 public double getElevation() {
050 return elevation;
051 }
052
053 /**
054 * Method to set the elevation in Meters <b>above </b> sea level.
055 *
056 * @param elevation
057 * The elevation to set in Meters. An IllegalArgumentException
058 * will be thrown if the value is a negative.
059 */
060 public void setElevation(double elevation) {
061 if (elevation < 0) {
062 throw new IllegalArgumentException("Elevation cannot be negative");
063 }
064 this.elevation = elevation;
065 }
066
067 /**
068 * GeoLocation constructor with parameters for all required fields.
069 *
070 * @param name
071 * The location name for display use such as "Lakewood,
072 * NJ"
073 * @param latitude
074 * the latitude in a double format such as 40.095965 for
075 * Lakewood, NJ
076 * @param longitude
077 * double the longitude in a double format such as -74.222130 for
078 * Lakewood, NJ. <br/> <b>Note: </b> For longitudes east of the
079 * <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
080 * Meridian </a> (Greenwich), a negative value should be used.
081 * @param timeZone
082 * the <code>TimeZone</code> for the location.
083 */
084 public GeoLocation(String name, double latitude, double longitude,
085 TimeZone timeZone) {
086 this(name, latitude, longitude, 0, timeZone);
087 this.setLocationName(name);
088 this.setLatitude(latitude);
089 this.setLongitude(longitude);
090 this.setTimeZone(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
102 * @param longitude
103 * double the longitude in a double format such as -74.222130 for
104 * Lakewood, NJ. <br/> <b>Note: </b> For longitudes east of the
105 * <a href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
106 * Meridian </a> (Greenwich), a negative value should be used.
107 * @param elevation
108 * the elevation above sea level in Meters. Elevation is not used
109 * in most algorithms used for calculating sunrise and set.
110 * @param timeZone
111 * the <code>TimeZone</code> for the location.
112 */
113 public GeoLocation(String name, double latitude, double longitude,
114 double elevation, TimeZone timeZone) {
115 setLocationName(name);
116 setLatitude(latitude);
117 setLongitude(longitude);
118 setElevation(elevation);
119 setTimeZone(timeZone);
120 }
121
122 /**
123 * Default GeoLocation constructor will set location to the Prime Meridian
124 * at Greenwich, England and a TimeZone of GMT. The longitude will be set to
125 * 0 and the latitude will be 51.4772 to match the location of the <a
126 * href="http://www.rog.nmm.ac.uk">Royal Observatory, Greenwich </a>. No
127 * daylight savings time will be used.
128 */
129 public GeoLocation() {
130 setLocationName("Greenwich, England");
131 setLongitude(0); // added for clarity
132 setLatitude(51.4772);
133 setTimeZone(TimeZone.getTimeZone("GMT"));
134 }
135
136 /**
137 * Method to set the latitude in a double format.
138 *
139 * @param latitude
140 * The degrees of latitude to set in a double format between
141 * -90° and 90°. An IllegalArgumentException will be
142 * thrown if the value exceeds the limit. For example 40.095965
143 * would be used for Lakewood, NJ.
144 */
145 public void setLatitude(double latitude) {
146 if (latitude > 90 || latitude < -90) {
147 throw new IllegalArgumentException(
148 "Latitude must be between -90 and 90");
149 }
150 this.latitude = latitude;
151 }
152
153 /**
154 * Method to set the latitude in degrees, minutes and seconds.
155 *
156 * @param degrees
157 * The degrees of latitude to set between -90 and 90. An
158 * IllegalArgumentException will be thrown if the value exceeds
159 * the limit. For example 40 would be used for Lakewood, NJ.
160 * @param minutes
161 * @param seconds
162 * @param direction
163 * N for north and S for south. An IllegalArgumentException will
164 * be thrown if the value is not S or N.
165 */
166 public void setLatitude(int degrees, int minutes, double seconds,
167 String direction) {
168 double tempLat = degrees + ((minutes + (seconds / 60.0)) / 60.0);
169 if (tempLat > 90 || tempLat < 0) {
170 throw new IllegalArgumentException(
171 "Latitude must be between 0 and 90. Use direction of S instead of negative.");
172 }
173 if (direction.equals("S")) {
174 tempLat *= -1;
175 } else if (!direction.equals("N")) {
176 throw new IllegalArgumentException(
177 "Latitude direction must be N or S");
178 }
179 this.latitude = tempLat;
180 }
181
182 /**
183 * @return Returns the latitude.
184 */
185 public double getLatitude() {
186 return latitude;
187 }
188
189 /**
190 * Method to set the longitude in a double format.
191 *
192 * @param longitude
193 * The degrees of longitude to set in a double format between
194 * -180° and 180°. An IllegalArgumentException will be
195 * thrown if the value exceeds the limit. For example -74.2094
196 * would be used for Lakewood, NJ. Note: for longitudes east of
197 * the <a
198 * href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
199 * Meridian</a> (Greenwich) a negative value should be used.
200 */
201 public void setLongitude(double longitude) {
202 if (longitude > 180 || longitude < -180) {
203 throw new IllegalArgumentException(
204 "Longitude must be between -180 and 180");
205 }
206 this.longitude = longitude;
207 }
208
209 /**
210 * Method to set the longitude in degrees, minutes and seconds.
211 *
212 * @param degrees
213 * The degrees of longitude to set between -180 and 180. An
214 * IllegalArgumentException will be thrown if the value exceeds
215 * the limit. For example -74 would be used for Lakewood, NJ.
216 * Note: for longitudes east of the <a
217 * href="http://en.wikipedia.org/wiki/Prime_Meridian">Prime
218 * Meridian </a> (Greenwich) a negative value should be used.
219 * @param minutes
220 * @param seconds
221 * @param direction
222 * E for east of the Prime Meridian or W for west of it. An
223 * IllegalArgumentException will be thrown if the value is not E
224 * or W.
225 */
226 public void setLongitude(int degrees, int minutes, double seconds,
227 String direction) {
228 double longTemp = degrees + ((minutes + (seconds / 60.0)) / 60.0);
229 if (longTemp > 180 || longitude < 0) {
230 throw new IllegalArgumentException(
231 "Longitude must be between 0 and 180. Use the ");
232 }
233 if (direction.equals("W")) {
234 longTemp *= -1;
235 } else if (!direction.equals("E")) {
236 throw new IllegalArgumentException(
237 "Longitude direction must be E or W");
238 }
239 this.longitude = longTemp;
240 }
241
242 /**
243 * @return Returns the longitude.
244 */
245 public double getLongitude() {
246 return longitude;
247 }
248
249 /**
250 * @return Returns the location name.
251 */
252 public String getLocationName() {
253 return locationName;
254 }
255
256 /**
257 * @param name
258 * The setter method for the display name.
259 */
260 public void setLocationName(String name) {
261 this.locationName = name;
262 }
263
264 /**
265 * @return Returns the timeZone.
266 */
267 public TimeZone getTimeZone() {
268 return timeZone;
269 }
270
271 /**
272 * Method to set the TimeZone. If this is ever set after the GeoLocation is
273 * set in the {@link net.sourceforge.zmanim.AstronomicalCalendar}, it is
274 * critical that
275 * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.{@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)}
276 * be called in order for the AstronomicalCalendar to output times in the
277 * expected offset. This situation will arise if the AstronomicalCalendar is
278 * ever {@link net.sourceforge.zmanim.AstronomicalCalendar#clone() cloned}.
279 *
280 * @param timeZone
281 * The timeZone to set.
282 */
283 public void setTimeZone(TimeZone timeZone) {
284 this.timeZone = timeZone;
285 }
286
287 /**
288 * A method that will return the location's local mean time offset in
289 * milliseconds from local standard time. The globe is split into 360°,
290 * with 15° per hour of the day. For a local that is at a longitude that
291 * is evenly divisible by 15 (longitude % 15 == 0), at solar
292 * {@link net.sourceforge.zmanim.AstronomicalCalendar#getSunTransit() noon}
293 * (with adjustment for the <a
294 * href="http://en.wikipedia.org/wiki/Equation_of_time">equation of time</a>)
295 * the sun should be directly overhead, so a user who is 1° west of this
296 * will have noon at 4 minutes after standard time noon, and conversely, a
297 * user who is 1° east of the 15° longitude will have noon at 11:56
298 * AM. The offset returned does not account for the <a
299 * href="http://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving
300 * time</a> offset since this class is unaware of dates.
301 *
302 * @return the offset in milliseconds not accounting for Daylight saving
303 * time. A positive value will be returned East of the timezone
304 * line, and a negative value West of it.
305 * @since 1.1
306 */
307 public long getLocalMeanTimeOffset() {
308 return (long) ( getLongitude() * 4 * MINUTE_MILLIS - getTimeZone().getRawOffset());
309 }
310
311 /**
312 * A method that returns an XML formatted <code>String</code> representing
313 * the serialized <code>Object</code>. Very similar to the toString
314 * method but the return value is in an xml format. The format currently
315 * used (subject to change) is:
316 *
317 * <pre>
318 * <GeoLocation>
319 * <LocationName>Lakewood, NJ</LocationName>
320 * <Latitude>40.0828&deg</Latitude>
321 * <Longitude>-74.2094&deg</Longitude>
322 * <Elevation>0 Meters</Elevation>
323 * <TimezoneName>America/New_York</TimezoneName>
324 * <TimeZoneDisplayName>Eastern Standard Time</TimeZoneDisplayName>
325 * <TimezoneGMTOffset>-5</TimezoneGMTOffset>
326 * <TimezoneDSTOffset>1</TimezoneDSTOffset>
327 * </GeoLocation>
328 * </pre>
329 *
330 * @return The XML formatted <code>String</code>.
331 */
332 public String toXML() {
333 StringBuffer sb = new StringBuffer();
334 sb.append("<GeoLocation>\n");
335 sb.append("\t<LocationName>").append(getLocationName()).append(
336 "</LocationName>\n");
337 sb.append("\t<Latitude>").append(getLatitude()).append("°").append(
338 "</Latitude>\n");
339 sb.append("\t<Longitude>").append(getLongitude()).append("°")
340 .append("</Longitude>\n");
341 sb.append("\t<Elevation>").append(getElevation()).append(" Meters")
342 .append("</Elevation>\n");
343 sb.append("\t<TimezoneName>").append(getTimeZone().getID()).append(
344 "</TimezoneName>\n");
345 sb.append("\t<TimeZoneDisplayName>").append(
346 getTimeZone().getDisplayName()).append(
347 "</TimeZoneDisplayName>\n");
348 sb.append("\t<TimezoneGMTOffset>").append(
349 getTimeZone().getRawOffset() / HOUR_MILLIS).append(
350 "</TimezoneGMTOffset>\n");
351 sb.append("\t<TimezoneDSTOffset>").append(
352 getTimeZone().getDSTSavings() / HOUR_MILLIS).append(
353 "</TimezoneDSTOffset>\n");
354 sb.append("</GeoLocation>");
355 return sb.toString();
356 }
357
358 /**
359 * @see java.lang.Object#equals(Object)
360 */
361 public boolean equals(Object object) {
362 if (this == object)
363 return true;
364 if (!(object instanceof GeoLocation))
365 return false;
366 GeoLocation geo = (GeoLocation) object;
367 return Double.doubleToLongBits(latitude) == Double
368 .doubleToLongBits(geo.latitude)
369 && Double.doubleToLongBits(longitude) == Double
370 .doubleToLongBits(geo.longitude)
371 && elevation == geo.elevation
372 && (locationName == null ? geo.locationName == null
373 : locationName.equals(geo.locationName))
374 && (timeZone == null ? geo.timeZone == null : timeZone
375 .equals(geo.timeZone));
376 }
377
378 /**
379 * @see java.lang.Object#hashCode()
380 */
381 public int hashCode() {
382
383 int result = 17;
384 long latLong = Double.doubleToLongBits(latitude);
385 long lonLong = Double.doubleToLongBits(longitude);
386 long elevLong = Double.doubleToLongBits(elevation);
387 int latInt = (int) (latLong ^ (latLong >>> 32));
388 int lonInt = (int) (lonLong ^ (lonLong >>> 32));
389 int elevInt = (int) (elevLong ^ (elevLong >>> 32));
390 result = 37 * result + getClass().hashCode();
391 result += 37 * result + latInt;
392 result += 37 * result + lonInt;
393 result += 37 * result + elevInt;
394 result += 37 * result
395 + (locationName == null ? 0 : locationName.hashCode());
396 result += 37 * result + (timeZone == null ? 0 : timeZone.hashCode());
397 return result;
398 }
399
400 /**
401 * @see java.lang.Object#toString()
402 */
403 public String toString() {
404 StringBuffer sb = new StringBuffer();
405 sb.append("\nLocation Name:\t\t\t").append(getLocationName());
406 sb.append("\nLatitude:\t\t\t").append(getLatitude()).append("°");
407 sb.append("\nLongitude:\t\t\t").append(getLongitude()).append("°");
408 sb.append("\nElevation:\t\t\t").append(getElevation())
409 .append(" Meters");
410 sb.append("\nTimezone Name:\t\t\t").append(getTimeZone().getID());
411 /*
412 * sb.append("\nTimezone Display Name:\t\t").append(
413 * getTimeZone().getDisplayName());
414 */
415 sb.append("\nTimezone GMT Offset:\t\t").append(
416 getTimeZone().getRawOffset() / HOUR_MILLIS);
417 sb.append("\nTimezone DST Offset:\t\t").append(
418 getTimeZone().getDSTSavings() / HOUR_MILLIS);
419 return sb.toString();
420 }
421
422 /**
423 * An implementation of the {@link java.lang.Object#clone()} method that
424 * creates a <a
425 * href="http://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a>
426 * of the object. <br/><b>Note:</b> If the {@link java.util.TimeZone} in
427 * the clone will be changed from the original, it is critical that
428 * {@link net.sourceforge.zmanim.AstronomicalCalendar#getCalendar()}.{@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)}
429 * is called after cloning in order for the AstronomicalCalendar to output
430 * times in the expected offset.
431 *
432 * @see java.lang.Object#clone()
433 * @since 1.1
434 */
435 public Object clone() {
436 GeoLocation clone = null;
437 try {
438 clone = (GeoLocation) super.clone();
439 } catch (CloneNotSupportedException cnse) {
440 System.out
441 .print("Required by the compiler. Should never be reached since we implement clone()");
442 }
443 clone.timeZone = (TimeZone) getTimeZone().clone();
444 clone.locationName = (String) getLocationName();
445 return clone;
446 }
447 }