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

Last change on this file since 12668 was 12620, checked in by Don-vip, 7 years 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.