// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.geo; /** * Utility for parsing one geographical coordinate * * @author arnej27959 */ class OneDegreeParser { /** The parsed latitude (degrees north if positive). */ public double latitude = 0; public boolean foundLatitude = false; /** The parsed longitude (degrees east if positive). */ public double longitude = 0; public boolean foundLongitude = false; public static boolean isDigit(char ch) { return (ch >= '0' && ch <= '9'); } public static boolean isCompassDirection(char ch) { return (ch == 'N' || ch == 'S' || ch == 'E' || ch == 'W'); } private String parseString = null; private int len = 0; private int pos = 0; public String toString() { if (foundLatitude) { return parseString + " -> latitude(" + latitude + ")"; } else { return parseString + " -> longitude(" + longitude + ")"; } } private char getNextChar() throws IllegalArgumentException { if (pos == len) { pos++; return 0; } else if (pos > len) { throw new IllegalArgumentException("position after end of string when parsing <"+parseString+">"); } else { return parseString.charAt(pos++); } } /** * Parse the given string. * * The string must contain either a latitude or a longitude. * A latitude should contain "N" or "S" and a number signifying * degrees north or south, or a signed number. * A longitude should contain "E" or "W" and a number * signifying degrees east or west, or a signed number. *
* Fractional degrees are recommended as the main input format, * but degrees plus fractional minutes may be used for testing. * You can use the degree sign (U+00B0 as seen in unicode at * http://www.unicode.org/charts/PDF/U0080.pdf) to separate * degrees from minutes, put the direction (NSEW) between as a * separator, or use a small letter 'o' as a replacement for the * degrees sign. *
* Some valid input formats:
* "37.416383" and "-122.024683" → Sunnyvale
* "N37.416383" and "W122.024683" → Sunnyvale
* "37N24.983" and "122W01.481" → same
* "N37\u00B024.983" and "W122\u00B001.481" → same
* "63.418417" and "10.433033" → Trondheim
* "N63.418417" and "E10.433033" → same
* "N63o25.105" and "E10o25.982" → same
* "E10o25.982" and "N63o25.105" → same
* "N63.418417" and "E10.433033" → same
* "63N25.105" and "10E25.982" → same
* * @param assumeNorthSouth Latitude assumed, otherwise longitude * @param toParse Latitude or longitude string to parse */ public OneDegreeParser(boolean assumeNorthSouth, String toParse) throws IllegalArgumentException { this.parseString = toParse; this.len = parseString.length(); consumeString(assumeNorthSouth); } private void consumeString(boolean assumeNorthSouth) throws IllegalArgumentException { char ch = getNextChar(); double degrees = 0.0; double minutes = 0.0; double seconds = 0.0; boolean degSet = false; boolean minSet = false; boolean secSet = false; boolean dirSet = false; boolean foundDot = false; boolean foundDigits = false; boolean findingLatitude = false; boolean findingLongitude = false; double sign = +1.0; int lastpos = -1; // sign must be first character in string if present: if (ch == '+') { // unary plus is a nop ch = getNextChar(); } else if (ch == '-') { sign = -1.0; ch = getNextChar(); } do { // did we find a valid char? boolean valid = false; if (pos == lastpos) { throw new IllegalArgumentException("internal logic error at <"+parseString+"> pos:"+pos); } else { lastpos = pos; } // first, see if we can find some number double accum = 0.0; if (isDigit(ch) || ch == '.') { valid = true; double divider = 1.0; foundDot = false; while (isDigit(ch)) { foundDigits = true; accum *= 10; accum += (ch - '0'); ch = getNextChar(); } if (ch == '.') { foundDot = true; ch = getNextChar(); while (isDigit(ch)) { foundDigits = true; accum *= 10; accum += (ch - '0'); divider *= 10; ch = getNextChar(); } } if (!foundDigits) { throw new IllegalArgumentException("just a . is not a valid number when parsing <"+parseString+">"); } accum /= divider; } // next, did we find a separator after the number? // degree sign is a separator after degrees, before minutes if (ch == '\u00B0' || ch == 'o') { valid = true; if (degSet) { throw new IllegalArgumentException("degrees sign only valid just after degrees when parsing <"+parseString+">"); } if (!foundDigits) { throw new IllegalArgumentException("must have number before degrees sign when parsing <"+parseString+">"); } if (foundDot) { throw new IllegalArgumentException("cannot have fractional degrees before degrees sign when parsing <"+parseString+">"); } ch = getNextChar(); } // apostrophe is a separator after minutes, before seconds if (ch == '\'') { if (minSet || !degSet || !foundDigits) { throw new IllegalArgumentException("minutes sign only valid just after minutes when parsing <"+parseString+">"); } if (foundDot) { throw new IllegalArgumentException("cannot have fractional minutes before minutes sign when parsing <"+parseString+">"); } ch = getNextChar(); } // if we found some number, assign it into the next unset variable if (foundDigits) { valid = true; if (degSet) { if (minSet) { if (secSet) { throw new IllegalArgumentException("extra number after full field when parsing <"+parseString+">"); } else { seconds = accum; secSet = true; } } else { minutes = accum; minSet = true; if (foundDot) { secSet = true; } } } else { degrees = accum; degSet = true; if (foundDot) { minSet = true; secSet = true; } } foundDot = false; foundDigits = false; } // there may to be a direction (NSEW) somewhere, too if (isCompassDirection(ch)) { valid = true; if (dirSet) { throw new IllegalArgumentException("already set direction once, cannot add direction: "+ch+" when parsing <"+parseString+">"); } dirSet = true; if (ch == 'S' || ch == 'W') { sign = -1; } else { sign = 1; } if (ch == 'E' || ch == 'W') { findingLongitude = true; } else { findingLatitude = true; } ch = getNextChar(); } // lastly, did we find the end-of-string? if (ch == 0) { valid = true; if (!dirSet) { if (assumeNorthSouth) { findingLatitude = true; } else { findingLongitude = true; } } if (!degSet) { throw new IllegalArgumentException("end of field without any number seen when parsing <"+parseString+">"); } degrees += minutes / 60.0; degrees += seconds / 3600.0; degrees *= sign; if (findingLatitude) { if (degrees < -90.0 || degrees > 90.0) { throw new IllegalArgumentException("out of range [-90,+90]: "+degrees+" when parsing <"+parseString+">"); } latitude = degrees; foundLatitude = true; } else if (findingLongitude) { if (degrees < -180.0 || degrees > 180.0) { throw new IllegalArgumentException("out of range [-180,+180]: "+degrees+" when parsing <"+parseString+">"); } longitude = degrees; foundLongitude = true; } break; } if (!valid) { throw new IllegalArgumentException("invalid character: "+ch+" when parsing <"+parseString+">"); } } while (ch != 0); // everything parsed OK if (foundLatitude && foundLongitude) { throw new IllegalArgumentException("found both latitude and longitude from: "+parseString); } if (foundLatitude || foundLongitude) { return; } throw new IllegalArgumentException("found neither latitude nor longitude from: "+parseString); } }