001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.rtklib;
003
004import java.io.BufferedReader;
005import java.io.IOException;
006import java.io.InputStream;
007import java.io.InputStreamReader;
008import java.nio.charset.StandardCharsets;
009import java.text.ParseException;
010import java.text.SimpleDateFormat;
011import java.util.ArrayList;
012import java.util.Collection;
013import java.util.Collections;
014import java.util.Date;
015import java.util.Locale;
016import java.util.Objects;
017
018import org.openstreetmap.josm.data.coor.LatLon;
019import org.openstreetmap.josm.data.gpx.GpxConstants;
020import org.openstreetmap.josm.data.gpx.GpxData;
021import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
022import org.openstreetmap.josm.data.gpx.WayPoint;
023import org.openstreetmap.josm.io.IGpxReader;
024import org.openstreetmap.josm.tools.Logging;
025import org.openstreetmap.josm.tools.date.DateUtils;
026import org.xml.sax.SAXException;
027
028/**
029 * Reads a RTKLib Positioning Solution file.
030 * <p>
031 * See <a href="https://github.com/tomojitakasu/RTKLIB/blob/rtklib_2.4.3/doc/manual_2.4.2.pdf">RTKLIB Manual</a>.
032 * @since 15247
033 */
034public class RtkLibPosReader implements IGpxReader {
035
036    private static final int IDX_DATE = 0;
037    private static final int IDX_TIME = 1;
038    private static final int IDX_LAT = 2;
039    private static final int IDX_LON = 3;
040    private static final int IDX_HEIGHT = 4;
041    private static final int IDX_Q = 5;
042    private static final int IDX_NS = 6;
043    private static final int IDX_SDN = 7;
044    private static final int IDX_SDE = 8;
045    private static final int IDX_SDU = 9;
046    private static final int IDX_SDNE = 10;
047    private static final int IDX_SDEU = 11;
048    private static final int IDX_SDUN = 12;
049    private static final int IDX_AGE = 13;
050    private static final int IDX_RATIO = 14;
051
052    private final SimpleDateFormat dateTimeFmtS = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.ENGLISH); // 2019/06/08 08:23:15
053    private final SimpleDateFormat dateTimeFmtL = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS", Locale.ENGLISH); // 2019/06/08 08:23:15.000
054
055    private final InputStream source;
056    private GpxData data;
057    private int success; // number of successfully parsed lines
058
059    /**
060     * Constructs a new {@code RtkLibPosReader}
061     * @param source RTKLib .pos file input stream
062     * @throws IOException if an I/O error occurs
063     */
064    public RtkLibPosReader(InputStream source) throws IOException {
065        this.source = Objects.requireNonNull(source);
066        dateTimeFmtS.setTimeZone(DateUtils.UTC);
067        dateTimeFmtL.setTimeZone(DateUtils.UTC);
068    }
069
070    private Date parseDate(String date) throws ParseException {
071        return (date.length() > 20 ? dateTimeFmtL : dateTimeFmtS).parse(date);
072    }
073
074    @Override
075    public boolean parse(boolean tryToFinish) throws SAXException, IOException {
076        data = new GpxData();
077        Collection<Collection<WayPoint>> currentTrack = new ArrayList<>();
078        Collection<WayPoint> waypoints = new ArrayList<>();
079        try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) {
080            String line = null;
081            do {
082                line = rd.readLine();
083                if (line != null) {
084                    if (line.startsWith("% ref pos   :")) {
085                        // TODO add marker
086                    } else if (!line.startsWith("%")) {
087                        try {
088                            String[] fields = line.split("[ ]+");
089                            WayPoint currentwp = new WayPoint(new LatLon(
090                                    Double.parseDouble(fields[IDX_LAT]),
091                                    Double.parseDouble(fields[IDX_LON])));
092                            currentwp.put(GpxConstants.PT_ELE, fields[IDX_HEIGHT]);
093                            currentwp.setTime(parseDate(fields[IDX_DATE]+" "+fields[IDX_TIME]));
094                            currentwp.put(GpxConstants.RTKLIB_Q, Integer.parseInt(fields[IDX_Q]));
095                            currentwp.put(GpxConstants.PT_SAT, fields[IDX_NS]);
096                            currentwp.put(GpxConstants.RTKLIB_SDN, fields[IDX_SDN]);
097                            currentwp.put(GpxConstants.RTKLIB_SDE, fields[IDX_SDE]);
098                            currentwp.put(GpxConstants.RTKLIB_SDE, fields[IDX_SDU]);
099                            currentwp.put(GpxConstants.RTKLIB_SDNE, fields[IDX_SDNE]);
100                            currentwp.put(GpxConstants.RTKLIB_SDEU, fields[IDX_SDEU]);
101                            currentwp.put(GpxConstants.RTKLIB_SDUN, fields[IDX_SDUN]);
102                            currentwp.put(GpxConstants.RTKLIB_AGE, fields[IDX_AGE]);
103                            currentwp.put(GpxConstants.RTKLIB_RATIO, fields[IDX_RATIO]);
104                            double sdn = Double.parseDouble(fields[IDX_SDN]);
105                            double sde = Double.parseDouble(fields[IDX_SDN]);
106                            currentwp.put(GpxConstants.PT_HDOP, (float) Math.sqrt(sdn*sdn + sde*sde));
107                            waypoints.add(currentwp);
108                            success++;
109                        } catch (ParseException | IllegalArgumentException e) {
110                            Logging.error(e);
111                        }
112                    }
113                }
114            } while (line != null);
115        }
116        currentTrack.add(waypoints);
117        data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
118        return true;
119    }
120
121    @Override
122    public GpxData getGpxData() {
123        return data;
124    }
125
126    /**
127     * Returns the number of coordinates that have been successfuly read.
128     * @return the number of coordinates that have been successfuly read
129     */
130    public int getNumberOfCoordinates() {
131        return success;
132    }
133}