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