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

Last change on this file since 1385 was 1385, checked in by mfloryan, 15 years ago

Fixed another bug in NMEA file import. Please test some NMEA data files
after changing this reader.

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