Index: /trunk/src/org/openstreetmap/josm/actions/JumpToAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/JumpToAction.java	(revision 12791)
+++ /trunk/src/org/openstreetmap/josm/actions/JumpToAction.java	(revision 12792)
@@ -20,4 +20,5 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -161,5 +162,5 @@
             } catch (NumberFormatException ex) {
                 try {
-                    ll = LatLon.parse(lat.getText() + "; " + lon.getText());
+                    ll = LatLonParser.parse(lat.getText() + "; " + lon.getText());
                 } catch (IllegalArgumentException ex2) {
                     JOptionPane.showMessageDialog(Main.parent,
Index: /trunk/src/org/openstreetmap/josm/data/coor/LatLon.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/coor/LatLon.java	(revision 12791)
+++ /trunk/src/org/openstreetmap/josm/data/coor/LatLon.java	(revision 12792)
@@ -15,11 +15,7 @@
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import org.openstreetmap.josm.Main;
@@ -27,4 +23,5 @@
 import org.openstreetmap.josm.data.coor.conversion.DMSCoordinateFormat;
 import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
+import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
 import org.openstreetmap.josm.data.coor.conversion.NauticalCoordinateFormat;
 import org.openstreetmap.josm.tools.Logging;
@@ -89,35 +86,28 @@
     }
 
-    /** Character denoting South, as string */
+    /**
+     * Character denoting South, as string.
+     * @deprecated use {@link LatLonParser#SOUTH}
+     */
+    @Deprecated
     public static final String SOUTH = trc("compass", "S");
-    /** Character denoting North, as string */
+    /**
+     * Character denoting North, as string.
+     * @deprecated use {@link LatLonParser#NORTH}
+     */
+    @Deprecated
     public static final String NORTH = trc("compass", "N");
-    /** Character denoting West, as string */
+    /**
+     * Character denoting West, as string.
+     * @deprecated use {@link LatLonParser#WEST}
+     */
+    @Deprecated
     public static final String WEST = trc("compass", "W");
-    /** Character denoting East, as string */
+    /**
+     * Character denoting East, as string.
+     * @deprecated use {@link LatLonParser#EAST}
+     */
+    @Deprecated
     public static final String EAST = trc("compass", "E");
-
-    private static final char N_TR = NORTH.charAt(0);
-    private static final char S_TR = SOUTH.charAt(0);
-    private static final char E_TR = EAST.charAt(0);
-    private static final char W_TR = WEST.charAt(0);
-
-    private static final String DEG = "\u00B0";
-    private static final String MIN = "\u2032";
-    private static final String SEC = "\u2033";
-
-    private static final Pattern P = Pattern.compile(
-            "([+|-]?\\d+[.,]\\d+)|"             // (1)
-            + "([+|-]?\\d+)|"                   // (2)
-            + "("+DEG+"|o|deg)|"                // (3)
-            + "('|"+MIN+"|min)|"                // (4)
-            + "(\"|"+SEC+"|sec)|"               // (5)
-            + "(,|;)|"                          // (6)
-            + "([NSEW"+N_TR+S_TR+E_TR+W_TR+"])|"// (7)
-            + "\\s+|"
-            + "(.+)", Pattern.CASE_INSENSITIVE);
-
-    private static final Pattern P_XML = Pattern.compile(
-            "lat=[\"']([+|-]?\\d+[.,]\\d+)[\"']\\s+lon=[\"']([+|-]?\\d+[.,]\\d+)[\"']");
 
     /**
@@ -517,44 +507,4 @@
     }
 
-    private static class LatLonHolder {
-        private double lat = Double.NaN;
-        private double lon = Double.NaN;
-    }
-
-    private static void setLatLonObj(final LatLonHolder latLon,
-            final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1,
-            final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) {
-
-        setLatLon(latLon,
-                (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1,
-                (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2);
-    }
-
-    private static void setLatLon(final LatLonHolder latLon,
-            final double coord1deg, final double coord1min, final double coord1sec, final String card1,
-            final double coord2deg, final double coord2min, final double coord2sec, final String card2) {
-
-        setLatLon(latLon, coord1deg, coord1min, coord1sec, card1);
-        setLatLon(latLon, coord2deg, coord2min, coord2sec, card2);
-        if (Double.isNaN(latLon.lat) || Double.isNaN(latLon.lon)) {
-            throw new IllegalArgumentException("invalid lat/lon parameters");
-        }
-    }
-
-    private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec,
-            final String card) {
-        if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) {
-            throw new IllegalArgumentException("out of range");
-        }
-
-        double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600);
-        coord = "N".equals(card) || "E".equals(card) ? coord : -coord;
-        if ("N".equals(card) || "S".equals(card)) {
-            latLon.lat = coord;
-        } else {
-            latLon.lon = coord;
-        }
-    }
-
     /**
      * Parses the given string as lat/lon.
@@ -562,93 +512,9 @@
      * @return parsed lat/lon
      * @since 11045
-     */
+     * @deprecated use {@link LatLonParser#parse(java.lang.String)}
+     */
+    @Deprecated
     public static LatLon parse(String coord) {
-        final LatLonHolder latLon = new LatLonHolder();
-        final Matcher mXml = P_XML.matcher(coord);
-        if (mXml.matches()) {
-            setLatLonObj(latLon,
-                    Double.valueOf(mXml.group(1).replace(',', '.')), 0.0, 0.0, "N",
-                    Double.valueOf(mXml.group(2).replace(',', '.')), 0.0, 0.0, "E");
-        } else {
-            final Matcher m = P.matcher(coord);
-
-            final StringBuilder sb = new StringBuilder();
-            final List<Object> list = new ArrayList<>();
-
-            while (m.find()) {
-                if (m.group(1) != null) {
-                    sb.append('R');     // floating point number
-                    list.add(Double.valueOf(m.group(1).replace(',', '.')));
-                } else if (m.group(2) != null) {
-                    sb.append('Z');     // integer number
-                    list.add(Double.valueOf(m.group(2)));
-                } else if (m.group(3) != null) {
-                    sb.append('o');     // degree sign
-                } else if (m.group(4) != null) {
-                    sb.append('\'');    // seconds sign
-                } else if (m.group(5) != null) {
-                    sb.append('"');     // minutes sign
-                } else if (m.group(6) != null) {
-                    sb.append(',');     // separator
-                } else if (m.group(7) != null) {
-                    sb.append('x');     // cardinal direction
-                    String c = m.group(7).toUpperCase(Locale.ENGLISH);
-                    if ("N".equalsIgnoreCase(c) || "S".equalsIgnoreCase(c) || "E".equalsIgnoreCase(c) || "W".equalsIgnoreCase(c)) {
-                        list.add(c);
-                    } else {
-                        list.add(c.replace(N_TR, 'N').replace(S_TR, 'S')
-                                  .replace(E_TR, 'E').replace(W_TR, 'W'));
-                    }
-                } else if (m.group(8) != null) {
-                    throw new IllegalArgumentException("invalid token: " + m.group(8));
-                }
-            }
-
-            final String pattern = sb.toString();
-
-            final Object[] params = list.toArray();
-
-            if (pattern.matches("Ro?,?Ro?")) {
-                setLatLonObj(latLon,
-                        params[0], 0.0, 0.0, "N",
-                        params[1], 0.0, 0.0, "E");
-            } else if (pattern.matches("xRo?,?xRo?")) {
-                setLatLonObj(latLon,
-                        params[1], 0.0, 0.0, params[0],
-                        params[3], 0.0, 0.0, params[2]);
-            } else if (pattern.matches("Ro?x,?Ro?x")) {
-                setLatLonObj(latLon,
-                        params[0], 0.0, 0.0, params[1],
-                        params[2], 0.0, 0.0, params[3]);
-            } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) {
-                setLatLonObj(latLon,
-                        params[0], params[1], 0.0, "N",
-                        params[2], params[3], 0.0, "E");
-            } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) {
-                setLatLonObj(latLon,
-                        params[1], params[2], 0.0, params[0],
-                        params[4], params[5], 0.0, params[3]);
-            } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) {
-                setLatLonObj(latLon,
-                        params[0], params[1], 0.0, params[2],
-                        params[3], params[4], 0.0, params[5]);
-            } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) {
-                setLatLonObj(latLon,
-                        params[0], params[1], params[2], params[3],
-                        params[4], params[5], params[6], params[7]);
-            } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) {
-                setLatLonObj(latLon,
-                        params[1], params[2], params[3], params[0],
-                        params[5], params[6], params[7], params[4]);
-            } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) {
-                setLatLonObj(latLon,
-                        params[0], params[1], params[2], "N",
-                        params[3], params[4], params[5], "E");
-            } else {
-                throw new IllegalArgumentException("invalid format: " + pattern);
-            }
-        }
-
-        return new LatLon(latLon.lat, latLon.lon);
+        return LatLonParser.parse(coord);
     }
 }
