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

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

Sonar/Findbugs - Avoid commented-out lines of code, javadoc

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