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

Last change on this file since 10088 was 10001, checked in by Don-vip, 8 years ago

sonar - Local variable and method parameter names should comply with a naming convention

  • Property svn:eol-style set to native
File size: 17.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
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;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.data.coor.LatLon;
18import org.openstreetmap.josm.data.gpx.GpxConstants;
19import org.openstreetmap.josm.data.gpx.GpxData;
20import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
21import org.openstreetmap.josm.data.gpx.WayPoint;
22
23/**
24 * Reads a NMEA file. Based on information from
25 * <a href="http://www.kowoma.de/gps/zusatzerklaerungen/NMEA.htm">http://www.kowoma.de</a>
26 *
27 * @author cbrill
28 */
29public class NmeaReader {
30
31 /** Handler for the different types that NMEA speaks. */
32 public enum NMEA_TYPE {
33
34 /** RMC = recommended minimum sentence C. */
35 GPRMC("$GPRMC"),
36 /** GPS positions. */
37 GPGGA("$GPGGA"),
38 /** SA = satellites active. */
39 GPGSA("$GPGSA"),
40 /** Course over ground and ground speed */
41 GPVTG("$GPVTG");
42
43 private final String type;
44
45 NMEA_TYPE(String type) {
46 this.type = type;
47 }
48
49 public String getType() {
50 return this.type;
51 }
52 }
53
54 // GPVTG
55 public enum GPVTG {
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 public final int position;
63
64 GPVTG(int position) {
65 this.position = position;
66 }
67 }
68
69 // The following only applies to GPRMC
70 public enum GPRMC {
71 TIME(1),
72 /** Warning from the receiver (A = data ok, V = warning) */
73 RECEIVER_WARNING(2),
74 WIDTH_NORTH(3), WIDTH_NORTH_NAME(4), // Latitude, NS
75 LENGTH_EAST(5), LENGTH_EAST_NAME(6), // Longitude, EW
76 SPEED(7), COURSE(8), DATE(9), // Speed in knots
77 MAGNETIC_DECLINATION(10), UNKNOWN(11), // magnetic declination
78 /**
79 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S
80 * = simulated)
81 *
82 * @since NMEA 2.3
83 */
84 MODE(12);
85
86 public final int position;
87
88 GPRMC(int position) {
89 this.position = position;
90 }
91 }
92
93 // The following only applies to GPGGA
94 public enum GPGGA {
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
98 * 2.3))
99 */
100 QUALITY(6), SATELLITE_COUNT(7),
101 HDOP(8), // HDOP (horizontal dilution of precision)
102 HEIGHT(9), HEIGHT_UNTIS(10), // height above NN (above geoid)
103 HEIGHT_2(11), HEIGHT_2_UNTIS(12), // height geoid - height ellipsoid (WGS84)
104 GPS_AGE(13), // Age of differential GPS data
105 REF(14); // REF station
106
107 public final int position;
108 GPGGA(int position) {
109 this.position = position;
110 }
111 }
112
113 public enum GPGSA {
114 AUTOMATIC(1),
115 FIX_TYPE(2), // 1 = not fixed, 2 = 2D fixed, 3 = 3D fixed)
116 // PRN numbers for max 12 satellites
117 PRN_1(3), PRN_2(4), PRN_3(5), PRN_4(6), PRN_5(7), PRN_6(8),
118 PRN_7(9), PRN_8(10), PRN_9(11), PRN_10(12), PRN_11(13), PRN_12(14),
119 PDOP(15), // PDOP (precision)
120 HDOP(16), // HDOP (horizontal precision)
121 VDOP(17); // VDOP (vertical precision)
122
123 public final int position;
124 GPGSA(int position) {
125 this.position = position;
126 }
127 }
128
129 public GpxData data;
130
131 private final SimpleDateFormat rmcTimeFmt = new SimpleDateFormat("ddMMyyHHmmss.SSS");
132 private final SimpleDateFormat rmcTimeFmtStd = new SimpleDateFormat("ddMMyyHHmmss");
133
134 private Date readTime(String p) {
135 Date d = rmcTimeFmt.parse(p, new ParsePosition(0));
136 if (d == null) {
137 d = rmcTimeFmtStd.parse(p, new ParsePosition(0));
138 }
139 if (d == null)
140 throw new RuntimeException("Date is malformed"); // malformed
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 public NmeaReader(InputStream source) throws IOException {
168
169 // create the data tree
170 data = new GpxData();
171 Collection<Collection<WayPoint>> currentTrack = new ArrayList<>();
172
173 try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) {
174 StringBuilder sb = new StringBuilder(1024);
175 int loopstartChar = rd.read();
176 ps = new NMEAParserState();
177 if (loopstartChar == -1)
178 //TODO tell user about the problem?
179 return;
180 sb.append((char) loopstartChar);
181 ps.pDate = "010100"; // TODO date problem
182 while (true) {
183 // don't load unparsable files completely to memory
184 if (sb.length() >= 1020) {
185 sb.delete(0, sb.length()-1);
186 }
187 int c = rd.read();
188 if (c == '$') {
189 parseNMEASentence(sb.toString(), ps);
190 sb.delete(0, sb.length());
191 sb.append('$');
192 } else if (c == -1) {
193 // EOF: add last WayPoint if it works out
194 parseNMEASentence(sb.toString(), ps);
195 break;
196 } else {
197 sb.append((char) c);
198 }
199 }
200 currentTrack.add(ps.waypoints);
201 data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
202
203 } catch (IllegalDataException e) {
204 Main.warn(e);
205 }
206 }
207
208 private static class NMEAParserState {
209 protected Collection<WayPoint> waypoints = new ArrayList<>();
210 protected String pTime;
211 protected String pDate;
212 protected WayPoint pWp;
213
214 protected int success; // number of successfully parsed sentences
215 protected int malformed;
216 protected int checksumErrors;
217 protected int noChecksum;
218 protected int unknown;
219 protected int zeroCoord;
220 }
221
222 // Parses split up sentences into WayPoints which are stored
223 // in the collection in the NMEAParserState object.
224 // Returns true if the input made sence, false otherwise.
225 private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException {
226 try {
227 if (s.isEmpty()) {
228 throw new IllegalArgumentException("s is empty");
229 }
230
231 // checksum check:
232 // the bytes between the $ and the * are xored
233 // if there is no * or other meanities it will throw
234 // and result in a malformed packet.
235 String[] chkstrings = s.split("\\*");
236 if (chkstrings.length > 1) {
237 byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8);
238 int chk = 0;
239 for (int i = 1; i < chb.length; i++) {
240 chk ^= chb[i];
241 }
242 if (Integer.parseInt(chkstrings[1].substring(0, 2), 16) != chk) {
243 ps.checksumErrors++;
244 ps.pWp = null;
245 return false;
246 }
247 } else {
248 ps.noChecksum++;
249 }
250 // now for the content
251 String[] e = chkstrings[0].split(",");
252 String accu;
253
254 WayPoint currentwp = ps.pWp;
255 String currentDate = ps.pDate;
256
257 // handle the packet content
258 if ("$GPGGA".equals(e[0]) || "$GNGGA".equals(e[0])) {
259 // Position
260 LatLon latLon = parseLatLon(
261 e[GPGGA.LATITUDE_NAME.position],
262 e[GPGGA.LONGITUDE_NAME.position],
263 e[GPGGA.LATITUDE.position],
264 e[GPGGA.LONGITUDE.position]
265 );
266 if (latLon == null) {
267 throw new IllegalDataException("Malformed lat/lon");
268 }
269
270 if (LatLon.ZERO.equals(latLon)) {
271 ps.zeroCoord++;
272 return false;
273 }
274
275 // time
276 accu = e[GPGGA.TIME.position];
277 Date d = readTime(currentDate+accu);
278
279 if ((ps.pTime == null) || (currentwp == null) || !ps.pTime.equals(accu)) {
280 // this node is newer than the previous, create a new waypoint.
281 // no matter if previous WayPoint was null, we got something better now.
282 ps.pTime = accu;
283 currentwp = new WayPoint(latLon);
284 }
285 if (!currentwp.attr.containsKey("time")) {
286 // As this sentence has no complete time only use it
287 // if there is no time so far
288 currentwp.setTime(d);
289 }
290 // elevation
291 accu = e[GPGGA.HEIGHT_UNTIS.position];
292 if ("M".equals(accu)) {
293 // Ignore heights that are not in meters for now
294 accu = e[GPGGA.HEIGHT.position];
295 if (!accu.isEmpty()) {
296 Double.parseDouble(accu);
297 // if it throws it's malformed; this should only happen if the
298 // device sends nonstandard data.
299 if (!accu.isEmpty()) { // FIX ? same check
300 currentwp.put(GpxConstants.PT_ELE, accu);
301 }
302 }
303 }
304 // number of sattelites
305 accu = e[GPGGA.SATELLITE_COUNT.position];
306 int sat = 0;
307 if (!accu.isEmpty()) {
308 sat = Integer.parseInt(accu);
309 currentwp.put(GpxConstants.PT_SAT, accu);
310 }
311 // h-dilution
312 accu = e[GPGGA.HDOP.position];
313 if (!accu.isEmpty()) {
314 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
315 }
316 // fix
317 accu = e[GPGGA.QUALITY.position];
318 if (!accu.isEmpty()) {
319 int fixtype = Integer.parseInt(accu);
320 switch(fixtype) {
321 case 0:
322 currentwp.put(GpxConstants.PT_FIX, "none");
323 break;
324 case 1:
325 if (sat < 4) {
326 currentwp.put(GpxConstants.PT_FIX, "2d");
327 } else {
328 currentwp.put(GpxConstants.PT_FIX, "3d");
329 }
330 break;
331 case 2:
332 currentwp.put(GpxConstants.PT_FIX, "dgps");
333 break;
334 default:
335 break;
336 }
337 }
338 } else if ("$GPVTG".equals(e[0]) || "$GNVTG".equals(e[0])) {
339 // COURSE
340 accu = e[GPVTG.COURSE_REF.position];
341 if ("T".equals(accu)) {
342 // other values than (T)rue are ignored
343 accu = e[GPVTG.COURSE.position];
344 if (!accu.isEmpty()) {
345 Double.parseDouble(accu);
346 currentwp.put("course", accu);
347 }
348 }
349 // SPEED
350 accu = e[GPVTG.SPEED_KMH_UNIT.position];
351 if (accu.startsWith("K")) {
352 accu = e[GPVTG.SPEED_KMH.position];
353 if (!accu.isEmpty()) {
354 double speed = Double.parseDouble(accu);
355 speed /= 3.6; // speed in m/s
356 currentwp.put("speed", Double.toString(speed));
357 }
358 }
359 } else if ("$GPGSA".equals(e[0]) || "$GNGSA".equals(e[0])) {
360 // vdop
361 accu = e[GPGSA.VDOP.position];
362 if (!accu.isEmpty()) {
363 currentwp.put(GpxConstants.PT_VDOP, Float.valueOf(accu));
364 }
365 // hdop
366 accu = e[GPGSA.HDOP.position];
367 if (!accu.isEmpty()) {
368 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));
369 }
370 // pdop
371 accu = e[GPGSA.PDOP.position];
372 if (!accu.isEmpty()) {
373 currentwp.put(GpxConstants.PT_PDOP, Float.valueOf(accu));
374 }
375 } else if ("$GPRMC".equals(e[0]) || "$GNRMC".equals(e[0])) {
376 // coordinates
377 LatLon latLon = parseLatLon(
378 e[GPRMC.WIDTH_NORTH_NAME.position],
379 e[GPRMC.LENGTH_EAST_NAME.position],
380 e[GPRMC.WIDTH_NORTH.position],
381 e[GPRMC.LENGTH_EAST.position]
382 );
383 if (LatLon.ZERO.equals(latLon)) {
384 ps.zeroCoord++;
385 return false;
386 }
387 // time
388 currentDate = e[GPRMC.DATE.position];
389 String time = e[GPRMC.TIME.position];
390
391 Date d = readTime(currentDate+time);
392
393 if (ps.pTime == null || currentwp == null || !ps.pTime.equals(time)) {
394 // this node is newer than the previous, create a new waypoint.
395 ps.pTime = time;
396 currentwp = new WayPoint(latLon);
397 }
398 // time: this sentence has complete time so always use it.
399 currentwp.setTime(d);
400 // speed
401 accu = e[GPRMC.SPEED.position];
402 if (!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {
403 double speed = Double.parseDouble(accu);
404 speed *= 0.514444444; // to m/s
405 currentwp.put("speed", Double.toString(speed));
406 }
407 // course
408 accu = e[GPRMC.COURSE.position];
409 if (!accu.isEmpty() && !currentwp.attr.containsKey("course")) {
410 Double.parseDouble(accu);
411 currentwp.put("course", accu);
412 }
413
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 }
425 ps.pDate = currentDate;
426 if (ps.pWp != currentwp) {
427 if (ps.pWp != null) {
428 ps.pWp.setTime();
429 }
430 ps.pWp = currentwp;
431 ps.waypoints.add(currentwp);
432 ps.success++;
433 return true;
434 }
435 return true;
436
437 } catch (RuntimeException x) {
438 // out of bounds and such
439 ps.malformed++;
440 ps.pWp = null;
441 return false;
442 }
443 }
444
445 private static LatLon parseLatLon(String ns, String ew, String dlat, String dlon)
446 throws NumberFormatException {
447 String widthNorth = dlat.trim();
448 String lengthEast = dlon.trim();
449
450 // return a zero latlon instead of null so it is logged as zero coordinate
451 // instead of malformed sentence
452 if (widthNorth.isEmpty() && lengthEast.isEmpty()) return LatLon.ZERO;
453
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;
460
461 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));
462 double latmin = Double.parseDouble(widthNorth.substring(latdegsep));
463 if (latdeg < 0) {
464 latmin *= -1.0;
465 }
466 double lat = latdeg + latmin / 60;
467 if ("S".equals(ns)) {
468 lat = -lat;
469 }
470
471 int londegsep = lengthEast.indexOf('.') - 2;
472 if (londegsep < 0) return null;
473
474 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));
475 double lonmin = Double.parseDouble(lengthEast.substring(londegsep));
476 if (londeg < 0) {
477 lonmin *= -1.0;
478 }
479 double lon = londeg + lonmin / 60;
480 if ("W".equals(ew)) {
481 lon = -lon;
482 }
483 return new LatLon(lat, lon);
484 }
485}
Note: See TracBrowser for help on using the repository browser.