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

Last change on this file since 9185 was 8870, checked in by Don-vip, 9 years ago

sonar - squid:S2325 - "private" methods that don't access instance data should be "static"

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