Index: /trunk/src/org/openstreetmap/josm/data/coor/conversion/LatLonParser.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/coor/conversion/LatLonParser.java	(revision 12792)
+++ /trunk/src/org/openstreetmap/josm/data/coor/conversion/LatLonParser.java	(revision 12792)
@@ -0,0 +1,231 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.coor.conversion;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * Support for parsing a {@link LatLon} object from a string.
+ * @since 12792
+ */
+public class LatLonParser {
+
+    /** Character denoting South, as string */
+    public static final String SOUTH = trc("compass", "S");
+    /** Character denoting North, as string */
+    public static final String NORTH = trc("compass", "N");
+    /** Character denoting West, as string */
+    public static final String WEST = trc("compass", "W");
+    /** Character denoting East, as string */
+    public static final String EAST = trc("compass", "E");
+
+    private static final char N_TR = NORTH.charAt(0);
+    private static final char S_TR = SOUTH.charAt(0);
+    private static final char E_TR = EAST.charAt(0);
+    private static final char W_TR = WEST.charAt(0);
+
+    private static final String DEG = "\u00B0";
+    private static final String MIN = "\u2032";
+    private static final String SEC = "\u2033";
+
+    private static final Pattern P = Pattern.compile(
+            "([+|-]?\\d+[.,]\\d+)|"             // (1)
+            + "([+|-]?\\d+)|"                   // (2)
+            + "("+DEG+"|o|deg)|"                // (3)
+            + "('|"+MIN+"|min)|"                // (4)
+            + "(\"|"+SEC+"|sec)|"               // (5)
+            + "(,|;)|"                          // (6)
+            + "([NSEW"+N_TR+S_TR+E_TR+W_TR+"])|"// (7)
+            + "\\s+|"
+            + "(.+)", Pattern.CASE_INSENSITIVE);
+
+    private static final Pattern P_XML = Pattern.compile(
+            "lat=[\"']([+|-]?\\d+[.,]\\d+)[\"']\\s+lon=[\"']([+|-]?\\d+[.,]\\d+)[\"']");
+
+    private static class LatLonHolder {
+        private double lat = Double.NaN;
+        private double lon = Double.NaN;
+    }
+
+    /**
+     * Parses the given string as lat/lon.
+     * @param coord String to parse
+     * @return parsed lat/lon
+     * @since 12792 (moved from {@link LatLon}, there since 11045)
+     */
+    public static LatLon parse(String coord) {
+        final LatLonHolder latLon = new LatLonHolder();
+        final Matcher mXml = P_XML.matcher(coord);
+        if (mXml.matches()) {
+            setLatLonObj(latLon,
+                    Double.valueOf(mXml.group(1).replace(',', '.')), 0.0, 0.0, "N",
+                    Double.valueOf(mXml.group(2).replace(',', '.')), 0.0, 0.0, "E");
+        } else {
+            final Matcher m = P.matcher(coord);
+
+            final StringBuilder sb = new StringBuilder();
+            final List<Object> list = new ArrayList<>();
+
+            while (m.find()) {
+                if (m.group(1) != null) {
+                    sb.append('R');     // floating point number
+                    list.add(Double.valueOf(m.group(1).replace(',', '.')));
+                } else if (m.group(2) != null) {
+                    sb.append('Z');     // integer number
+                    list.add(Double.valueOf(m.group(2)));
+                } else if (m.group(3) != null) {
+                    sb.append('o');     // degree sign
+                } else if (m.group(4) != null) {
+                    sb.append('\'');    // seconds sign
+                } else if (m.group(5) != null) {
+                    sb.append('"');     // minutes sign
+                } else if (m.group(6) != null) {
+                    sb.append(',');     // separator
+                } else if (m.group(7) != null) {
+                    sb.append('x');     // cardinal direction
+                    String c = m.group(7).toUpperCase(Locale.ENGLISH);
+                    if ("N".equalsIgnoreCase(c) || "S".equalsIgnoreCase(c) || "E".equalsIgnoreCase(c) || "W".equalsIgnoreCase(c)) {
+                        list.add(c);
+                    } else {
+                        list.add(c.replace(N_TR, 'N').replace(S_TR, 'S')
+                                  .replace(E_TR, 'E').replace(W_TR, 'W'));
+                    }
+                } else if (m.group(8) != null) {
+                    throw new IllegalArgumentException("invalid token: " + m.group(8));
+                }
+            }
+
+            final String pattern = sb.toString();
+
+            final Object[] params = list.toArray();
+
+            if (pattern.matches("Ro?,?Ro?")) {
+                setLatLonObj(latLon,
+                        params[0], 0.0, 0.0, "N",
+                        params[1], 0.0, 0.0, "E");
+            } else if (pattern.matches("xRo?,?xRo?")) {
+                setLatLonObj(latLon,
+                        params[1], 0.0, 0.0, params[0],
+                        params[3], 0.0, 0.0, params[2]);
+            } else if (pattern.matches("Ro?x,?Ro?x")) {
+                setLatLonObj(latLon,
+                        params[0], 0.0, 0.0, params[1],
+                        params[2], 0.0, 0.0, params[3]);
+            } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) {
+                setLatLonObj(latLon,
+                        params[0], params[1], 0.0, "N",
+                        params[2], params[3], 0.0, "E");
+            } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) {
+                setLatLonObj(latLon,
+                        params[1], params[2], 0.0, params[0],
+                        params[4], params[5], 0.0, params[3]);
+            } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) {
+                setLatLonObj(latLon,
+                        params[0], params[1], 0.0, params[2],
+                        params[3], params[4], 0.0, params[5]);
+            } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) {
+                setLatLonObj(latLon,
+                        params[0], params[1], params[2], params[3],
+                        params[4], params[5], params[6], params[7]);
+            } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) {
+                setLatLonObj(latLon,
+                        params[1], params[2], params[3], params[0],
+                        params[5], params[6], params[7], params[4]);
+            } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) {
+                setLatLonObj(latLon,
+                        params[0], params[1], params[2], "N",
+                        params[3], params[4], params[5], "E");
+            } else {
+                throw new IllegalArgumentException("invalid format: " + pattern);
+            }
+        }
+
+        return new LatLon(latLon.lat, latLon.lon);
+    }
+
+    private static void setLatLonObj(final LatLonHolder latLon,
+            final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1,
+            final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) {
+
+        setLatLon(latLon,
+                (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1,
+                (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2);
+    }
+
+    private static void setLatLon(final LatLonHolder latLon,
+            final double coord1deg, final double coord1min, final double coord1sec, final String card1,
+            final double coord2deg, final double coord2min, final double coord2sec, final String card2) {
+
+        setLatLon(latLon, coord1deg, coord1min, coord1sec, card1);
+        setLatLon(latLon, coord2deg, coord2min, coord2sec, card2);
+        if (Double.isNaN(latLon.lat) || Double.isNaN(latLon.lon)) {
+            throw new IllegalArgumentException("invalid lat/lon parameters");
+        }
+    }
+
+    private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec,
+            final String card) {
+        if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) {
+            throw new IllegalArgumentException("out of range");
+        }
+
+        double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600);
+        coord = "N".equals(card) || "E".equals(card) ? coord : -coord;
+        if ("N".equals(card) || "S".equals(card)) {
+            latLon.lat = coord;
+        } else {
+            latLon.lon = coord;
+        }
+    }
+
+    /**
+     * Parse string coordinate from floating point or DMS format.
+     * @param angleStr the string to parse as coordinate e.g. -1.1 or 50d10'3"W
+     * @return the value, in degrees
+     * @throws IllegalArgumentException in case parsing fails
+     * @since 12792
+     */
+    public static double parseCoordinate(String angleStr) {
+        final String floatPattern = "(\\d+(\\.\\d*)?)";
+        // pattern does all error handling.
+        Matcher in = Pattern.compile("^(?<neg1>-)?"
+                + "(?=\\d)(?:(?<single>" + floatPattern + ")|"
+                + "((?<degree>" + floatPattern + ")d)?"
+                + "((?<minutes>" + floatPattern + ")\')?"
+                + "((?<seconds>" + floatPattern + ")\")?)"
+                + "(?:[NE]|(?<neg2>[SW]))?$").matcher(angleStr);
+
+        if (!in.find()) {
+            throw new IllegalArgumentException(
+                    tr("Unable to parse as coordinate value: '{0}'", angleStr));
+        }
+
+        double value = 0;
+        if (in.group("single") != null) {
+            value += Double.parseDouble(in.group("single"));
+        }
+        if (in.group("degree") != null) {
+            value += Double.parseDouble(in.group("degree"));
+        }
+        if (in.group("minutes") != null) {
+            value += Double.parseDouble(in.group("minutes")) / 60;
+        }
+        if (in.group("seconds") != null) {
+            value += Double.parseDouble(in.group("seconds")) / 3600;
+        }
+
+        if (in.group("neg1") != null ^ in.group("neg2") != null) {
+            value = -value;
+        }
+        return value;
+    }
+
+}
Index: /trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java	(revision 12791)
+++ /trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java	(revision 12792)
@@ -19,4 +19,5 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
 import org.openstreetmap.josm.data.projection.datum.CentricDatum;
 import org.openstreetmap.josm.data.projection.datum.Datum;
