package javax.microedition.location; import java.util.Enumeration; import java.util.Vector; /** * This class has been designed to manage coordinates * using JSR-179 Location API * http://www.jcp.org/en/jsr/detail?id=179 * * @author Juan Antonio Brenha Moral (calculateDistanceAndAzimuth by Charles Manning) */ public class Coordinates{ private double latitude; private double longitude; private double altitude; /** * Identifier for string coordinate representation Degrees, Minutes, decimal fractions of a minute * See Also:Constant Field Values */ public static final int DD_MM=2; /** * Identifier for string coordinate representation Degrees, Minutes, Seconds and decimal fractions of a second * See Also:Constant Field Values */ public static final int DD_MM_SS=1; static final double EARTH_RADIUS = 6378137D; static float calculatedDistance = Float.NaN; static float calculatedAzimuth = Float.NaN; /* Constructor */ /** * Create a Coordinate object with 3 parameters: * latitude, longitude and altitude * * @param latitude * @param longitude * @param altitude */ public Coordinates(double latitude, double longitude,double altitude) { setLatitude(latitude); setLongitude(longitude); setAltitude(altitude); } public Coordinates(double latitude, double longitude) { this(latitude,longitude,0); } /** * Get latitude * * @return the latitude */ public double getLatitude() { return latitude; } /* * Set Latitude */ public void setLatitude(double latitude) { this.latitude = latitude; } /* * Set Longitude */ public void setLongitude(double longitude) { this.longitude = longitude; } /** * Get Longitude * * @return the longitude */ public double getLongitude() { return longitude; } /* * Set Altitude in meters */ public void setAltitude(double altitude) { this.altitude = altitude; } /** * Altitude above mean sea level. * * @return the altitude */ public double getAltitude() { return altitude; } /** * Calculates the azimuth between the two points according to * the ellipsoid model of WGS84. The azimuth is relative to true north. * The Coordinates object on which this method is called is considered * the origin for the calculation and the Coordinates object passed * as a parameter is the destination which the azimuth is calculated to. * When the origin is the North pole and the destination * is not the North pole, this method returns 180.0. * When the origin is the South pole and the destination is not * the South pole, this method returns 0.0. If the origin is equal * to the destination, this method returns 0.0. * The implementation shall calculate the result as exactly as it can. * However, it is required that the result is within 1 degree of the correct result. * */ public double azimuthTo(Coordinates to){ if(to == null){ throw new NullPointerException(); }else{ // TODO: Is there some way to make it not recalculate if it already calculated for these coordinates? Keep in mind coordinates can change. // Perhaps it keeps a reference to last to coordinate. If values are the same, then doesn't recalculate. calculateDistanceAndAzimuth(getLatitude(), getLongitude(), to.getLatitude(), to.getLongitude()); while (calculatedAzimuth < 0) calculatedAzimuth += 360; while(calculatedAzimuth > 360) calculatedAzimuth -= 360; return calculatedAzimuth; } } /*********************************** UNTESTED as of April 7, 2009 - BB ********************************* / /** * Converts a double representation of a coordinate with decimal degrees into a string * representation. There are string syntaxes supported are the same as for the * #convert(String) method. The implementation shall provide as many significant * digits for the decimal fractions as are allowed by the string syntax definition. * * @param coordinate * a double representation of a coordinate * @param outputType * identifier of the type of the string representation wanted for output * The constant {@link #DD_MM_SS} identifies the syntax 1 and the constant * {@link #DD_MM} identifies the syntax 2. * @throws IllegalArgumentException * if the outputType is not one of the two constant values defined in this * class or if the coordinate value is not within the range [-180.0, * 180.0) or is Double.NaN * @return a string representation of the coordinate in a representation indicated by * the parameter * @see #convert(String) */ public static String convert(double coordinate, int outputType) throws IllegalArgumentException { if ((coordinate < -180.0) || (coordinate > 180.0)) throw new IllegalArgumentException(); // treat negative values correctly int degrees; if (coordinate >= 0.0) { degrees = (int) Math.floor(coordinate); } else { degrees = (int) Math.ceil(coordinate); } // The decimal string String DD = Integer.toString(degrees); // The minute string double decimalFracDegrees = Math.abs((coordinate - degrees) * 100.0); int minutes = (int) (Math.floor(decimalFracDegrees * 0.6)); String MM = Integer.toString(minutes); if (outputType == DD_MM_SS) { // The seconds string double decimalFracMin = (decimalFracDegrees * 0.6 - minutes) * 100.0; // Math.round(x) does not exist in CLDC/MIDP but it is equivalent to // Math.floor(x + 0.5d) int ss = (int) Math.floor(decimalFracMin * 0.6 + 0.5d); String SS = Integer.toString(ss); if (SS.length() == 1) SS = "0" + SS; // The decimal fraction part of seconds, up to 3 significant digits int decimalFracSec = (int) Math .floor((decimalFracMin * 0.6 - ss) * 1000.0 + 0.5d); String SS_d = dropTrailingZeros(decimalFracSec); String out = DD + ":" + MM; // output only significant figures if (SS_d != null) { out = out + ":" + SS + "." + SS_d; } else if (!SS.equals("00")) { out = out + ":" + SS; } return out; } else if (outputType == DD_MM) { // The decimal fraction part of minutes, up to 5 significant digits double decimalFracMin = (decimalFracDegrees * 0.6 - minutes) * 100000.0; int ss = (int) Math.floor(decimalFracMin + 0.5d); String MM_d = dropTrailingZeros(ss); String out = DD + ":" + MM; // output only significant figures if (MM_d != null) { out = out + "." + MM_d; } return out; } else throw new IllegalArgumentException(); } /** * Takes an integer and removes trailing zeros. * * @param number * must be positive * @return the number as a String, with trailing zeros removed. Returns null if the * number was zero or negative. */ private static String dropTrailingZeros(int number) { if (number <= 0) return null; while ((number % 10) == 0) { number = number / 10; } return Integer.toString(number); } /** * Converts a String representation of a coordinate into the double representation as * used in this API. There are two string syntaxes supported: * <p> * 1. Degrees, minutes, seconds and decimal fractions of seconds. This is expressed as * a string complying with the following BNF definition where the degrees are within * the range [-179, 179] and the minutes and seconds are within the range [0, 59], or * the degrees is -180 and the minutes, seconds and decimal fractions are 0: * <p> * coordinate = degrees ":" minutes ":" seconds "." * decimalfrac | degrees ":" minutes ":" seconds | degrees * ":" minutes<br /> * degrees = degreedigits | "-" degreedigits<br /> * degreedigits = digit | nonzerodigit digit | "1" digit digit<br /> * minutes = minsecfirstdigit digit<br /> * seconds = minsecfirstdigit digit<br /> * decimalfrac = 1*3digit <br /> * digit = "0" | "1" | "2" | "3" | * "4" | "5" | "6" | "7" | "8" | * "9"<br /> * nonzerodigit = "1" | "2" | "3" | "4" | * "5" | "6" | "7" | "8" | "9"<br /> * minsecfirstdigit = "0" | "1" | "2" | "3" | * "4" | "5"<br /> * <p> * 2. Degrees, minutes and decimal fractions of minutes. This is expressed as a string * complying with the following BNF definition where the degrees are within the range * [-179, 179] and the minutes are within the range [0, 59], or the degrees is -180 * and the minutes and decimal fractions are 0: * <p> * coordinate = degrees ":" minutes "." decimalfrac | degrees * ":" minutes<br/> degrees = degreedigits | "-" degreedigits<br/> * degreedigits = digit | nonzerodigit digit | "1" digit digit<br/> minutes = * minsecfirstdigit digit<br/> decimalfrac = 1*5digit<br/> digit = "0" | * "1" | "2" | "3" | "4" | "5" | * "6" | "7" | "8" | "9"<br/> nonzerodigit = * "1" | "2" | "3" | "4" | "5" | * "6" | "7" | "8" | "9"<br/> * minsecfirstdigit = "0" | "1" | "2" | "3" | * "4" | "5" * <p> * For example, for the double value of the coordinate 61.51d, the corresponding * syntax 1 string is "61:30:36" and the corresponding syntax 2 string is "61:30.6". * * @param coordinate * a String in either of the two representation specified above * @return a double value with decimal degrees that matches the string representation * given as the parameter * @throws IllegalArgumentException * if the coordinate input parameter does not comply with the defined * syntax for the specified types * @throws NullPointerException * if the coordinate string is null convert */ // TODO: This method similar to NMEASentence.degreesMintoDegrees(). Use that? public static double convert(String coordinate) throws IllegalArgumentException, NullPointerException { /* * A much more academic way to do this would be to generate some tree-based parser * code using the BNF definition, but that seems a little too heavyweight for such * short strings. */ if (coordinate == null) throw new NullPointerException(); /* * We don't have Java 5 regex or split support in Java 1.3, making this task a bit * of a pain to code. */ /* * First we check that all the characters are valid, whilst also counting the * number of colons and decimal points (we check that colons do not follow * decimals). This allows us to know what type the string is. */ int length = coordinate.length(); int colons = 0; int decimals = 0; for (int i = 0; i < length; i++) { char element = coordinate.charAt(i); if (!convertIsValidChar(element)) throw new IllegalArgumentException(); if (element == ':') { if (decimals > 0) throw new IllegalArgumentException(); colons++; } else if (element == '.') { decimals++; if (decimals > 1) throw new IllegalArgumentException(); } } /* * Then we break the string into its components and parse the individual pieces * (whilst also doing bounds checking). Code looks ugly because there is a lot of * Exception throwing for bad syntax. */ String[] parts = convertSplit(coordinate); try { double out = 0.0; // the first 2 parts are the same, regardless of type int degrees = Integer.valueOf(parts[0]).intValue(); if ((degrees < -180) || (degrees > 179)) throw new IllegalArgumentException(); boolean negative = false; if (degrees < 0) { negative = true; degrees = Math.abs(degrees); } out += degrees; int minutes = Integer.valueOf(parts[1]).intValue(); if ((minutes < 0) || (minutes > 59)) throw new IllegalArgumentException(); out += minutes * 0.1 / 6; if (colons == 2) { // type 1 int seconds = Integer.valueOf(parts[2]).intValue(); if ((seconds < 0) || (seconds > 59)) throw new IllegalArgumentException(); // degrees:minutes:seconds out += seconds * 0.01 / 36; if (decimals == 1) { // degrees:minutes:seconds.decimalfrac double decimalfrac = Double.valueOf("0." + parts[3]) .doubleValue(); // note that spec says this should be 1*3digit, but we don't // restrict the digit count if ((decimalfrac < 0) || (decimalfrac >= 1)) throw new IllegalArgumentException(); out += decimalfrac * 0.01 / 36; } } else if ((colons == 1) && (decimals == 1)) { // type 2 // degrees:minutes.decimalfrac double decimalfrac = Double.valueOf("0." + parts[2]) .doubleValue(); // note that spec says this should be 1*5digit, but we don't // restrict the digit count if ((decimalfrac < 0) || (decimalfrac >= 1)) throw new IllegalArgumentException(); out += decimalfrac * 0.1 / 6; } else throw new IllegalArgumentException(); if (negative) { out = -out; } // do a final check on bounds if ((out < -180.0) || (out >= 180.0)) throw new IllegalArgumentException(); return out; } catch (NumberFormatException e) { throw new IllegalArgumentException(); } } /** * Helper method for {@link #convert(String)} * * @param element * @return */ private static boolean convertIsValidChar(char element) { if ((element == '-') || (element == ':') || (element == '.') || Character.isDigit(element)) return true; return false; } /** * Helper method for {@link #convert(String)} * * @param in * @return */ private static String[] convertSplit(String in) throws IllegalArgumentException { Vector parts = new Vector(4); int start = 0; int length = in.length(); for (int i = 0; i <= length; i++) { if ((i == length) || (in.charAt(i) == ':') || (in.charAt(i) == '.')) { // syntax checking if (start - i == 0) throw new IllegalArgumentException(); String part = in.substring(start, i); parts.addElement(part); start = i + 1; } } // syntax checking if ((parts.size() < 2) || (parts.size() > 4)) throw new IllegalArgumentException(); // return an array String[] partsArray = new String[parts.size()]; Enumeration en = parts.elements(); for (int i = 0; en.hasMoreElements(); i++) { partsArray[i] = (String) en.nextElement(); } return partsArray; } /***********************************/ /** * * Calculates the geodetic distance between the two points according * to the ellipsoid model of WGS84. Altitude is neglected from calculations. * * The implementation shall calculate this as exactly as it can. * However, it is required that the result is within 0.36% of * the correct result. * * @param to the point to calculate the geodetic to * @return the distance */ public double distance(Coordinates to){ if(to == null){ throw new NullPointerException(); }else{ // TODO: Is there some way to make it not recalculate if it already // calculated for these coordinates? Keep in mind coordinates can change. calculateDistanceAndAzimuth(getLatitude(), getLongitude(), to.getLatitude(), to.getLongitude()); return calculatedDistance; } } private static void calculateDistanceAndAzimuth(double d, double d1, double d2, double d3){ // TODO: This code is huge. Can it be minimized? double d4 = Math.toRadians(d); double d5 = Math.toRadians(d1); double d6 = Math.toRadians(d2); double d7 = Math.toRadians(d3); double d8 = 0.0033528106647474805D; // TODO: Why are these given 0 values? double d9 = 0.0D; double d10 = 0.0D; double d20 = 0.0D; double d22 = 0.0D; double d24 = 0.0D; double d25 = 0.0D; double d26 = 0.0D; double d28 = 0.0D; double d29 = 0.0D; double d30 = 0.0D; double d31 = 0.0D; double d32 = 0.0D; double d33 = 5.0000000000000003E-10D; int i = 1; byte byte0 = 100; if(d4 == d6 && (d5 == d7 || Math.abs(Math.abs(d5 - d7) - 6.2831853071795862D) < d33)) { calculatedDistance = 0.0F; calculatedAzimuth = 0.0F; return; } // TODO: Use our version of Math.PI throughout, including 2pi. if(d4 + d6 == 0.0D && Math.abs(d5 - d7) == 3.1415926535897931D) d4 += 1.0000000000000001E-05D; double d11 = 1.0D - d8; double d12 = d11 * Math.tan(d4); double d13 = d11 * Math.tan(d6); double d14 = 1.0D / Math.sqrt(1.0D + d12 * d12); double d15 = d14 * d12; double d16 = 1.0D / Math.sqrt(1.0D + d13 * d13); double d17 = d14 * d16; double d18 = d17 * d13; double d19 = d18 * d12; d9 = d7 - d5; for(d32 = d9 + 1.0D; i < byte0 && Math.abs(d32 - d9) > d33; d9 = ((1.0D - d31) * d9 * d8 + d7) - d5) { i++; double d21 = Math.sin(d9); double d23 = Math.cos(d9); d12 = d16 * d21; d13 = d18 - d15 * d16 * d23; d24 = Math.sqrt(d12 * d12 + d13 * d13); d25 = d17 * d23 + d19; d10 = Math.atan2(d24, d25); double d27 = (d17 * d21) / d24; d28 = 1.0D - d27 * d27; d29 = 2D * d19; if(d28 > 0.0D) d29 = d25 - d29 / d28; d30 = -1D + 2D * d29 * d29; d31 = (((-3D * d28 + 4D) * d8 + 4D) * d28 * d8) / 16D; d32 = d9; d9 = ((d30 * d25 * d31 + d29) * d24 * d31 + d10) * d27; } double d34 = mod(Math.atan2(d12, d13), 6.2831853071795862D); d9 = Math.sqrt((1.0D / (d11 * d11) - 1.0D) * d28 + 1.0D); d9++; d9 = (d9 - 2D) / d9; d31 = ((d9 * d9) / 4D + 1.0D) / (1.0D - d9); d32 = (d9 * d9 * 0.375D - 1.0D) * d9; d9 = d30 * d25; double d35 = ((((((d24 * d24 * 4D - 3D) * (1.0D - d30 - d30) * d29 * d32) / 6D - d9) * d32) / 4D + d29) * d24 * d32 + d10) * d31 * 6378137D * d11; if((double)Math.abs(i - byte0) < d33) { calculatedDistance = (0.0F / 0.0F); calculatedAzimuth = (0.0F / 0.0F); return; } d34 = (180D * d34) / 3.1415926535897931D; calculatedDistance = (float)d35; calculatedAzimuth = (float)d34; if(d == 90D) calculatedAzimuth = 180F; else if(d == -90D) calculatedAzimuth = 0.0F; } // TODO: A mod method? Why not use % private static double mod(double d, double d1){ return d - d1 * Math.floor(d / d1); } }