// ********************************************************************** // // <copyright> // // BBN Technologies // 10 Moulton Street // Cambridge, MA 02138 // (617) 873-8000 // // Copyright (C) BBNT Solutions LLC. All rights reserved. // // </copyright> // ********************************************************************** package com.jwetherell.openmap.common; public class MGRSPoint extends ZonedUTMPoint { /** * UTM zones are grouped, and assigned to one of a group of 6 sets. */ private final static int NUM_100K_SETS = 6; /** * The column letters (for easting) of the lower left value, per set. */ private final static int[] SET_ORIGIN_COLUMN_LETTERS = { 'A', 'J', 'S', 'A', 'J', 'S' }; /** * The row letters (for northing) of the lower left value, per set. */ private final static int[] SET_ORIGIN_ROW_LETTERS = { 'A', 'F', 'A', 'F', 'A', 'F' }; public final static int ACCURACY_1_METER = 5; public final static int ACCURACY_10_METER = 4; public final static int ACCURACY_100_METER = 3; public final static int ACCURACY_1000_METER = 2; public final static int ACCURACY_10000_METER = 1; /** The set origin column letters to use. */ private int[] originColumnLetters = SET_ORIGIN_COLUMN_LETTERS; /** The set origin row letters to use. */ private int[] originRowLetters = SET_ORIGIN_ROW_LETTERS; private final static int A = 'A'; private final static int I = 'I'; private final static int O = 'O'; private final static int V = 'V'; private final static int Z = 'Z'; /** The String holding the MGRS coordinate value. */ private String mgrs = null; /** * Controls the number of digits that the MGRS coordinate will have, which * directly affects the accuracy of the coordinate. Default is * ACCURACY_1_METER, which indicates that MGRS coordinates will have 10 * digits (5 easting, 5 northing) after the 100k two letter code, indicating * 1 meter resolution. */ private int accuracy = ACCURACY_1_METER; public MGRSPoint() { } public MGRSPoint(LatLonPoint llpoint) { this(llpoint, Ellipsoid.WGS_84); } public MGRSPoint(LatLonPoint llpoint, Ellipsoid ellip) { this(); LLtoMGRS(llpoint, ellip, this); } public MGRSPoint(String mgrsString) throws NumberFormatException { this(); setMGRS(mgrsString); } public MGRSPoint(double northing, double easting, int zoneNumber, char zoneLetter) { super(northing, easting, zoneNumber, zoneLetter); } /** * Set the MGRS value for this Point. Will be decoded, and the UTM values * figured out. You can call toLatLonPoint() to translate it to lat/lon * decimal degrees. */ public void setMGRS(String mgrsString) throws NumberFormatException { try { mgrs = mgrsString.toUpperCase(); // Just to make sure. decode(mgrs); } catch (StringIndexOutOfBoundsException sioobe) { throw new NumberFormatException("MGRSPoint has bad string: " + mgrsString); } catch (NullPointerException npe) { // Blow off } } /** * Set the UTM parameters from a MGRS string. * * @param mgrsString * an UPPERCASE coordinate string is expected. */ protected void decode(String mgrsString) throws NumberFormatException { if (mgrsString == null || mgrsString.length() == 0) { throw new NumberFormatException("MGRSPoint coverting from nothing"); } // Ensure an upper-case string mgrsString = mgrsString.toUpperCase(); int length = mgrsString.length(); String hunK = null; StringBuffer sb = new StringBuffer(); char testChar; int i = 0; // get Zone number while (!Character.isLetter(testChar = mgrsString.charAt(i))) { if (i >= 2) { throw new NumberFormatException("MGRSPoint bad conversion from: " + mgrsString + ", first two characters need to be a number between 1-60."); } sb.append(testChar); i++; } zone_number = Integer.parseInt(sb.toString()); if (zone_number < 1 || zone_number > 60) { throw new NumberFormatException("MGRSPoint bad conversion from: " + mgrsString + ", first two characters need to be a number between 1-60."); } if (i == 0 || i + 3 > length) { // A good MGRS string has to be 4-5 digits long, // ##AAA/#AAA at least. throw new NumberFormatException("MGRSPoint bad conversion from: " + mgrsString + ", MGRS string must be at least 4-5 digits long"); } zone_letter = mgrsString.charAt(i++); // Should we check the zone letter here? Why not. if (zone_letter <= 'A' || zone_letter == 'B' || zone_letter == 'Y' || zone_letter >= 'Z' || zone_letter == 'I' || zone_letter == 'O') { throw new NumberFormatException("MGRSPoint zone letter " + zone_letter + " not handled: " + mgrsString); } hunK = mgrsString.substring(i, i += 2); // Validate, check the zone, make sure each letter is between A-Z, not I // or O char char1 = hunK.charAt(0); char char2 = hunK.charAt(1); if (char1 < 'A' || char2 < 'A' || char1 > 'Z' || char2 > 'Z' || char1 == 'I' || char2 == 'I' || char1 == 'O' || char2 == 'O') { throw new NumberFormatException("MGRSPoint bad conversion from " + mgrsString + ", invalid 100k designator"); } int set = get100kSetForZone(zone_number); float east100k = getEastingFromChar(char1, set); float north100k = getNorthingFromChar(char2, set); // We have a bug where the northing may be 2000000 too low. // How do we know when to roll over? while (north100k < getMinNorthing(zone_letter)) { north100k += 2000000; } // calculate the char index for easting/northing separator int remainder = length - i; if (remainder % 2 != 0) { throw new NumberFormatException( "MGRSPoint has to have an even number \nof digits after the zone letter and two 100km letters - front \nhalf for easting meters, second half for \nnorthing meters" + mgrsString); } int sep = remainder / 2; float sepEasting = 0f; float sepNorthing = 0f; if (sep > 0) { float accuracyBonus = 100000f / (float) Math.pow(10, sep); String sepEastingString = mgrsString.substring(i, i + sep); sepEasting = Float.parseFloat(sepEastingString) * accuracyBonus; String sepNorthingString = mgrsString.substring(i + sep); sepNorthing = Float.parseFloat(sepNorthingString) * accuracyBonus; } easting = sepEasting + east100k; northing = sepNorthing + north100k; } /** * Given the first letter from a two-letter MGRS 100k zone, and given the * MGRS table set for the zone number, figure out the easting value that * should be added to the other, secondary easting value. */ protected float getEastingFromChar(char e, int set) { int baseCol[] = getOriginColumnLetters(); // colOrigin is the letter at the origin of the set for the // column int curCol = baseCol[set - 1]; float eastingValue = 100000f; boolean rewindMarker = false; while (curCol != e) { curCol++; if (curCol == I) curCol++; if (curCol == O) curCol++; if (curCol > Z) { if (rewindMarker) { throw new NumberFormatException("Bad character: " + e); } curCol = A; rewindMarker = true; } eastingValue += 100000f; } return eastingValue; } /** * Given the second letter from a two-letter MGRS 100k zone, and given the * MGRS table set for the zone number, figure out the northing value that * should be added to the other, secondary northing value. You have to * remember that Northings are determined from the equator, and the vertical * cycle of letters mean a 2000000 additional northing meters. This happens * approx. every 18 degrees of latitude. This method does *NOT* count any * additional northings. You have to figure out how many 2000000 meters need * to be added for the zone letter of the MGRS coordinate. * * @param n * second letter of the MGRS 100k zone * @param set * the MGRS table set number, which is dependent on the UTM zone * number. */ protected float getNorthingFromChar(char n, int set) { if (n > 'V') { throw new NumberFormatException("MGRSPoint given invalid Northing " + n); } int baseRow[] = getOriginRowLetters(); // rowOrigin is the letter at the origin of the set for the // column int curRow = baseRow[set - 1]; float northingValue = 0f; boolean rewindMarker = false; while (curRow != n) { curRow++; if (curRow == I) curRow++; if (curRow == O) curRow++; // fixing a bug making whole application hang in this loop // when 'n' is a wrong character if (curRow > V) { if (rewindMarker) { // making sure that this loop ends throw new NumberFormatException("Bad character: " + n); } curRow = A; rewindMarker = true; } northingValue += 100000f; } return northingValue; } /** * The function getMinNorthing returns the minimum northing value of a MGRS * zone. * * portted from Geotrans' c Latitude_Band_Value structure table. zoneLetter * : MGRS zone (input) */ protected float getMinNorthing(char zoneLetter) throws NumberFormatException { float northing; switch (zoneLetter) { case 'C': northing = 1100000.0f; break; case 'D': northing = 2000000.0f; break; case 'E': northing = 2800000.0f; break; case 'F': northing = 3700000.0f; break; case 'G': northing = 4600000.0f; break; case 'H': northing = 5500000.0f; break; case 'J': northing = 6400000.0f; break; case 'K': northing = 7300000.0f; break; case 'L': northing = 8200000.0f; break; case 'M': northing = 9100000.0f; break; case 'N': northing = 0.0f; break; case 'P': northing = 800000.0f; break; case 'Q': northing = 1700000.0f; break; case 'R': northing = 2600000.0f; break; case 'S': northing = 3500000.0f; break; case 'T': northing = 4400000.0f; break; case 'U': northing = 5300000.0f; break; case 'V': northing = 6200000.0f; break; case 'W': northing = 7000000.0f; break; case 'X': northing = 7900000.0f; break; default: northing = -1.0f; } if (northing >= 0.0) return northing; // else throw new NumberFormatException("Invalid zone letter: " + zone_letter); } /** * Convert this MGRSPoint to a LatLonPoint, and assume a WGS_84 ellipsoid. */ public LatLonPoint toLatLonPoint() { return toLatLonPoint(Ellipsoid.WGS_84, new LatLonPoint()); } /** * Convert this MGRSPoint to a LatLonPoint, and use the given ellipsoid. */ public LatLonPoint toLatLonPoint(Ellipsoid ellip) { return toLatLonPoint(ellip, new LatLonPoint()); } /** * Fill in the given LatLonPoint with the converted values of this * MGRSPoint, and use the given ellipsoid. */ public LatLonPoint toLatLonPoint(Ellipsoid ellip, LatLonPoint llpoint) { return MGRStoLL(this, ellip, llpoint); } /** * Create a LatLonPoint from a MGRSPoint. * * @param mgrsp * to convert. * @param ellip * Ellipsoid for earth model. * @param llp * a LatLonPoint to fill in values for. If null, a new * LatLonPoint will be returned. If not null, the new values will * be set in this object, and it will be returned. * @return LatLonPoint with values converted from MGRS coordinate. */ public static LatLonPoint MGRStoLL(MGRSPoint mgrsp, Ellipsoid ellip, LatLonPoint llp) { return UTMtoLL(ellip, mgrsp.northing, mgrsp.easting, mgrsp.zone_number, MGRSPoint.MGRSZoneToUTMZone(mgrsp.zone_letter), llp); } /** * Converts a LatLonPoint to a MGRS Point, assuming the WGS_84 ellipsoid. * * @return MGRSPoint, or null if something bad happened. */ public static MGRSPoint LLtoMGRS(LatLonPoint llpoint) { return LLtoMGRS(llpoint, Ellipsoid.WGS_84, new MGRSPoint()); } /** * Create a MGRSPoint from a LatLonPoint. * * @param llp * LatLonPoint to convert. * @param ellip * Ellipsoid for earth model. * @param mgrsp * a MGRSPoint to fill in values for. If null, a new MGRSPoint * will be returned. If not null, the new values will be set in * this object, and it will be returned. * @return MGRSPoint with values converted from lat/lon. */ public static MGRSPoint LLtoMGRS(LatLonPoint llp, Ellipsoid ellip, MGRSPoint mgrsp) { if (mgrsp == null) { mgrsp = new MGRSPoint(); } // Calling LLtoUTM here results in N/S zone letters! wrong! mgrsp = (MGRSPoint) LLtoUTM(llp, ellip, mgrsp); // Need to add this to set the right letter for the latitude. mgrsp.zone_letter = mgrsp.getLetterDesignator(llp.getLatitude()); mgrsp.resolve(); return mgrsp; } /** * Convert MGRS zone letter to UTM zone letter, N or S. * * @param mgrsZone * @return N of given zone is equal or larger than N, S otherwise. */ public static char MGRSZoneToUTMZone(char mgrsZone) { if (Character.toUpperCase(mgrsZone) >= 'N') return 'N'; // else return 'S'; } /** * Method that provides a check for MGRS zone letters. Returns an uppercase * version of any valid letter passed in. */ public char checkZone(char zone) { zone = Character.toUpperCase(zone); if (zone <= 'A' || zone == 'B' || zone == 'Y' || zone >= 'Z' || zone == 'I' || zone == 'O') { throw new NumberFormatException("Invalid MGRSPoint zone letter: " + zone); } return zone; } /** * Set the number of digits to use for easting and northing numbers in the * mgrs string, which reflects the accuracy of the coordinate. From 5 (1 * meter) to 1 (10,000 meter). */ public void setAccuracy(int value) { accuracy = value; mgrs = null; } public int getAccuracy() { return accuracy; } /** * Create the mgrs string based on the internal UTM settings, should be * called if the accuracy changes. * * @param digitAccuracy * The number of digits to use for the northing and easting * numbers. 5 digits reflect a 1 meter accuracy, 4 - 10 meter, 3 * - 100 meter, 2 - 1000 meter, 1 - 10,000 meter. */ public void resolve(int digitAccuracy) { setAccuracy(digitAccuracy); resolve(); } /** * Create the mgrs string based on the internal UTM settings, using the * accuracy set in the MGRSPoint. */ public void resolve() { if (zone_letter == 'Z') { mgrs = "Latitude limit exceeded"; } else { StringBuffer sb = new StringBuffer(Integer.toString(zone_number)).append(zone_letter).append(get100kID(easting, northing, zone_number)); StringBuffer seasting = new StringBuffer(Integer.toString((int) easting)); StringBuffer snorthing = new StringBuffer(Integer.toString((int) northing)); while (accuracy + 1 > seasting.length()) { seasting.insert(0, '0'); } // We have to be careful here, the 100k values shouldn't // be // used for calculating stuff here. while (accuracy + 1 > snorthing.length()) { snorthing.insert(0, '0'); } while (snorthing.length() > 6) { snorthing.deleteCharAt(0); } try { sb.append(seasting.substring(1, accuracy + 1)).append(snorthing.substring(1, accuracy + 1)); mgrs = sb.toString(); } catch (IndexOutOfBoundsException ioobe) { mgrs = null; } } } /** * Given a UTM zone number, figure out the MGRS 100K set it is in. */ private int get100kSetForZone(int i) { int set = i % NUM_100K_SETS; if (set == 0) set = NUM_100K_SETS; return set; } /** * Provided so that extensions to this class can provide different origin * letters, in case of different ellipsoids. The int[] represents all of the * first letters in the bottom left corner of each set box, as shown in an * MGRS 100K box layout. */ private int[] getOriginColumnLetters() { return originColumnLetters; } /** * Provided so that extensions to this class can provide different origin * letters, in case of different ellipsoids. The int[] represents all of the * second letters in the bottom left corner of each set box, as shown in an * MGRS 100K box layout. */ private int[] getOriginRowLetters() { return originRowLetters; } /** * Get the two letter 100k designator for a given UTM easting, northing and * zone number value. */ private String get100kID(double easting, double northing, int zone_number) { int set = get100kSetForZone(zone_number); int setColumn = ((int) easting / 100000); int setRow = ((int) northing / 100000) % 20; return get100kID(setColumn, setRow, set); } /** * Get the two-letter MGRS 100k designator given information translated from * the UTM northing, easting and zone number. * * @param setColumn * the column index as it relates to the MGRS 100k set * spreadsheet, created from the UTM easting. Values are 1-8. * @param setRow * the row index as it relates to the MGRS 100k set spreadsheet, * created from the UTM northing value. Values are from 0-19. * @param set * the set block, as it relates to the MGRS 100k set spreadsheet, * created from the UTM zone. Values are from 1-60. * @return two letter MGRS 100k code. */ private String get100kID(int setColumn, int setRow, int set) { int baseCol[] = getOriginColumnLetters(); int baseRow[] = getOriginRowLetters(); // colOrigin and rowOrigin are the letters at the origin of // the set int colOrigin = baseCol[set - 1]; int rowOrigin = baseRow[set - 1]; // colInt and rowInt are the letters to build to return int colInt = colOrigin + setColumn - 1; int rowInt = rowOrigin + setRow; boolean rollover = false; if (colInt > Z) { colInt = colInt - Z + A - 1; rollover = true; } if (colInt == I || (colOrigin < I && colInt > I) || ((colInt > I || colOrigin < I) && rollover)) { colInt++; } if (colInt == O || (colOrigin < O && colInt > O) || ((colInt > O || colOrigin < O) && rollover)) { colInt++; if (colInt == I) { colInt++; } } if (colInt > Z) { colInt = colInt - Z + A - 1; } if (rowInt > V) { rowInt = rowInt - V + A - 1; rollover = true; } else { rollover = false; } if (rowInt == I || (rowOrigin < I && rowInt > I) || ((rowInt > I || rowOrigin < I) && rollover)) { rowInt++; } if (rowInt == O || (rowOrigin < O && rowInt > O) || ((rowInt > O || rowOrigin < O) && rollover)) { rowInt++; if (rowInt == I) { rowInt++; } } if (rowInt > V) { rowInt = rowInt - V + A - 1; } String twoLetter = (char) colInt + "" + (char) rowInt; return twoLetter; } /** * {@inheritDoc} */ @Override public String toString() { return mgrs; } }