001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.nmea;
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.ParsePosition;
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.Optional;
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.IllegalDataException;
024import org.openstreetmap.josm.tools.Logging;
025import org.openstreetmap.josm.tools.date.DateUtils;
026
027/**
028 * Reads a NMEA 0183 file. Based on information from
029 * <a href="http://www.catb.org/gpsd/NMEA.html">http://www.catb.org/gpsd</a>.
030 *
031 * NMEA files are in printable ASCII form and may include information such as position,
032 * speed, depth, frequency allocation, etc.
033 * Typical messages might be 11 to a maximum of 79 characters in length.
034 *
035 * NMEA standard aims to support one-way serial data transmission from a single "talker"
036 * to one or more "listeners". The type of talker is identified by a 2-character mnemonic.
037 *
038 * NMEA information is encoded through a list of "sentences".
039 *
040 * @author cbrill
041 */
042public class NmeaReader {
043
044    enum VTG {
045        COURSE(1), COURSE_REF(2), // true course
046        COURSE_M(3), COURSE_M_REF(4), // magnetic course
047        SPEED_KN(5), SPEED_KN_UNIT(6), // speed in knots
048        SPEED_KMH(7), SPEED_KMH_UNIT(8), // speed in km/h
049        REST(9); // version-specific rest
050
051        final int position;
052
053        VTG(int position) {
054            this.position = position;
055        }
056    }
057
058    enum RMC {
059        TIME(1),
060        /** Warning from the receiver (A = data ok, V = warning) */
061        RECEIVER_WARNING(2),
062        WIDTH_NORTH(3), WIDTH_NORTH_NAME(4), // Latitude, NS
063        LENGTH_EAST(5), LENGTH_EAST_NAME(6), // Longitude, EW
064        SPEED(7), COURSE(8), DATE(9),           // Speed in knots
065        MAGNETIC_DECLINATION(10), UNKNOWN(11),  // magnetic declination
066        /**
067         * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
068         *
069         * @since NMEA 2.3
070         */
071        MODE(12);
072
073        final int position;
074
075        RMC(int position) {
076            this.position = position;
077        }
078    }
079
080    enum GGA {
081        TIME(1), LATITUDE(2), LATITUDE_NAME(3), LONGITUDE(4), LONGITUDE_NAME(5),
082        /**
083         * Quality (0 = invalid, 1 = GPS, 2 = DGPS, 6 = estimanted (@since NMEA 2.3))
084         */
085        QUALITY(6), SATELLITE_COUNT(7),
086        HDOP(8), // HDOP (horizontal dilution of precision)
087        HEIGHT(9), HEIGHT_UNTIS(10), // height above NN (above geoid)
088        HEIGHT_2(11), HEIGHT_2_UNTIS(12), // height geoid - height ellipsoid (WGS84)
089        GPS_AGE(13), // Age of differential GPS data
090        REF(14); // REF station
091
092        final int position;
093        GGA(int position) {
094            this.position = position;
095        }
096    }
097
098    enum GSA {
099        AUTOMATIC(1),
100        FIX_TYPE(2), // 1 = not fixed, 2 = 2D fixed, 3 = 3D fixed)
101        // PRN numbers for max 12 satellites
102        PRN_1(3), PRN_2(4), PRN_3(5), PRN_4(6), PRN_5(7), PRN_6(8),
103        PRN_7(9), PRN_8(10), PRN_9(11), PRN_10(12), PRN_11(13), PRN_12(14),
104        PDOP(15),   // PDOP (precision)
105        HDOP(16),   // HDOP (horizontal precision)
106        VDOP(17);   // VDOP (vertical precision)
107
108        final int position;
109        GSA(int position) {
110            this.position = position;
111        }
112    }
113
114    enum GLL {
115        LATITUDE(1), LATITUDE_NS(2), // Latitude, NS
116        LONGITUDE(3), LONGITUDE_EW(4), // Latitude, EW
117        UTC(5), // Universal Time Coordinated
118        STATUS(6), // Status: A = Data valid, V = Data not valid
119        /**
120         * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
121         * @since NMEA 2.3
122         */
123        MODE(7);
124
125        final int position;
126        GLL(int position) {
127            this.position = position;
128        }
129    }
130
131    public GpxData data;
132
133    private final SimpleDateFormat rmcTimeFmt = new SimpleDateFormat("ddMMyyHHmmss.SSS", Locale.ENGLISH);
134    private final SimpleDateFormat rmcTimeFmtStd = new SimpleDateFormat("ddMMyyHHmmss", Locale.ENGLISH);
135
136    private Date readTime(String p) throws IllegalDataException {
137        Date d = Optional.ofNullable(rmcTimeFmt.parse(p, new ParsePosition(0)))
138                .orElseGet(() -> rmcTimeFmtStd.parse(p, new ParsePosition(0)));
139        if (d == null)
140            throw new IllegalDataException("Date is malformed: '" + p + "'");
141        return d;
142    }
143
144    // functons for reading the error stats
145    public NMEAParserState ps;
146
147    public int getParserUnknown() {
148        return ps.unknown;
149    }
150
151    public int getParserZeroCoordinates() {
152        return ps.zeroCoord;
153    }
154
155    public int getParserChecksumErrors() {
156        return ps.checksumErrors+ps.noChecksum;
157    }
158
159    public int getParserMalformed() {
160        return ps.malformed;
161    }
162
163    public int getNumberOfCoordinates() {
164        return ps.success;
165    }
166
167    /**
168     * Constructs a new {@code NmeaReader}
169     * @param source NMEA file input stream
170     * @throws IOException if an I/O error occurs
171     */
172    public NmeaReader(InputStream source) throws IOException {
173        rmcTimeFmt.setTimeZone(DateUtils.UTC);
174        rmcTimeFmtStd.setTimeZone(DateUtils.UTC);
175
176        // create the data tree
177        data = new GpxData();
178        Collection<Collection<WayPoint>> currentTrack = new ArrayList<>();
179
180        try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) {
181            StringBuilder sb = new StringBuilder(1024);
182            int loopstartChar = rd.read();
183            ps = new NMEAParserState();
184            if (loopstartChar == -1)
185                //TODO tell user about the problem?
186                return;
187            sb.append((char) loopstartChar);
188            ps.pDate = "010100"; // TODO date problem
189            while (true) {
190                // don't load unparsable files completely to memory
191                if (sb.length() >= 1020) {
192                    sb.delete(0, sb.length()-1);
193                }
194                int c = rd.read();
195                if (c == '$') {
196                    parseNMEASentence(sb.toString(), ps);
197                    sb.delete(0, sb.length());
198                    sb.append('$');
199                } else if (c == -1) {
200                    // EOF: add last WayPoint if it works out
201                    parseNMEASentence(sb.toString(), ps);
202                    break;
203                } else {
204                    sb.append((char) c);
205                }
206            }
207            currentTrack.add(ps.waypoints);
208            data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
209
210        } catch (IllegalDataException e) {
211            Logging.warn(e);
212        }
213    }
214
215    private static class NMEAParserState {
216        protected Collection<WayPoint> waypoints = new ArrayList<>();
217        protected String pTime;
218        protected String pDate;
219        protected WayPoint pWp;
220
221        protected int success; // number of successfully parsed sentences
222        protected int malformed;
223        protected int checksumErrors;
224        protected int noChecksum;
225        protected int unknown;
226        protected int zeroCoord;
227    }
228
229    /**
230     * Determines if the given address denotes the given NMEA sentence formatter of a known talker.
231     * @param address first tag of an NMEA sentence
232     * @param formatter sentence formatter mnemonic code
233     * @return {@code true} if the {@code address} denotes the given NMEA sentence formatter of a known talker
234     */
235    static boolean isSentence(String address, Sentence formatter) {
236        for (TalkerId talker : TalkerId.values()) {
237            if (address.equals('$' + talker.name() + formatter.name())) {
238                return true;
239            }
240        }
241        return false;
242    }
243
244    // Parses split up sentences into WayPoints which are stored
245    // in the collection in the NMEAParserState object.
246    // Returns true if the input made sense, false otherwise.
247    private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException {
248        try {
249            if (s.isEmpty()) {
250                throw new IllegalArgumentException("s is empty");
251            }
252
253            // checksum check:
254            // the bytes between the $ and the * are xored
255            // if there is no * or other meanities it will throw
256            // and result in a malformed packet.
257            String[] chkstrings = s.split("\\*");
258            if (chkstrings.length > 1) {
259                byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8);
260                int chk = 0;
261                for (int i = 1; i < chb.length; i++) {
262                    chk ^= chb[i];
263                }
264                if (Integer.parseInt(chkstrings[1].substring(0, 2), 16) != chk) {
265                    ps.checksumErrors++;
266                    ps.pWp = null;
267                    return false;
268                }
269            } else {
270                ps.noChecksum++;
271            }
272            // now for the content
273            String[] e = chkstrings[0].split(",");
274            String accu;
275
276            WayPoint currentwp = ps.pWp;
277            String currentDate = ps.pDate;
278
279            // handle the packet content
280            if (isSentence(e[0], Sentence.GGA)) {
281                // Position
282                LatLon latLon = parseLatLon(
283                        e[GGA.LATITUDE_NAME.position],
284                        e[GGA.LONGITUDE_NAME.position],
285                        e[GGA.LATITUDE.position],
286                        e[GGA.LONGITUDE.position]
287                );
288                if (latLon == null) {
289                    throw new IllegalDataException("Malformed lat/lon");
290                }
291
292                if (LatLon.ZERO.equals(latLon)) {
293                    ps.zeroCoord++;
294                    return false;
295                }
296
297                // time
298                accu = e[GGA.TIME.position];
299                Date d = readTime(currentDate+accu);
300
301                if ((ps.pTime == null) || (currentwp == null) || !ps.pTime.equals(accu)) {
302                    // this node is newer than the previous, create a new waypoint.
303                    // no matter if previous WayPoint was null, we got something better now.
304                    ps.pTime = accu;
305                    currentwp = new WayPoint(latLon);
306                }
307                if (!currentwp.attr.containsKey("time")) {
308                    // As this sentence has no complete time only use it
309                    // if there is no time so far
310                    currentwp.setTime(d);
311                }
312                // elevation
313                accu = e[GGA.HEIGHT_UNTIS.position];
314                if ("M".equals(accu)) {
315                    // Ignore heights that are not in meters for now
316                    accu = e[GGA.HEIGHT.position];
317                    if (!accu.isEmpty()) {
318                        Double.parseDouble(accu);
319                        // if it throws it's malformed; this should only happen if the
320                        // device sends nonstandard data.
321                        if (!accu.isEmpty()) { // FIX ? same check
322                            currentwp.put(GpxConstants.PT_ELE, accu);
323                        }
324                    }
325                }
326                // number of satellites
327                accu = e[GGA.SATELLITE_COUNT.position];
328                int sat = 0;
329                if (!accu.isEmpty()) {
330                    sat = Integer.parseInt(accu);
331                    currentwp.put(GpxConstants.PT_SAT, accu);
332                }
333                // h-dilution
334                accu = e[GGA.HDOP.position];
335                if (!accu.isEmpty()) {
336                    currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
337                }
338                // fix
339                accu = e[GGA.QUALITY.position];
340                if (!accu.isEmpty()) {
341                    int fixtype = Integer.parseInt(accu);
342                    switch(fixtype) {
343                    case 0:
344                        currentwp.put(GpxConstants.PT_FIX, "none");
345                        break;
346                    case 1:
347                        if (sat < 4) {
348                            currentwp.put(GpxConstants.PT_FIX, "2d");
349                        } else {
350                            currentwp.put(GpxConstants.PT_FIX, "3d");
351                        }
352                        break;
353                    case 2:
354                        currentwp.put(GpxConstants.PT_FIX, "dgps");
355                        break;
356                    default:
357                        break;
358                    }
359                }
360            } else if (isSentence(e[0], Sentence.VTG)) {
361                // COURSE
362                accu = e[VTG.COURSE_REF.position];
363                if ("T".equals(accu)) {
364                    // other values than (T)rue are ignored
365                    accu = e[VTG.COURSE.position];
366                    if (!accu.isEmpty() && currentwp != null) {
367                        Double.parseDouble(accu);
368                        currentwp.put("course", accu);
369                    }
370                }
371                // SPEED
372                accu = e[VTG.SPEED_KMH_UNIT.position];
373                if (accu.startsWith("K")) {
374                    accu = e[VTG.SPEED_KMH.position];
375                    if (!accu.isEmpty() && currentwp != null) {
376                        double speed = Double.parseDouble(accu);
377                        speed /= 3.6; // speed in m/s
378                        currentwp.put("speed", Double.toString(speed));
379                    }
380                }
381            } else if (isSentence(e[0], Sentence.GSA)) {
382                // vdop
383                accu = e[GSA.VDOP.position];
384                if (!accu.isEmpty() && currentwp != null) {
385                    currentwp.put(GpxConstants.PT_VDOP, Float.valueOf(accu));
386                }
387                // hdop
388                accu = e[GSA.HDOP.position];
389                if (!accu.isEmpty() && currentwp != null) {
390                    currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
391                }
392                // pdop
393                accu = e[GSA.PDOP.position];
394                if (!accu.isEmpty() && currentwp != null) {
395                    currentwp.put(GpxConstants.PT_PDOP, Float.valueOf(accu));
396                }
397            } else if (isSentence(e[0], Sentence.RMC)) {
398                // coordinates
399                LatLon latLon = parseLatLon(
400                        e[RMC.WIDTH_NORTH_NAME.position],
401                        e[RMC.LENGTH_EAST_NAME.position],
402                        e[RMC.WIDTH_NORTH.position],
403                        e[RMC.LENGTH_EAST.position]
404                );
405                if (LatLon.ZERO.equals(latLon)) {
406                    ps.zeroCoord++;
407                    return false;
408                }
409                // time
410                currentDate = e[RMC.DATE.position];
411                String time = e[RMC.TIME.position];
412
413                Date d = readTime(currentDate+time);
414
415                if (ps.pTime == null || currentwp == null || !ps.pTime.equals(time)) {
416                    // this node is newer than the previous, create a new waypoint.
417                    ps.pTime = time;
418                    currentwp = new WayPoint(latLon);
419                }
420                // time: this sentence has complete time so always use it.
421                currentwp.setTime(d);
422                // speed
423                accu = e[RMC.SPEED.position];
424                if (!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {
425                    double speed = Double.parseDouble(accu);
426                    speed *= 0.514444444; // to m/s
427                    currentwp.put("speed", Double.toString(speed));
428                }
429                // course
430                accu = e[RMC.COURSE.position];
431                if (!accu.isEmpty() && !currentwp.attr.containsKey("course")) {
432                    Double.parseDouble(accu);
433                    currentwp.put("course", accu);
434                }
435
436                // TODO fix?
437                // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
438                // *
439                // * @since NMEA 2.3
440                //
441                //MODE(12);
442            } else if (isSentence(e[0], Sentence.GLL)) {
443                // coordinates
444                LatLon latLon = parseLatLon(
445                        e[GLL.LATITUDE_NS.position],
446                        e[GLL.LONGITUDE_EW.position],
447                        e[GLL.LATITUDE.position],
448                        e[GLL.LONGITUDE.position]
449                );
450                if (LatLon.ZERO.equals(latLon)) {
451                    ps.zeroCoord++;
452                    return false;
453                }
454                // only consider valid data
455                if (!"A".equals(e[GLL.STATUS.position])) {
456                    return false;
457                }
458
459                // RMC sentences contain a full date while GLL sentences contain only time,
460                // so create new waypoints only of the NMEA file does not contain RMC sentences
461                if (ps.pTime == null || currentwp == null) {
462                    currentwp = new WayPoint(latLon);
463                }
464            } else {
465                ps.unknown++;
466                return false;
467            }
468            ps.pDate = currentDate;
469            if (ps.pWp != currentwp) {
470                if (ps.pWp != null) {
471                    ps.pWp.setTime();
472                }
473                ps.pWp = currentwp;
474                ps.waypoints.add(currentwp);
475                ps.success++;
476                return true;
477            }
478            return true;
479
480        } catch (IllegalArgumentException | IndexOutOfBoundsException | IllegalDataException ex) {
481            if (ps.malformed < 5) {
482                Logging.warn(ex);
483            } else {
484                Logging.debug(ex);
485            }
486            ps.malformed++;
487            ps.pWp = null;
488            return false;
489        }
490    }
491
492    private static LatLon parseLatLon(String ns, String ew, String dlat, String dlon) {
493        String widthNorth = dlat.trim();
494        String lengthEast = dlon.trim();
495
496        // return a zero latlon instead of null so it is logged as zero coordinate
497        // instead of malformed sentence
498        if (widthNorth.isEmpty() && lengthEast.isEmpty()) return LatLon.ZERO;
499
500        // The format is xxDDLL.LLLL
501        // xx optional whitespace
502        // DD (int) degres
503        // LL.LLLL (double) latidude
504        int latdegsep = widthNorth.indexOf('.') - 2;
505        if (latdegsep < 0) return null;
506
507        int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));
508        double latmin = Double.parseDouble(widthNorth.substring(latdegsep));
509        if (latdeg < 0) {
510            latmin *= -1.0;
511        }
512        double lat = latdeg + latmin / 60;
513        if ("S".equals(ns)) {
514            lat = -lat;
515        }
516
517        int londegsep = lengthEast.indexOf('.') - 2;
518        if (londegsep < 0) return null;
519
520        int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));
521        double lonmin = Double.parseDouble(lengthEast.substring(londegsep));
522        if (londeg < 0) {
523            lonmin *= -1.0;
524        }
525        double lon = londeg + lonmin / 60;
526        if ("W".equals(ew)) {
527            lon = -lon;
528        }
529        return new LatLon(lat, lon);
530    }
531}