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

Last change on this file since 8340 was 7596, checked in by Don-vip, 10 years ago

fix various warnings

  • Property svn:eol-style set to native
File size: 17.4 KB
Line 
1//License: GPL. See README for details.
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 static 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 static 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 static 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 static 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 static 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 public int getParserZeroCoordinates() {
156 return ps.zero_coord;
157 }
158 public int getParserChecksumErrors() {
159 return ps.checksum_errors+ps.no_checksum;
160 }
161 public int getParserMalformed() {
162 return ps.malformed;
163 }
164 public int getNumberOfCoordinates() {
165 return ps.success;
166 }
167
168 public NmeaReader(InputStream source) throws IOException {
169
170 // create the data tree
171 data = new GpxData();
172 Collection<Collection<WayPoint>> currentTrack = new ArrayList<>();
173
174 try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) {
175 StringBuilder sb = new StringBuilder(1024);
176 int loopstart_char = rd.read();
177 ps = new NMEAParserState();
178 if(loopstart_char == -1)
179 //TODO tell user about the problem?
180 return;
181 sb.append((char)loopstart_char);
182 ps.p_Date="010100"; // TODO date problem
183 while(true) {
184 // don't load unparsable files completely to memory
185 if(sb.length()>=1020) {
186 sb.delete(0, sb.length()-1);
187 }
188 int c = rd.read();
189 if(c=='$') {
190 parseNMEASentence(sb.toString(), ps);
191 sb.delete(0, sb.length());
192 sb.append('$');
193 } else if(c == -1) {
194 // EOF: add last WayPoint if it works out
195 parseNMEASentence(sb.toString(),ps);
196 break;
197 } else {
198 sb.append((char)c);
199 }
200 }
201 currentTrack.add(ps.waypoints);
202 data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
203
204 } catch (IllegalDataException e) {
205 Main.warn(e);
206 }
207 }
208
209 private static class NMEAParserState {
210 protected Collection<WayPoint> waypoints = new ArrayList<>();
211 protected String p_Time;
212 protected String p_Date;
213 protected WayPoint p_Wp;
214
215 protected int success = 0; // number of successfully parsend sentences
216 protected int malformed = 0;
217 protected int checksum_errors = 0;
218 protected int no_checksum = 0;
219 protected int unknown = 0;
220 protected int zero_coord = 0;
221 }
222
223 // Parses split up sentences into WayPoints which are stored
224 // in the collection in the NMEAParserState object.
225 // Returns true if the input made sence, false otherwise.
226 private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException {
227 try {
228 if (s.isEmpty()) {
229 throw new IllegalArgumentException("s is empty");
230 }
231
232 // checksum check:
233 // the bytes between the $ and the * are xored
234 // if there is no * or other meanities it will throw
235 // and result in a malformed packet.
236 String[] chkstrings = s.split("\\*");
237 if(chkstrings.length > 1)
238 {
239 byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8);
240 int chk=0;
241 for (int i = 1; i < chb.length; i++) {
242 chk ^= chb[i];
243 }
244 if (Integer.parseInt(chkstrings[1].substring(0,2),16) != chk) {
245 ps.checksum_errors++;
246 ps.p_Wp=null;
247 return false;
248 }
249 } else {
250 ps.no_checksum++;
251 }
252 // now for the content
253 String[] e = chkstrings[0].split(",");
254 String accu;
255
256 WayPoint currentwp = ps.p_Wp;
257 String currentDate = ps.p_Date;
258
259 // handle the packet content
260 if("$GPGGA".equals(e[0]) || "$GNGGA".equals(e[0])) {
261 // Position
262 LatLon latLon = parseLatLon(
263 e[GPGGA.LATITUDE_NAME.position],
264 e[GPGGA.LONGITUDE_NAME.position],
265 e[GPGGA.LATITUDE.position],
266 e[GPGGA.LONGITUDE.position]
267 );
268 if (latLon==null) {
269 throw new IllegalDataException("Malformed lat/lon");
270 }
271
272 if ((latLon.lat()==0.0) && (latLon.lon()==0.0)) {
273 ps.zero_coord++;
274 return false;
275 }
276
277 // time
278 accu = e[GPGGA.TIME.position];
279 Date d = readTime(currentDate+accu);
280
281 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(accu)) {
282 // this node is newer than the previous, create a new waypoint.
283 // no matter if previous WayPoint was null, we got something
284 // better now.
285 ps.p_Time=accu;
286 currentwp = new WayPoint(latLon);
287 }
288 if(!currentwp.attr.containsKey("time")) {
289 // As this sentence has no complete time only use it
290 // if there is no time so far
291 currentwp.put(GpxConstants.PT_TIME, DateUtils.fromDate(d));
292 }
293 // elevation
294 accu=e[GPGGA.HEIGHT_UNTIS.position];
295 if("M".equals(accu)) {
296 // Ignore heights that are not in meters for now
297 accu=e[GPGGA.HEIGHT.position];
298 if(!accu.isEmpty()) {
299 Double.parseDouble(accu);
300 // if it throws it's malformed; this should only happen if the
301 // device sends nonstandard data.
302 if(!accu.isEmpty()) { // FIX ? same check
303 currentwp.put(GpxConstants.PT_ELE, accu);
304 }
305 }
306 }
307 // number of sattelites
308 accu=e[GPGGA.SATELLITE_COUNT.position];
309 int sat = 0;
310 if(!accu.isEmpty()) {
311 sat = Integer.parseInt(accu);
312 currentwp.put(GpxConstants.PT_SAT, accu);
313 }
314 // h-dilution
315 accu=e[GPGGA.HDOP.position];
316 if(!accu.isEmpty()) {
317 currentwp.put(GpxConstants.PT_HDOP, Float.parseFloat(accu));
318 }
319 // fix
320 accu=e[GPGGA.QUALITY.position];
321 if(!accu.isEmpty()) {
322 int fixtype = Integer.parseInt(accu);
323 switch(fixtype) {
324 case 0:
325 currentwp.put(GpxConstants.PT_FIX, "none");
326 break;
327 case 1:
328 if(sat < 4) {
329 currentwp.put(GpxConstants.PT_FIX, "2d");
330 } else {
331 currentwp.put(GpxConstants.PT_FIX, "3d");
332 }
333 break;
334 case 2:
335 currentwp.put(GpxConstants.PT_FIX, "dgps");
336 break;
337 default:
338 break;
339 }
340 }
341 } else if("$GPVTG".equals(e[0]) || "$GNVTG".equals(e[0])) {
342 // COURSE
343 accu = e[GPVTG.COURSE_REF.position];
344 if("T".equals(accu)) {
345 // other values than (T)rue are ignored
346 accu = e[GPVTG.COURSE.position];
347 if(!accu.isEmpty()) {
348 Double.parseDouble(accu);
349 currentwp.put("course", accu);
350 }
351 }
352 // SPEED
353 accu = e[GPVTG.SPEED_KMH_UNIT.position];
354 if(accu.startsWith("K")) {
355 accu = e[GPVTG.SPEED_KMH.position];
356 if(!accu.isEmpty()) {
357 double speed = Double.parseDouble(accu);
358 speed /= 3.6; // speed in m/s
359 currentwp.put("speed", Double.toString(speed));
360 }
361 }
362 } else if("$GPGSA".equals(e[0]) || "$GNGSA".equals(e[0])) {
363 // vdop
364 accu=e[GPGSA.VDOP.position];
365 if(!accu.isEmpty()) {
366 currentwp.put(GpxConstants.PT_VDOP, Float.parseFloat(accu));
367 }
368 // hdop
369 accu=e[GPGSA.HDOP.position];
370 if(!accu.isEmpty()) {
371 currentwp.put(GpxConstants.PT_HDOP, Float.parseFloat(accu));
372 }
373 // pdop
374 accu=e[GPGSA.PDOP.position];
375 if(!accu.isEmpty()) {
376 currentwp.put(GpxConstants.PT_PDOP, Float.parseFloat(accu));
377 }
378 }
379 else if("$GPRMC".equals(e[0]) || "$GNRMC".equals(e[0])) {
380 // coordinates
381 LatLon latLon = parseLatLon(
382 e[GPRMC.WIDTH_NORTH_NAME.position],
383 e[GPRMC.LENGTH_EAST_NAME.position],
384 e[GPRMC.WIDTH_NORTH.position],
385 e[GPRMC.LENGTH_EAST.position]
386 );
387 if((latLon.lat()==0.0) && (latLon.lon()==0.0)) {
388 ps.zero_coord++;
389 return false;
390 }
391 // time
392 currentDate = e[GPRMC.DATE.position];
393 String time = e[GPRMC.TIME.position];
394
395 Date d = readTime(currentDate+time);
396
397 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(time)) {
398 // this node is newer than the previous, create a new waypoint.
399 ps.p_Time=time;
400 currentwp = new WayPoint(latLon);
401 }
402 // time: this sentence has complete time so always use it.
403 currentwp.put(GpxConstants.PT_TIME, DateUtils.fromDate(d));
404 // speed
405 accu = e[GPRMC.SPEED.position];
406 if(!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {
407 double speed = Double.parseDouble(accu);
408 speed *= 0.514444444; // to m/s
409 currentwp.put("speed", Double.toString(speed));
410 }
411 // course
412 accu = e[GPRMC.COURSE.position];
413 if(!accu.isEmpty() && !currentwp.attr.containsKey("course")) {
414 Double.parseDouble(accu);
415 currentwp.put("course", accu);
416 }
417
418 // TODO fix?
419 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S
420 // * = simulated)
421 // *
422 // * @since NMEA 2.3
423 //
424 //MODE(12);
425 } else {
426 ps.unknown++;
427 return false;
428 }
429 ps.p_Date = currentDate;
430 if(ps.p_Wp != currentwp) {
431 if(ps.p_Wp!=null) {
432 ps.p_Wp.setTime();
433 }
434 ps.p_Wp = currentwp;
435 ps.waypoints.add(currentwp);
436 ps.success++;
437 return true;
438 }
439 return true;
440
441 } catch (RuntimeException x) {
442 // out of bounds and such
443 ps.malformed++;
444 ps.p_Wp=null;
445 return false;
446 }
447 }
448
449 private LatLon parseLatLon(String ns, String ew, String dlat, String dlon)
450 throws NumberFormatException {
451 String widthNorth = dlat.trim();
452 String lengthEast = dlon.trim();
453
454 // return a zero latlon instead of null so it is logged as zero coordinate
455 // instead of malformed sentence
456 if(widthNorth.isEmpty() && lengthEast.isEmpty()) return new LatLon(0.0,0.0);
457
458 // The format is xxDDLL.LLLL
459 // xx optional whitespace
460 // DD (int) degres
461 // LL.LLLL (double) latidude
462 int latdegsep = widthNorth.indexOf('.') - 2;
463 if (latdegsep < 0) return null;
464
465 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));
466 double latmin = Double.parseDouble(widthNorth.substring(latdegsep));
467 if(latdeg < 0) {
468 latmin *= -1.0;
469 }
470 double lat = latdeg + latmin / 60;
471 if ("S".equals(ns)) {
472 lat = -lat;
473 }
474
475 int londegsep = lengthEast.indexOf('.') - 2;
476 if (londegsep < 0) return null;
477
478 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));
479 double lonmin = Double.parseDouble(lengthEast.substring(londegsep));
480 if(londeg < 0) {
481 lonmin *= -1.0;
482 }
483 double lon = londeg + lonmin / 60;
484 if ("W".equals(ew)) {
485 lon = -lon;
486 }
487 return new LatLon(lat, lon);
488 }
489}
Note: See TracBrowser for help on using the repository browser.