source: josm/trunk/src/org/openstreetmap/josm/io/nmea/NmeaParser.java@ 18787

Last change on this file since 18787 was 18787, checked in by stoecker, 10 months ago

separate NmeaParser from NmeaReader

File size: 22.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.nmea;
3
4import java.nio.charset.StandardCharsets;
5import java.text.ParsePosition;
6import java.text.SimpleDateFormat;
7import java.time.Instant;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.Date;
13import java.util.Locale;
14import java.util.regex.Matcher;
15import java.util.regex.Pattern;
16
17import org.openstreetmap.josm.data.coor.LatLon;
18import org.openstreetmap.josm.data.gpx.GpxConstants;
19import org.openstreetmap.josm.data.gpx.WayPoint;
20import org.openstreetmap.josm.io.IllegalDataException;
21import org.openstreetmap.josm.tools.Logging;
22import org.openstreetmap.josm.tools.date.DateUtils;
23
24/**
25 * Parses NMEA 0183 data. Based on information from
26 * <a href="http://www.catb.org/gpsd/NMEA.html">http://www.catb.org/gpsd</a>.
27 *
28 * NMEA data is in printable ASCII form and may include information such as position,
29 * speed, depth, frequency allocation, etc.
30 * Typical messages might be 11 to a maximum of 79 characters in length.
31 *
32 * NMEA standard aims to support one-way serial data transmission from a single "talker"
33 * to one or more "listeners". The type of talker is identified by a 2-character mnemonic.
34 *
35 * NMEA information is encoded through a list of "sentences".
36 * @since 18787
37 */
38public class NmeaParser {
39
40 /**
41 * Course Over Ground and Ground Speed.
42 * <p>
43 * The actual course and speed relative to the ground
44 */
45 enum VTG {
46 COURSE(1), COURSE_REF(2), // true course
47 COURSE_M(3), COURSE_M_REF(4), // magnetic course
48 SPEED_KN(5), SPEED_KN_UNIT(6), // speed in knots
49 SPEED_KMH(7), SPEED_KMH_UNIT(8), // speed in km/h
50 REST(9); // version-specific rest
51
52 final int position;
53
54 VTG(int position) {
55 this.position = position;
56 }
57 }
58
59 /**
60 * Recommended Minimum Specific GNSS Data.
61 * <p>
62 * Time, date, position, course and speed data provided by a GNSS navigation receiver.
63 * This sentence is transmitted at intervals not exceeding 2-seconds.
64 * RMC is the recommended minimum data to be provided by a GNSS receiver.
65 * All data fields must be provided, null fields used only when data is temporarily unavailable.
66 */
67 enum RMC {
68 TIME(1),
69 /** Warning from the receiver (A = data ok, V = warning) */
70 RECEIVER_WARNING(2),
71 WIDTH_NORTH(3), WIDTH_NORTH_NAME(4), // Latitude, NS
72 LENGTH_EAST(5), LENGTH_EAST_NAME(6), // Longitude, EW
73 SPEED(7), COURSE(8), DATE(9), // Speed in knots
74 MAGNETIC_DECLINATION(10), UNKNOWN(11), // magnetic declination
75 /**
76 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
77 *
78 * @since NMEA 2.3
79 */
80 MODE(12);
81
82 final int position;
83
84 RMC(int position) {
85 this.position = position;
86 }
87 }
88
89 /**
90 * Global Positioning System Fix Data.
91 * <p>
92 * Time, position and fix related data for a GPS receiver.
93 */
94 enum GGA {
95 TIME(1), LATITUDE(2), LATITUDE_NAME(3), LONGITUDE(4), LONGITUDE_NAME(5),
96 /**
97 * Quality (0 = invalid, 1 = GPS, 2 = DGPS, 6 = estimanted (@since NMEA 2.3))
98 */
99 QUALITY(6), SATELLITE_COUNT(7),
100 HDOP(8), // HDOP (horizontal dilution of precision)
101 HEIGHT(9), HEIGHT_UNTIS(10), // height above NN (above geoid)
102 HEIGHT_2(11), HEIGHT_2_UNTIS(12), // height geoid - height ellipsoid (WGS84)
103 GPS_AGE(13), // Age of differential GPS data
104 REF(14); // REF station
105
106 final int position;
107 GGA(int position) {
108 this.position = position;
109 }
110 }
111
112 /**
113 * GNSS DOP and Active Satellites.
114 * <p>
115 * GNSS receiver operating mode, satellites used in the navigation solution reported by the GGA or GNS sentence,
116 * and DOP values.
117 * If only GPS, GLONASS, etc. is used for the reported position solution the talker ID is GP, GL, etc.
118 * and the DOP values pertain to the individual system. If GPS, GLONASS, etc. are combined to obtain the
119 * reported position solution multiple GSA sentences are produced, one with the GPS satellites, another with
120 * the GLONASS satellites, etc. Each of these GSA sentences shall have talker ID GN, to indicate that the
121 * satellites are used in a combined solution and each shall have the PDOP, HDOP and VDOP for the
122 * combined satellites used in the position.
123 */
124 enum GSA {
125 AUTOMATIC(1),
126 FIX_TYPE(2), // 1 = not fixed, 2 = 2D fixed, 3 = 3D fixed)
127 // PRN numbers for max 12 satellites
128 PRN_1(3), PRN_2(4), PRN_3(5), PRN_4(6), PRN_5(7), PRN_6(8),
129 PRN_7(9), PRN_8(10), PRN_9(11), PRN_10(12), PRN_11(13), PRN_12(14),
130 PDOP(15), // PDOP (precision)
131 HDOP(16), // HDOP (horizontal precision)
132 VDOP(17); // VDOP (vertical precision)
133
134 final int position;
135 GSA(int position) {
136 this.position = position;
137 }
138 }
139
140 /**
141 * Geographic Position - Latitude/Longitude.
142 * <p>
143 * Latitude and Longitude of vessel position, time of position fix and status.
144 */
145 enum GLL {
146 LATITUDE(1), LATITUDE_NS(2), // Latitude, NS
147 LONGITUDE(3), LONGITUDE_EW(4), // Latitude, EW
148 UTC(5), // Universal Time Coordinated
149 STATUS(6), // Status: A = Data valid, V = Data not valid
150 /**
151 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
152 * @since NMEA 2.3
153 */
154 MODE(7);
155
156 final int position;
157 GLL(int position) {
158 this.position = position;
159 }
160 }
161
162 private static final Pattern DATE_TIME_PATTERN = Pattern.compile("(\\d{12})(\\.\\d+)?");
163
164 private final SimpleDateFormat rmcTimeFmt = new SimpleDateFormat("ddMMyyHHmmss.SSS", Locale.ENGLISH);
165
166 private Instant readTime(String p) throws IllegalDataException {
167 // NMEA defines time with "a variable number of digits for decimal-fraction of seconds"
168 // This variable decimal fraction cannot be parsed by SimpleDateFormat
169 Matcher m = DATE_TIME_PATTERN.matcher(p);
170 if (m.matches()) {
171 String date = m.group(1);
172 double milliseconds = 0d;
173 if (m.groupCount() > 1 && m.group(2) != null) {
174 milliseconds = 1000d * Double.parseDouble("0" + m.group(2));
175 }
176 // Add milliseconds on three digits to match SimpleDateFormat pattern
177 date += String.format(".%03d", (int) milliseconds);
178 Date d = rmcTimeFmt.parse(date, new ParsePosition(0));
179 if (d != null)
180 return d.toInstant();
181 }
182 throw new IllegalDataException("Date is malformed: '" + p + "'");
183 }
184
185 protected Collection<WayPoint> waypoints = new ArrayList<>();
186 protected String pTime;
187 protected String pDate;
188 /* Waypoint currently in work */
189 protected WayPoint pWp;
190 /* number of successfully parsed sentences */
191 protected int success;
192 protected int malformed;
193 protected int checksumErrors;
194 protected int noChecksum;
195 protected int unknown;
196 protected int zeroCoord;
197
198 /**
199 * Number of unknown sentences
200 * @return the number of unknown sentences encountered
201 */
202 public int getParserUnknown() {
203 return unknown;
204 }
205
206 /**
207 * Number of empty coordinates
208 * @return the number of coordinates which have been zero
209 */
210 public int getParserZeroCoordinates() {
211 return zeroCoord;
212 }
213
214 /**
215 * Number of checksum errors
216 * @return the number of sentences with checksum errors
217 */
218 public int getParserChecksumErrors() {
219 return checksumErrors+noChecksum;
220 }
221
222 /**
223 * Number of malformed errors
224 * @return the number of malformed sentences
225 */
226 public int getParserMalformed() {
227 return malformed;
228 }
229
230 /**
231 * Number of successful coordinates
232 * @return the number of successfully read coordinates
233 */
234 public int getSuccess() {
235 return success;
236 }
237
238 /**
239 * List of collected coordinates
240 * When parsing a stream the last entry may be still incomplete
241 * @return the collection of points collected
242 */
243 public Collection<WayPoint> getWaypoints() {
244 return Collections.unmodifiableCollection(waypoints);
245 }
246
247 /**
248 * Return list of collected coordinates and drop old data
249 * When parsing a stream the last entry may be still incomplete and usually
250 * will not be dropped
251 * @return the collection of points collected
252 */
253 public Collection<WayPoint> getAndDropWaypoints() {
254 Collection<WayPoint> r = getWaypoints();
255 dropOldWaypoints();
256 return r;
257 }
258
259 /**
260 * Get rid of older data no longer needed, drops everything except current
261 * coordinate
262 */
263 public void dropOldWaypoints() {
264 waypoints.clear();
265 if (pWp != null)
266 waypoints.add(pWp);
267 }
268
269 /**
270 * Constructs a new {@code NmeaParser}
271 */
272 public NmeaParser() {
273 rmcTimeFmt.setTimeZone(DateUtils.UTC);
274 pDate = "010100"; // TODO date problem
275 }
276
277 /**
278 * Determines if the given address denotes the given NMEA sentence formatter of a known talker.
279 * @param address first tag of an NMEA sentence
280 * @param formatter sentence formatter mnemonic code
281 * @return {@code true} if the {@code address} denotes the given NMEA sentence formatter of a known talker
282 */
283 static boolean isSentence(String address, Sentence formatter) {
284 return Arrays.stream(TalkerId.values())
285 .anyMatch(talker -> address.equals('$' + talker.name() + formatter.name()));
286 }
287
288 /**
289 * Parses split up sentences into WayPoints which are stored
290 * in the collection in the NMEAParserState object.
291 * @param s data to parse
292 * @return {@code true} if the input made sense, {@code false} otherwise.
293 */
294 public boolean parseNMEASentence(String s) throws IllegalDataException {
295 try {
296 if (s.isEmpty()) {
297 throw new IllegalArgumentException("s is empty");
298 }
299
300 // checksum check:
301 // the bytes between the $ and the * are xored
302 // if there is no * or other meanities it will throw
303 // and result in a malformed packet.
304 String[] chkstrings = s.split("\\*", -1);
305 if (chkstrings.length > 1) {
306 byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8);
307 int chk = 0;
308 for (int i = 1; i < chb.length; i++) {
309 chk ^= chb[i];
310 }
311 if (Integer.parseInt(chkstrings[1].substring(0, 2), 16) != chk) {
312 checksumErrors++;
313 pWp = null;
314 return false;
315 }
316 } else {
317 noChecksum++;
318 }
319 // now for the content
320 String[] e = chkstrings[0].split(",", -1);
321 String accu;
322
323 WayPoint currentwp = pWp;
324 String currentDate = pDate;
325
326 // handle the packet content
327 if (isSentence(e[0], Sentence.GGA)) {
328 // Position
329 LatLon latLon = parseLatLon(
330 e[GGA.LATITUDE_NAME.position],
331 e[GGA.LONGITUDE_NAME.position],
332 e[GGA.LATITUDE.position],
333 e[GGA.LONGITUDE.position]
334 );
335 if (latLon == null) {
336 throw new IllegalDataException("Malformed lat/lon");
337 }
338
339 if (LatLon.ZERO.equals(latLon)) {
340 zeroCoord++;
341 return false;
342 }
343
344 // time
345 accu = e[GGA.TIME.position];
346 Instant instant = readTime(currentDate+accu);
347
348 if ((pTime == null) || (currentwp == null) || !pTime.equals(accu)) {
349 // this node is newer than the previous, create a new waypoint.
350 // no matter if previous WayPoint was null, we got something better now.
351 pTime = accu;
352 currentwp = new WayPoint(latLon);
353 }
354 if (!currentwp.attr.containsKey("time")) {
355 // As this sentence has no complete time only use it
356 // if there is no time so far
357 currentwp.setInstant(instant);
358 }
359 // elevation
360 accu = e[GGA.HEIGHT_UNTIS.position];
361 if ("M".equals(accu)) {
362 // Ignore heights that are not in meters for now
363 accu = e[GGA.HEIGHT.position];
364 if (!accu.isEmpty()) {
365 Double.parseDouble(accu);
366 // if it throws it's malformed; this should only happen if the
367 // device sends nonstandard data.
368 if (!accu.isEmpty()) { // FIX ? same check
369 currentwp.put(GpxConstants.PT_ELE, accu);
370 }
371 }
372 }
373 // number of satellites
374 accu = e[GGA.SATELLITE_COUNT.position];
375 int sat = 0;
376 if (!accu.isEmpty()) {
377 sat = Integer.parseInt(accu);
378 currentwp.put(GpxConstants.PT_SAT, accu);
379 }
380 // h-dilution
381 accu = e[GGA.HDOP.position];
382 if (!accu.isEmpty()) {
383 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
384 }
385 // fix
386 accu = e[GGA.QUALITY.position];
387 if (!accu.isEmpty()) {
388 int fixtype = Integer.parseInt(accu);
389 switch(fixtype) {
390 case 0:
391 currentwp.put(GpxConstants.PT_FIX, "none");
392 break;
393 case 1:
394 if (sat < 4) {
395 currentwp.put(GpxConstants.PT_FIX, "2d");
396 } else {
397 currentwp.put(GpxConstants.PT_FIX, "3d");
398 }
399 break;
400 case 2:
401 currentwp.put(GpxConstants.PT_FIX, "dgps");
402 break;
403 case 3:
404 currentwp.put(GpxConstants.PT_FIX, "pps");
405 break;
406 case 4:
407 currentwp.put(GpxConstants.PT_FIX, "rtk");
408 break;
409 case 5:
410 currentwp.put(GpxConstants.PT_FIX, "float rtk");
411 break;
412 case 6:
413 currentwp.put(GpxConstants.PT_FIX, "estimated");
414 break;
415 case 7:
416 currentwp.put(GpxConstants.PT_FIX, "manual");
417 break;
418 case 8:
419 currentwp.put(GpxConstants.PT_FIX, "simulated");
420 break;
421 default:
422 break;
423 }
424 }
425 // reference ID
426 if (GGA.REF.position < e.length) {
427 accu = e[GGA.REF.position];
428 if (!accu.isEmpty()) {
429 currentwp.put(GpxConstants.PT_DGPSID, accu);
430 }
431 }
432 } else if (isSentence(e[0], Sentence.VTG)) {
433 // COURSE
434 accu = e[VTG.COURSE_REF.position];
435 if ("T".equals(accu)) {
436 // other values than (T)rue are ignored
437 accu = e[VTG.COURSE.position];
438 if (!accu.isEmpty() && currentwp != null) {
439 Double.parseDouble(accu);
440 currentwp.put("course", accu);
441 }
442 }
443 // SPEED
444 accu = e[VTG.SPEED_KMH_UNIT.position];
445 if (accu.startsWith("K")) {
446 accu = e[VTG.SPEED_KMH.position];
447 if (!accu.isEmpty() && currentwp != null) {
448 double speed = Double.parseDouble(accu);
449 currentwp.put("speed", Double.toString(speed)); // speed in km/h
450 }
451 }
452 } else if (isSentence(e[0], Sentence.GSA)) {
453 // vdop
454 accu = e[GSA.VDOP.position];
455 if (!accu.isEmpty() && currentwp != null) {
456 currentwp.put(GpxConstants.PT_VDOP, Float.valueOf(accu));
457 }
458 // hdop
459 accu = e[GSA.HDOP.position];
460 if (!accu.isEmpty() && currentwp != null) {
461 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
462 }
463 // pdop
464 accu = e[GSA.PDOP.position];
465 if (!accu.isEmpty() && currentwp != null) {
466 currentwp.put(GpxConstants.PT_PDOP, Float.valueOf(accu));
467 }
468 } else if (isSentence(e[0], Sentence.RMC)) {
469 // coordinates
470 LatLon latLon = parseLatLon(
471 e[RMC.WIDTH_NORTH_NAME.position],
472 e[RMC.LENGTH_EAST_NAME.position],
473 e[RMC.WIDTH_NORTH.position],
474 e[RMC.LENGTH_EAST.position]
475 );
476 if (LatLon.ZERO.equals(latLon)) {
477 zeroCoord++;
478 return false;
479 }
480 // time
481 currentDate = e[RMC.DATE.position];
482 String time = e[RMC.TIME.position];
483
484 Instant instant = readTime(currentDate+time);
485
486 if (pTime == null || currentwp == null || !pTime.equals(time)) {
487 // this node is newer than the previous, create a new waypoint.
488 pTime = time;
489 currentwp = new WayPoint(latLon);
490 }
491 // time: this sentence has complete time so always use it.
492 currentwp.setInstant(instant);
493 // speed
494 accu = e[RMC.SPEED.position];
495 if (!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {
496 double speed = Double.parseDouble(accu);
497 speed *= 0.514444444 * 3.6; // to km/h
498 currentwp.put("speed", Double.toString(speed));
499 }
500 // course
501 accu = e[RMC.COURSE.position];
502 if (!accu.isEmpty() && !currentwp.attr.containsKey("course")) {
503 Double.parseDouble(accu);
504 currentwp.put("course", accu);
505 }
506
507 // TODO fix?
508 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)
509 // *
510 // * @since NMEA 2.3
511 //
512 //MODE(12);
513 } else if (isSentence(e[0], Sentence.GLL)) {
514 // coordinates
515 LatLon latLon = parseLatLon(
516 e[GLL.LATITUDE_NS.position],
517 e[GLL.LONGITUDE_EW.position],
518 e[GLL.LATITUDE.position],
519 e[GLL.LONGITUDE.position]
520 );
521 if (LatLon.ZERO.equals(latLon)) {
522 zeroCoord++;
523 return false;
524 }
525 // only consider valid data
526 if (!"A".equals(e[GLL.STATUS.position])) {
527 return false;
528 }
529
530 // RMC sentences contain a full date while GLL sentences contain only time,
531 // so create new waypoints only of the NMEA file does not contain RMC sentences
532 if (pTime == null || currentwp == null) {
533 currentwp = new WayPoint(latLon);
534 }
535 } else {
536 unknown++;
537 return false;
538 }
539 pDate = currentDate;
540 if (pWp != currentwp) {
541 if (pWp != null) {
542 pWp.getInstant();
543 }
544 pWp = currentwp;
545 waypoints.add(currentwp);
546 success++;
547 return true;
548 }
549 return true;
550
551 } catch (IllegalArgumentException | IndexOutOfBoundsException | IllegalDataException ex) {
552 if (malformed < 5) {
553 Logging.warn(ex);
554 } else {
555 Logging.debug(ex);
556 }
557 malformed++;
558 pWp = null;
559 return false;
560 }
561 }
562
563 private static LatLon parseLatLon(String ns, String ew, String dlat, String dlon) {
564 String widthNorth = dlat.trim();
565 String lengthEast = dlon.trim();
566
567 // return a zero latlon instead of null so it is logged as zero coordinate
568 // instead of malformed sentence
569 if (widthNorth.isEmpty() && lengthEast.isEmpty()) return LatLon.ZERO;
570
571 // The format is xxDDLL.LLLL
572 // xx optional whitespace
573 // DD (int) degres
574 // LL.LLLL (double) latidude
575 int latdegsep = widthNorth.indexOf('.') - 2;
576 if (latdegsep < 0) return null;
577
578 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));
579 double latmin = Double.parseDouble(widthNorth.substring(latdegsep));
580 if (latdeg < 0) {
581 latmin *= -1.0;
582 }
583 double lat = latdeg + latmin / 60;
584 if ("S".equals(ns)) {
585 lat = -lat;
586 }
587
588 int londegsep = lengthEast.indexOf('.') - 2;
589 if (londegsep < 0) return null;
590
591 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));
592 double lonmin = Double.parseDouble(lengthEast.substring(londegsep));
593 if (londeg < 0) {
594 lonmin *= -1.0;
595 }
596 double lon = londeg + lonmin / 60;
597 if ("W".equals(ew)) {
598 lon = -lon;
599 }
600 return new LatLon(lat, lon);
601 }
602}
Note: See TracBrowser for help on using the repository browser.