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}