@@ -655,5 +656,5 @@
     /**
      * Convert an angle string to a double value
-     * @param angleStr The string. e.g. -1.1 or 50d 10' 3"
+     * @param angleStr The string. e.g. -1.1 or 50d10'3"
      * @param parameterName Only for error message.
      * @return The angle value, in degrees.
@@ -661,36 +662,10 @@
      */
     public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
-        final String floatPattern = "(\\d+(\\.\\d*)?)";
-        // pattern does all error handling.
-        Matcher in = Pattern.compile("^(?<neg1>-)?"
-                + "(?=\\d)(?:(?<single>" + floatPattern + ")|"
-                + "((?<degree>" + floatPattern + ")d)?"
-                + "((?<minutes>" + floatPattern + ")\')?"
-                + "((?<seconds>" + floatPattern + ")\")?)"
-                + "(?:[NE]|(?<neg2>[SW]))?$").matcher(angleStr);
-
-        if (!in.find()) {
+        try {
+            return LatLonParser.parseCoordinate(angleStr);
+        } catch (IllegalArgumentException e) {
             throw new ProjectionConfigurationException(
                     tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
         }
-
-        double value = 0;
-        if (in.group("single") != null) {
-            value += Double.parseDouble(in.group("single"));
-        }
-        if (in.group("degree") != null) {
-            value += Double.parseDouble(in.group("degree"));
-        }
-        if (in.group("minutes") != null) {
-            value += Double.parseDouble(in.group("minutes")) / 60;
-        }
-        if (in.group("seconds") != null) {
-            value += Double.parseDouble(in.group("seconds")) / 3600;
-        }
-
-        if (in.group("neg1") != null ^ in.group("neg2") != null) {
-            value = -value;
-        }
-        return value;
     }
 
@@ -915,3 +890,16 @@
         return result;
     }
