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}