source: josm/trunk/src/org/openstreetmap/josm/io/nmea/NmeaReader.java @ 12620

Last change on this file since 12620 was 12620, checked in by Don-vip, 14 months ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • Property svn:eol-style set to native
File size: 19.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.nmea;
3
4import java.io.BufferedReader;
5import java.io.IOException;
6import java.io.InputStream;
7import java.io.InputStreamReader;
8import java.nio.charset.StandardCharsets;
9import java.text.ParsePosition;
10import java.text.SimpleDateFormat;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.Date;
15import java.util.Locale;
16import java.util.Optional;
17
18import org.openstreetmap.josm.data.coor.LatLon;
19import org.openstreetmap.josm.data.gpx.GpxConstants;
20import org.openstreetmap.josm.data.gpx.GpxData;
21import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
22import org.openstreetmap.josm.data.gpx.WayPoint;
23import org.openstreetmap.josm.io.IllegalDataException;
24import org.openstreetmap.josm.tools.Logging;
25import org.openstreetmap.josm.tools.date.DateUtils;
26
27/**
28 * Reads a NMEA 0183 file. Based on information from
29 * <a href="http://www.catb.org/gpsd/NMEA.html">http://www.catb.org/gpsd</a>.
30 *
31 * NMEA files are in printable ASCII form and may include information such as position,
32 * speed, depth, frequency allocation, etc.
33 * Typical messages might be 11 to a maximum of 79 characters in length.
34 *
35 * NMEA standard aims to support one-way serial data transmission from a single "talker"
36 * to one or more "listeners". The type of talker is identified by a 2-character mnemonic.
37 *
38 * NMEA information is encoded through a list of "sentences".
39 *
40 * @author cbrill
41 */
42public class NmeaReader {
43
44    enum VTG {
45        COURSE(1), COURSE_REF(2), // true course
46        COURSE_M(3), COURSE_M_REF(4), // magnetic course
47        SPEED_KN(5), SPEED_KN_UNIT(6), // speed in knots
48        SPEED_KMH(7), SPEED_KMH_UNIT(8), // speed in km/h
49        REST(9); // version-specific rest
50
51        final int position;
52
53        VTG(int position) {
54            this.position = position;
55        }
56    }
57
58    enum RMC {
59        TIME(1),
60        /** Warning from the receiver (A = data ok, V = warning) */
61        RECEIVER_WARNING(2),
62        WIDTH_NORTH(3), WIDTH_NORTH_NAME(4), // Latitude, NS
63        LENGTH_EAST(5), LENGTH_EAST_NAME(6), // Longitude, EW
64        SPEED(7), COURSE(8), DATE(9),           // Speed in knots
65        MAGNETIC_DECLINATION(10), UNKNOWN(11),  // magnetic declination
66        /**
67         * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
68         *
69         * @since NMEA 2.3
70         */
71        MODE(12);
72
73        final int position;
74
75        RMC(int position) {
76            this.position = position;
77        }
78    }
79
80    enum GGA {
81        TIME(1), LATITUDE(2), LATITUDE_NAME(3), LONGITUDE(4), LONGITUDE_NAME(5),
82        /**
83         * Quality (0 = invalid, 1 = GPS, 2 = DGPS, 6 = estimanted (@since NMEA 2.3))
84         */
85        QUALITY(6), SATELLITE_COUNT(7),
86        HDOP(8), // HDOP (horizontal dilution of precision)
87        HEIGHT(9), HEIGHT_UNTIS(10), // height above NN (above geoid)
88        HEIGHT_2(11), HEIGHT_2_UNTIS(12), // height geoid - height ellipsoid (WGS84)
89        GPS_AGE(13), // Age of differential GPS data
90        REF(14); // REF station
91
92        final int position;
93        GGA(int position) {
94            this.position = position;
95        }
96    }
97
98    enum GSA {
99        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}
Note: See TracBrowser for help on using the repository browser.