package me.shkschneider.openlocationcodes; // Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the 'License'); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an 'AS IS' BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import android.location.Location; import android.support.annotation.NonNull; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import java.util.Locale; // Open Location Codes were developed at Google's Zurich engineering office, and then open sourced so that they can be freely used. // The main author is Doug Rinckes (@drinckes), with the help of lots of colleagues including: // Philipp Bunge, Aner Ben-Artzi, Jarda Bengl, Prasenit Phukan, Sacha van Ginhoven and Zongwei Li // // http://openlocationcode.com // // Java port by ShkSchneider <https://shkschneider.me> // From <https://github.com/google/open-location-code> public class OpenLocationCodes { public static final int CODE_DEFAULT_LENGTH = 11; private static final String SEPARATOR = "+"; private static final int SEPARATOR_POSITION = 8; private static final String PADDING_CHARACTER = "0"; private static final String CODE_ALPHABET = "23456789CFGHJMPQRVWX"; private static final int ENCODING_BASE = CODE_ALPHABET.length(); private static final int LATITUDE_MAX = 90; private static final int LONGITUDE_MAX = 180; private static final int PAIR_CODE_LENGTH = 10; private static final float[] PAIR_RESOLUTIONS = { 20.0F, 1.0F, 0.05F, 0.0025F, 0.000125F }; private static final int CODE_MIN_LENGTH = SEPARATOR_POSITION; private static final int CODE_MAX_LENGTH = 11; private static final int GRID_COLUMNS = 4; private static final int GRID_ROWS = 5; private static final float GRID_SIZE_DEGREES = 0.000125F; // Checks private static boolean isValid(String code) { if (code == null || code.length() < 2) { return false; } // There must be exactly one separator. final int position = code.indexOf(SEPARATOR); if (position == -1) { return false; } if (position != code.lastIndexOf(SEPARATOR)) { return false; } if ((position % 2) != 0) { return false; } // Check characters before separator: padding or alphabet final String beforeSeparator = code.substring(0, position); for (final char c : beforeSeparator.toCharArray()) { final String s = String.valueOf(c); if (! s.equals(PADDING_CHARACTER) && ! CODE_ALPHABET.contains(s)) { return false; } } // Check characters after separator: alphabet only final String afterSeparator = code.substring(position + 1, code.length()); for (final char c : afterSeparator.toCharArray()) { final String s = String.valueOf(c); if (! CODE_ALPHABET.contains(s)) { return false; } } return true; } private static boolean isShort(String code) { // Check it's valid. if (! isValid(code)) { return false; } // If there are less characters than expected before the SEPARATOR. if (code.contains(SEPARATOR) && code.indexOf(SEPARATOR) < SEPARATOR_POSITION) { return true; } return false; } private static boolean isFull(String code) { if (! isValid(code)) { return false; } // If it's short, it's not full. if (isShort(code)) { return false; } // Work out what the first latitude character indicates for latitude. final int firstLatValue = CODE_ALPHABET.indexOf(code.charAt(0)) * ENCODING_BASE; if (firstLatValue >= LATITUDE_MAX * 2) { // The code would decode to a latitude of >= 90 degrees. return false; } if (code.length() > 1) { // Work out what the first longitude character indicates for longitude. final int firstLngValue = CODE_ALPHABET.indexOf(code.charAt(0)) * ENCODING_BASE; if (firstLngValue >= LONGITUDE_MAX * 2) { // The code would decode to a longitude of >= 180 degrees. return false; } } return true; } public static boolean isPadded(final String code) { return code.contains(PADDING_CHARACTER); } public static boolean contains(final String code, final double latitude, final double longitude) { if (! isValid(code)) { return false; } final CodeArea codeArea = decode(code); return (codeArea.bounds().contains(new LatLng(latitude, longitude))); } // Encode public static String encode(final double latitude, final double longitude) { return encode(latitude, longitude, CODE_MAX_LENGTH); } public static String encode(double latitude, double longitude, final int codeLength) throws IllegalArgumentException { if (codeLength < 2 || (codeLength < 10 & (codeLength % 2) == 1)) { throw new IllegalArgumentException("Invalid Open Location Code length"); } // Ensure that latitude and longitude are valid. latitude = clipLatitude(latitude); longitude = normalizeLongitude(longitude); // Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded. if (latitude == 90) { latitude = latitude - computeLatitudePrecision(codeLength); } String code = encodePairs(latitude, longitude, Math.min(codeLength, PAIR_CODE_LENGTH)); // If the requested length indicates we want grid refined codes. if (codeLength > PAIR_CODE_LENGTH) { code += encodeGrid(latitude, longitude, codeLength - PAIR_CODE_LENGTH); } // Tricky last fail-safe filling while (code.length() < CODE_MIN_LENGTH) { code += PADDING_CHARACTER; } if (code.length() == SEPARATOR_POSITION) { code += SEPARATOR; } return code; } private static double clipLatitude(final double latitude) { return Math.min(90, Math.max(-90, latitude)); } private static double computeLatitudePrecision(final int codeLength) { if (codeLength <= 10) { return Math.pow(20, Math.floor(codeLength / -2 + 2)); } return Math.pow(20, -3) / Math.pow(GRID_ROWS, codeLength - 10); } private static double normalizeLongitude(double longitude) { while (longitude < -180) { longitude = longitude + 360; } while (longitude >= 180) { longitude = longitude - 360; } return longitude; } private static String encodePairs(final double latitude, final double longitude, final int codeLength) { String code = ""; // Adjust latitude and longitude so they fall into positive ranges. double adjustedLatitude = latitude + LATITUDE_MAX; double adjustedLongitude = longitude + LONGITUDE_MAX; // Count digits - can't use string length because it may include a separator character. int digitCount = 0; while (digitCount < codeLength) { // Provides the value of digits in this place in decimal degrees. float placeValue = PAIR_RESOLUTIONS[(int) Math.floor(digitCount / 2)]; // Do the latitude - gets the digit for this place and subtracts that for the next digit. int digitValue = (int) Math.floor(adjustedLatitude / placeValue); adjustedLatitude -= digitValue * placeValue; code += CODE_ALPHABET.charAt(digitValue); digitCount += 1; if (digitCount == codeLength) { break ; } // And do the longitude - gets the digit for this place and subtracts that for the next digit. digitValue = (int) Math.floor(adjustedLongitude / placeValue); adjustedLongitude -= digitValue * placeValue; code += CODE_ALPHABET.charAt(digitValue); digitCount += 1; // Should we add a separator here? if (digitCount == SEPARATOR_POSITION && digitCount < codeLength) { code += SEPARATOR; } } if (code.length() < SEPARATOR_POSITION) { for (int x = 0 ; x < SEPARATOR_POSITION - code.length() + 1; x++) { code += PADDING_CHARACTER; } } if (code.length() == SEPARATOR_POSITION) { code += SEPARATOR; } return code; } private static String encodeGrid(final double latitude, final double longitude, final int codeLength) { String code = ""; float latPlaceValue = GRID_SIZE_DEGREES; float lngPlaceValue = GRID_SIZE_DEGREES; // Adjust latitude and longitude so they fall into positive ranges and get the offset for the required places. double adjustedLatitude = (latitude + LATITUDE_MAX) % latPlaceValue; double adjustedLongitude = (longitude + LONGITUDE_MAX) % lngPlaceValue; for (int i = 0; i < codeLength; i++) { // Work out the row and column. int row = (int) Math.floor(adjustedLatitude / (latPlaceValue / GRID_ROWS)); int col = (int) Math.floor(adjustedLongitude / (lngPlaceValue / GRID_COLUMNS)); latPlaceValue /= GRID_ROWS; lngPlaceValue /= GRID_COLUMNS; adjustedLatitude -= row * latPlaceValue; adjustedLongitude -= col * lngPlaceValue; code += CODE_ALPHABET.charAt(row * GRID_COLUMNS + col); } return code; } // Decode public static CodeArea decode(String code) throws IllegalArgumentException { if (! isFull(code)) { throw new IllegalArgumentException("Passed Open Location Code is not a valid full code: " + code); } // Strip out separator character (we've already established the code is valid so the maximum is one), // padding characters and convert to upper case. code = code.replace(SEPARATOR, ""); code = code.replaceAll("[0+]", ""); code = code.toUpperCase(Locale.US); // Decode the lat/lng pair component. final CodeArea codeArea = decodePairs(((code.length() <= PAIR_CODE_LENGTH) ? code : code.substring(0, PAIR_CODE_LENGTH))); // If there is a grid refinement component, decode that. if (code.length() <= PAIR_CODE_LENGTH) { return codeArea; } final CodeArea gridArea = decodeGrid(code.substring(PAIR_CODE_LENGTH)); return new CodeArea(codeArea.latitudeLo + gridArea.latitudeLo, codeArea.longitudeLo + gridArea.longitudeLo, codeArea.latitudeLo + gridArea.latitudeHi, codeArea.longitudeLo + gridArea.longitudeHi, codeArea.codeLength + gridArea.codeLength); } private static CodeArea decodePairs(final String code) { // Get the latitude and longitude values. These will need correcting from positive ranges. final double[] latitude = decodePairsSequence(code, 0); final double[] longitude = decodePairsSequence(code, 1); // Correct the values and set them into the CodeArea object. return new CodeArea(latitude[0] - LATITUDE_MAX, longitude[0] - LONGITUDE_MAX, latitude[1] - LATITUDE_MAX, longitude[1] - LONGITUDE_MAX, code.length()); } private static double[] decodePairsSequence(final String code, final int offset) { int i = 0; double value = 0; while (i * 2 + offset < code.length()) { value += CODE_ALPHABET.indexOf(code.charAt(i * 2 + offset)) * PAIR_RESOLUTIONS[i]; i += 1; } return new double[] { value, value + PAIR_RESOLUTIONS[i - 1] }; } private static CodeArea decodeGrid(final String code) { double latitudeLo = 0.0D; double longitudeLo = 0.0D; float latPlaceValue = GRID_SIZE_DEGREES; float lngPlaceValue = GRID_SIZE_DEGREES; int i = 0; while (i < code.length()) { int codeIndex = CODE_ALPHABET.indexOf(code.charAt(i)); double row = Math.floor(codeIndex / GRID_COLUMNS); double col = codeIndex % GRID_COLUMNS; latPlaceValue /= GRID_ROWS; lngPlaceValue /= GRID_COLUMNS; latitudeLo += row * latPlaceValue; longitudeLo += col * lngPlaceValue; i += 1; } return new CodeArea(latitudeLo, longitudeLo, latitudeLo + latPlaceValue, longitudeLo + lngPlaceValue, code.length()); } // Shorten public static String shorten(String code, double latitude, double longitude) throws IllegalArgumentException { if (! isFull(code)) { throw new IllegalArgumentException("Passed code is not valid and full: " + code); } if (code.contains(PADDING_CHARACTER)) { throw new IllegalArgumentException("Cannot shorten padded codes: " + code); } final CodeArea codeArea = decode(code); final double latitudeDiff = Math.abs(latitude - codeArea.latitudeCenter); final double longitudeDiff = Math.abs(longitude - codeArea.longitudeCenter); if (latitudeDiff < (computeLatitudePrecision(8) / 4) && longitudeDiff < (computeLatitudePrecision(8) / 4)) { return encode(latitude, longitude).substring(6); } if (latitudeDiff < (computeLatitudePrecision(6) / 4) && longitudeDiff < (computeLatitudePrecision(6) / 4)) { return encode(latitude, longitude).substring(6); } if (latitudeDiff < (computeLatitudePrecision(4) / 4) && longitudeDiff < (computeLatitudePrecision(4) / 4)) { return encode(latitude, longitude).substring(4); } throw new IllegalArgumentException("Reference location is too far from the Open Location Code center."); } // Recover (from shorten) public static String recover(String shortCode, double referenceLatitude, double referenceLongitude) { return recover(shortCode, referenceLatitude, referenceLongitude, CODE_MAX_LENGTH); } public static String recover(String shortCode, double referenceLatitude, double referenceLongitude, final int codeLength) { if (! isShort(shortCode)) { if (isFull(shortCode)) { return shortCode; } else { throw new IllegalArgumentException("Passed short code is not valid: " + shortCode); } } referenceLatitude = clipLatitude(referenceLatitude); referenceLongitude = normalizeLongitude(referenceLongitude); final int digitsToRecover = SEPARATOR_POSITION - shortCode.indexOf(SEPARATOR); // The resolution (height and width) of the padded area in degrees. final double paddedAreaSize = Math.pow(20, 2 - (PAIR_CODE_LENGTH / 2)); // Use the reference location to pad the supplied short code and decode it. final String recovered = encode(referenceLatitude, referenceLongitude).substring(0, digitsToRecover) + shortCode; final CodeArea codeArea = decode(recovered); double recoveredLatitude = codeArea.latitudeCenter; double recoveredLongitude = codeArea.longitudeCenter; // Move the recovered latitude by one resolution up or down if it is too far from the reference. double latitudeDiff = recoveredLatitude - referenceLatitude; if (latitudeDiff > paddedAreaSize / 2) { recoveredLatitude -= paddedAreaSize; } else if (latitudeDiff < -paddedAreaSize / 2) { recoveredLatitude += paddedAreaSize; } // Move the recovered longitude by one resolution up or down if it is too far from the reference. double longitudeDiff = codeArea.longitudeCenter - referenceLongitude; if (longitudeDiff > paddedAreaSize / 2) { recoveredLongitude -= paddedAreaSize; } else if (longitudeDiff < -paddedAreaSize / 2) { recoveredLongitude += paddedAreaSize; } return encode(recoveredLatitude, recoveredLongitude, codeLength); } // Distance public static float distance(@NonNull final CodeArea codeArea) { float[] results = new float[1]; Location.distanceBetween(codeArea.northwest().latitude, codeArea.northwest().longitude, codeArea.northeast().latitude, codeArea.northeast().longitude, results); final float distance1 = results[0]; Location.distanceBetween(codeArea.northwest().latitude, codeArea.northwest().longitude, codeArea.southeast().latitude, codeArea.southeast().longitude, results); final float distance2 = results[0]; return (distance1 + distance2) / 2; } // CodeArea public static class CodeArea { public double latitudeLo; public double longitudeLo; public double latitudeHi; public double longitudeHi; public int codeLength; public double latitudeCenter; public double longitudeCenter; CodeArea(final double latitudeLo, final double longitudeLo, final double latitudeHi, final double longitudeHi, final int codeLength) { this.latitudeLo = latitudeLo; this.longitudeLo = longitudeLo; this.latitudeHi = latitudeHi; this.longitudeHi = longitudeHi; this.codeLength = codeLength; this.latitudeCenter = Math.min(latitudeLo + (latitudeHi - latitudeLo) / 2, LATITUDE_MAX); this.longitudeCenter = Math.min(longitudeLo + (longitudeHi - longitudeLo) / 2, LONGITUDE_MAX); } public LatLng center() { return new LatLng(this.latitudeCenter, this.longitudeCenter); } public LatLng northwest() { return new LatLng(this.latitudeHi, this.longitudeLo); } public LatLng northeast() { return new LatLng(this.latitudeHi, this.longitudeHi); } public LatLng southwest() { return new LatLng(this.latitudeLo, this.longitudeLo); } public LatLng southeast() { return new LatLng(this.latitudeLo, this.longitudeHi); } public LatLngBounds bounds() { return new LatLngBounds(southwest(), northeast()); } @Override public String toString() { return bounds().toString(); } } }