1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.data.coor.conversion;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
5 | import static org.openstreetmap.josm.tools.I18n.trc;
|
---|
6 |
|
---|
7 | import java.util.ArrayList;
|
---|
8 | import java.util.List;
|
---|
9 | import java.util.Locale;
|
---|
10 | import java.util.regex.Matcher;
|
---|
11 | import java.util.regex.Pattern;
|
---|
12 |
|
---|
13 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
14 |
|
---|
15 | /**
|
---|
16 | * Support for parsing a {@link LatLon} object from a string.
|
---|
17 | * @since 12792
|
---|
18 | */
|
---|
19 | public final class LatLonParser {
|
---|
20 |
|
---|
21 | /** Character denoting South, as string */
|
---|
22 | public static final String SOUTH = trc("compass", "S");
|
---|
23 | /** Character denoting North, as string */
|
---|
24 | public static final String NORTH = trc("compass", "N");
|
---|
25 | /** Character denoting West, as string */
|
---|
26 | public static final String WEST = trc("compass", "W");
|
---|
27 | /** Character denoting East, as string */
|
---|
28 | public static final String EAST = trc("compass", "E");
|
---|
29 |
|
---|
30 | private static final char N_TR = NORTH.charAt(0);
|
---|
31 | private static final char S_TR = SOUTH.charAt(0);
|
---|
32 | private static final char E_TR = EAST.charAt(0);
|
---|
33 | private static final char W_TR = WEST.charAt(0);
|
---|
34 |
|
---|
35 | private static final String DEG = "\u00B0";
|
---|
36 | private static final String MIN = "\u2032";
|
---|
37 | private static final String SEC = "\u2033";
|
---|
38 |
|
---|
39 | private static final Pattern P = Pattern.compile(
|
---|
40 | "([+|-]?\\d+[.,]\\d+)|" // (1)
|
---|
41 | + "([+|-]?\\d+)|" // (2)
|
---|
42 | + "("+DEG+"|o|deg)|" // (3)
|
---|
43 | + "('|"+MIN+"|min)|" // (4)
|
---|
44 | + "(\"|"+SEC+"|sec)|" // (5)
|
---|
45 | + "([,;])|" // (6)
|
---|
46 | + "([NSEW"+N_TR+S_TR+E_TR+W_TR+"])|"// (7)
|
---|
47 | + "\\s+|"
|
---|
48 | + "(.+)", Pattern.CASE_INSENSITIVE);
|
---|
49 |
|
---|
50 | private static final Pattern P_XML = Pattern.compile(
|
---|
51 | "lat=[\"']([+|-]?\\d+[.,]\\d+)[\"']\\s+lon=[\"']([+|-]?\\d+[.,]\\d+)[\"']");
|
---|
52 |
|
---|
53 | private static final String FLOAT = "(\\d+(\\.\\d*)?)";
|
---|
54 | /** Degree-Minute-Second pattern **/
|
---|
55 | private static final String DMS = "(?<neg1>-)?"
|
---|
56 | + "(?=\\d)(?:(?<single>" + FLOAT + ")|"
|
---|
57 | + "((?<degree>" + FLOAT + ")d)?"
|
---|
58 | + "((?<minutes>" + FLOAT + ")\')?"
|
---|
59 | + "((?<seconds>" + FLOAT + ")\")?)"
|
---|
60 | + "(?:[NE]|(?<neg2>[SW]))?";
|
---|
61 | private static final Pattern P_DMS = Pattern.compile("^" + DMS + "$");
|
---|
62 |
|
---|
63 | private static class LatLonHolder {
|
---|
64 | private double lat = Double.NaN;
|
---|
65 | private double lon = Double.NaN;
|
---|
66 | }
|
---|
67 |
|
---|
68 | private LatLonParser() {
|
---|
69 | // private constructor
|
---|
70 | }
|
---|
71 |
|
---|
72 | /**
|
---|
73 | * Parses the given string as lat/lon.
|
---|
74 | * @param coord String to parse
|
---|
75 | * @return parsed lat/lon
|
---|
76 | * @since 12792 (moved from {@link LatLon}, there since 11045)
|
---|
77 | */
|
---|
78 | public static LatLon parse(String coord) {
|
---|
79 | final LatLonHolder latLon = new LatLonHolder();
|
---|
80 | final Matcher mXml = P_XML.matcher(coord);
|
---|
81 | if (mXml.matches()) {
|
---|
82 | setLatLonObj(latLon,
|
---|
83 | Double.valueOf(mXml.group(1).replace(',', '.')), 0.0, 0.0, "N",
|
---|
84 | Double.valueOf(mXml.group(2).replace(',', '.')), 0.0, 0.0, "E");
|
---|
85 | } else {
|
---|
86 | final Matcher m = P.matcher(coord);
|
---|
87 |
|
---|
88 | final StringBuilder sb = new StringBuilder();
|
---|
89 | final List<Object> list = new ArrayList<>();
|
---|
90 |
|
---|
91 | while (m.find()) {
|
---|
92 | if (m.group(1) != null) {
|
---|
93 | sb.append('R'); // floating point number
|
---|
94 | list.add(Double.valueOf(m.group(1).replace(',', '.')));
|
---|
95 | } else if (m.group(2) != null) {
|
---|
96 | sb.append('Z'); // integer number
|
---|
97 | list.add(Double.valueOf(m.group(2)));
|
---|
98 | } else if (m.group(3) != null) {
|
---|
99 | 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 | }
|
---|