/**
* Graticule.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 com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.PolygonOptions;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
/**
* <p>
* A <code>Graticule</code> represents, well, a graticule. A 1x1 square degree
* space on the earth's surface. The very heart of Geohashing*. The base
* implementation of a Graticule is designed to be immutable owing to a few odd
* things that happen around the equator and Prime Meridian.
* </p>
*
* <p>
* Note that Graticules are immutable.
* </p>
*
* <p>
* *: Well, maybe not the heart. At least the kidneys for sure.
* </p>
*
* @author Nicholas Killewald
*/
public class Graticule implements Parcelable {
private int mLatitude;
private int mLongitude;
// These are to account for the "negative zero" graticules.
private boolean mSouth = false;
private boolean mWest = false;
/**
* Constructs a new Graticule with the given Location object.
*
* @param location Location to make a new Graticule out of
*/
public Graticule(Location location) {
this(location.getLatitude(), location.getLongitude());
}
/**
* Constructs a new Graticule with the given LatLng object, because GeoPoint
* isn't good enough for the v2 API anymore, apparently.
*
* @param latLng LatLng to make a new Graticule out of
*/
public Graticule(LatLng latLng) {
this(latLng.latitude, latLng.longitude);
}
/**
* <p>
* Constructs a new Graticule with the given latitude and longitude. Note
* that values that shoot around the planet will be clamped to 89 degrees
* latitude and 179 degrees longitude (positive or negative).
* </p>
*
* <p>
* With this constructor, you <b>MUST</b> specify if this is south or west
* (that is, negative values). This is to account for the "negative zero"
* graticules, for those living on the Prime Meridian or equator, as you
* can't very well input -0 as a Java int and have it distinct from 0.
* </p>
*
* <p>
* This will also ignore any negatives in your inputs (-75 will become 75).
* </p>
*
* @param latitude latitude to set
* @param south true if south, false if north
* @param longitude longitude to set
* @param west true if west, false if east
*/
public Graticule(int latitude, boolean south, int longitude, boolean west) {
this.mSouth = south;
this.mWest = west;
this.setLatitude(Math.abs(latitude));
this.setLongitude(Math.abs(longitude));
}
/**
* <p>
* Constructs a new Graticule with the given latitude and longitude as
* doubles. This can thus make Graticules directly from GPS inputs. Note
* that values that shoot around the planet will be clamped to 89 degrees
* latitude and 179 degrees longitude (positive or negative).
* </p>
*
* <p>
* Negative values will be interpreted as south and west. Please don't use
* this if you're standing directly on the equator and/or Prime Meridian and
* GPS gives you a direct zero.
* </p>
*
* @param latitude latitude to set
* @param longitude longitude to set
*/
public Graticule(double latitude, double longitude) {
mSouth = latitude < 0;
mWest = longitude < 0;
this.setLatitude(Math.abs((int)latitude));
this.setLongitude(Math.abs((int)longitude));
}
/**
* Constructs a new Graticule with the given String forms of the latitude
* and longitude.
*
* @param latitude latitude to set
* @param longitude longitude to set
* @throws NullPointerException either of the input strings were empty
* @throws NumberFormatException either of the input strings weren't numbers
*/
public Graticule(String latitude, String longitude)
throws NullPointerException, NumberFormatException {
mSouth = latitude.charAt(0) == '-';
mWest = longitude.charAt(0) == '-';
this.setLatitude(Math.abs(Integer.valueOf(latitude)));
this.setLongitude(Math.abs(Integer.valueOf(longitude)));
}
/**
* <p>
* Constructs a new Graticule offset from an existing one. That is to say,
* copy an existing Graticule and move it by however many degrees as is
* specified. Under the current implementation, if this gets offset past
* the edges of the earth, it will attempt to wrap around. This allows
* people in the far eastern regions of Russia to see the nearby meetup
* points if they happen to live near the 180E/W longitude line. It does
* not, however, allow for penguins and Santa Claus yet, so don't try to
* fling yourself over the poles.
* </p>
*
* <p>
* Note carefully that moving one degree west of zero longitude will go to
* "negative zero" longitude. Same with latitude. There is a distinction.
* Therefore, be very careful when crossing the Prime Meridian and/or the
* equator.
* </p>
*
* @param g Graticule to copy
* @param latOff number of degrees north to offset (negative is south)
* @param lonOff number of degrees east to offset (negative is west)
* @return a brand spankin' new Graticule, offset as per suggestion
*/
@NonNull
public static Graticule createOffsetFrom(@NonNull Graticule g, int latOff, int lonOff) {
// If we're just returning the same Graticule, seriously, come on now.
if(latOff == 0 && lonOff == 0) return g;
// We already have all the data we need from the old Graticule. But,
// we need to account for passing through the Prime Meridian and/or
// equator. If the sign changes, decrement the amount of the change by
// one. This logic is gratuitously loopy.
boolean goingSouth = (latOff < 0);
latOff = Math.abs(latOff);
int finalLat = g.getLatitude();
int finalLon = g.getLongitude();
boolean finalSouth = g.isSouth();
boolean finalWest = g.isWest();
// Skip the following if latitude is unaffected.
if (latOff != 0) {
if (g.isSouth() == goingSouth) {
// Going the same direction, no equator-hacking needed.
finalLat = g.getLatitude() + latOff;
} else {
// Going opposite directions, check for equator-hacking.
if (g.getLatitude() < latOff) {
// We cross the equator!
latOff--;
finalSouth = !finalSouth;
}
finalLat = Math.abs(g.getLatitude() - latOff);
}
}
// Meridian hacking can be handled differently to also cover planet-
// wrapping at the same time. This entire stunt depends on treating
// the longitude as a value between 0 and 359, inclusive. In this,
// 179W is 0, 0W is 179, 0E is 180, and 179E is 359.
// Adjust us properly. Remember the negative zero graticules!
if(finalWest)
finalLon = -finalLon + 179;
else
finalLon += 180;
finalLon += lonOff;
finalLon %= 360;
if(finalLon < 0) finalLon = 360 - Math.abs(finalLon);
if(finalLon >= 180) {
finalWest = false;
finalLon -= 180;
} else {
finalWest = true;
finalLon -= 179;
}
finalLon = Math.abs(finalLon);
// Now make the new Graticule object and return it.
return new Graticule(finalLat, finalSouth, finalLon, finalWest);
}
/**
* Deparcelizinate a Graticule.
*
* @param in the parcel to deparcelize
*/
private Graticule(Parcel in) {
readFromParcel(in);
}
public static final Parcelable.Creator<Graticule> CREATOR = new Parcelable.Creator<Graticule>() {
public Graticule createFromParcel(Parcel in) {
return new Graticule(in);
}
public Graticule[] newArray(int size) {
return new Graticule[size];
}
};
/**
* Deparcel. Read from parcel. Deparcelize. This constructs a Graticule
* from a Parcel.
*
* @param in parcel to deparcelize
*/
public void readFromParcel(Parcel in) {
// For the sake of efficiency, we store exactly two things in the
// parcel. Specifically, the latitude and longitude, represented from
// 0-179 and 0-356, respectively, going from 89 south to 89 north and
// 179 west to 179 east (both including a negative zero). We can
// determine mSouth and mWest from there.
int absLat = in.readInt();
int absLon = in.readInt();
// I swear, if these wind up not being valid, I reserve the right to
// dope slap you.
if(absLat < 90) {
mSouth = true;
setLatitude(89 - absLat);
} else {
mSouth = false;
setLatitude(absLat - 90);
}
if(absLon < 180) {
mWest = true;
setLongitude(179 - absLon);
} else {
mWest = false;
setLongitude(absLon - 180);
}
}
@Override
public int describeContents() {
// BLAH BLAH BLAH
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Hey! We've got a parcel to write out! To compress this down a bit
// further, we want to only store two ints (instead of two ints and two
// booleans). See the comments in readFromParcel for details. To wit:
// Latitude!
if(mSouth)
dest.writeInt(Math.abs(mLatitude - 89));
else
dest.writeInt(mLatitude + 90);
if(mWest)
dest.writeInt(Math.abs(mLongitude - 179));
else
dest.writeInt(mLongitude + 180);
}
/**
* Returns true if the 30W Rule is in effect. Which is to say, anything east
* of -30 longitude uses yesterday's stock value, regardless of if the DJIA
* was updated to that point. Note that this only determines if the
* graticule itself abides by the 30W Rule; if the date is May 26, 2008 or
* earlier, 30W is ignored.
*
* @return true if the 30W Rule is in effect, false otherwise
*/
public boolean uses30WRule() {
return ((mLongitude < 30 && isWest()) || !isWest());
}
private void setLatitude(int latitude) {
// Work out invalid entries by clamping 'em down.
if (latitude > 89)
latitude = 89;
this.mLatitude = latitude;
}
/**
* Returns the absolute value of the current latitude. Run this against
* isSouth() to figure out what the negative should be.
*
* @return the absolute value of the current latitude
*/
public int getLatitude() {
return mLatitude;
}
/**
* Returns the current latitude as a String to account for negative zero
* graticule wackiness.
* @param useNegativeValues true to return values as negative for south and positive for north, false to return values with N and S indicators
* @return the current latitude as a String
*/
@NonNull
public String getLatitudeString(boolean useNegativeValues) {
if (mSouth) {
if(useNegativeValues) {
return "-" + mLatitude;
} else {
return mLatitude + "S";
}
} else {
if(useNegativeValues) {
return Integer.valueOf(mLatitude).toString();
} else {
return mLatitude + "N";
}
}
}
private void setLongitude(int longitude) {
// Clamp! Clamp! Clamp!
if (longitude > 179)
longitude = 179;
this.mLongitude = longitude;
}
/**
* Returns the absolute value of the current longitude. Run this against
* isEast() to figure out what the negative should be.
*
* @return the absolute value of the current longitude
*/
public int getLongitude() {
return mLongitude;
}
/**
* Returns the current longitude as a String to account for negative zero
* graticule madness.
*
* @param useNegativeValues true to return values as negative for west and positive for east, false to return values with E and W indicators
* @return the current longitude as a String
*/
@NonNull
public String getLongitudeString(boolean useNegativeValues) {
if (mWest) {
if(useNegativeValues) {
return "-" + mLongitude;
} else {
return mLongitude + "W";
}
} else {
if(useNegativeValues) {
return Integer.valueOf(mLongitude).toString();
} else {
return mLongitude + "E";
}
}
}
/**
* Returns whether or not this is a southern latitude (negative).
*
* @return true if south, false if north
*/
public boolean isSouth() {
return mSouth;
}
/**
* Returns whether or not this is an western longitude (negative).
*
* @return true if west, false if east.
*/
public boolean isWest() {
return mWest;
}
/**
* Returns the center of this Graticule as a LatLng.
*
* @return a LatLng representing the center of this Graticule.
*/
@NonNull
public LatLng getCenterLatLng() {
double lat, lon;
if(isSouth()) {
lat = -getLatitude() - 0.5;
} else {
lat = getLatitude() + 0.5;
}
if(isWest()) {
lon = -getLongitude() - 0.5;
} else {
lon = getLongitude() + 0.5;
}
return new LatLng(lat, lon);
}
/**
* Make a Maps v2 PolygonOptions out of this Graticule. You can then style
* it yourself and toss it into a map as need be.
*
* @return a PolygonOptions set up as this Graticule sits.
*/
@NonNull
public PolygonOptions getPolygon() {
PolygonOptions toReturn = new PolygonOptions();
int top, left, bottom, right;
if(isSouth()) {
bottom = -getLatitude() - 1;
top = -getLatitude();
} else {
bottom = getLatitude();
top = getLatitude() + 1;
}
if(isWest()) {
right = -getLongitude() - 1;
left = -getLongitude();
} else {
right = getLongitude();
left = getLongitude() + 1;
}
// Now, draw the polygon. Er... make the options.
toReturn.add(new LatLng(top, left))
.add(new LatLng(top, right))
.add(new LatLng(bottom, right))
.add(new LatLng(bottom, left));
// Shove this into a GoogleMap, and style it as need be.
return toReturn;
}
/**
* <p>
* Makes a LatLng out of this Graticule and component fractional hash parts.
* In other words, this forces the fractional bits into a proper location
* based on this Graticule.
* </p>
*
* <p>
* TODO: HashBuilder could start calling this instead...
* </p>
*
* @param latHash the fractional latitude portion of the hash
* @param lonHash the fractional longitude portion of the hash
* @return a new LatLng
* @throws IllegalArgumentException if latHash or lonHash are less than 0 or greater than 1
*/
@NonNull
public LatLng makePointFromHash(double latHash, double lonHash) {
if(latHash < 0 || latHash > 1 || lonHash < 0 || latHash > 1)
throw new IllegalArgumentException("Those aren't valid hash values!");
// getLatitude and getLongitude are absolute values, so we can do this:
latHash += getLatitude();
lonHash += getLongitude();
// And then we adjust for south/west like so...
if(isSouth()) latHash *= -1;
if(isWest()) lonHash *= -1;
// And out it goes!
return new LatLng(latHash, lonHash);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
// First, this better be a Graticule.
if(o == this) return true;
if (!(o instanceof Graticule))
return false;
final Graticule g = (Graticule)o;
// If everything matches up, these are identical. Two int checks and
// two boolean checks are probably a lot faster than two String checks,
// right?
return !(g.getLatitude() != getLatitude()
|| g.getLongitude() != getLongitude()
|| g.isSouth() != isSouth() || g.isWest() != isWest());
}
@Override
public int hashCode() {
// Um... 11! That's a prime number, right?
int toReturn = 11;
// And so's 37!
toReturn = 37 * toReturn + mLatitude;
toReturn = 37 * toReturn + mLongitude;
toReturn = 37 * toReturn + (mSouth ? 0 : 1);
toReturn = 37 * toReturn + (mWest ? 0 : 1);
return toReturn;
}
@Override
public String toString() {
return "Graticule for " + getLatitudeString(false) + " " + getLongitudeString(false);
}
}