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

Last change on this file since 18742 was 18742, checked in by stoecker, 16 months ago

ckeckstyle

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