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 class LatLonHolder { 054 private double lat = Double.NaN; 055 private double lon = Double.NaN; 056 } 057 058 private LatLonParser() { 059 // private constructor 060 } 061 062 /** 063 * Parses the given string as lat/lon. 064 * @param coord String to parse 065 * @return parsed lat/lon 066 * @since 12792 (moved from {@link LatLon}, there since 11045) 067 */ 068 public static LatLon parse(String coord) { 069 final LatLonHolder latLon = new LatLonHolder(); 070 final Matcher mXml = P_XML.matcher(coord); 071 if (mXml.matches()) { 072 setLatLonObj(latLon, 073 Double.valueOf(mXml.group(1).replace(',', '.')), 0.0, 0.0, "N", 074 Double.valueOf(mXml.group(2).replace(',', '.')), 0.0, 0.0, "E"); 075 } else { 076 final Matcher m = P.matcher(coord); 077 078 final StringBuilder sb = new StringBuilder(); 079 final List<Object> list = new ArrayList<>(); 080 081 while (m.find()) { 082 if (m.group(1) != null) { 083 sb.append('R'); // floating point number 084 list.add(Double.valueOf(m.group(1).replace(',', '.'))); 085 } else if (m.group(2) != null) { 086 sb.append('Z'); // integer number 087 list.add(Double.valueOf(m.group(2))); 088 } else if (m.group(3) != null) { 089 sb.append('o'); // degree sign 090 } else if (m.group(4) != null) { 091 sb.append('\''); // seconds sign 092 } else if (m.group(5) != null) { 093 sb.append('"'); // minutes sign 094 } else if (m.group(6) != null) { 095 sb.append(','); // separator 096 } else if (m.group(7) != null) { 097 sb.append('x'); // cardinal direction 098 String c = m.group(7).toUpperCase(Locale.ENGLISH); 099 if ("N".equalsIgnoreCase(c) || "S".equalsIgnoreCase(c) || "E".equalsIgnoreCase(c) || "W".equalsIgnoreCase(c)) { 100 list.add(c); 101 } else { 102 list.add(c.replace(N_TR, 'N').replace(S_TR, 'S') 103 .replace(E_TR, 'E').replace(W_TR, 'W')); 104 } 105 } else if (m.group(8) != null) { 106 throw new IllegalArgumentException("invalid token: " + m.group(8)); 107 } 108 } 109 110 final String pattern = sb.toString(); 111 112 final Object[] params = list.toArray(); 113 114 if (pattern.matches("Ro?,?Ro?")) { 115 setLatLonObj(latLon, 116 params[0], 0.0, 0.0, "N", 117 params[1], 0.0, 0.0, "E"); 118 } else if (pattern.matches("xRo?,?xRo?")) { 119 setLatLonObj(latLon, 120 params[1], 0.0, 0.0, params[0], 121 params[3], 0.0, 0.0, params[2]); 122 } else if (pattern.matches("Ro?x,?Ro?x")) { 123 setLatLonObj(latLon, 124 params[0], 0.0, 0.0, params[1], 125 params[2], 0.0, 0.0, params[3]); 126 } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) { 127 setLatLonObj(latLon, 128 params[0], params[1], 0.0, "N", 129 params[2], params[3], 0.0, "E"); 130 } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) { 131 setLatLonObj(latLon, 132 params[1], params[2], 0.0, params[0], 133 params[4], params[5], 0.0, params[3]); 134 } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) { 135 setLatLonObj(latLon, 136 params[0], params[1], 0.0, params[2], 137 params[3], params[4], 0.0, params[5]); 138 } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) { 139 setLatLonObj(latLon, 140 params[0], params[1], params[2], params[3], 141 params[4], params[5], params[6], params[7]); 142 } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) { 143 setLatLonObj(latLon, 144 params[1], params[2], params[3], params[0], 145 params[5], params[6], params[7], params[4]); 146 } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) { 147 setLatLonObj(latLon, 148 params[0], params[1], params[2], "N", 149 params[3], params[4], params[5], "E"); 150 } else { 151 throw new IllegalArgumentException("invalid format: " + pattern); 152 } 153 } 154 155 return new LatLon(latLon.lat, latLon.lon); 156 } 157 158 private static void setLatLonObj(final LatLonHolder latLon, 159 final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1, 160 final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) { 161 162 setLatLon(latLon, 163 (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1, 164 (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2); 165 } 166 167 private static void setLatLon(final LatLonHolder latLon, 168 final double coord1deg, final double coord1min, final double coord1sec, final String card1, 169 final double coord2deg, final double coord2min, final double coord2sec, final String card2) { 170 171 setLatLon(latLon, coord1deg, coord1min, coord1sec, card1); 172 setLatLon(latLon, coord2deg, coord2min, coord2sec, card2); 173 if (Double.isNaN(latLon.lat) || Double.isNaN(latLon.lon)) { 174 throw new IllegalArgumentException("invalid lat/lon parameters"); 175 } 176 } 177 178 private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec, 179 final String card) { 180 if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) { 181 throw new IllegalArgumentException("out of range"); 182 } 183 184 double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600); 185 coord = "N".equals(card) || "E".equals(card) ? coord : -coord; 186 if ("N".equals(card) || "S".equals(card)) { 187 latLon.lat = coord; 188 } else { 189 latLon.lon = coord; 190 } 191 } 192 193 /** 194 * Parse string coordinate from floating point or DMS format. 195 * @param angleStr the string to parse as coordinate e.g. -1.1 or 50d10'3"W 196 * @return the value, in degrees 197 * @throws IllegalArgumentException in case parsing fails 198 * @since 12792 199 */ 200 public static double parseCoordinate(String angleStr) { 201 final String floatPattern = "(\\d+(\\.\\d*)?)"; 202 // pattern does all error handling. 203 Matcher in = Pattern.compile("^(?<neg1>-)?" 204 + "(?=\\d)(?:(?<single>" + floatPattern + ")|" 205 + "((?<degree>" + floatPattern + ")d)?" 206 + "((?<minutes>" + floatPattern + ")\')?" 207 + "((?<seconds>" + floatPattern + ")\")?)" 208 + "(?:[NE]|(?<neg2>[SW]))?$").matcher(angleStr); 209 210 if (!in.find()) { 211 throw new IllegalArgumentException( 212 tr("Unable to parse as coordinate value: ''{0}''", angleStr)); 213 } 214 215 double value = 0; 216 if (in.group("single") != null) { 217 value += Double.parseDouble(in.group("single")); 218 } 219 if (in.group("degree") != null) { 220 value += Double.parseDouble(in.group("degree")); 221 } 222 if (in.group("minutes") != null) { 223 value += Double.parseDouble(in.group("minutes")) / 60; 224 } 225 if (in.group("seconds") != null) { 226 value += Double.parseDouble(in.group("seconds")) / 3600; 227 } 228 229 if (in.group("neg1") != null ^ in.group("neg2") != null) { 230 value = -value; 231 } 232 return value; 233 } 234 235}