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

Last change on this file since 12422 was 12422, checked in by Don-vip, 7 years ago

fix #14924 - fix NPE when reading NMEA files starting with a (0,0) coordinate followed by a VTG sentence

  • Property svn:eol-style set to native
File size: 17.8 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[12421]2package org.openstreetmap.josm.io.nmea;
[738]3
4import java.io.BufferedReader;
[7596]5import java.io.IOException;
[738]6import java.io.InputStream;
7import java.io.InputStreamReader;
[7082]8import java.nio.charset.StandardCharsets;
[1167]9import java.text.ParsePosition;
10import java.text.SimpleDateFormat;
[738]11import java.util.ArrayList;
12import java.util.Collection;
[2907]13import java.util.Collections;
[1167]14import java.util.Date;
[11553]15import java.util.Optional;
[738]16
[6287]17import org.openstreetmap.josm.Main;
[738]18import org.openstreetmap.josm.data.coor.LatLon;
[7518]19import org.openstreetmap.josm.data.gpx.GpxConstants;
[738]20import org.openstreetmap.josm.data.gpx.GpxData;
[2907]21import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
[738]22import org.openstreetmap.josm.data.gpx.WayPoint;
[12421]23import org.openstreetmap.josm.io.IllegalDataException;
[11374]24import org.openstreetmap.josm.tools.JosmRuntimeException;
[10475]25import org.openstreetmap.josm.tools.date.DateUtils;
[738]26
27/**
[12421]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>.
[1167]30 *
[12421]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 *
[738]40 * @author cbrill
41 */
42public class NmeaReader {
43
[12421]44 public enum VTG {
[8510]45 COURSE(1), COURSE_REF(2), // true course
[1169]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
[738]50
[1169]51 public final int position;
[1167]52
[12421]53 VTG(int position) {
[1169]54 this.position = position;
55 }
56 }
[1167]57
[12421]58 public enum RMC {
[1169]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 /**
[12421]67 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
[1169]68 *
69 * @since NMEA 2.3
70 */
71 MODE(12);
[738]72
[1169]73 public final int position;
[738]74
[12421]75 RMC(int position) {
[1169]76 this.position = position;
77 }
78 }
[738]79
[12421]80 public enum GGA {
[1169]81 TIME(1), LATITUDE(2), LATITUDE_NAME(3), LONGITUDE(4), LONGITUDE_NAME(5),
82 /**
[12421]83 * Quality (0 = invalid, 1 = GPS, 2 = DGPS, 6 = estimanted (@since NMEA 2.3))
[1169]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)
[8510]89 GPS_AGE(13), // Age of differential GPS data
[1169]90 REF(14); // REF station
[738]91
[1169]92 public final int position;
[12421]93 GGA(int position) {
[1169]94 this.position = position;
95 }
96 }
[738]97
[12421]98 public enum GSA {
[1169]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)
[8449]106 VDOP(17); // VDOP (vertical precision)
[738]107
[1169]108 public final int position;
[12421]109 GSA(int position) {
[1169]110 this.position = position;
111 }
112 }
[738]113
[1169]114 public GpxData data;
[738]115
[7049]116 private final SimpleDateFormat rmcTimeFmt = new SimpleDateFormat("ddMMyyHHmmss.SSS");
117 private final SimpleDateFormat rmcTimeFmtStd = new SimpleDateFormat("ddMMyyHHmmss");
[1167]118
[6889]119 private Date readTime(String p) {
[11553]120 Date d = Optional.ofNullable(rmcTimeFmt.parse(p, new ParsePosition(0)))
121 .orElseGet(() -> rmcTimeFmtStd.parse(p, new ParsePosition(0)));
[1381]122 if (d == null)
[11374]123 throw new JosmRuntimeException("Date is malformed");
[1381]124 return d;
125 }
126
[1169]127 // functons for reading the error stats
128 public NMEAParserState ps;
[1167]129
[1169]130 public int getParserUnknown() {
131 return ps.unknown;
132 }
[8510]133
[1169]134 public int getParserZeroCoordinates() {
[8346]135 return ps.zeroCoord;
[1169]136 }
[8510]137
[1169]138 public int getParserChecksumErrors() {
[8346]139 return ps.checksumErrors+ps.noChecksum;
[1169]140 }
[8510]141
[1169]142 public int getParserMalformed() {
143 return ps.malformed;
144 }
[8510]145
[1169]146 public int getNumberOfCoordinates() {
147 return ps.success;
148 }
[1167]149
[7596]150 public NmeaReader(InputStream source) throws IOException {
[10475]151 rmcTimeFmt.setTimeZone(DateUtils.UTC);
152 rmcTimeFmtStd.setTimeZone(DateUtils.UTC);
[1167]153
[1169]154 // create the data tree
155 data = new GpxData();
[7005]156 Collection<Collection<WayPoint>> currentTrack = new ArrayList<>();
[738]157
[7082]158 try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) {
[6822]159 StringBuilder sb = new StringBuilder(1024);
[10001]160 int loopstartChar = rd.read();
[1453]161 ps = new NMEAParserState();
[10001]162 if (loopstartChar == -1)
[1169]163 //TODO tell user about the problem?
164 return;
[10001]165 sb.append((char) loopstartChar);
[8510]166 ps.pDate = "010100"; // TODO date problem
167 while (true) {
[1169]168 // don't load unparsable files completely to memory
[8510]169 if (sb.length() >= 1020) {
[2626]170 sb.delete(0, sb.length()-1);
171 }
[1169]172 int c = rd.read();
[8510]173 if (c == '$') {
[6287]174 parseNMEASentence(sb.toString(), ps);
[1169]175 sb.delete(0, sb.length());
176 sb.append('$');
[8510]177 } else if (c == -1) {
[1169]178 // EOF: add last WayPoint if it works out
[8510]179 parseNMEASentence(sb.toString(), ps);
[1169]180 break;
[2626]181 } else {
[8510]182 sb.append((char) c);
[2626]183 }
[1169]184 }
[2907]185 currentTrack.add(ps.waypoints);
186 data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
[1167]187
[7596]188 } catch (IllegalDataException e) {
[6287]189 Main.warn(e);
[1169]190 }
191 }
[7037]192
[2626]193 private static class NMEAParserState {
[7005]194 protected Collection<WayPoint> waypoints = new ArrayList<>();
[8346]195 protected String pTime;
196 protected String pDate;
197 protected WayPoint pWp;
[738]198
[8840]199 protected int success; // number of successfully parsed sentences
200 protected int malformed;
201 protected int checksumErrors;
202 protected int noChecksum;
203 protected int unknown;
204 protected int zeroCoord;
[1169]205 }
[738]206
[12421]207 /**
208 * Determines if the given address denotes the given NMEA sentence formatter of a known talker.
209 * @param address first tag of an NMEA sentence
210 * @param formatter sentence formatter mnemonic code
211 * @return {@code true} if the {@code address} denotes the given NMEA sentence formatter of a known talker
212 */
213 static boolean isSentence(String address, Sentence formatter) {
214 for (TalkerId talker : TalkerId.values()) {
215 if (address.equals('$' + talker.name() + formatter.name())) {
216 return true;
217 }
218 }
219 return false;
220 }
221
[1169]222 // Parses split up sentences into WayPoints which are stored
223 // in the collection in the NMEAParserState object.
[12421]224 // Returns true if the input made sense, false otherwise.
[6287]225 private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException {
[1169]226 try {
[6287]227 if (s.isEmpty()) {
228 throw new IllegalArgumentException("s is empty");
229 }
[1167]230
[1169]231 // checksum check:
[6296]232 // the bytes between the $ and the * are xored
[1169]233 // if there is no * or other meanities it will throw
234 // and result in a malformed packet.
235 String[] chkstrings = s.split("\\*");
[8395]236 if (chkstrings.length > 1) {
[7082]237 byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8);
[8510]238 int chk = 0;
[6248]239 for (int i = 1; i < chb.length; i++) {
[2626]240 chk ^= chb[i];
241 }
[8510]242 if (Integer.parseInt(chkstrings[1].substring(0, 2), 16) != chk) {
[8346]243 ps.checksumErrors++;
[8510]244 ps.pWp = null;
[1388]245 return false;
246 }
[2626]247 } else {
[8346]248 ps.noChecksum++;
[1169]249 }
250 // now for the content
251 String[] e = chkstrings[0].split(",");
252 String accu;
[1167]253
[8346]254 WayPoint currentwp = ps.pWp;
255 String currentDate = ps.pDate;
[1167]256
[1169]257 // handle the packet content
[12421]258 if (isSentence(e[0], Sentence.GGA)) {
[1169]259 // Position
260 LatLon latLon = parseLatLon(
[12421]261 e[GGA.LATITUDE_NAME.position],
262 e[GGA.LONGITUDE_NAME.position],
263 e[GGA.LATITUDE.position],
264 e[GGA.LONGITUDE.position]
[2626]265 );
[8510]266 if (latLon == null) {
[6287]267 throw new IllegalDataException("Malformed lat/lon");
268 }
[1167]269
[8384]270 if (LatLon.ZERO.equals(latLon)) {
[8346]271 ps.zeroCoord++;
[1169]272 return false;
273 }
[1167]274
[1169]275 // time
[12421]276 accu = e[GGA.TIME.position];
[1381]277 Date d = readTime(currentDate+accu);
[1167]278
[8510]279 if ((ps.pTime == null) || (currentwp == null) || !ps.pTime.equals(accu)) {
[1169]280 // this node is newer than the previous, create a new waypoint.
[8510]281 // no matter if previous WayPoint was null, we got something better now.
282 ps.pTime = accu;
[1169]283 currentwp = new WayPoint(latLon);
284 }
[8510]285 if (!currentwp.attr.containsKey("time")) {
[1169]286 // As this sentence has no complete time only use it
287 // if there is no time so far
[9740]288 currentwp.setTime(d);
[1169]289 }
290 // elevation
[12421]291 accu = e[GGA.HEIGHT_UNTIS.position];
[8510]292 if ("M".equals(accu)) {
[2626]293 // Ignore heights that are not in meters for now
[12421]294 accu = e[GGA.HEIGHT.position];
[8510]295 if (!accu.isEmpty()) {
[1169]296 Double.parseDouble(accu);
297 // if it throws it's malformed; this should only happen if the
298 // device sends nonstandard data.
[8510]299 if (!accu.isEmpty()) { // FIX ? same check
[7518]300 currentwp.put(GpxConstants.PT_ELE, accu);
[2626]301 }
[1169]302 }
303 }
[12421]304 // number of satellites
305 accu = e[GGA.SATELLITE_COUNT.position];
[1169]306 int sat = 0;
[8510]307 if (!accu.isEmpty()) {
[1169]308 sat = Integer.parseInt(accu);
[7518]309 currentwp.put(GpxConstants.PT_SAT, accu);
[1169]310 }
311 // h-dilution
[12421]312 accu = e[GGA.HDOP.position];
[8510]313 if (!accu.isEmpty()) {
[8390]314 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
[2626]315 }
[1169]316 // fix
[12421]317 accu = e[GGA.QUALITY.position];
[8510]318 if (!accu.isEmpty()) {
[1169]319 int fixtype = Integer.parseInt(accu);
320 switch(fixtype) {
321 case 0:
[7518]322 currentwp.put(GpxConstants.PT_FIX, "none");
[1169]323 break;
324 case 1:
[8510]325 if (sat < 4) {
[7518]326 currentwp.put(GpxConstants.PT_FIX, "2d");
[2626]327 } else {
[7518]328 currentwp.put(GpxConstants.PT_FIX, "3d");
[2626]329 }
[1169]330 break;
331 case 2:
[7518]332 currentwp.put(GpxConstants.PT_FIX, "dgps");
[1169]333 break;
334 default:
335 break;
336 }
337 }
[12421]338 } else if (isSentence(e[0], Sentence.VTG)) {
[1169]339 // COURSE
[12421]340 accu = e[VTG.COURSE_REF.position];
[8510]341 if ("T".equals(accu)) {
[1169]342 // other values than (T)rue are ignored
[12421]343 accu = e[VTG.COURSE.position];
[12422]344 if (!accu.isEmpty() && currentwp != null) {
[1169]345 Double.parseDouble(accu);
[7518]346 currentwp.put("course", accu);
[1169]347 }
348 }
349 // SPEED
[12421]350 accu = e[VTG.SPEED_KMH_UNIT.position];
[8510]351 if (accu.startsWith("K")) {
[12421]352 accu = e[VTG.SPEED_KMH.position];
[12422]353 if (!accu.isEmpty() && currentwp != null) {
[1169]354 double speed = Double.parseDouble(accu);
355 speed /= 3.6; // speed in m/s
[7518]356 currentwp.put("speed", Double.toString(speed));
[1169]357 }
358 }
[12421]359 } else if (isSentence(e[0], Sentence.GSA)) {
[1169]360 // vdop
[12421]361 accu = e[GSA.VDOP.position];
[12059]362 if (!accu.isEmpty() && currentwp != null) {
[8390]363 currentwp.put(GpxConstants.PT_VDOP, Float.valueOf(accu));
[2626]364 }
[1169]365 // hdop
[12421]366 accu = e[GSA.HDOP.position];
[12059]367 if (!accu.isEmpty() && currentwp != null) {
[8390]368 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
[2626]369 }
[1169]370 // pdop
[12421]371 accu = e[GSA.PDOP.position];
[12059]372 if (!accu.isEmpty() && currentwp != null) {
[8390]373 currentwp.put(GpxConstants.PT_PDOP, Float.valueOf(accu));
[2626]374 }
[12421]375 } else if (isSentence(e[0], Sentence.RMC)) {
[1169]376 // coordinates
377 LatLon latLon = parseLatLon(
[12421]378 e[RMC.WIDTH_NORTH_NAME.position],
379 e[RMC.LENGTH_EAST_NAME.position],
380 e[RMC.WIDTH_NORTH.position],
381 e[RMC.LENGTH_EAST.position]
[2626]382 );
[8384]383 if (LatLon.ZERO.equals(latLon)) {
[8346]384 ps.zeroCoord++;
[1169]385 return false;
386 }
387 // time
[12421]388 currentDate = e[RMC.DATE.position];
389 String time = e[RMC.TIME.position];
[1167]390
[1381]391 Date d = readTime(currentDate+time);
[1167]392
[8510]393 if (ps.pTime == null || currentwp == null || !ps.pTime.equals(time)) {
[1169]394 // this node is newer than the previous, create a new waypoint.
[8510]395 ps.pTime = time;
[1169]396 currentwp = new WayPoint(latLon);
397 }
398 // time: this sentence has complete time so always use it.
[9740]399 currentwp.setTime(d);
[1169]400 // speed
[12421]401 accu = e[RMC.SPEED.position];
[8510]402 if (!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {
[1169]403 double speed = Double.parseDouble(accu);
404 speed *= 0.514444444; // to m/s
[7518]405 currentwp.put("speed", Double.toString(speed));
[1169]406 }
407 // course
[12421]408 accu = e[RMC.COURSE.position];
[8510]409 if (!accu.isEmpty() && !currentwp.attr.containsKey("course")) {
[1169]410 Double.parseDouble(accu);
[7518]411 currentwp.put("course", accu);
[1169]412 }
[1167]413
[1169]414 // TODO fix?
415 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S
416 // * = simulated)
417 // *
418 // * @since NMEA 2.3
419 //
420 //MODE(12);
421 } else {
422 ps.unknown++;
423 return false;
424 }
[8346]425 ps.pDate = currentDate;
[8510]426 if (ps.pWp != currentwp) {
427 if (ps.pWp != null) {
[8346]428 ps.pWp.setTime();
[1169]429 }
[8346]430 ps.pWp = currentwp;
[1169]431 ps.waypoints.add(currentwp);
432 ps.success++;
433 return true;
434 }
435 return true;
[1167]436
[11768]437 } catch (IllegalArgumentException | IndexOutOfBoundsException | IllegalDataException ex) {
[1169]438 // out of bounds and such
[11620]439 Main.debug(ex);
[1169]440 ps.malformed++;
[8510]441 ps.pWp = null;
[1169]442 return false;
443 }
444 }
[738]445
[10632]446 private static LatLon parseLatLon(String ns, String ew, String dlat, String dlon) {
[1169]447 String widthNorth = dlat.trim();
448 String lengthEast = dlon.trim();
[1167]449
[1169]450 // return a zero latlon instead of null so it is logged as zero coordinate
451 // instead of malformed sentence
[9214]452 if (widthNorth.isEmpty() && lengthEast.isEmpty()) return LatLon.ZERO;
[1167]453
[1169]454 // The format is xxDDLL.LLLL
455 // xx optional whitespace
456 // DD (int) degres
457 // LL.LLLL (double) latidude
458 int latdegsep = widthNorth.indexOf('.') - 2;
459 if (latdegsep < 0) return null;
[1167]460
[1169]461 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));
462 double latmin = Double.parseDouble(widthNorth.substring(latdegsep));
[8510]463 if (latdeg < 0) {
[1388]464 latmin *= -1.0;
[2626]465 }
[1169]466 double lat = latdeg + latmin / 60;
467 if ("S".equals(ns)) {
468 lat = -lat;
469 }
[738]470
[1169]471 int londegsep = lengthEast.indexOf('.') - 2;
472 if (londegsep < 0) return null;
[1167]473
[1169]474 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));
475 double lonmin = Double.parseDouble(lengthEast.substring(londegsep));
[8510]476 if (londeg < 0) {
[1388]477 lonmin *= -1.0;
[2626]478 }
[1169]479 double lon = londeg + lonmin / 60;
480 if ("W".equals(ew)) {
481 lon = -lon;
482 }
483 return new LatLon(lat, lon);
484 }
[738]485}
Note: See TracBrowser for help on using the repository browser.