From 85053db810365c3964dc92a5bb0ea79d93e1d775 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 29 Apr 2018 01:56:07 +0300 Subject: [PATCH 1/6] Remove duplicate toUpperCase() calls --- java/com/google/openlocationcode/OpenLocationCode.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/java/com/google/openlocationcode/OpenLocationCode.java b/java/com/google/openlocationcode/OpenLocationCode.java index dae5c75f..5275ec34 100644 --- a/java/com/google/openlocationcode/OpenLocationCode.java +++ b/java/com/google/openlocationcode/OpenLocationCode.java @@ -153,11 +153,12 @@ public double getEastLongitude() { * @constructor */ public OpenLocationCode(String code) throws IllegalArgumentException { - if (!isValidCode(code.toUpperCase())) { + String newCode = code.toUpperCase(); + if (!isValidCode(newCode)) { throw new IllegalArgumentException( "The provided code '" + code + "' is not a valid Open Location Code."); } - this.code = code.toUpperCase(); + this.code = newCode; } /** From 803cdd5aa5486e7ca20b45954ecab2f82a171747 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 29 Apr 2018 20:20:41 +0300 Subject: [PATCH 2/6] Use Locale.ROOT with toUpper to avoid ambiguities While technically this might not be needed in this specific case, it is a good practice to be explicit about the locale. --- java/com/google/openlocationcode/OpenLocationCode.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/java/com/google/openlocationcode/OpenLocationCode.java b/java/com/google/openlocationcode/OpenLocationCode.java index 5275ec34..ddd317d3 100644 --- a/java/com/google/openlocationcode/OpenLocationCode.java +++ b/java/com/google/openlocationcode/OpenLocationCode.java @@ -15,6 +15,7 @@ package com.google.openlocationcode; import java.math.BigDecimal; +import java.util.Locale; /** * Convert locations to and from convenient short codes. @@ -153,7 +154,7 @@ public double getEastLongitude() { * @constructor */ public OpenLocationCode(String code) throws IllegalArgumentException { - String newCode = code.toUpperCase(); + String newCode = code.toUpperCase(Locale.ROOT); if (!isValidCode(newCode)) { throw new IllegalArgumentException( "The provided code '" + code + "' is not a valid Open Location Code."); @@ -497,7 +498,7 @@ public static boolean isValidCode(String code) { if (code == null || code.length() < 2) { return false; } - code = code.toUpperCase(); + code = code.toUpperCase(Locale.ROOT); // There must be exactly one separator. int separatorPosition = code.indexOf(SEPARATOR); From 1e8d0c34a074a4da65cd8c6be3b3715a10753f19 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 22 Apr 2021 17:46:58 +0200 Subject: [PATCH 3/6] Update to recent version while keeping changes This updates your version of OpenLocationCode.java to the most recent upstream version while keeping your changes. --- .../openlocationcode/OpenLocationCode.java | 428 ++++++++++-------- 1 file changed, 249 insertions(+), 179 deletions(-) diff --git a/java/com/google/openlocationcode/OpenLocationCode.java b/java/com/google/openlocationcode/OpenLocationCode.java index ddd317d3..78989f9d 100644 --- a/java/com/google/openlocationcode/OpenLocationCode.java +++ b/java/com/google/openlocationcode/OpenLocationCode.java @@ -14,32 +14,29 @@ package com.google.openlocationcode; -import java.math.BigDecimal; import java.util.Locale; +import java.util.Objects; /** * Convert locations to and from convenient short codes. * - * Open Location Codes are short, ~10 character codes that can be used instead of street + *

Open Location Codes are short, ~10 character codes that can be used instead of street * addresses. The codes can be generated and decoded offline, and use a reduced character set that * minimises the chance of codes including words. * - * This provides both object and static methods. + *

This provides both object and static methods. * - * Create an object with: - * OpenLocationCode code = new OpenLocationCode("7JVW52GR+2V"); - * OpenLocationCode code = new OpenLocationCode("52GR+2V"); - * OpenLocationCode code = new OpenLocationCode(27.175063, 78.042188); - * OpenLocationCode code = new OpenLocationCode(27.175063, 78.042188, 11); + *

Create an object with: OpenLocationCode code = new OpenLocationCode("7JVW52GR+2V"); + * OpenLocationCode code = new OpenLocationCode("52GR+2V"); OpenLocationCode code = new + * OpenLocationCode(27.175063, 78.042188); OpenLocationCode code = new OpenLocationCode(27.175063, + * 78.042188, 11); * - * Once you have a code object, you can apply the other methods to it, such as to shorten: + *

Once you have a code object, you can apply the other methods to it, such as to shorten: * code.shorten(27.176, 78.05) * - * Recover the nearest match (if the code was a short code): - * code.recover(27.176, 78.05) + *

Recover the nearest match (if the code was a short code): code.recover(27.176, 78.05) * - * Or decode a code into its coordinates, returning a CodeArea object. - * code.decode() + *

Or decode a code into its coordinates, returning a CodeArea object. code.decode() * * @author Jiri Semecky * @author Doug Rinckes @@ -49,43 +46,55 @@ public final class OpenLocationCode { // Provides a normal precision code, approximately 14x14 meters. public static final int CODE_PRECISION_NORMAL = 10; - // Provides an extra precision code, approximately 2x3 meters. - public static final int CODE_PRECISION_EXTRA = 11; + // The character set used to encode the values. + public static final String CODE_ALPHABET = "23456789CFGHJMPQRVWX"; // A separator used to break the code into two parts to aid memorability. - private static final char SEPARATOR = '+'; + public static final char SEPARATOR = '+'; + + // The character used to pad codes. + public static final char PADDING_CHARACTER = '0'; // The number of characters to place before the separator. private static final int SEPARATOR_POSITION = 8; - // The character used to pad codes. - private static final char PADDING_CHARACTER = '0'; + // The max number of digits to process in a plus code. + public static final int MAX_DIGIT_COUNT = 15; - // The character set used to encode the values. - private static final String CODE_ALPHABET = "23456789CFGHJMPQRVWX"; + // Maximum code length using just lat/lng pair encoding. + private static final int PAIR_CODE_LENGTH = 10; - // Note: The double type can't be used because of the rounding arithmetic due to floating point - // implementation. Eg. "8.95 - 8" can give result 0.9499999999999 instead of 0.95 which - // incorrectly classify the points on the border of a cell. Therefore all the calculation is done - // using BigDecimal. + // Number of digits in the grid coding section. + private static final int GRID_CODE_LENGTH = MAX_DIGIT_COUNT - PAIR_CODE_LENGTH; // The base to use to convert numbers to/from. - private static final BigDecimal ENCODING_BASE = new BigDecimal(CODE_ALPHABET.length()); + private static final int ENCODING_BASE = CODE_ALPHABET.length(); // The maximum value for latitude in degrees. - private static final BigDecimal LATITUDE_MAX = new BigDecimal(90); + private static final long LATITUDE_MAX = 90; // The maximum value for longitude in degrees. - private static final BigDecimal LONGITUDE_MAX = new BigDecimal(180); - - // Maximum code length using just lat/lng pair encoding. - private static final int PAIR_CODE_LENGTH = 10; + private static final long LONGITUDE_MAX = 180; // Number of columns in the grid refinement method. - private static final BigDecimal GRID_COLUMNS = new BigDecimal(4); + private static final int GRID_COLUMNS = 4; // Number of rows in the grid refinement method. - private static final BigDecimal GRID_ROWS = new BigDecimal(5); + private static final int GRID_ROWS = 5; + + // Value to multiple latitude degrees to convert it to an integer with the maximum encoding + // precision. I.e. ENCODING_BASE**3 * GRID_ROWS**GRID_CODE_LENGTH + private static final long LAT_INTEGER_MULTIPLIER = 8000 * 3125; + + // Value to multiple longitude degrees to convert it to an integer with the maximum encoding + // precision. I.e. ENCODING_BASE**3 * GRID_COLUMNS**GRID_CODE_LENGTH + private static final long LNG_INTEGER_MULTIPLIER = 8000 * 1024; + + // Value of the most significant latitude digit after it has been converted to an integer. + private static final long LAT_MSP_VALUE = LAT_INTEGER_MULTIPLIER * ENCODING_BASE * ENCODING_BASE; + + // Value of the most significant longitude digit after it has been converted to an integer. + private static final long LNG_MSP_VALUE = LNG_INTEGER_MULTIPLIER * ENCODING_BASE * ENCODING_BASE; /** * Coordinates of a decoded Open Location Code. @@ -93,54 +102,61 @@ public final class OpenLocationCode { *

The coordinates include the latitude and longitude of the lower left and upper right corners * and the center of the bounding box for the area the code represents. */ - public static class CodeArea { + public static class CodeArea { - private final BigDecimal southLatitude; - private final BigDecimal westLongitude; - private final BigDecimal northLatitude; - private final BigDecimal eastLongitude; + private final double southLatitude; + private final double westLongitude; + private final double northLatitude; + private final double eastLongitude; + private final int length; public CodeArea( - BigDecimal southLatitude, - BigDecimal westLongitude, - BigDecimal northLatitude, - BigDecimal eastLongitude) { + double southLatitude, + double westLongitude, + double northLatitude, + double eastLongitude, + int length) { this.southLatitude = southLatitude; this.westLongitude = westLongitude; this.northLatitude = northLatitude; this.eastLongitude = eastLongitude; + this.length = length; } public double getSouthLatitude() { - return southLatitude.doubleValue(); + return southLatitude; } public double getWestLongitude() { - return westLongitude.doubleValue(); + return westLongitude; } public double getLatitudeHeight() { - return northLatitude.subtract(southLatitude).doubleValue(); + return northLatitude - southLatitude; } public double getLongitudeWidth() { - return eastLongitude.subtract(westLongitude).doubleValue(); + return eastLongitude - westLongitude; } public double getCenterLatitude() { - return southLatitude.add(northLatitude).doubleValue() / 2; + return (southLatitude + northLatitude) / 2; } public double getCenterLongitude() { - return westLongitude.add(eastLongitude).doubleValue() / 2; + return (westLongitude + eastLongitude) / 2; } public double getNorthLatitude() { - return northLatitude.doubleValue(); + return northLatitude; } public double getEastLongitude() { - return eastLongitude.doubleValue(); + return eastLongitude; + } + + public int getLength() { + return length; } } @@ -149,11 +165,11 @@ public double getEastLongitude() { /** * Creates Open Location Code object for the provided code. + * * @param code A valid OLC code. Can be a full code or a shortened code. * @throws IllegalArgumentException when the passed code is not valid. - * @constructor - */ - public OpenLocationCode(String code) throws IllegalArgumentException { + */ + public OpenLocationCode(String code) { String newCode = code.toUpperCase(Locale.ROOT); if (!isValidCode(newCode)) { throw new IllegalArgumentException( @@ -164,16 +180,17 @@ public OpenLocationCode(String code) throws IllegalArgumentException { /** * Creates Open Location Code. + * * @param latitude The latitude in decimal degrees. * @param longitude The longitude in decimal degrees. * @param codeLength The desired number of digits in the code. * @throws IllegalArgumentException if the code length is not valid. - * @constructor */ - public OpenLocationCode(double latitude, double longitude, int codeLength) - throws IllegalArgumentException { + public OpenLocationCode(double latitude, double longitude, int codeLength) { + // Limit the maximum number of digits in the code. + codeLength = Math.min(codeLength, MAX_DIGIT_COUNT); // Check that the code length requested is valid. - if (codeLength < 4 || (codeLength < PAIR_CODE_LENGTH && codeLength % 2 == 1)) { + if (codeLength < PAIR_CODE_LENGTH && codeLength % 2 == 1 || codeLength < 4) { throw new IllegalArgumentException("Illegal code length " + codeLength); } // Ensure that latitude and longitude are valid. @@ -181,67 +198,68 @@ public OpenLocationCode(double latitude, double longitude, int codeLength) longitude = normalizeLongitude(longitude); // Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded. - if (latitude == LATITUDE_MAX.doubleValue()) { + if (latitude == LATITUDE_MAX) { latitude = latitude - 0.9 * computeLatitudePrecision(codeLength); } - // Adjust latitude and longitude to be in positive number ranges. - BigDecimal remainingLatitude = new BigDecimal(latitude).add(LATITUDE_MAX); - BigDecimal remainingLongitude = new BigDecimal(longitude).add(LONGITUDE_MAX); - - // Count how many digits have been created. - int generatedDigits = 0; - // Store the code. - StringBuilder codeBuilder = new StringBuilder(); - // The precisions are initially set to ENCODING_BASE^2 because they will be immediately - // divided. - BigDecimal latPrecision = ENCODING_BASE.multiply(ENCODING_BASE); - BigDecimal lngPrecision = ENCODING_BASE.multiply(ENCODING_BASE); - while (generatedDigits < codeLength) { - if (generatedDigits < PAIR_CODE_LENGTH) { - // Use the normal algorithm for the first set of digits. - latPrecision = latPrecision.divide(ENCODING_BASE); - lngPrecision = lngPrecision.divide(ENCODING_BASE); - BigDecimal latDigit = remainingLatitude.divide(latPrecision, 0, BigDecimal.ROUND_FLOOR); - BigDecimal lngDigit = remainingLongitude.divide(lngPrecision, 0, BigDecimal.ROUND_FLOOR); - remainingLatitude = remainingLatitude.subtract(latPrecision.multiply(latDigit)); - remainingLongitude = remainingLongitude.subtract(lngPrecision.multiply(lngDigit)); - codeBuilder.append(CODE_ALPHABET.charAt(latDigit.intValue())); - codeBuilder.append(CODE_ALPHABET.charAt(lngDigit.intValue())); - generatedDigits += 2; - } else { - // Use the 4x5 grid for remaining digits. - latPrecision = latPrecision.divide(GRID_ROWS); - lngPrecision = lngPrecision.divide(GRID_COLUMNS); - BigDecimal row = remainingLatitude.divide(latPrecision, 0, BigDecimal.ROUND_FLOOR); - BigDecimal col = remainingLongitude.divide(lngPrecision, 0, BigDecimal.ROUND_FLOOR); - remainingLatitude = remainingLatitude.subtract(latPrecision.multiply(row)); - remainingLongitude = remainingLongitude.subtract(lngPrecision.multiply(col)); - codeBuilder.append( - CODE_ALPHABET.charAt(row.intValue() * GRID_COLUMNS.intValue() + col.intValue())); - generatedDigits += 1; + // Store the code - we build it in reverse and reorder it afterwards. + StringBuilder revCodeBuilder = new StringBuilder(); + + // Compute the code. + // This approach converts each value to an integer after multiplying it by + // the final precision. This allows us to use only integer operations, so + // avoiding any accumulation of floating point representation errors. + + // Multiply values by their precision and convert to positive. Rounding + // avoids/minimises errors due to floating point precision. + long latVal = + (long) (Math.round((latitude + LATITUDE_MAX) * LAT_INTEGER_MULTIPLIER * 1e6) / 1e6); + long lngVal = + (long) (Math.round((longitude + LONGITUDE_MAX) * LNG_INTEGER_MULTIPLIER * 1e6) / 1e6); + + // Compute the grid part of the code if necessary. + if (codeLength > PAIR_CODE_LENGTH) { + for (int i = 0; i < GRID_CODE_LENGTH; i++) { + long latDigit = latVal % GRID_ROWS; + long lngDigit = lngVal % GRID_COLUMNS; + int ndx = (int) (latDigit * GRID_COLUMNS + lngDigit); + revCodeBuilder.append(CODE_ALPHABET.charAt(ndx)); + latVal /= GRID_ROWS; + lngVal /= GRID_COLUMNS; } + } else { + latVal = (long) (latVal / Math.pow(GRID_ROWS, GRID_CODE_LENGTH)); + lngVal = (long) (lngVal / Math.pow(GRID_COLUMNS, GRID_CODE_LENGTH)); + } + // Compute the pair section of the code. + for (int i = 0; i < PAIR_CODE_LENGTH / 2; i++) { + revCodeBuilder.append(CODE_ALPHABET.charAt((int) (lngVal % ENCODING_BASE))); + revCodeBuilder.append(CODE_ALPHABET.charAt((int) (latVal % ENCODING_BASE))); + latVal /= ENCODING_BASE; + lngVal /= ENCODING_BASE; // If we are at the separator position, add the separator. - if (generatedDigits == SEPARATOR_POSITION) { - codeBuilder.append(SEPARATOR); + if (i == 0) { + revCodeBuilder.append(SEPARATOR); } } - // If the generated code is shorter than the separator position, pad the code and add the - // separator. - if (generatedDigits < SEPARATOR_POSITION) { - for (; generatedDigits < SEPARATOR_POSITION; generatedDigits++) { - codeBuilder.append(PADDING_CHARACTER); + // Reverse the code. + StringBuilder codeBuilder = revCodeBuilder.reverse(); + + // If we need to pad the code, replace some of the digits. + if (codeLength < SEPARATOR_POSITION) { + for (int i = codeLength; i < SEPARATOR_POSITION; i++) { + codeBuilder.setCharAt(i, PADDING_CHARACTER); } - codeBuilder.append(SEPARATOR); } - this.code = codeBuilder.toString(); + this.code = + codeBuilder.subSequence(0, Math.max(SEPARATOR_POSITION + 1, codeLength + 1)).toString(); } /** * Creates Open Location Code with the default precision length. + * * @param latitude The latitude in decimal degrees. * @param longitude The longitude in decimal degrees. - * @constructor */ public OpenLocationCode(double latitude, double longitude) { this(latitude, longitude, CODE_PRECISION_NORMAL); @@ -249,6 +267,8 @@ public OpenLocationCode(double latitude, double longitude) { /** * Returns the string representation of the code. + * + * @return The code. */ public String getCode() { return code; @@ -257,8 +277,10 @@ public String getCode() { /** * Encodes latitude/longitude into 10 digit Open Location Code. This method is equivalent to * creating the OpenLocationCode object and getting the code from it. + * * @param latitude The latitude in decimal degrees. * @param longitude The longitude in decimal degrees. + * @return The code. */ public static String encode(double latitude, double longitude) { return new OpenLocationCode(latitude, longitude).getCode(); @@ -267,8 +289,11 @@ public static String encode(double latitude, double longitude) { /** * Encodes latitude/longitude into Open Location Code of the provided length. This method is * equivalent to creating the OpenLocationCode object and getting the code from it. + * * @param latitude The latitude in decimal degrees. * @param longitude The longitude in decimal degrees. + * @param codeLength The number of digits in the returned code. + * @return The code. */ public static String encode(double latitude, double longitude, int codeLength) { return new OpenLocationCode(latitude, longitude, codeLength).getCode(); @@ -277,6 +302,8 @@ public static String encode(double latitude, double longitude, int codeLength) { /** * Decodes {@link OpenLocationCode} object into {@link CodeArea} object encapsulating * latitude/longitude bounding box. + * + * @return A CodeArea object. */ public CodeArea decode() { if (!isFullCode(code)) { @@ -284,46 +311,40 @@ public CodeArea decode() { "Method decode() could only be called on valid full codes, code was " + code + "."); } // Strip padding and separator characters out of the code. - String decoded = code.replace(String.valueOf(SEPARATOR), "") - .replace(String.valueOf(PADDING_CHARACTER), ""); - - int digit = 0; - // The precisions are initially set to ENCODING_BASE^2 because they will be immediately - // divided. - BigDecimal latPrecision = ENCODING_BASE.multiply(ENCODING_BASE); - BigDecimal lngPrecision = ENCODING_BASE.multiply(ENCODING_BASE); - // Save the coordinates. - BigDecimal southLatitude = new BigDecimal(0); - BigDecimal westLongitude = new BigDecimal(0); - - // Decode the digits. - while (digit < decoded.length()) { - if (digit < PAIR_CODE_LENGTH) { - // Decode a pair of digits, the first being latitude and the second being longitude. - latPrecision = latPrecision.divide(ENCODING_BASE); - lngPrecision = lngPrecision.divide(ENCODING_BASE); - int digitVal = CODE_ALPHABET.indexOf(decoded.charAt(digit)); - southLatitude = southLatitude.add(latPrecision.multiply(new BigDecimal(digitVal))); - digitVal = CODE_ALPHABET.indexOf(decoded.charAt(digit + 1)); - westLongitude = westLongitude.add(lngPrecision.multiply(new BigDecimal(digitVal))); - digit += 2; - } else { - // Use the 4x5 grid for digits after 10. - int digitVal = CODE_ALPHABET.indexOf(decoded.charAt(digit)); - int row = (int) (digitVal / GRID_COLUMNS.intValue()); - int col = digitVal % GRID_COLUMNS.intValue(); - latPrecision = latPrecision.divide(GRID_ROWS); - lngPrecision = lngPrecision.divide(GRID_COLUMNS); - southLatitude = southLatitude.add(latPrecision.multiply(new BigDecimal(row))); - westLongitude = westLongitude.add(lngPrecision.multiply(new BigDecimal(col))); - digit += 1; - } - } + String clean = + code.replace(String.valueOf(SEPARATOR), "").replace(String.valueOf(PADDING_CHARACTER), ""); + + // Initialise the values. We work them out as integers and convert them to doubles at the end. + long latVal = -LATITUDE_MAX * LAT_INTEGER_MULTIPLIER; + long lngVal = -LONGITUDE_MAX * LNG_INTEGER_MULTIPLIER; + // Define the place value for the digits. We'll divide this down as we work through the code. + long latPlaceVal = LAT_MSP_VALUE; + long lngPlaceVal = LNG_MSP_VALUE; + for (int i = 0; i < Math.min(clean.length(), PAIR_CODE_LENGTH); i += 2) { + latPlaceVal /= ENCODING_BASE; + lngPlaceVal /= ENCODING_BASE; + latVal += CODE_ALPHABET.indexOf(clean.charAt(i)) * latPlaceVal; + lngVal += CODE_ALPHABET.indexOf(clean.charAt(i + 1)) * lngPlaceVal; + } + for (int i = PAIR_CODE_LENGTH; i < Math.min(clean.length(), MAX_DIGIT_COUNT); i++) { + latPlaceVal /= GRID_ROWS; + lngPlaceVal /= GRID_COLUMNS; + int digit = CODE_ALPHABET.indexOf(clean.charAt(i)); + int row = digit / GRID_COLUMNS; + int col = digit % GRID_COLUMNS; + latVal += row * latPlaceVal; + lngVal += col * lngPlaceVal; + } + double latitudeLo = (double) latVal / LAT_INTEGER_MULTIPLIER; + double longitudeLo = (double) lngVal / LNG_INTEGER_MULTIPLIER; + double latitudeHi = (double) (latVal + latPlaceVal) / LAT_INTEGER_MULTIPLIER; + double longitudeHi = (double) (lngVal + lngPlaceVal) / LNG_INTEGER_MULTIPLIER; return new CodeArea( - southLatitude.subtract(LATITUDE_MAX), - westLongitude.subtract(LONGITUDE_MAX), - southLatitude.subtract(LATITUDE_MAX).add(latPrecision), - westLongitude.subtract(LONGITUDE_MAX).add(lngPrecision)); + latitudeLo, + longitudeLo, + latitudeHi, + longitudeHi, + Math.min(clean.length(), MAX_DIGIT_COUNT)); } /** @@ -331,28 +352,47 @@ public CodeArea decode() { * latitude/longitude bounding box. * * @param code Open Location Code to be decoded. + * @return A CodeArea object. * @throws IllegalArgumentException if the provided code is not a valid Open Location Code. */ public static CodeArea decode(String code) throws IllegalArgumentException { return new OpenLocationCode(code).decode(); } - /** Returns whether this {@link OpenLocationCode} is a full Open Location Code. */ + /** + * Returns whether this {@link OpenLocationCode} is a full Open Location Code. + * + * @return True if it is a full code. + */ public boolean isFull() { return code.indexOf(SEPARATOR) == SEPARATOR_POSITION; } - /** Returns whether the provided Open Location Code is a full Open Location Code. */ + /** + * Returns whether the provided Open Location Code is a full Open Location Code. + * + * @param code The code to check. + * @return True if it is a full code. + */ public static boolean isFull(String code) throws IllegalArgumentException { return new OpenLocationCode(code).isFull(); } - /** Returns whether this {@link OpenLocationCode} is a short Open Location Code. */ + /** + * Returns whether this {@link OpenLocationCode} is a short Open Location Code. + * + * @return True if it is short. + */ public boolean isShort() { return code.indexOf(SEPARATOR) >= 0 && code.indexOf(SEPARATOR) < SEPARATOR_POSITION; } - /** Returns whether the provided Open Location Code is a short Open Location Code. */ + /** + * Returns whether the provided Open Location Code is a short Open Location Code. + * + * @param code The code to check. + * @return True if it is short. + */ public static boolean isShort(String code) throws IllegalArgumentException { return new OpenLocationCode(code).isShort(); } @@ -360,6 +400,8 @@ public static boolean isShort(String code) throws IllegalArgumentException { /** * Returns whether this {@link OpenLocationCode} is a padded Open Location Code, meaning that it * contains less than 8 valid digits. + * + * @return True if this code is padded. */ private boolean isPadded() { return code.indexOf(PADDING_CHARACTER) >= 0; @@ -368,6 +410,9 @@ private boolean isPadded() { /** * Returns whether the provided Open Location Code is a padded Open Location Code, meaning that it * contains less than 8 valid digits. + * + * @param code The code to check. + * @return True if it is padded. */ public static boolean isPadded(String code) throws IllegalArgumentException { return new OpenLocationCode(code).isPadded(); @@ -377,6 +422,10 @@ public static boolean isPadded(String code) throws IllegalArgumentException { * Returns short {@link OpenLocationCode} from the full Open Location Code created by removing * four or six digits, depending on the provided reference point. It removes as many digits as * possible. + * + * @param referenceLatitude Degrees. + * @param referenceLongitude Degrees. + * @return A short code if possible. */ public OpenLocationCode shorten(double referenceLatitude, double referenceLongitude) { if (!isFull()) { @@ -387,9 +436,10 @@ public OpenLocationCode shorten(double referenceLatitude, double referenceLongit } CodeArea codeArea = decode(); - double range = Math.max( - Math.abs(referenceLatitude - codeArea.getCenterLatitude()), - Math.abs(referenceLongitude - codeArea.getCenterLongitude())); + double range = + Math.max( + Math.abs(referenceLatitude - codeArea.getCenterLatitude()), + Math.abs(referenceLongitude - codeArea.getCenterLongitude())); // We are going to check to see if we can remove three pairs, two pairs or just one pair of // digits from the code. for (int i = 4; i >= 1; i--) { @@ -408,6 +458,10 @@ public OpenLocationCode shorten(double referenceLatitude, double referenceLongit /** * Returns an {@link OpenLocationCode} object representing a full Open Location Code from this * (short) Open Location Code, given the reference location. + * + * @param referenceLatitude Degrees. + * @param referenceLongitude Degrees. + * @return The nearest matching full code. */ public OpenLocationCode recover(double referenceLatitude, double referenceLongitude) { if (isFull()) { @@ -419,7 +473,7 @@ public OpenLocationCode recover(double referenceLatitude, double referenceLongit int digitsToRecover = SEPARATOR_POSITION - code.indexOf(SEPARATOR); // The precision (height and width) of the missing prefix in degrees. - double prefixPrecision = Math.pow(ENCODING_BASE.intValue(), 2 - (digitsToRecover / 2)); + double prefixPrecision = Math.pow(ENCODING_BASE, 2 - (digitsToRecover / 2)); // Use the reference location to generate the prefix. String recoveredPrefix = @@ -437,11 +491,10 @@ public OpenLocationCode recover(double referenceLatitude, double referenceLongit // Move the recovered latitude by one precision up or down if it is too far from the reference, // unless doing so would lead to an invalid latitude. double latitudeDiff = recoveredLatitude - referenceLatitude; - if (latitudeDiff > prefixPrecision / 2 - && recoveredLatitude - prefixPrecision > -LATITUDE_MAX.intValue()) { + if (latitudeDiff > prefixPrecision / 2 && recoveredLatitude - prefixPrecision > -LATITUDE_MAX) { recoveredLatitude -= prefixPrecision; } else if (latitudeDiff < -prefixPrecision / 2 - && recoveredLatitude + prefixPrecision < LATITUDE_MAX.intValue()) { + && recoveredLatitude + prefixPrecision < LATITUDE_MAX) { recoveredLatitude += prefixPrecision; } @@ -460,6 +513,10 @@ public OpenLocationCode recover(double referenceLatitude, double referenceLongit /** * Returns whether the bounding box specified by the Open Location Code contains provided point. + * + * @param latitude Degrees. + * @param longitude Degrees. + * @return True if the coordinates are contained by the code. */ public boolean contains(double latitude, double longitude) { CodeArea codeArea = decode(); @@ -478,7 +535,7 @@ public boolean equals(Object o) { return false; } OpenLocationCode that = (OpenLocationCode) o; - return hashCode() == that.hashCode(); + return Objects.equals(code, that.code); } @Override @@ -493,7 +550,12 @@ public String toString() { // Exposed static helper methods. - /** Returns whether the provided string is a valid Open Location code. */ + /** + * Returns whether the provided string is a valid Open Location code. + * + * @param code The code to check. + * @return True if it is a valid full or short code. + */ public static boolean isValidCode(String code) { if (code == null || code.length() < 2) { return false; @@ -508,22 +570,20 @@ public static boolean isValidCode(String code) { if (separatorPosition != code.lastIndexOf(SEPARATOR)) { return false; } - - if (separatorPosition % 2 != 0) { + // There must be an even number of at most 8 characters before the separator. + if (separatorPosition % 2 != 0 || separatorPosition > SEPARATOR_POSITION) { return false; } // Check first two characters: only some values from the alphabet are permitted. - if (separatorPosition == 8) { + if (separatorPosition == SEPARATOR_POSITION) { // First latitude character can only have first 9 values. - Integer index0 = CODE_ALPHABET.indexOf(code.charAt(0)); - if (index0 == null || index0 > 8) { + if (CODE_ALPHABET.indexOf(code.charAt(0)) > 8) { return false; } // First longitude character can only have first 18 values. - Integer index1 = CODE_ALPHABET.indexOf(code.charAt(1)); - if (index1 == null || index1 > 17) { + if (CODE_ALPHABET.indexOf(code.charAt(1)) > 17) { return false; } } @@ -531,25 +591,26 @@ public static boolean isValidCode(String code) { // Check the characters before the separator. boolean paddingStarted = false; for (int i = 0; i < separatorPosition; i++) { + if (CODE_ALPHABET.indexOf(code.charAt(i)) == -1 && code.charAt(i) != PADDING_CHARACTER) { + // Invalid character. + return false; + } if (paddingStarted) { // Once padding starts, there must not be anything but padding. if (code.charAt(i) != PADDING_CHARACTER) { return false; } - continue; - } - if (CODE_ALPHABET.indexOf(code.charAt(i)) != -1) { - continue; - } - if (PADDING_CHARACTER == code.charAt(i)) { + } else if (code.charAt(i) == PADDING_CHARACTER) { paddingStarted = true; + // Short codes cannot have padding + if (separatorPosition < SEPARATOR_POSITION) { + return false; + } // Padding can start on even character: 2, 4 or 6. if (i != 2 && i != 4 && i != 6) { return false; } - continue; } - return false; // Illegal character. } // Check the characters after the separator. @@ -571,7 +632,12 @@ public static boolean isValidCode(String code) { return true; } - /** Returns if the code is a valid full Open Location Code. */ + /** + * Returns if the code is a valid full Open Location Code. + * + * @param code The code to check. + * @return True if it is a valid full code. + */ public static boolean isFullCode(String code) { try { return new OpenLocationCode(code).isFull(); @@ -580,7 +646,12 @@ public static boolean isFullCode(String code) { } } - /** Returns if the code is a valid short Open Location Code. */ + /** + * Returns if the code is a valid short Open Location Code. + * + * @param code The code to check. + * @return True if it is a valid short code. + */ public static boolean isShortCode(String code) { try { return new OpenLocationCode(code).isShort(); @@ -592,15 +663,15 @@ public static boolean isShortCode(String code) { // Private static methods. private static double clipLatitude(double latitude) { - return Math.min(Math.max(latitude, -LATITUDE_MAX.intValue()), LATITUDE_MAX.intValue()); + return Math.min(Math.max(latitude, -LATITUDE_MAX), LATITUDE_MAX); } private static double normalizeLongitude(double longitude) { - while (longitude < -LONGITUDE_MAX.intValue()) { - longitude = longitude + LONGITUDE_MAX.intValue() * 2; + while (longitude < -LONGITUDE_MAX) { + longitude = longitude + LONGITUDE_MAX * 2; } - while (longitude >= LONGITUDE_MAX.intValue()) { - longitude = longitude - LONGITUDE_MAX.intValue() * 2; + while (longitude >= LONGITUDE_MAX) { + longitude = longitude - LONGITUDE_MAX * 2; } return longitude; } @@ -612,9 +683,8 @@ private static double normalizeLongitude(double longitude) { */ private static double computeLatitudePrecision(int codeLength) { if (codeLength <= CODE_PRECISION_NORMAL) { - return Math.pow(ENCODING_BASE.intValue(), Math.floor(codeLength / -2 + 2)); + return Math.pow(ENCODING_BASE, (double) (codeLength / -2 + 2)); } - return Math.pow(ENCODING_BASE.intValue(), -3) - / Math.pow(GRID_ROWS.intValue(), codeLength - PAIR_CODE_LENGTH); + return Math.pow(ENCODING_BASE, -3) / Math.pow(GRID_ROWS, codeLength - PAIR_CODE_LENGTH); } } From 92e2e400bd65ae21e56eaf9545363680d884918c Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 21 May 2021 16:02:37 -0400 Subject: [PATCH 4/6] simplified per suggestion --- .../java/com/google/openlocationcode/OpenLocationCode.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java b/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java index 78989f9d..4bc1521b 100644 --- a/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java +++ b/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java @@ -170,12 +170,11 @@ public int getLength() { * @throws IllegalArgumentException when the passed code is not valid. */ public OpenLocationCode(String code) { - String newCode = code.toUpperCase(Locale.ROOT); - if (!isValidCode(newCode)) { + if (!isValidCode(code)) { throw new IllegalArgumentException( "The provided code '" + code + "' is not a valid Open Location Code."); } - this.code = newCode; + this.code = code.toUpperCase(Locale.ROOT); } /** From 9101f2567b8bb2ce58a65ba49d55d2114fe48029 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 21 May 2021 16:07:19 -0400 Subject: [PATCH 5/6] Further optimization to remove dup toUpperCase --- .../openlocationcode/OpenLocationCode.java | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java b/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java index 4bc1521b..8cbaef29 100644 --- a/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java +++ b/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java @@ -170,11 +170,11 @@ public int getLength() { * @throws IllegalArgumentException when the passed code is not valid. */ public OpenLocationCode(String code) { - if (!isValidCode(code)) { + this.code = toValidCode(code); + if (this.code == null) { throw new IllegalArgumentException( "The provided code '" + code + "' is not a valid Open Location Code."); } - this.code = code.toUpperCase(Locale.ROOT); } /** @@ -556,58 +556,68 @@ public String toString() { * @return True if it is a valid full or short code. */ public static boolean isValidCode(String code) { + return toValidCode(code) != null; + } + + /** + * Normalizes Open Location, or returns `null` if the code is not valid. + * + * @param code The code to check and normalize. + * @return Normalized code if it is a valid full or short code, null otherwise. + */ + private static String toValidCode(String code) { if (code == null || code.length() < 2) { - return false; + return null; } code = code.toUpperCase(Locale.ROOT); // There must be exactly one separator. int separatorPosition = code.indexOf(SEPARATOR); if (separatorPosition == -1) { - return false; + return null; } if (separatorPosition != code.lastIndexOf(SEPARATOR)) { - return false; + return null; } // There must be an even number of at most 8 characters before the separator. if (separatorPosition % 2 != 0 || separatorPosition > SEPARATOR_POSITION) { - return false; + return null; } // Check first two characters: only some values from the alphabet are permitted. if (separatorPosition == SEPARATOR_POSITION) { // First latitude character can only have first 9 values. if (CODE_ALPHABET.indexOf(code.charAt(0)) > 8) { - return false; + return null; } // First longitude character can only have first 18 values. if (CODE_ALPHABET.indexOf(code.charAt(1)) > 17) { - return false; + return null; } } // Check the characters before the separator. - boolean paddingStarted = false; + boolean paddingStarted = null; for (int i = 0; i < separatorPosition; i++) { if (CODE_ALPHABET.indexOf(code.charAt(i)) == -1 && code.charAt(i) != PADDING_CHARACTER) { // Invalid character. - return false; + return null; } if (paddingStarted) { // Once padding starts, there must not be anything but padding. if (code.charAt(i) != PADDING_CHARACTER) { - return false; + return null; } } else if (code.charAt(i) == PADDING_CHARACTER) { paddingStarted = true; // Short codes cannot have padding if (separatorPosition < SEPARATOR_POSITION) { - return false; + return null; } // Padding can start on even character: 2, 4 or 6. if (i != 2 && i != 4 && i != 6) { - return false; + return null; } } } @@ -615,20 +625,20 @@ public static boolean isValidCode(String code) { // Check the characters after the separator. if (code.length() > separatorPosition + 1) { if (paddingStarted) { - return false; + return null; } // Only one character after separator is forbidden. if (code.length() == separatorPosition + 2) { - return false; + return null; } for (int i = separatorPosition + 1; i < code.length(); i++) { if (CODE_ALPHABET.indexOf(code.charAt(i)) == -1) { - return false; + return null; } } } - return true; + return code; } /** From efe3cde61593266599b90f8cbc5d26700518d274 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 18 Nov 2021 16:25:46 -0500 Subject: [PATCH 6/6] Update java/src/main/java/com/google/openlocationcode/OpenLocationCode.java Co-authored-by: Sonya Alexandrova --- .../main/java/com/google/openlocationcode/OpenLocationCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java b/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java index 8cbaef29..73a78870 100644 --- a/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java +++ b/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java @@ -598,7 +598,7 @@ private static String toValidCode(String code) { } // Check the characters before the separator. - boolean paddingStarted = null; + boolean paddingStarted = false; for (int i = 0; i < separatorPosition; i++) { if (CODE_ALPHABET.indexOf(code.charAt(i)) == -1 && code.charAt(i) != PADDING_CHARACTER) { // Invalid character.