aboutsummaryrefslogtreecommitdiffstats
path: root/main/src/cgeo/geocaching/location/GeopointParser.java
blob: a6b8e450949a00d5b8c68adc73a4ee1c64c7a81a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package cgeo.geocaching.location;

import cgeo.geocaching.utils.MatcherWrapper;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.NonNull;

import java.util.regex.Pattern;

/**
 * Parse coordinates.
 */
class GeopointParser {

    private static class ResultWrapper {
        final double result;
        final int matcherPos;
        final int matcherLength;

        public ResultWrapper(final double result, final int matcherPos, final int stringLength) {
            this.result = result;
            this.matcherPos = matcherPos;
            this.matcherLength = stringLength;
        }
    }

    //                                                            ( 1  )    ( 2  )         ( 3  )       ( 4  )       (        5        )
    private static final Pattern PATTERN_LAT = Pattern.compile("\\b([NS]|)\\s*(\\d+)°?(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE);
    private static final Pattern PATTERN_LON = Pattern.compile("\\b([WE]|)\\s*(\\d+)°?(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE);

    private static final Pattern PATTERN_BAD_BLANK = Pattern.compile("(\\d)[,.] (\\d{2,})");

    enum LatLon {
        LAT,
        LON
    }

    /**
     * Parses a pair of coordinates (latitude and longitude) out of a String.
     * Accepts following formats and combinations of it:
     * X DD
     * X DD°
     * X DD° MM
     * X DD° MM.MMM
     * X DD° MM SS
     *
     * as well as:
     * DD.DDDDDDD
     *
     * Both . and , are accepted, also variable count of spaces (also 0)
     *
     * @param text
     *            the string to parse
     * @return an Geopoint with parsed latitude and longitude
     * @throws Geopoint.ParseException
     *             if lat or lon could not be parsed
     */
    public static Geopoint parse(@NonNull final String text) {
        final ResultWrapper latitudeWrapper = parseHelper(text, LatLon.LAT);
        // cut away the latitude part when parsing the longitude
        final ResultWrapper longitudeWrapper = parseHelper(text.substring(latitudeWrapper.matcherPos + latitudeWrapper.matcherLength), LatLon.LON);

        if (longitudeWrapper.matcherPos - (latitudeWrapper.matcherPos + latitudeWrapper.matcherLength) >= 10) {
            throw new Geopoint.ParseException("Distance between latitude and longitude text is to large.", LatLon.LON);
        }

        final double lat = latitudeWrapper.result;
        final double lon = longitudeWrapper.result;
        if (!Geopoint.isValidLatitude(lat)) {
            throw new Geopoint.ParseException(text, LatLon.LAT);
        }
        if (!Geopoint.isValidLongitude(lon)) {
            throw new Geopoint.ParseException(text, LatLon.LON);
        }
        return new Geopoint(lat, lon);
    }

    /**
     * Helper for coordinates-parsing
     * 
     * @param text the text to parse
     * @param latlon the kind of coordinate to parse
     * @return a wrapper with the result and the corresponding matching part
     * @throws Geopoint.ParseException if the text cannot be parsed
     */
    private static ResultWrapper parseHelper(@NonNull final String text, final LatLon latlon) {
        MatcherWrapper matcher = new MatcherWrapper(PATTERN_BAD_BLANK, text);
        final String replaceSpaceAfterComma = matcher.replaceAll("$1.$2");

        final Pattern pattern = LatLon.LAT == latlon ? PATTERN_LAT : PATTERN_LON;
        matcher = new MatcherWrapper(pattern, replaceSpaceAfterComma);

        try {
            return new ResultWrapper(Double.valueOf(replaceSpaceAfterComma), 0, text.length());
        } catch (final NumberFormatException ignored) {
            // fall through to advanced parsing
        }

        try {
            if (matcher.find()) {
                final double sign = matcher.group(1).equalsIgnoreCase("S") || matcher.group(1).equalsIgnoreCase("W") ? -1.0 : 1.0;
                final double degree = Integer.valueOf(matcher.group(2)).doubleValue();

                double minutes = 0.0;
                double seconds = 0.0;

                if (null != matcher.group(3)) {
                    minutes = Integer.valueOf(matcher.group(3)).doubleValue();

                    if (null != matcher.group(4)) {
                        seconds = Double.parseDouble("0." + matcher.group(4)) * 60.0;
                    } else if (null != matcher.group(5)) {
                        seconds = Double.parseDouble(matcher.group(5).replace(",", "."));
                    }
                }

                return new ResultWrapper(sign * (degree + minutes / 60.0 + seconds / 3600.0), matcher.start(), matcher.group().length());
            }
        } catch (final NumberFormatException ignored) {
            // We might have encountered too large a number. This was not the right way to do it, try another.
        }

        // Nothing found with "N 52...", try to match string as decimal degree parts (i.e. multiple doubles)
        try {
            final String[] items = StringUtils.split(StringUtils.trimToEmpty(text));
            if (items.length > 0 && items.length <= 2) {
                final int index = (latlon == LatLon.LON ? items.length - 1 : 0);
                final String textPart = items[index];
                final int pos = (latlon == LatLon.LON ? text.lastIndexOf(textPart) : text.indexOf(textPart));
                return new ResultWrapper(Double.parseDouble(textPart), pos, textPart.length());
            }
        } catch (final NumberFormatException ignored) {
            // The right exception will be raised below.
        }

        throw new Geopoint.ParseException("Could not parse coordinates as " + latlon + ": \"" + text + "\"", latlon);
    }

    /**
     * Parses latitude out of a given string.
     *
     * @see #parse(String)
     * @param text
     *            the string to be parsed
     * @return the latitude as decimal degree
     * @throws Geopoint.ParseException
     *             if latitude could not be parsed
     */
    public static double parseLatitude(final String text) {
        return parseHelper(text, LatLon.LAT).result;
    }

    /**
     * Parses longitude out of a given string.
     *
     * @see #parse(String)
     * @param text
     *            the string to be parsed
     * @return the longitude as decimal degree
     * @throws Geopoint.ParseException
     *             if longitude could not be parsed
     */
    public static double parseLongitude(final String text) {
        return parseHelper(text, LatLon.LON).result;
    }
}