+
+    /**
+     * Return true, if a geographic coordinate reference system is represented.
+     *
+     * I.e. if it returns latitude/longitude values rather than Cartesian
+     * east/north coordinates on a flat surface.
+     * @return true, if it is geographic
+     * @since 12792
+     */
+    public boolean isGeographic() {
+        return proj.isGeographic();
+    }
+
 }
Index: /trunk/src/org/openstreetmap/josm/data/projection/ProjectionCLI.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/projection/ProjectionCLI.java	(revision 12792)
+++ /trunk/src/org/openstreetmap/josm/data/projection/ProjectionCLI.java	(revision 12792)
@@ -0,0 +1,207 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.projection;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import gnu.getopt.Getopt;
+import gnu.getopt.LongOpt;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.CLIModule;
+import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
+
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Command line interface for projecting coordinates.
+ * @since 12792
+ */
+public class ProjectionCLI implements CLIModule {
+
+    public static final ProjectionCLI INSTANCE = new ProjectionCLI();
+
+    private boolean argInverse = false;
+    private boolean argSwitchInput = false;
+    private boolean argSwitchOutput = false;
+
+    @Override
+    public String getActionKeyword() {
+        return "project";
+    }
+
+    @Override
+    public void processArguments(String[] argArray) {
+        Getopt getopt = new Getopt("JOSM projection", argArray, "Irh", new LongOpt[] {
+                new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h')});
+
+        int c;
+        while ((c = getopt.getopt()) != -1) {
+            switch (c) {
+            case 'h':
+                showHelp();
+                System.exit(0);
+            case 'I':
+                argInverse = true;
+                break;
+            case 'r':
+                argSwitchInput = true;
+                break;
+            case 's':
+                argSwitchOutput = true;
+                break;
+            }
+        }
+
+        List<String> projParamFrom = new ArrayList<>();
+        List<String> projParamTo = new ArrayList<>();
+        List<String> otherPositional = new ArrayList<>();
+        boolean toTokenSeen = false;
+        // positional arguments:
+        for (int i = getopt.getOptind(); i < argArray.length; ++i) {
+            String arg = argArray[i];
+            if (arg.isEmpty()) throw new IllegalArgumentException("non-empty argument expected");
+            if (arg.startsWith("+")) {
+                if (arg.equals("+to")) {
+                    toTokenSeen = true;
+                } else {
+                    (toTokenSeen ? projParamTo : projParamFrom).add(arg);
+                }
+            } else {
+                otherPositional.add(arg);
+            }
+        }
+        String fromStr = Utils.join(" ", projParamFrom);
+        String toStr = Utils.join(" ", projParamTo);
+        try {
+            run(fromStr, toStr, otherPositional);
+        } catch (ProjectionConfigurationException | IllegalArgumentException | IOException ex) {
+            System.err.println(tr("Error: {0}", ex.getMessage()));
+            System.exit(1);
+        }
+        System.exit(0);
+    }
+
+    /**
+     * Displays help on the console
+     */
+    public static void showHelp() {
+        System.out.println(getHelp());
+    }
+
+    private static String getHelp() {
+        return tr("JOSM projection command line interface")+"\n\n"+
+                tr("Usage")+":\n"+
+                "\tjava -jar josm.jar project <options> <crs> +to <crs> [file]\n\n"+
+                tr("Description")+":\n"+
+                tr("Converts coordinates from one coordinate reference system to another.")+"\n\n"+
+                tr("Options")+":\n"+
+                "\t--help|-h         "+tr("Show this help")+"\n"+
+                "\t-I                "+tr("Switch input and output crs")+"\n"+
+                "\t-r                "+tr("Switch order of input coordinates (east/north, lon/lat)")+"\n"+
+                "\t-s                "+tr("Switch order of output coordinates (east/north, lon/lat)")+"\n\n"+
+                tr("<crs>")+":\n"+
+                tr("The format for input and output coordinate reference system"
+                        + " is similar to that of the PROJ.4 software.")+"\n\n"+
+                tr("[file]")+":\n"+
+                tr("Reads input data from one or more files listed as positional arguments. "
+                + "When no files are given, or the filename is \"-\", data is read from "
+                + "standard input.")+"\n\n"+
+                tr("Examples")+":\n"+
+                "    java -jar josm.jar project +init=epsg:4326 +to +init=epsg:3857 <<<\"11.232274 50.5685716\"\n"+
+                "       => 1250371.1334500168 6545331.055189664\n\n"+
+                "    java -jar josm.jar project +proj=lonlat +datum=WGS84 +to +proj=merc +a=6378137 +b=6378137 +nadgrids=@null <<EOF\n" +
+                "    11d13'56.19\"E 50d34'6.86\"N\n" +
+                "    118d39'30.42\"W 37d20'18.76\"N\n"+
+                "    EOF\n"+
+                "       => 1250371.1334500168 6545331.055189664\n" +
+                "          -1.3208998232319113E7 4486401.160664663\n" +
+                "";
+    }
+
+    private void run(String fromStr, String toStr, List<String> files) throws ProjectionConfigurationException, IOException {
+        CustomProjection fromProj = createProjection(fromStr);
+        CustomProjection toProj = createProjection(toStr);
+        if (this.argInverse) {
+            CustomProjection tmp = fromProj;
+            fromProj = toProj;
+            toProj = tmp;
+        }
+
+        if (files.isEmpty() || files.get(0).equals("-")) {
+            processInput(fromProj, toProj, new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset())));
+        } else {
+            for (String file : files) {
+                try (BufferedReader br = Files.newBufferedReader(Paths.get(file), StandardCharsets.UTF_8)) {
+                    processInput(fromProj, toProj, br);
+                }
+            }
+        }
+    }
+
+    private void processInput(CustomProjection fromProj, CustomProjection toProj, BufferedReader reader) throws IOException {
+        String line;
+        while ((line = reader.readLine()) != null) {
+            line = line.trim();
+            if (line.isEmpty() || line.startsWith("#"))
+                continue;
+            EastNorth enIn;
+            if (fromProj.isGeographic()) {
+                enIn = parseEastNorth(line, LatLonParser::parseCoordinate);
+            } else {
+                enIn = parseEastNorth(line, ProjectionCLI::parseDouble);
+            }
+            LatLon ll = fromProj.eastNorth2latlon(enIn);
+            EastNorth enOut = toProj.latlon2eastNorth(ll);
+            double cOut1 = argSwitchOutput ? enOut.north() : enOut.east();
+            double cOut2 = argSwitchOutput ? enOut.east() : enOut.north();
+            System.out.println(Double.toString(cOut1) + " " + Double.toString(cOut2));
+            System.out.flush();
+        }
+    }
+
+    private CustomProjection createProjection(String params) throws ProjectionConfigurationException {
+        CustomProjection proj = new CustomProjection();
+        proj.update(params);
+        return proj;
+    }
+
+    private EastNorth parseEastNorth(String s, Function<String, Double> parser) {
+        String[] en = s.split("[;, ]+");
+        if (en.length != 2)
+            throw new IllegalArgumentException(tr("Expected two coordinates, separated by white space, found {0} in ''{1}''", en.length, s));
+        double east = parser.apply(en[0]);
+        double north = parser.apply(en[1]);
+        if (this.argSwitchInput)
+            return new EastNorth(north, east);
+        else
+            return new EastNorth(east, north);
+    }
+
+    private static double parseDouble(String s) {
+        try {
+            return Double.parseDouble(s);
+        } catch (NumberFormatException nfe) {
+            throw new IllegalArgumentException(tr("Unable to parse number ''{0}''", s));
+        }
+    }
+
+    /**
+     * Main class to run just the projection CLI.
+     * @param args command line arguments
+     */
+    public static void main(String[] args) {
+        ProjectionCLI.INSTANCE.processArguments(args);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/MainApplication.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 12791)
+++ /trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 12792)
@@ -34,4 +34,5 @@
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -61,4 +62,5 @@
 import org.jdesktop.swinghelper.debug.CheckThreadViolationRepaintManager;
 import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
