001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.coor.conversion; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006 007import java.util.ArrayList; 008import java.util.List; 009import java.util.Locale; 010import java.util.regex.Matcher; 011import java.util.regex.Pattern; 012 013import org.openstreetmap.josm.data.coor.LatLon; 014 015/** 016 * Support for parsing a {@link LatLon} object from a string. 017 * @since 12792 018 */ 019public final class LatLonParser { 020 021 /** Character denoting South, as string */ 022 public static final String SOUTH = trc("compass", "S"); 023 /** Character denoting North, as string */ 024 public static final String NORTH = trc("compass", "N"); 025 /** Character denoting West, as string */ 026 public static final String WEST = trc("compass", "W"); 027 /** Character denoting East, as string */ 028 public static final String EAST = trc("compass", "E"); 029 030 private static final char N_TR = NORTH.charAt(0); 031 private static final char S_TR = SOUTH.charAt(0); 032 private static final char E_TR = EAST.charAt(0); 033 private static final char W_TR = WEST.charAt(0); 034 035 private static final String DEG = "\u00B0"; 036 private static final String MIN = "\u2032"; 037 private static final String SEC = "\u2033"; 038 039 private static final Pattern P = Pattern.compile( 040 "([+|-]?\\d+[.,]\\d+)|" // (1) 041 + "([+|-]?\\d+)|" // (2) 042 + "("+DEG+"|o|deg)|" // (3) 043 + "('|"+MIN+"|min)|" // (4) 044 + "(\"|"+SEC+"|sec)|" // (5) 045 + "([,;])|" // (6) 046 + "([NSEW"+N_TR+S_TR+E_TR+W_TR+"])|"// (7) 047 + "\\s+|" 048 + "(.+)", Pattern.CASE_INSENSITIVE); 049 050 private static final Pattern P_XML = Pattern.compile( 051 "lat=[\"']([+|-]?\\d+[.,]\\d+)[\"']\\s+lon=[\"']([+|-]?\\d+[.,]\\d+)[\"']"); 052 053 private static final String FLOAT = "(\\d+(\\.\\d*)?)"; 054 /** Degree-Minute-Second pattern **/ 055 private static final String DMS = "(?<neg1>-)?" 056 + "(?=\\d)(?:(?<single>" + FLOAT + ")|" 057 + "((?<degree>" + FLOAT + ")d)?" 058 + "((?<minutes>" + FLOAT + ")\')?" 059 + "((?<seconds>" + FLOAT + ")\")?)" 060 + "(?:[NE]|(?<neg2>[SW]))?"; 061 private static final Pattern P_DMS = Pattern.compile("^" + DMS + "$"); 062 063 private static class LatLonHolder { 064 private double lat = Double.NaN; 065 private double lon = Double.NaN; 066 } 067 068 private LatLonParser() { 069 // private constructor 070 } 071 072 /** 073 * Parses the given string as lat/lon. 074 * @param coord String to parse 075 * @return parsed lat/lon 076 * @since 12792 (moved from {@link LatLon}, there since 11045) 077 */ 078 public static LatLon parse(String coord) { 079 final LatLonHolder latLon = new LatLonHolder(); 080 final Matcher mXml = P_XML.matcher(coord); 081 if (mXml.matches()) { 082 setLatLonObj(latLon, 083 Double.valueOf(mXml.group(1).replace(',', '.')), 0.0, 0.0, "N", 084 Double.valueOf(mXml.group(2).replace(',', '.')), 0.0, 0.0, "E"); 085 } else { 086 final Matcher m = P.matcher(coord); 087 088 final StringBuilder sb = new StringBuilder(); 089 final List<Object> list = new ArrayList<>(); 090 091 while (m.find()) { 092 if (m.group(1) != null) { 093 sb.append('R'); // floating point number 094 list.add(Double.valueOf(m.group(1).replace(',', '.'))); 095 } else if (m.group(2) != null) { 096 sb.append('Z'); // integer number 097 list.add(Double.valueOf(m.group(2))); 098 } else if (m.group(3) != null) { 099 sb.append('o'); // degree sign 100 } else if (m.group(4) != null) { 101 sb.append('\''); // seconds sign 102 } else if (m.group(5) != null) { 103 sb.append('"'); // minutes sign 104 } else if (m.group(6) != null) { 105 sb.append(','); // separator 106 } else if (m.group(7) != null) { 107 sb.append('x'); // cardinal direction 108 String c = m.group(7).toUpperCase(Locale.ENGLISH); 109 if ("N".equalsIgnoreCase(c) || "S".equalsIgnoreCase(c) || "E".equalsIgnoreCase(c) || "W".equalsIgnoreCase(c)) { 110 list.add(c); 111 } else { 112 list.add(c.replace(N_TR, 'N').replace(S_TR, 'S') 113 .replace(E_TR, 'E').replace(W_TR, 'W')); 114 } 115 } else if (m.group(8) != null) { 116 throw new IllegalArgumentException("invalid token: " + m.group(8)); 117 } 118 } 119 120 final String pattern = sb.toString(); 121 122 final Object[] params = list.toArray(); 123 124 if (pattern.matches("Ro?,?Ro?")) { 125 setLatLonObj(latLon, 126 params[0], 0.0, 0.0, "N", 127 params[1], 0.0, 0.0, "E"); 128 } else if (pattern.matches("xRo?,?xRo?")) { 129 setLatLonObj(latLon, 130 params[1], 0.0, 0.0, params[0], 131 params[3], 0.0, 0.0, params[2]); 132 } else if (pattern.matches("Ro?x,?Ro?x")) { 133 setLatLonObj(latLon, 134 params[0], 0.0, 0.0, params[1], 135 params[2], 0.0, 0.0, params[3]); 136 } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) { 137 setLatLonObj(latLon, 138 params[0], params[1], 0.0, "N", 139 params[2], params[3], 0.0, "E"); 140 } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) { 141 setLatLonObj(latLon, 142 params[1], params[2], 0.0, params[0], 143 params[4], params[5], 0.0, params[3]); 144 } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) { 145 setLatLonObj(latLon, 146 params[0], params[1], 0.0, params[2], 147 params[3], params[4], 0.0, params[5]); 148 } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) { 149 setLatLonObj(latLon, 150 params[0], params[1], params[2], params[3], 151 params[4], params[5], params[6], params[7]); 152 } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) { 153 setLatLonObj(latLon, 154 params[1], params[2], params[3], params[0], 155 params[5], params[6], params[7], params[4]); 156 } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) { 157 setLatLonObj(latLon, 158 params[0], params[1], params[2], "N", 159 params[3], params[4], params[5], "E"); 160 } else { 161 throw new IllegalArgumentException("invalid format: " + pattern); 162 } 163 } 164 165 return new LatLon(latLon.lat, latLon.lon); 166 } 167 168 private static void setLatLonObj(final LatLonHolder latLon, 169 final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1, 170 final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) { 171 172 setLatLon(latLon, 173 (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1, 174 (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2); 175 } 176 177 private static void setLatLon(final LatLonHolder latLon, 178 final double coord1deg, final double coord1min, final double coord1sec, final String card1, 179 final double coord2deg, final double coord2min, final double coord2sec, final String card2) { 180 181 setLatLon(latLon, coord1deg, coord1min, coord1sec, card1); 182 setLatLon(latLon, coord2deg, coord2min, coord2sec, card2); 183 if (Double.isNaN(latLon.lat) || Double.isNaN(latLon.lon)) { 184 throw new IllegalArgumentException("invalid lat/lon parameters"); 185 } 186 } 187 188 private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec, 189 final String card) { 190 if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) { 191 throw new IllegalArgumentException("out of range"); 192 } 193 194 double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600); 195 coord = "N".equals(card) || "E".equals(card) ? coord : -coord; 196 if ("N".equals(card) || "S".equals(card)) { 197 latLon.lat = coord; 198 } else { 199 latLon.lon = coord; 200 } 201 } 202 203 /** 204 * Parse string coordinate from floating point or DMS format. 205 * @param angleStr the string to parse as coordinate e.g. -1.1 or 50d10'3"W 206 * @return the value, in degrees 207 * @throws IllegalArgumentException in case parsing fails 208 * @since 12792 209 */ 210 public static double parseCoordinate(String angleStr) { 211 // pattern does all error handling. 212 Matcher in = P_DMS.matcher(angleStr); 213 214 if (!in.find()) { 215 throw new IllegalArgumentException( 216 tr("Unable to parse as coordinate value: ''{0}''", angleStr)); 217 } 218 219 double value = 0; 220 if (in.group("single") != null) { 221 value += Double.parseDouble(in.group("single")); 222 } 223 if (in.group("degree") != null) { 224 value += Double.parseDouble(in.group("degree")); 225 } 226 if (in.group("minutes") != null) { 227 value += Double.parseDouble(in.group("minutes")) / 60; 228 } 229 if (in.group("seconds") != null) { 230 value += Double.parseDouble(in.group("seconds")) / 3600; 231 } 232 233 if (in.group("neg1") != null ^ in.group("neg2") != null) { 234 value = -value; 235 } 236 return value; 237 } 238 239}