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