+import org.openstreetmap.josm.CLIModule;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.DeleteAction;
@@ -85,4 +87,5 @@
 import org.openstreetmap.josm.data.osm.UserInfo;
 import org.openstreetmap.josm.data.osm.search.SearchMode;
+import org.openstreetmap.josm.data.projection.ProjectionCLI;
 import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileSource;
 import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
@@ -163,5 +166,5 @@
      * Command-line arguments used to run the application.
      */
-    private static final List<String> COMMAND_LINE_ARGS = new ArrayList<>();
+    private static List<String> commandLineArgs;
 
     /**
@@ -230,4 +233,32 @@
         public void layerAdded(LayerAddEvent e) {
             // Do nothing
+        }
+    };
+
+    private static final List<CLIModule> cliModules = new ArrayList<>();
+
+    /**
+     * Default JOSM command line interface.
+     * <p>
+     * Runs JOSM and performs some action, depending on the options and positional
+     * arguments.
+     */
+    public static final CLIModule JOSM_CLI_MODULE = new CLIModule() {
+        @Override
+        public String getActionKeyword() {
+            return "runjosm";
+        }
+
+        @Override
+        public void processArguments(String[] argArray) {
+            ProgramArguments args = null;
+            // construct argument table
+            try {
+                args = new ProgramArguments(argArray);
+            } catch (IllegalArgumentException e) {
+                System.err.println(e.getMessage());
+                System.exit(1);
+            }
+            mainJOSM(args);
         }
     };
