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

Last change on this file since 5874 was 5874, checked in by Don-vip, 12 years ago

see #8570, #7406 - I/O refactorization:

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