001//License: GPL. See README for details.
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 static 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 static 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 static 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 static 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 static 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    public int getParserZeroCoordinates() {
156        return ps.zero_coord;
157    }
158    public int getParserChecksumErrors() {
159        return ps.checksum_errors+ps.no_checksum;
160    }
161    public int getParserMalformed() {
162        return ps.malformed;
163    }
164    public int getNumberOfCoordinates() {
165        return ps.success;
166    }
167
168    public NmeaReader(InputStream source) throws IOException {
169
170        // create the data tree
171        data = new GpxData();
172        Collection<Collection<WayPoint>> currentTrack = new ArrayList<>();
173
174        try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) {
175            StringBuilder sb = new StringBuilder(1024);
176            int loopstart_char = rd.read();
177            ps = new NMEAParserState();
178            if(loopstart_char == -1)
179                //TODO tell user about the problem?
180                return;
181            sb.append((char)loopstart_char);
182            ps.p_Date="010100"; // TODO date problem
183            while(true) {
184                // don't load unparsable files completely to memory
185                if(sb.length()>=1020) {
186                    sb.delete(0, sb.length()-1);
187                }
188                int c = rd.read();
189                if(c=='$') {
190                    parseNMEASentence(sb.toString(), ps);
191                    sb.delete(0, sb.length());
192                    sb.append('$');
193                } else if(c == -1) {
194                    // EOF: add last WayPoint if it works out
195                    parseNMEASentence(sb.toString(),ps);
196                    break;
197                } else {
198                    sb.append((char)c);
199                }
200            }
201            currentTrack.add(ps.waypoints);
202            data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
203
204        } catch (IllegalDataException e) {
205            Main.warn(e);
206        }
207    }
208
209    private static class NMEAParserState {
210        protected Collection<WayPoint> waypoints = new ArrayList<>();
211        protected String p_Time;
212        protected String p_Date;
213        protected WayPoint p_Wp;
214
215        protected int success = 0; // number of successfully parsend sentences
216        protected int malformed = 0;
217        protected int checksum_errors = 0;
218        protected int no_checksum = 0;
219        protected int unknown = 0;
220        protected int zero_coord = 0;
221    }
222
223    // Parses split up sentences into WayPoints which are stored
224    // in the collection in the NMEAParserState object.
225    // Returns true if the input made sence, false otherwise.
226    private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException {
227        try {
228            if (s.isEmpty()) {
229                throw new IllegalArgumentException("s is empty");
230            }
231
232            // checksum check:
233            // the bytes between the $ and the * are xored
234            // if there is no * or other meanities it will throw
235            // and result in a malformed packet.
236            String[] chkstrings = s.split("\\*");
237            if(chkstrings.length > 1)
238            {
239                byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8);
240                int chk=0;
241                for (int i = 1; i < chb.length; i++) {
242                    chk ^= chb[i];
243                }
244                if (Integer.parseInt(chkstrings[1].substring(0,2),16) != chk) {
245                    ps.checksum_errors++;
246                    ps.p_Wp=null;
247                    return false;
248                }
249            } else {
250                ps.no_checksum++;
251            }
252            // now for the content
253            String[] e = chkstrings[0].split(",");
254            String accu;
255
256            WayPoint currentwp = ps.p_Wp;
257            String currentDate = ps.p_Date;
258
259            // handle the packet content
260            if("$GPGGA".equals(e[0]) || "$GNGGA".equals(e[0])) {
261                // Position
262                LatLon latLon = parseLatLon(
263                        e[GPGGA.LATITUDE_NAME.position],
264                        e[GPGGA.LONGITUDE_NAME.position],
265                        e[GPGGA.LATITUDE.position],
266                        e[GPGGA.LONGITUDE.position]
267                );
268                if (latLon==null) {
269                    throw new IllegalDataException("Malformed lat/lon");
270                }
271
272                if ((latLon.lat()==0.0) && (latLon.lon()==0.0)) {
273                    ps.zero_coord++;
274                    return false;
275                }
276
277                // time
278                accu = e[GPGGA.TIME.position];
279                Date d = readTime(currentDate+accu);
280
281                if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(accu)) {
282                    // this node is newer than the previous, create a new waypoint.
283                    // no matter if previous WayPoint was null, we got something
284                    // better now.
285                    ps.p_Time=accu;
286                    currentwp = new WayPoint(latLon);
287                }
288                if(!currentwp.attr.containsKey("time")) {
289                    // As this sentence has no complete time only use it
290                    // if there is no time so far
291                    currentwp.put(GpxConstants.PT_TIME, DateUtils.fromDate(d));
292                }
293                // elevation
294                accu=e[GPGGA.HEIGHT_UNTIS.position];
295                if("M".equals(accu)) {
296                    // Ignore heights that are not in meters for now
297                    accu=e[GPGGA.HEIGHT.position];
298                    if(!accu.isEmpty()) {
299                        Double.parseDouble(accu);
300                        // if it throws it's malformed; this should only happen if the
301                        // device sends nonstandard data.
302                        if(!accu.isEmpty()) { // FIX ? same check
303                            currentwp.put(GpxConstants.PT_ELE, accu);
304                        }
305                    }
306                }
307                // number of sattelites
308                accu=e[GPGGA.SATELLITE_COUNT.position];
309                int sat = 0;
310                if(!accu.isEmpty()) {
311                    sat = Integer.parseInt(accu);
312                    currentwp.put(GpxConstants.PT_SAT, accu);
313                }
314                // h-dilution
315                accu=e[GPGGA.HDOP.position];
316                if(!accu.isEmpty()) {
317                    currentwp.put(GpxConstants.PT_HDOP, Float.parseFloat(accu));
318                }
319                // fix
320                accu=e[GPGGA.QUALITY.position];
321                if(!accu.isEmpty()) {
322                    int fixtype = Integer.parseInt(accu);
323                    switch(fixtype) {
324                    case 0:
325                        currentwp.put(GpxConstants.PT_FIX, "none");
326                        break;
327                    case 1:
328                        if(sat < 4) {
329                            currentwp.put(GpxConstants.PT_FIX, "2d");
330                        } else {
331                            currentwp.put(GpxConstants.PT_FIX, "3d");
332                        }
333                        break;
334                    case 2:
335                        currentwp.put(GpxConstants.PT_FIX, "dgps");
336                        break;
337                    default:
338                        break;
339                    }
340                }
341            } else if("$GPVTG".equals(e[0]) || "$GNVTG".equals(e[0])) {
342                // COURSE
343                accu = e[GPVTG.COURSE_REF.position];
344                if("T".equals(accu)) {
345                    // other values than (T)rue are ignored
346                    accu = e[GPVTG.COURSE.position];
347                    if(!accu.isEmpty()) {
348                        Double.parseDouble(accu);
349                        currentwp.put("course", accu);
350                    }
351                }
352                // SPEED
353                accu = e[GPVTG.SPEED_KMH_UNIT.position];
354                if(accu.startsWith("K")) {
355                    accu = e[GPVTG.SPEED_KMH.position];
356                    if(!accu.isEmpty()) {
357                        double speed = Double.parseDouble(accu);
358                        speed /= 3.6; // speed in m/s
359                        currentwp.put("speed", Double.toString(speed));
360                    }
361                }
362            } else if("$GPGSA".equals(e[0]) || "$GNGSA".equals(e[0])) {
363                // vdop
364                accu=e[GPGSA.VDOP.position];
365                if(!accu.isEmpty()) {
366                    currentwp.put(GpxConstants.PT_VDOP, Float.parseFloat(accu));
367                }
368                // hdop
369                accu=e[GPGSA.HDOP.position];
370                if(!accu.isEmpty()) {
371                    currentwp.put(GpxConstants.PT_HDOP, Float.parseFloat(accu));
372                }
373                // pdop
374                accu=e[GPGSA.PDOP.position];
375                if(!accu.isEmpty()) {
376                    currentwp.put(GpxConstants.PT_PDOP, Float.parseFloat(accu));
377                }
378            }
379            else if("$GPRMC".equals(e[0]) || "$GNRMC".equals(e[0])) {
380                // coordinates
381                LatLon latLon = parseLatLon(
382                        e[GPRMC.WIDTH_NORTH_NAME.position],
383                        e[GPRMC.LENGTH_EAST_NAME.position],
384                        e[GPRMC.WIDTH_NORTH.position],
385                        e[GPRMC.LENGTH_EAST.position]
386                );
387                if((latLon.lat()==0.0) && (latLon.lon()==0.0)) {
388                    ps.zero_coord++;
389                    return false;
390                }
391                // time
392                currentDate = e[GPRMC.DATE.position];
393                String time = e[GPRMC.TIME.position];
394
395                Date d = readTime(currentDate+time);
396
397                if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(time)) {
398                    // this node is newer than the previous, create a new waypoint.
399                    ps.p_Time=time;
400                    currentwp = new WayPoint(latLon);
401                }
402                // time: this sentence has complete time so always use it.
403                currentwp.put(GpxConstants.PT_TIME, DateUtils.fromDate(d));
404                // speed
405                accu = e[GPRMC.SPEED.position];
406                if(!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {
407                    double speed = Double.parseDouble(accu);
408                    speed *= 0.514444444; // to m/s
409                    currentwp.put("speed", Double.toString(speed));
410                }
411                // course
412                accu = e[GPRMC.COURSE.position];
413                if(!accu.isEmpty() && !currentwp.attr.containsKey("course")) {
414                    Double.parseDouble(accu);
415                    currentwp.put("course", accu);
416                }
417
418                // TODO fix?
419                // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S
420                // * = simulated)
421                // *
422                // * @since NMEA 2.3
423                //
424                //MODE(12);
425            } else {
426                ps.unknown++;
427                return false;
428            }
429            ps.p_Date = currentDate;
430            if(ps.p_Wp != currentwp) {
431                if(ps.p_Wp!=null) {
432                    ps.p_Wp.setTime();
433                }
434                ps.p_Wp = currentwp;
435                ps.waypoints.add(currentwp);
436                ps.success++;
437                return true;
438            }
439            return true;
440
441        } catch (RuntimeException x) {
442            // out of bounds and such
443            ps.malformed++;
444            ps.p_Wp=null;
445            return false;
446        }
447    }
448
449    private LatLon parseLatLon(String ns, String ew, String dlat, String dlon)
450    throws NumberFormatException {
451        String widthNorth = dlat.trim();
452        String lengthEast = dlon.trim();
453
454        // return a zero latlon instead of null so it is logged as zero coordinate
455        // instead of malformed sentence
456        if(widthNorth.isEmpty() && lengthEast.isEmpty()) return new LatLon(0.0,0.0);
457
458        // The format is xxDDLL.LLLL
459        // xx optional whitespace
460        // DD (int) degres
461        // LL.LLLL (double) latidude
462        int latdegsep = widthNorth.indexOf('.') - 2;
463        if (latdegsep < 0) return null;
464
465        int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));
466        double latmin = Double.parseDouble(widthNorth.substring(latdegsep));
467        if(latdeg < 0) {
468            latmin *= -1.0;
469        }
470        double lat = latdeg + latmin / 60;
471        if ("S".equals(ns)) {
472            lat = -lat;
473        }
474
475        int londegsep = lengthEast.indexOf('.') - 2;
476        if (londegsep < 0) return null;
477
478        int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));
479        double lonmin = Double.parseDouble(lengthEast.substring(londegsep));
480        if(londeg < 0) {
481            lonmin *= -1.0;
482        }
483        double lon = londeg + lonmin / 60;
484        if ("W".equals(ew)) {
485            lon = -lon;
486        }
487        return new LatLon(lat, lon);
488    }
489}