001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.coor;
003
004import static java.lang.Math.PI;
005import static java.lang.Math.asin;
006import static java.lang.Math.atan2;
007import static java.lang.Math.cos;
008import static java.lang.Math.sin;
009import static java.lang.Math.sqrt;
010import static java.lang.Math.toRadians;
011import static org.openstreetmap.josm.data.projection.Ellipsoid.WGS84;
012import static org.openstreetmap.josm.tools.I18n.trc;
013
014import java.awt.geom.Area;
015import java.text.DecimalFormat;
016import java.text.NumberFormat;
017import java.util.ArrayList;
018import java.util.Arrays;
019import java.util.List;
020import java.util.Locale;
021import java.util.Objects;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
026import org.openstreetmap.josm.Main;
027import org.openstreetmap.josm.data.Bounds;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * LatLon are unprojected latitude / longitude coordinates.
032 * <br>
033 * <b>Latitude</b> specifies the north-south position in degrees
034 * where valid values are in the [-90,90] and positive values specify positions north of the equator.
035 * <br>
036 * <b>Longitude</b> specifies the east-west position in degrees
037 * where valid values are in the [-180,180] and positive values specify positions east of the prime meridian.
038 * <br>
039 * <img alt="lat/lon" src="https://upload.wikimedia.org/wikipedia/commons/6/62/Latitude_and_Longitude_of_the_Earth.svg">
040 * <br>
041 * This class is immutable.
042 *
043 * @author Imi
044 */
045public class LatLon extends Coordinate {
046
047    private static final long serialVersionUID = 1L;
048
049    /**
050     * Minimum difference in location to not be represented as the same position.
051     * The API returns 7 decimals.
052     */
053    public static final double MAX_SERVER_PRECISION = 1e-7;
054    public static final double MAX_SERVER_INV_PRECISION = 1e7;
055
056    /**
057     * The (0,0) coordinates.
058     * @since 6178
059     */
060    public static final LatLon ZERO = new LatLon(0, 0);
061
062    /** North pole. */
063    public static final LatLon NORTH_POLE = new LatLon(90, 0);
064    /** South pole. */
065    public static final LatLon SOUTH_POLE = new LatLon(-90, 0);
066
067    private static DecimalFormat cDmsMinuteFormatter = new DecimalFormat("00");
068    private static DecimalFormat cDmsSecondFormatter = new DecimalFormat(
069            Main.pref == null ? "00.0" : Main.pref.get("latlon.dms.decimal-format", "00.0"));
070    private static DecimalFormat cDmMinuteFormatter = new DecimalFormat(
071            Main.pref == null ? "00.000" : Main.pref.get("latlon.dm.decimal-format", "00.000"));
072    public static final DecimalFormat cDdFormatter;
073    public static final DecimalFormat cDdHighPecisionFormatter;
074    static {
075        // Don't use the localized decimal separator. This way we can present
076        // a comma separated list of coordinates.
077        cDdFormatter = (DecimalFormat) NumberFormat.getInstance(Locale.UK);
078        cDdFormatter.applyPattern("###0.0######");
079        cDdHighPecisionFormatter = (DecimalFormat) NumberFormat.getInstance(Locale.UK);
080        cDdHighPecisionFormatter.applyPattern("###0.0##########");
081    }
082
083    private static final String cDms60 = cDmsSecondFormatter.format(60.0);
084    private static final String cDms00 = cDmsSecondFormatter.format(0.0);
085    private static final String cDm60 = cDmMinuteFormatter.format(60.0);
086    private static final String cDm00 = cDmMinuteFormatter.format(0.0);
087
088    /** Character denoting South, as string */
089    public static final String SOUTH = trc("compass", "S");
090    /** Character denoting North, as string */
091    public static final String NORTH = trc("compass", "N");
092    /** Character denoting West, as string */
093    public static final String WEST = trc("compass", "W");
094    /** Character denoting East, as string */
095    public static final String EAST = trc("compass", "E");
096
097    private static final char N_TR = NORTH.charAt(0);
098    private static final char S_TR = SOUTH.charAt(0);
099    private static final char E_TR = EAST.charAt(0);
100    private static final char W_TR = WEST.charAt(0);
101
102    private static final String DEG = "\u00B0";
103    private static final String MIN = "\u2032";
104    private static final String SEC = "\u2033";
105
106    private static final Pattern P = Pattern.compile(
107            "([+|-]?\\d+[.,]\\d+)|"             // (1)
108            + "([+|-]?\\d+)|"                   // (2)
109            + "("+DEG+"|o|deg)|"                // (3)
110            + "('|"+MIN+"|min)|"                // (4)
111            + "(\"|"+SEC+"|sec)|"               // (5)
112            + "(,|;)|"                          // (6)
113            + "([NSEW"+N_TR+S_TR+E_TR+W_TR+"])|"// (7)
114            + "\\s+|"
115            + "(.+)", Pattern.CASE_INSENSITIVE);
116
117    private static final Pattern P_XML = Pattern.compile(
118            "lat=[\"']([+|-]?\\d+[.,]\\d+)[\"']\\s+lon=[\"']([+|-]?\\d+[.,]\\d+)[\"']");
119
120    /**
121     * Replies true if lat is in the range [-90,90]
122     *
123     * @param lat the latitude
124     * @return true if lat is in the range [-90,90]
125     */
126    public static boolean isValidLat(double lat) {
127        return lat >= -90d && lat <= 90d;
128    }
129
130    /**
131     * Replies true if lon is in the range [-180,180]
132     *
133     * @param lon the longitude
134     * @return true if lon is in the range [-180,180]
135     */
136    public static boolean isValidLon(double lon) {
137        return lon >= -180d && lon <= 180d;
138    }
139
140    /**
141     * Make sure longitude value is within <code>[-180, 180]</code> range.
142     * @param lon the longitude in degrees
143     * @return lon plus/minus multiples of <code>360</code>, as needed to get
144     * in <code>[-180, 180]</code> range
145     */
146    public static double normalizeLon(double lon) {
147        if (lon >= -180 && lon <= 180)
148            return lon;
149        else {
150            lon = lon % 360.0;
151            if (lon > 180) {
152                return lon - 360;
153            } else if (lon < -180) {
154                return lon + 360;
155            }
156            return lon;
157        }
158    }
159
160    /**
161     * Replies true if lat is in the range [-90,90] and lon is in the range [-180,180]
162     *
163     * @return true if lat is in the range [-90,90] and lon is in the range [-180,180]
164     */
165    public boolean isValid() {
166        return isValidLat(lat()) && isValidLon(lon());
167    }
168
169    /**
170     * Clamp the lat value to be inside the world.
171     * @param value The value
172     * @return The value clamped to the world.
173     */
174    public static double toIntervalLat(double value) {
175        return Utils.clamp(value, -90, 90);
176    }
177
178    /**
179     * Returns a valid OSM longitude [-180,+180] for the given extended longitude value.
180     * For example, a value of -181 will return +179, a value of +181 will return -179.
181     * @param value A longitude value not restricted to the [-180,+180] range.
182     * @return a valid OSM longitude [-180,+180]
183     */
184    public static double toIntervalLon(double value) {
185        if (isValidLon(value))
186            return value;
187        else {
188            int n = (int) (value + Math.signum(value)*180.0) / 360;
189            return value - n*360.0;
190        }
191    }
192
193    /**
194     * Replies the coordinate in degrees/minutes/seconds format
195     * @param pCoordinate The coordinate to convert
196     * @return The coordinate in degrees/minutes/seconds format
197     */
198    public static String dms(double pCoordinate) {
199
200        double tAbsCoord = Math.abs(pCoordinate);
201        int tDegree = (int) tAbsCoord;
202        double tTmpMinutes = (tAbsCoord - tDegree) * 60;
203        int tMinutes = (int) tTmpMinutes;
204        double tSeconds = (tTmpMinutes - tMinutes) * 60;
205
206        String sDegrees = Integer.toString(tDegree);
207        String sMinutes = cDmsMinuteFormatter.format(tMinutes);
208        String sSeconds = cDmsSecondFormatter.format(tSeconds);
209
210        if (cDms60.equals(sSeconds)) {
211            sSeconds = cDms00;
212            sMinutes = cDmsMinuteFormatter.format(tMinutes+1L);
213        }
214        if ("60".equals(sMinutes)) {
215            sMinutes = "00";
216            sDegrees = Integer.toString(tDegree+1);
217        }
218
219        return sDegrees + '\u00B0' + sMinutes + '\'' + sSeconds + '\"';
220    }
221
222    /**
223     * Replies the coordinate in degrees/minutes format
224     * @param pCoordinate The coordinate to convert
225     * @return The coordinate in degrees/minutes format
226     */
227    public static String dm(double pCoordinate) {
228
229        double tAbsCoord = Math.abs(pCoordinate);
230        int tDegree = (int) tAbsCoord;
231        double tMinutes = (tAbsCoord - tDegree) * 60;
232
233        String sDegrees = Integer.toString(tDegree);
234        String sMinutes = cDmMinuteFormatter.format(tMinutes);
235
236        if (sMinutes.equals(cDm60)) {
237            sMinutes = cDm00;
238            sDegrees = Integer.toString(tDegree+1);
239        }
240
241        return sDegrees + '\u00B0' + sMinutes + '\'';
242    }
243
244    /**
245     * Constructs a new object representing the given latitude/longitude.
246     * @param lat the latitude, i.e., the north-south position in degrees
247     * @param lon the longitude, i.e., the east-west position in degrees
248     */
249    public LatLon(double lat, double lon) {
250        super(lon, lat);
251    }
252
253    protected LatLon(LatLon coor) {
254        super(coor.lon(), coor.lat());
255    }
256
257    /**
258     * Constructs a new object for the given coordinate
259     * @param coor the coordinate
260     */
261    public LatLon(ICoordinate coor) {
262        this(coor.getLat(), coor.getLon());
263    }
264
265    /**
266     * Returns the latitude, i.e., the north-south position in degrees.
267     * @return the latitude
268     */
269    public double lat() {
270        return y;
271    }
272
273    /**
274     * Formats the latitude part according to the given format
275     * @param d the coordinate format to use
276     * @return the formatted latitude
277     */
278    public String latToString(CoordinateFormat d) {
279        switch(d) {
280        case DECIMAL_DEGREES: return cDdFormatter.format(y);
281        case DEGREES_MINUTES_SECONDS: return dms(y) + ((y < 0) ? SOUTH : NORTH);
282        case NAUTICAL: return dm(y) + ((y < 0) ? SOUTH : NORTH);
283        case EAST_NORTH: return cDdFormatter.format(Main.getProjection().latlon2eastNorth(this).north());
284        default: return "ERR";
285        }
286    }
287
288    /**
289     * Returns the longitude, i.e., the east-west position in degrees.
290     * @return the longitude
291     */
292    public double lon() {
293        return x;
294    }
295
296    /**
297     * Formats the longitude part according to the given format
298     * @param d the coordinate format to use
299     * @return the formatted longitude
300     */
301    public String lonToString(CoordinateFormat d) {
302        switch(d) {
303        case DECIMAL_DEGREES: return cDdFormatter.format(x);
304        case DEGREES_MINUTES_SECONDS: return dms(x) + ((x < 0) ? WEST : EAST);
305        case NAUTICAL: return dm(x) + ((x < 0) ? WEST : EAST);
306        case EAST_NORTH: return cDdFormatter.format(Main.getProjection().latlon2eastNorth(this).east());
307        default: return "ERR";
308        }
309    }
310
311    /**
312     * @param other other lat/lon
313     * @return <code>true</code> if the other point has almost the same lat/lon
314     * values, only differing by no more than 1 / {@link #MAX_SERVER_PRECISION MAX_SERVER_PRECISION}.
315     */
316    public boolean equalsEpsilon(LatLon other) {
317        double p = MAX_SERVER_PRECISION / 2;
318        return Math.abs(lat()-other.lat()) <= p && Math.abs(lon()-other.lon()) <= p;
319    }
320
321    /**
322     * Determines if this lat/lon is outside of the world
323     * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon.
324     */
325    public boolean isOutSideWorld() {
326        Bounds b = Main.getProjection().getWorldBoundsLatLon();
327        return lat() < b.getMinLat() || lat() > b.getMaxLat() ||
328                lon() < b.getMinLon() || lon() > b.getMaxLon();
329    }
330
331    /**
332     * Determines if this lat/lon is within the given bounding box.
333     * @param b bounding box
334     * @return <code>true</code> if this is within the given bounding box.
335     */
336    public boolean isWithin(Bounds b) {
337        return b.contains(this);
338    }
339
340    /**
341     * Check if this is contained in given area or area is null.
342     *
343     * @param a Area
344     * @return <code>true</code> if this is contained in given area or area is null.
345     */
346    public boolean isIn(Area a) {
347        return a == null || a.contains(x, y);
348    }
349
350    /**
351     * Computes the distance between this lat/lon and another point on the earth.
352     * Uses Haversine formular.
353     * @param other the other point.
354     * @return distance in metres.
355     */
356    public double greatCircleDistance(LatLon other) {
357        double sinHalfLat = sin(toRadians(other.lat() - this.lat()) / 2);
358        double sinHalfLon = sin(toRadians(other.lon() - this.lon()) / 2);
359        double d = 2 * WGS84.a * asin(
360                sqrt(sinHalfLat*sinHalfLat +
361                        cos(toRadians(this.lat()))*cos(toRadians(other.lat()))*sinHalfLon*sinHalfLon));
362        // For points opposite to each other on the sphere,
363        // rounding errors could make the argument of asin greater than 1
364        // (This should almost never happen.)
365        if (java.lang.Double.isNaN(d)) {
366            Main.error("NaN in greatCircleDistance");
367            d = PI * WGS84.a;
368        }
369        return d;
370    }
371
372    /**
373     * Returns the heading that you have to use to get from this lat/lon to another.
374     *
375     * Angle starts from north and increases counterclockwise (!), PI/2 means west.
376     * You can get usual clockwise angle from {@link #bearing(LatLon)} method.
377     * This method is kept as deprecated because it is called from many plugins.
378     *
379     * (I don't know the original source of this formula, but see
380     * <a href="https://math.stackexchange.com/questions/720/how-to-calculate-a-heading-on-the-earths-surface">this question</a>
381     * for some hints how it is derived.)
382     *
383     * @deprecated see bearing method
384     * @param other the "destination" position
385     * @return heading in radians in the range 0 &lt;= hd &lt; 2*PI
386     */
387    @Deprecated
388    public double heading(LatLon other) {
389        double hd = atan2(sin(toRadians(this.lon() - other.lon())) * cos(toRadians(other.lat())),
390                cos(toRadians(this.lat())) * sin(toRadians(other.lat())) -
391                sin(toRadians(this.lat())) * cos(toRadians(other.lat())) * cos(toRadians(this.lon() - other.lon())));
392        hd %= 2 * PI;
393        if (hd < 0) {
394            hd += 2 * PI;
395        }
396        return hd;
397    }
398
399    /**
400     * Returns bearing from this point to another.
401     *
402     * Angle starts from north and increases clockwise, PI/2 means east.
403     * Old deprecated method {@link #heading(LatLon)} used unusual reverse angle.
404     *
405     * Please note that reverse bearing (from other point to this point) should NOT be
406     * calculated from return value of this method, because great circle path
407     * between the two points have different bearings at each position.
408     *
409     * To get bearing from another point to this point call other.bearing(this)
410     *
411     * @param other the "destination" position
412     * @return heading in radians in the range 0 &lt;= hd &lt; 2*PI
413     */
414    public double bearing(LatLon other) {
415        double lat1 = toRadians(this.lat());
416        double lat2 = toRadians(other.lat());
417        double dlon = toRadians(other.lon() - this.lon());
418        double bearing = atan2(
419            sin(dlon) * cos(lat2),
420            cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon)
421        );
422        bearing %= 2 * PI;
423        if (bearing < 0) {
424            bearing += 2 * PI;
425        }
426        return bearing;
427    }
428
429    /**
430     * Returns this lat/lon pair in human-readable format.
431     *
432     * @return String in the format "lat=1.23456 deg, lon=2.34567 deg"
433     */
434    public String toDisplayString() {
435        NumberFormat nf = NumberFormat.getInstance();
436        nf.setMaximumFractionDigits(5);
437        return "lat=" + nf.format(lat()) + "\u00B0, lon=" + nf.format(lon()) + '\u00B0';
438    }
439
440    /**
441     * Returns this lat/lon pair in human-readable format separated by {@code separator}.
442     * @param separator values separator
443     * @return String in the format {@code "1.23456[separator]2.34567"}
444     */
445    public String toStringCSV(String separator) {
446        return Utils.join(separator, Arrays.asList(
447                latToString(CoordinateFormat.DECIMAL_DEGREES),
448                lonToString(CoordinateFormat.DECIMAL_DEGREES)
449        ));
450    }
451
452    /**
453     * Interpolate between this and a other latlon
454     * @param ll2 The other lat/lon object
455     * @param proportion The proportion to interpolate
456     * @return a new latlon at this position if proportion is 0, at the other position it proportion is 1 and lineary interpolated otherwise.
457     */
458    public LatLon interpolate(LatLon ll2, double proportion) {
459        // this is an alternate form of this.lat() + proportion * (ll2.lat() - this.lat()) that is slightly faster
460        return new LatLon((1 - proportion) * this.lat() + proportion * ll2.lat(),
461                (1 - proportion) * this.lon() + proportion * ll2.lon());
462    }
463
464    /**
465     * Get the center between two lat/lon points
466     * @param ll2 The other {@link LatLon}
467     * @return The center at the average coordinates of the two points. Does not take the 180° meridian into account.
468     */
469    public LatLon getCenter(LatLon ll2) {
470        // The JIT will inline this for us, it is as fast as the normal /2 approach
471        return interpolate(ll2, .5);
472    }
473
474    /**
475     * Returns the euclidean distance from this {@code LatLon} to a specified {@code LatLon}.
476     *
477     * @param ll the specified coordinate to be measured against this {@code LatLon}
478     * @return the euclidean distance from this {@code LatLon} to a specified {@code LatLon}
479     * @since 6166
480     */
481    public double distance(final LatLon ll) {
482        return super.distance(ll);
483    }
484
485    /**
486     * Returns the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon}.
487     *
488     * @param ll the specified coordinate to be measured against this {@code LatLon}
489     * @return the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon}
490     * @since 6166
491     */
492    public double distanceSq(final LatLon ll) {
493        return super.distanceSq(ll);
494    }
495
496    @Override
497    public String toString() {
498        return "LatLon[lat="+lat()+",lon="+lon()+']';
499    }
500
501    /**
502     * Returns the value rounded to OSM precisions, i.e. to {@link #MAX_SERVER_PRECISION}.
503     * @param value lat/lon value
504     *
505     * @return rounded value
506     */
507    public static double roundToOsmPrecision(double value) {
508        return Math.round(value * MAX_SERVER_INV_PRECISION) / MAX_SERVER_INV_PRECISION;
509    }
510
511    /**
512     * Replies a clone of this lat LatLon, rounded to OSM precisions, i.e. to {@link #MAX_SERVER_PRECISION}
513     *
514     * @return a clone of this lat LatLon
515     */
516    public LatLon getRoundedToOsmPrecision() {
517        return new LatLon(
518                roundToOsmPrecision(lat()),
519                roundToOsmPrecision(lon())
520                );
521    }
522
523    @Override
524    public int hashCode() {
525        return Objects.hash(x, y);
526    }
527
528    @Override
529    public boolean equals(Object obj) {
530        if (this == obj) return true;
531        if (obj == null || getClass() != obj.getClass()) return false;
532        LatLon that = (LatLon) obj;
533        return Double.compare(that.x, x) == 0 &&
534               Double.compare(that.y, y) == 0;
535    }
536
537    /**
538     * Converts this latitude/longitude to an instance of {@link ICoordinate}.
539     * @return a {@link ICoordinate} instance of this latitude/longitude
540     */
541    public ICoordinate toCoordinate() {
542        return new org.openstreetmap.gui.jmapviewer.Coordinate(lat(), lon());
543    }
544
545    private static class LatLonHolder {
546        private double lat = Double.NaN;
547        private double lon = Double.NaN;
548    }
549
550    private static void setLatLonObj(final LatLonHolder latLon,
551            final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1,
552            final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) {
553
554        setLatLon(latLon,
555                (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1,
556                (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2);
557    }
558
559    private static void setLatLon(final LatLonHolder latLon,
560            final double coord1deg, final double coord1min, final double coord1sec, final String card1,
561            final double coord2deg, final double coord2min, final double coord2sec, final String card2) {
562
563        setLatLon(latLon, coord1deg, coord1min, coord1sec, card1);
564        setLatLon(latLon, coord2deg, coord2min, coord2sec, card2);
565        if (Double.isNaN(latLon.lat) || Double.isNaN(latLon.lon)) {
566            throw new IllegalArgumentException("invalid lat/lon parameters");
567        }
568    }
569
570    private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec,
571            final String card) {
572        if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) {
573            throw new IllegalArgumentException("out of range");
574        }
575
576        double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600);
577        coord = "N".equals(card) || "E".equals(card) ? coord : -coord;
578        if ("N".equals(card) || "S".equals(card)) {
579            latLon.lat = coord;
580        } else {
581            latLon.lon = coord;
582        }
583    }
584
585    /**
586     * Parses the given string as lat/lon.
587     * @param coord String to parse
588     * @return parsed lat/lon
589     * @since 11045
590     */
591    public static LatLon parse(String coord) {
592        final LatLonHolder latLon = new LatLonHolder();
593        final Matcher mXml = P_XML.matcher(coord);
594        if (mXml.matches()) {
595            setLatLonObj(latLon,
596                    Double.valueOf(mXml.group(1).replace(',', '.')), 0.0, 0.0, "N",
597                    Double.valueOf(mXml.group(2).replace(',', '.')), 0.0, 0.0, "E");
598        } else {
599            final Matcher m = P.matcher(coord);
600
601            final StringBuilder sb = new StringBuilder();
602            final List<Object> list = new ArrayList<>();
603
604            while (m.find()) {
605                if (m.group(1) != null) {
606                    sb.append('R');     // floating point number
607                    list.add(Double.valueOf(m.group(1).replace(',', '.')));
608                } else if (m.group(2) != null) {
609                    sb.append('Z');     // integer number
610                    list.add(Double.valueOf(m.group(2)));
611                } else if (m.group(3) != null) {
612                    sb.append('o');     // degree sign
613                } else if (m.group(4) != null) {
614                    sb.append('\'');    // seconds sign
615                } else if (m.group(5) != null) {
616                    sb.append('"');     // minutes sign
617                } else if (m.group(6) != null) {
618                    sb.append(',');     // separator
619                } else if (m.group(7) != null) {
620                    sb.append('x');     // cardinal direction
621                    String c = m.group(7).toUpperCase(Locale.ENGLISH);
622                    if ("N".equalsIgnoreCase(c) || "S".equalsIgnoreCase(c) || "E".equalsIgnoreCase(c) || "W".equalsIgnoreCase(c)) {
623                        list.add(c);
624                    } else {
625                        list.add(c.replace(N_TR, 'N').replace(S_TR, 'S')
626                                  .replace(E_TR, 'E').replace(W_TR, 'W'));
627                    }
628                } else if (m.group(8) != null) {
629                    throw new IllegalArgumentException("invalid token: " + m.group(8));
630                }
631            }
632
633            final String pattern = sb.toString();
634
635            final Object[] params = list.toArray();
636
637            if (pattern.matches("Ro?,?Ro?")) {
638                setLatLonObj(latLon,
639                        params[0], 0.0, 0.0, "N",
640                        params[1], 0.0, 0.0, "E");
641            } else if (pattern.matches("xRo?,?xRo?")) {
642                setLatLonObj(latLon,
643                        params[1], 0.0, 0.0, params[0],
644                        params[3], 0.0, 0.0, params[2]);
645            } else if (pattern.matches("Ro?x,?Ro?x")) {
646                setLatLonObj(latLon,
647                        params[0], 0.0, 0.0, params[1],
648                        params[2], 0.0, 0.0, params[3]);
649            } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) {
650                setLatLonObj(latLon,
651                        params[0], params[1], 0.0, "N",
652                        params[2], params[3], 0.0, "E");
653            } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) {
654                setLatLonObj(latLon,
655                        params[1], params[2], 0.0, params[0],
656                        params[4], params[5], 0.0, params[3]);
657            } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) {
658                setLatLonObj(latLon,
659                        params[0], params[1], 0.0, params[2],
660                        params[3], params[4], 0.0, params[5]);
661            } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) {
662                setLatLonObj(latLon,
663                        params[0], params[1], params[2], params[3],
664                        params[4], params[5], params[6], params[7]);
665            } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) {
666                setLatLonObj(latLon,
667                        params[1], params[2], params[3], params[0],
668                        params[5], params[6], params[7], params[4]);
669            } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) {
670                setLatLonObj(latLon,
671                        params[0], params[1], params[2], "N",
672                        params[3], params[4], params[5], "E");
673            } else {
674                throw new IllegalArgumentException("invalid format: " + pattern);
675            }
676        }
677
678        return new LatLon(latLon.lat, latLon.lon);
679    }
680}