@@ -256,4 +287,18 @@
         }
     };
+
+    static {
+        registerCLIModue(JOSM_CLI_MODULE);
+        registerCLIModue(ProjectionCLI.INSTANCE);
+    }
+
+    /**
+     * Register a command line interface module.
+     * @param module the module
+     * @since 12792
+     */
+    public static void registerCLIModue(CLIModule module) {
+        cliModules.add(module);
+    }
 
     /**
@@ -488,5 +533,5 @@
      */
     public static List<String> getCommandLineArgs() {
-        return Collections.unmodifiableList(COMMAND_LINE_ARGS);
+        return Collections.unmodifiableList(commandLineArgs);
     }
 
@@ -766,14 +811,25 @@
     public static void main(final String[] argArray) {
         I18n.init();
-
-        ProgramArguments args = null;
-        // construct argument table
-        try {
-            args = new ProgramArguments(argArray);
-        } catch (IllegalArgumentException e) {
-            System.err.println(e.getMessage());
-            System.exit(1);
-            return;
-        }
+        commandLineArgs = Arrays.asList(Arrays.copyOf(argArray, argArray.length));
+
+        if (argArray.length > 0) {
+            String moduleStr = argArray[0];
+            for (CLIModule module : cliModules) {
+                if (Objects.equals(moduleStr, module.getActionKeyword())) {
+                   String[] argArrayCdr = Arrays.copyOfRange(argArray, 1, argArray.length);
+                   module.processArguments(argArrayCdr);
+                   return;
+                }
+            }
+        }
+        // no module specified, use default (josm)
+        JOSM_CLI_MODULE.processArguments(argArray);
+    }
+
+    /**
+     * Main method to run the JOSM GUI.
+     * @param args program arguments
+     */
+    public static void mainJOSM(ProgramArguments args) {
 
         if (!GraphicsEnvironment.isHeadless()) {
@@ -821,6 +877,4 @@
             return;
         }
-
-        COMMAND_LINE_ARGS.addAll(Arrays.asList(argArray));
 
         boolean skipLoadingPlugins = args.hasOption(Option.SKIP_PLUGINS);
Index: /trunk/src/org/openstreetmap/josm/gui/ProgramArguments.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/ProgramArguments.java	(revision 12791)
+++ /trunk/src/org/openstreetmap/josm/gui/ProgramArguments.java	(revision 12792)
@@ -113,5 +113,5 @@
      */
     private void buildCommandLineArgumentMap(String... args) {
-        LongOpt[] los = Stream.of(Option.values()).map(Option::toLongOpt).toArray(i -> new LongOpt[i]);
+        LongOpt[] los = Stream.of(Option.values()).map(Option::toLongOpt).toArray(LongOpt[]::new);
 
         Getopt g = new Getopt("JOSM", args, "hv", los);
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/LatLonDialog.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/LatLonDialog.java	(revision 12791)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/LatLonDialog.java	(revision 12792)
@@ -27,4 +27,5 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager;
+import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.util.WindowGeometry;
@@ -245,5 +246,5 @@
         LatLon latLon;
         try {
-            latLon = LatLon.parse(tfLatLon.getText());
+            latLon = LatLonParser.parse(tfLatLon.getText());
             if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) {
                 latLon = null;
