001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.projection;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.BufferedReader;
007import java.io.IOException;
008import java.io.InputStreamReader;
009import java.nio.charset.Charset;
010import java.nio.charset.StandardCharsets;
011import java.nio.file.Files;
012import java.nio.file.Paths;
013import java.util.ArrayList;
014import java.util.List;
015import java.util.function.Function;
016
017import org.openstreetmap.josm.CLIModule;
018import org.openstreetmap.josm.data.coor.EastNorth;
019import org.openstreetmap.josm.data.coor.LatLon;
020import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
021import org.openstreetmap.josm.tools.I18n;
022import org.openstreetmap.josm.tools.Utils;
023
024import gnu.getopt.Getopt;
025import gnu.getopt.LongOpt;
026
027/**
028 * Command line interface for projecting coordinates.
029 * @since 12792
030 */
031public class ProjectionCLI implements CLIModule {
032
033    public static final ProjectionCLI INSTANCE = new ProjectionCLI();
034
035    private boolean argInverse = false;         // NOPMD
036    private boolean argSwitchInput = false;     // NOPMD
037    private boolean argSwitchOutput = false;    // NOPMD
038
039    @Override
040    public String getActionKeyword() {
041        return "project";
042    }
043
044    @Override
045    public void processArguments(String[] argArray) {
046        Getopt.setI18nHandler(I18n::tr);
047        Getopt getopt = new Getopt("JOSM projection", argArray, "Irh", new LongOpt[] {
048                new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h')});
049
050        int c;
051        while ((c = getopt.getopt()) != -1) {
052            switch (c) {
053            case 'h':
054                showHelp();
055                System.exit(0);
056            case 'I':
057                argInverse = true;
058                break;
059            case 'r':
060                argSwitchInput = true;
061                break;
062            case 's':
063                argSwitchOutput = true;
064                break;
065            default:
066                // ignore
067            }
068        }
069
070        List<String> projParamFrom = new ArrayList<>();
071        List<String> projParamTo = new ArrayList<>();
072        List<String> otherPositional = new ArrayList<>();
073        boolean toTokenSeen = false;
074        // positional arguments:
075        for (int i = getopt.getOptind(); i < argArray.length; ++i) {
076            String arg = argArray[i];
077            if (arg.isEmpty()) throw new IllegalArgumentException("non-empty argument expected");
078            if (arg.startsWith("+")) {
079                if (arg.equals("+to")) {
080                    toTokenSeen = true;
081                } else {
082                    (toTokenSeen ? projParamTo : projParamFrom).add(arg);
083                }
084            } else {
085                otherPositional.add(arg);
086            }
087        }
088        String fromStr = Utils.join(" ", projParamFrom);
089        String toStr = Utils.join(" ", projParamTo);
090        try {
091            run(fromStr, toStr, otherPositional);
092        } catch (ProjectionConfigurationException | IllegalArgumentException | IOException ex) {
093            System.err.println(tr("Error: {0}", ex.getMessage()));
094            System.exit(1);
095        }
096        System.exit(0);
097    }
098
099    /**
100     * Displays help on the console
101     */
102    public static void showHelp() {
103        System.out.println(getHelp());
104    }
105
106    private static String getHelp() {
107        return tr("JOSM projection command line interface")+"\n\n"+
108                tr("Usage")+":\n"+
109                "\tjava -jar josm.jar project <options> <crs> +to <crs> [file]\n\n"+
110                tr("Description")+":\n"+
111                tr("Converts coordinates from one coordinate reference system to another.")+"\n\n"+
112                tr("Options")+":\n"+
113                "\t--help|-h         "+tr("Show this help")+"\n"+
114                "\t-I                "+tr("Switch input and output crs")+"\n"+
115                "\t-r                "+tr("Switch order of input coordinates (east/north, lon/lat)")+"\n"+
116                "\t-s                "+tr("Switch order of output coordinates (east/north, lon/lat)")+"\n\n"+
117                tr("<crs>")+":\n"+
118                tr("The format for input and output coordinate reference system"
119                        + " is similar to that of the PROJ.4 software.")+"\n\n"+
120                tr("[file]")+":\n"+
121                tr("Reads input data from one or more files listed as positional arguments. "
122                + "When no files are given, or the filename is \"-\", data is read from "
123                + "standard input.")+"\n\n"+
124                tr("Examples")+":\n"+
125                "    java -jar josm.jar project +init=epsg:4326 +to +init=epsg:3857 <<<\"11.232274 50.5685716\"\n"+
126                "       => 1250371.1334500168 6545331.055189664\n\n"+
127                "    java -jar josm.jar project +proj=lonlat +datum=WGS84 +to +proj=merc +a=6378137 +b=6378137 +nadgrids=@null <<EOF\n" +
128                "    11d13'56.19\"E 50d34'6.86\"N\n" +
129                "    118d39'30.42\"W 37d20'18.76\"N\n"+
130                "    EOF\n"+
131                "       => 1250371.1334500168 6545331.055189664\n" +
132                "          -1.3208998232319113E7 4486401.160664663\n";
133    }
134
135    private void run(String fromStr, String toStr, List<String> files) throws ProjectionConfigurationException, IOException {
136        CustomProjection fromProj = createProjection(fromStr);
137        CustomProjection toProj = createProjection(toStr);
138        if (this.argInverse) {
139            CustomProjection tmp = fromProj;
140            fromProj = toProj;
141            toProj = tmp;
142        }
143
144        if (files.isEmpty() || files.get(0).equals("-")) {
145            processInput(fromProj, toProj, new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset())));
146        } else {
147            for (String file : files) {
148                try (BufferedReader br = Files.newBufferedReader(Paths.get(file), StandardCharsets.UTF_8)) {
149                    processInput(fromProj, toProj, br);
150                }
151            }
152        }
153    }
154
155    private void processInput(CustomProjection fromProj, CustomProjection toProj, BufferedReader reader) throws IOException {
156        String line;
157        while ((line = reader.readLine()) != null) {
158            line = line.trim();
159            if (line.isEmpty() || line.startsWith("#"))
160                continue;
161            EastNorth enIn;
162            if (fromProj.isGeographic()) {
163                enIn = parseEastNorth(line, LatLonParser::parseCoordinate);
164            } else {
165                enIn = parseEastNorth(line, ProjectionCLI::parseDouble);
166            }
167            LatLon ll = fromProj.eastNorth2latlon(enIn);
168            EastNorth enOut = toProj.latlon2eastNorth(ll);
169            double cOut1 = argSwitchOutput ? enOut.north() : enOut.east();
170            double cOut2 = argSwitchOutput ? enOut.east() : enOut.north();
171            System.out.println(Double.toString(cOut1) + " " + Double.toString(cOut2));
172            System.out.flush();
173        }
174    }
175
176    private CustomProjection createProjection(String params) throws ProjectionConfigurationException {
177        CustomProjection proj = new CustomProjection();
178        proj.update(params);
179        return proj;
180    }
181
182    private EastNorth parseEastNorth(String s, Function<String, Double> parser) {
183        String[] en = s.split("[;, ]+");
184        if (en.length != 2)
185            throw new IllegalArgumentException(tr("Expected two coordinates, separated by white space, found {0} in ''{1}''", en.length, s));
186        double east = parser.apply(en[0]);
187        double north = parser.apply(en[1]);
188        if (this.argSwitchInput)
189            return new EastNorth(north, east);
190        else
191            return new EastNorth(east, north);
192    }
193
194    private static double parseDouble(String s) {
195        try {
196            return Double.parseDouble(s);
197        } catch (NumberFormatException nfe) {
198            throw new IllegalArgumentException(tr("Unable to parse number ''{0}''", s), nfe);
199        }
200    }
201
202    /**
203     * Main class to run just the projection CLI.
204     * @param args command line arguments
205     */
206    public static void main(String[] args) {
207        ProjectionCLI.INSTANCE.processArguments(args);
208    }
209}