package cgeo.geocaching.location;
import cgeo.geocaching.utils.MatcherWrapper;
import android.support.annotation.NonNull;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* A class representing a UTM co-ordinate.
* <p>
*
* Derived from https://github.com/OpenMap-java/openmap
*
* Adapted to Java by Colin Mummery (colin_mummery@yahoo.com) from C++ code by
* Chuck Gantz (chuck.gantz@globalstar.com)
*/
public class UTMPoint {
private static final double WGS_84_RADIUS = 6378137.0d;
private static final double WGS_84_ECC_SQUARED = 0.00669438d;
private static final double ECC_PRIME_SQUARED = WGS_84_ECC_SQUARED / (1 - WGS_84_ECC_SQUARED);
private static final double ECC_SQUARED_2 = WGS_84_ECC_SQUARED * WGS_84_ECC_SQUARED;
private static final double ECC_SQUARED_3 = ECC_SQUARED_2 * WGS_84_ECC_SQUARED;
private static final double E_1 = (1 - Math.sqrt(1 - WGS_84_ECC_SQUARED)) / (1 + Math.sqrt(1 - WGS_84_ECC_SQUARED));
private static final double K_0 = 0.9996;
private static final double FALSE_EASTING = 500000.0d;
private static final double FALSE_NORTHING = 10000000.0d;
/**
* The northing component of the coordinate.
*/
private double northing;
/**
* The easting component of the coordinate.
*/
private double easting;
/**
* The zone number of the coordinate, must be between 1 and 60.
*/
private int zoneNumber;
/**
* A-Z
*/
private char zoneLetter;
// ( 1 )( 2 ) ( 3 ) ( 4 ) ( 5 )
static final Pattern PATTERN_UTM = Pattern.compile("(^|\\s)(\\d\\d?)[ \\t]*([A-Z])[\\sE]+(\\d+(?:[.,]\\d+)?)[\\sN]+(\\d+(?:[.,]\\d+)?)", Pattern.CASE_INSENSITIVE);
/**
* Point to create if you are going to use the static methods to fill the
* values in.
*/
public UTMPoint(final String utmString) {
final MatcherWrapper matcher = new MatcherWrapper(PATTERN_UTM, utmString);
try {
if (matcher.find()) {
this.zoneNumber = Integer.parseInt(matcher.group(2));
this.zoneLetter = checkZone(matcher.group(3).charAt(0));
this.easting = Double.parseDouble(matcher.group(4).replace(",", "."));
this.northing = Double.parseDouble(matcher.group(5).replace(",", "."));
if (zoneNumber < 0 || zoneNumber > 60) {
throw new ParseException("ZoneNumber out of range [0-60] in String '" + utmString + "'");
}
// Filter some invalid combinations with small numbers
if (easting < 160000) {
throw new ParseException("Easting too small for UTM format in String '" + utmString + "'");
}
if (northing < 650000 && "ABNZ".indexOf(zoneNumber) == -1) {
throw new ParseException("Northing too small for UTM zone in String '" + utmString + "'");
}
} else {
throw new ParseException("Unable to recognize UTM format in String '" + utmString + "'");
}
} catch (final NumberFormatException ignored) {
throw new ParseException("Error parsing UTM numbers in String '" + utmString + "'");
}
}
/**
* Constructs a new UTM instance.
*
* @param zoneNumber The zone of the coordinate.
* @param zoneLetter For UTM, 'A' .. 'Z'
* @param easting The easting component.
* @param northing The northing component.
* @throws IllegalArgumentException zoneLetter is out of range.
*/
public UTMPoint(final int zoneNumber, final char zoneLetter, final double easting, final double northing) {
this.zoneNumber = zoneNumber;
this.zoneLetter = checkZone(zoneLetter);
this.easting = easting;
this.northing = northing;
}
/**
* Method that provides a check for UTM zone letters. Returns an uppercase
* version of any valid letter passed in, 'A' .. 'Z'.
*
* @throws IllegalArgumentException if zone letter is invalid.
*/
private static char checkZone(final char inZone) {
final char zone = Character.toUpperCase(inZone);
if (zone < 'A' || zone > 'Z') {
throw new IllegalArgumentException("Invalid UTMPoint zone letter: " + zone);
}
return zone;
}
/**
* Returns a string representation of the object.
*
* @return String representation
*/
@Override
public String toString() {
return String.format(Locale.getDefault(), "%d%c E %d N %d", zoneNumber, zoneLetter, Math.round(easting), Math.round(northing));
}
/**
* Converts UTM coords to lat/long.
* <p>
* Equations from USGS Bulletin 1532 <br>
* East Longitudes are positive, West longitudes are negative. <br>
* North latitudes are positive, South latitudes are negative.
*
* @throws IllegalArgumentException if zoneNumber is out of range.
*/
@NonNull
public Geopoint toLatLong() {
// check the ZoneNumber is valid
if (zoneNumber < 0 || zoneNumber > 60) {
throw new IllegalArgumentException("ZoneNumber out of range [0-60]: " + zoneNumber);
}
// remove 500,000 meter offset for longitude
final double x = easting - FALSE_EASTING;
final double y = zoneLetter < 'N' ? northing - FALSE_NORTHING : northing;
// There are 60 zones with zone 1 being at West -180 to -174
final double longOrigin = (zoneNumber - 1) * 6 - 180 + 3; // +3 puts origin in middle of zone
final double m = y / K_0;
final double mu = m / (WGS_84_RADIUS * (1 - WGS_84_ECC_SQUARED / 4 - 3 * ECC_SQUARED_2 / 64 - 5 * ECC_SQUARED_3 / 256));
final double phi1Rad =
mu + (3 * E_1 / 2 - 27 * E_1 * E_1 * E_1 / 32) * Math.sin(2 * mu) + (21 * E_1 * E_1 / 16 - 55 * E_1 * E_1 * E_1 * E_1 / 32)
* Math.sin(4 * mu) + (151 * E_1 * E_1 * E_1 / 96) * Math.sin(6 * mu);
final double n1 = WGS_84_RADIUS / Math.sqrt(1 - WGS_84_ECC_SQUARED * Math.sin(phi1Rad) * Math.sin(phi1Rad));
final double t1 = Math.tan(phi1Rad) * Math.tan(phi1Rad);
final double c1 = ECC_PRIME_SQUARED * Math.cos(phi1Rad) * Math.cos(phi1Rad);
final double r1 = WGS_84_RADIUS * (1 - WGS_84_ECC_SQUARED) / Math.pow(1 - WGS_84_ECC_SQUARED * Math.sin(phi1Rad) * Math.sin(phi1Rad), 1.5);
final double d = x / (n1 * K_0);
final double latRad =
phi1Rad
- (n1 * Math.tan(phi1Rad) / r1)
* (d * d / 2 - (5 + 3 * t1 + 10 * c1 - 4 * c1 * c1 - 9 * ECC_PRIME_SQUARED) * d * d * d * d / 24 + (61 + 90
* t1 + 298 * c1 + 45 * t1 * t1 - 252 * ECC_PRIME_SQUARED - 3 * c1 * c1)
* d * d * d * d * d * d / 720);
final double lonRad =
(d - (1 + 2 * t1 + c1) * d * d * d / 6 + (5 - 2 * c1 + 28 * t1 - 3 * c1 * c1 + 8 * ECC_PRIME_SQUARED + 24 * t1 * t1)
* d * d * d * d * d / 120)
/ Math.cos(phi1Rad);
return new Geopoint(Math.toDegrees(latRad), longOrigin + Math.toDegrees(lonRad));
}
/**
* Converts a set of Longitude and Latitude co-ordinates to UTM.
*
* @param geopoint the coordinate
* @return An UTM class instance
*/
@NonNull
public static UTMPoint latLong2UTM(final Geopoint geopoint) {
final int zoneNumber = getZoneNumber(geopoint.getLatitude(), geopoint.getLongitude());
final double latRad = Math.toRadians(geopoint.getLatitude());
final double longRad = Math.toRadians(geopoint.getLongitude());
// in middle of zone
final double longOrigin = (zoneNumber - 1) * 6 - 180 + 3; // +3 puts origin
final double longOriginRad = Math.toRadians(longOrigin);
final double tanLatRad = Math.tan(latRad);
final double sinLatRad = Math.sin(latRad);
final double cosLatRad = Math.cos(latRad);
final double n = WGS_84_RADIUS / Math.sqrt(1 - WGS_84_ECC_SQUARED * sinLatRad * sinLatRad);
final double t = tanLatRad * tanLatRad;
final double c = ECC_PRIME_SQUARED * cosLatRad * cosLatRad;
final double a = cosLatRad * (longRad - longOriginRad);
final double m =
WGS_84_RADIUS
* ((1 - WGS_84_ECC_SQUARED / 4 - 3 * ECC_SQUARED_2 / 64 - 5 * ECC_SQUARED_3 / 256) * latRad
- (3 * WGS_84_ECC_SQUARED / 8 + 3 * ECC_SQUARED_2 / 32 + 45 * ECC_SQUARED_3 / 1024) * Math.sin(2 * latRad)
+ (15 * ECC_SQUARED_2 / 256 + 45 * ECC_SQUARED_3 / 1024) * Math.sin(4 * latRad) - (35 * ECC_SQUARED_3 / 3072)
* Math.sin(6 * latRad));
final double utmEasting =
K_0
* n
* (a + (1 - t + c) * a * a * a / 6.0d + (5 - 18 * t + t * t + 72 * c - 58 * ECC_PRIME_SQUARED) * a * a * a
* a * a / 120.0d) + FALSE_EASTING;
final double utmNorthing =
K_0 * (m + n
* Math.tan(latRad)
* (a * a / 2 + (5 - t + 9 * c + 4 * c * c) * a * a * a * a / 24.0d + (61 - 58 * t + t * t + 600 * c - 330 * ECC_PRIME_SQUARED)
* a * a * a * a * a * a / 720.0d));
final char zoneLetter = getLetterDesignator(geopoint.getLatitude());
return new UTMPoint(zoneNumber, zoneLetter, utmEasting, geopoint.getLatitude() < 0f ? utmNorthing + FALSE_NORTHING : utmNorthing);
}
/**
* Find zone number based on the given latitude and longitude in *degrees*.
*
* @param lat in decimal degrees
* @param lon in decimal degrees
* @return zone number for UTM zone for lat, lon
*/
private static int getZoneNumber(final double lat, final double lon) {
// Make sure the longitude 180.00 is in Zone 60
if (lon == 180) {
return 60;
}
// Special zone for Norway
if (lat >= 56.0f && lat < 64.0f && lon >= 3.0f && lon < 12.0f) {
return 32;
}
// Special zones for Svalbard
if (lat >= 72.0f && lat < 84.0f) {
if (lon >= 0.0f && lon < 9.0f) {
return 31;
} else if (lon >= 9.0f && lon < 21.0f) {
return 33;
} else if (lon >= 21.0f && lon < 33.0f) {
return 35;
} else if (lon >= 33.0f && lon < 42.0f) {
return 37;
}
}
return (int) ((lon + 180) / 6) + 1;
}
/**
* Determines the correct MGRS letter designator for the given latitude
* returns 'Z' if latitude is outside the MGRS limits of 84N to 80S.
*
* TODO: maybe we should handle the zones A, B, Y and Z
*
* @param lat The float value of the latitude.
* @return A char value which is the MGRS zone letter.
*/
private static char getLetterDesignator(final double lat) {
if ((84 >= lat) && (lat >= 72)) {
return 'X';
} else if ((72 > lat) && (lat >= 64)) {
return 'W';
} else if ((64 > lat) && (lat >= 56)) {
return 'V';
} else if ((56 > lat) && (lat >= 48)) {
return 'U';
} else if ((48 > lat) && (lat >= 40)) {
return 'T';
} else if ((40 > lat) && (lat >= 32)) {
return 'S';
} else if ((32 > lat) && (lat >= 24)) {
return 'R';
} else if ((24 > lat) && (lat >= 16)) {
return 'Q';
} else if ((16 > lat) && (lat >= 8)) {
return 'P';
} else if ((8 > lat) && (lat >= 0)) {
return 'N';
} else if ((0 > lat) && (lat >= -8)) {
return 'M';
} else if ((-8 > lat) && (lat >= -16)) {
return 'L';
} else if ((-16 > lat) && (lat >= -24)) {
return 'K';
} else if ((-24 > lat) && (lat >= -32)) {
return 'J';
} else if ((-32 > lat) && (lat >= -40)) {
return 'H';
} else if ((-40 > lat) && (lat >= -48)) {
return 'G';
} else if ((-48 > lat) && (lat >= -56)) {
return 'F';
} else if ((-56 > lat) && (lat >= -64)) {
return 'E';
} else if ((-64 > lat) && (lat >= -72)) {
return 'D';
} else if ((-72 > lat) && (lat >= -80)) {
return 'C';
}
// This is here as an error flag to show that the Latitude is outside MGRS limits
return 'Z';
}
public int getZoneNumber() {
return this.zoneNumber;
}
public char getZoneLetter() {
return this.zoneLetter;
}
public double getEasting() {
return easting;
}
public double getNorthing() {
return northing;
}
public static class ParseException extends NumberFormatException {
private static final long serialVersionUID = 1L;
public ParseException(final String msg) {
super(msg);
}
}
}