/**
* Info.java
* Copyright (C)2009 Nicholas Killewald
*
* This file is distributed under the terms of the BSD license.
* The source package should have a LICENSE file at the toplevel.
*/
package net.exclaimindustries.geohashdroid.util;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.annotation.NonNull;
import com.google.android.gms.maps.model.LatLng;
import net.exclaimindustries.tools.DateTools;
/**
* <p>
* An <code>Info</code> object holds all the relevant info that involves the
* map. This, for the most part, involves the final destination, the current
* date, and the graticule. It also includes utility methods for calculating
* data from this information, most importantly the distance between some
* location and the final destination.
* </p>
*
* <p>
* <code>Info</code> objects are immutable and are meant to be generated from
* HashBuilder as the last step once it has all the data it needs. It can,
* however, be built from anything else as need be.
* </p>
*
* @author Nicholas Killewald
*
*/
public class Info implements Parcelable {
/** The earliest date at which the 30W Rule is used. */
private static final Calendar LIMIT_30W = new GregorianCalendar(2008, Calendar.MAY, 26);
private double mLatitude;
private double mLongitude;
private Graticule mGraticule;
private Calendar mDate;
private boolean mRetroHash;
private boolean mValid;
/**
* Creates an Info object with the given data. That's it. If making a
* globalhash, give the latitude and longitude as the hash, not the full
* coordinates (and make sure the graticule is null).
*
* TODO: I've really got to find a better way to do globalhashes...
*
* @param latitude
* the destination's latitude, as a double
* @param longitude
* the destination's longitude, as a double
* @param graticule
* the graticule
* @param date
* the date
*/
public Info(double latitude, double longitude, @Nullable Graticule graticule,
@NonNull Calendar date) {
mLatitude = latitude;
mLongitude = longitude;
mGraticule = graticule;
setDate(date);
mValid = true;
}
/**
* Creates an Info object with the given graticule and date, but which is
* invalid (i.e. has no valid latitude/longitude data). This is used when
* StockRunner reports an error; this way, any Handler can at least figure
* out what was going on in the first place.
*
* @param graticule the graticule
* @param date the date
*/
public Info(@Nullable Graticule graticule, @NonNull Calendar date) {
mLatitude = 0;
mLongitude = 0;
mGraticule = graticule;
setDate(date);
mValid = false;
}
/**
* Deparcelizes an Info object. Obviously, this is used internally when we
* need to rebuild Info from a Parcel, such as during Service operations.
*
* @param in the parcel to deparcelize
*/
private Info(Parcel in) {
readFromParcel(in);
}
/**
* Gets the latitude of the final destination.
*
* @return the latitude
*/
public double getLatitude() {
if(mGraticule != null)
return mLatitude;
else
return mLatitude * 180 - 90;
}
/**
* Gets the longitude of the final destination.
*
* @return the longitude
*/
public double getLongitude() {
if(mGraticule != null)
return mLongitude;
else
return mLongitude * 360 - 180;
}
/**
* Gets the fractional part of the latitude of the final destination. That
* is, the part determined by the hash.
*
* @return the fractional part of the latitude
*/
public double getLatitudeHash() {
if(mGraticule != null)
return Math.abs(mLatitude) - mGraticule.getLatitude();
else
return mLatitude;
}
/**
* Gets the fractional part of the longitude of the final destination. That
* is, the part determined by the hash.
*
* @return the fractional part of the longitude
*/
public double getLongitudeHash() {
if(mGraticule != null)
return Math.abs(mLongitude) - mGraticule.getLongitude();
else
return mLongitude;
}
/**
* Returns the final destination as a LatLng object, convenient for the Maps
* v2 API.
*
* @return a LatLng based on the data obtained from the connection
*/
@NonNull
public LatLng getFinalDestinationLatLng() {
return new LatLng(getLatitude(), getLongitude());
}
/**
* Returns the final destination as a Location object, which isn't quite as
* useful as a LatLng object, but you never know, it could come in handy.
*
* @return a providerless Location based on the data obtained from the
* connection
*/
@NonNull
public Location getFinalLocation() {
Location loc = new Location("");
loc.setLatitude(getLatitude());
loc.setLongitude(getLongitude());
return loc;
}
/**
* Gets the graticule. This will be null if this is a globalhash.
*
* @return the graticule
*/
@Nullable
public Graticule getGraticule() {
return mGraticule;
}
/**
* Gets the Calendar used to generate this set of information.
*
* @return the Calendar
*/
@NonNull
public Calendar getCalendar() {
return mDate;
}
/**
* Gets the Date object from the Calendar object used to generate this set
* of information.
*
* @return the Date of the Calendar
*/
@NonNull
public Date getDate() {
return mDate.getTime();
}
/**
* Gets the distance, in meters, from the given Location and the final
* destination.
*
* @param loc
* Location to compare
* @return the distance, in meters, to the final destination
*/
public float getDistanceInMeters(@NonNull Location loc) {
return loc.distanceTo(getFinalLocation());
}
/**
* Gets the distance, in meters, from the given LatLng and the final
* destination.
*
* @param latLng LatLng to compare
* @return the distance, in meters, to the final destination
*/
public float getDistanceInMeters(@NonNull LatLng latLng) {
return locationFromLatLng(latLng).distanceTo(getFinalLocation());
}
/**
* Returns a calendar representing the date from which the stock price was
* pulled. That is, back a day for the 30W Rule and rewinding to Friday if
* it falls on a weekend.
*
* @return a new adjusted Calendar
*/
@NonNull
public Calendar getStockCalendar() {
return makeAdjustedCalendar(mDate, mGraticule);
}
/**
* Returns a calendar representing the date from which the stock price was
* pulled from a given date/graticule pair. That is, back a day for the 30W
* Rule or globalhashes and rewinding to Friday if it falls on a weekend.
*
* @param c date to adjust
* @param g Graticule to use to determine if the 30W Rule is in effect (if
* null, assumes this is a globalhash which is always back a day)
* @return a new adjusted Calendar
*/
@NonNull
public static Calendar makeAdjustedCalendar(@NonNull Calendar c, @Nullable Graticule g) {
// This adjusts the calendar for both the 30W Rule and to clamp all
// weekend stocks to the preceding Friday. This saves a few database
// entries, as the weekend will always be Friday's value. Note that
// this doesn't account for holidays when the US stocks aren't trading.
// First, clone the calendar. We don't want to muck about with the
// original for various reasons.
Calendar cal = (Calendar)(c.clone());
// Second, 30W Rule hackery. If g is null, assume we're in a globalhash
// (that is, adjustment is needed). If the date is May 26, 2008 or
// earlier (and this isn't a globalhash), ignore it anyway (the 30W Rule
// only applies to non-globalhashes AFTER it was created).
if(g == null || (cal.after(LIMIT_30W) && g.uses30WRule()))
cal.add(Calendar.DAY_OF_MONTH, -1);
// Third, if this new date is a weekend, clamp it back to Friday.
if(cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY)
// Saturday: Back one day
cal.add(Calendar.DAY_OF_MONTH, -1);
else if(cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)
// SUNDAY SUNDAY SUNDAY!!!!!!: Back two days
cal.add(Calendar.DAY_OF_MONTH, -2);
// There! Done!
return cal;
}
@NonNull
private static Location locationFromLatLng(@NonNull LatLng latLng) {
Location loc = new Location("");
loc.setLatitude(latLng.latitude);
loc.setLongitude(latLng.longitude);
return loc;
}
/**
* Determines if this Info represents a point whose date follows the 30W
* Rule. Note that globalhashes always follow the 30W Rule.
*
* @return true if 30W or global, false if not
*/
public boolean uses30WRule() {
// If mGraticule is null, this is always 30W.
if(mGraticule == null) return true;
// Otherwise, just forward it to the graticule itself.
return mDate.after(LIMIT_30W) && mGraticule.uses30WRule();
}
/**
* Determines if this Info represents a globalhash (and thus doesn't have
* any sort of valid Graticule data). Note that there's no way to set up
* inspections to understand that isGlobalHash() == true implies
* getGraticule() == null, so there may be issues that mean this rarely gets
* called.
*
* @return true if global, false if not
*/
public boolean isGlobalHash() {
return mGraticule == null;
}
/**
* Determines if this Info represents a retrohash; that is, a geohash or
* globalhash from a date in the past. Note that this will return false for
* geohashes from the future (i.e. a weekend when we already have the stock
* values).
*
* @return true if a retrohash, false if a current hash
*/
public boolean isRetroHash() {
return mRetroHash;
}
/**
* Determines if this Info is valid. A valid Info has latitude and
* longitude data and can thus be sent straight to the map. An invalid one
* doesn't and shouldn't be used for hashing, but CAN be used in a
* StockRunner handler to know what the date and graticule was.
*
* @return true if valid, false if not
*/
public boolean isValid() {
return mValid;
}
public static final Parcelable.Creator<Info> CREATOR = new Parcelable.Creator<Info>() {
public Info createFromParcel(Parcel in) {
return new Info(in);
}
public Info[] newArray(int size) {
return new Info[size];
}
};
@Override
public int describeContents() {
// We don't do anything special with this.
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Let's make us a parcel. Order is important, remember!
dest.writeDouble(mLatitude);
dest.writeDouble(mLongitude);
dest.writeParcelable(mGraticule, 0);
dest.writeInt(mDate.get(Calendar.YEAR));
dest.writeInt(mDate.get(Calendar.MONTH));
dest.writeInt(mDate.get(Calendar.DAY_OF_MONTH));
dest.writeInt(mRetroHash ? 1 : 0);
}
/**
* Reads an incoming Parcel and deparcelizes it. I'm going to keep using
* the term "deparcelize" and its most logical forms until it catches on.
*
* @param in parcel to deparcelize
*/
public void readFromParcel(Parcel in) {
// Same order! Go!
mLatitude = in.readDouble();
mLongitude = in.readDouble();
mGraticule = in.readParcelable(Graticule.class.getClassLoader());
mDate = Calendar.getInstance();
// In order, this better be year, month, day-of-month.
mDate.set(in.readInt(), in.readInt(), in.readInt());
mRetroHash = (in.readInt() == 1);
}
private void setDate(@NonNull Calendar cal) {
// First, actually set the date.
mDate = cal;
// Then, determine if this is before or after today's date. Since a
// straight comparison also takes time into account, we need to force
// today's date to midnight.
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
// Yes, this means that if the hash is in the future, mRetroHash will
// be false. The only way that can happen is if this is a weekend hash
// and we're checking on Friday or something.
mRetroHash = cal.before(today);
}
/**
* Determines which Info of those given is closest to the also-given
* Location. The presence of the single Info param is because this is
* generally called from the results of StockService with nearby points.
* Any of the Infos (the single or the array) may be null; if both are null,
* this will throw an exception.
*
* @param loc Location to compare against
* @param info a single Info
* @param nearby a bunch of Infos
* @return the closest Info
* @throws IllegalArgumentException info was null and nearby was either null or empty
*/
@NonNull
public static Info measureClosest(@NonNull Location loc, @Nullable Info info, @Nullable Info[] nearby)
throws IllegalArgumentException {
if(nearby == null || nearby.length == 0) {
// If we were only given the single Info, return it. Unless it's
// null.
if(info == null) {
// If it's null, throw a fit.
throw new IllegalArgumentException("You need to include at least one Info in measureClosest!");
} else {
return info;
}
}
Info nearest = null;
float bestDistance = Float.MAX_VALUE;
// First, if we got a single Info, start with that.
if(info != null) {
nearest = info;
bestDistance = loc.distanceTo(info.getFinalLocation());
}
// Now, loop through all the nearby Infos to see if any of those are any
// better.
for(Info i : nearby) {
if(i == null) continue;
float dist = loc.distanceTo(i.getFinalLocation());
if(dist < bestDistance) {
nearest = i;
bestDistance = dist;
}
}
// nearest can't be null here. nearest gets assigned to be the single
// info if it's not null, or at least one of the nearbys. The only way
// nearest can be null is if the distance of ALL the nearbys is equal to
// Float.MAX_VALUE, which is just absurd.
if(nearest == null)
throw new IllegalArgumentException("You have impossible graticules that are somehow infinitely away from anything!");
// And hey presto, we've got us a winner!
return nearest;
}
@Override
public String toString() {
// This is mostly used for debugging purposes, so we may as well make it
// useful.
return "Info for "
+ (mGraticule == null ? "Globalhash" : "Graticule")
+ " on " + DateTools.getDateString(mDate)
+ "; point is at "
+ getLatitude() + "," + getLongitude();
}
@Override
public boolean equals(Object o) {
if(o == this) return true;
if(!(o instanceof Info)) return false;
final Info other = (Info)o;
// I'm really sure I could make this clearer and/or more efficient if I
// really thought more about it, and was not in a room full of other
// reveling nerds as I tried writing it, but to compare the graticules
// while making sure it doesn't NPE if either Info is a Globalhash...
if(isGlobalHash() != other.isGlobalHash()) return false;
// ...then, we see if the graticules match (if neither is a
// Globalhash)...
if(!isGlobalHash() && !(mGraticule.equals(other.mGraticule))) return false;
// ...and also check the date, latitude, and longitude.
if(!mDate.equals(other.mDate)
|| (getLatitude() != other.getLatitude())
|| (getLongitude() != other.getLongitude()))
return false;
// Otherwise, we match!
return true;
}
@Override
public int hashCode() {
// Hash!
int toReturn = 13;
toReturn = 27 * toReturn + (mValid ? 1 : 0);
toReturn = 27 * toReturn + (mRetroHash ? 1 : 0);
long convert = Double.doubleToLongBits(mLatitude);
toReturn = 27 * toReturn + (int)(convert ^ (convert >>> 32));
convert = Double.doubleToLongBits(mLongitude);
toReturn = 27 * toReturn + (int)(convert ^ (convert >>> 32));
toReturn = 27 * toReturn + (mGraticule == null ? 0 : mGraticule.hashCode());
toReturn = 27 * toReturn + (mDate == null ? 0 : mDate.hashCode());
return toReturn;
}
}