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

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

see #8465 - last batch of try-with-resources

  • Property svn:eol-style set to native
File size: 17.3 KB
Line 
1//License: GPL. See README for details.
2package org.openstreetmap.josm.io;
3
4import java.io.BufferedReader;
5import java.io.InputStream;
6import java.io.InputStreamReader;
7import java.text.ParsePosition;
8import java.text.SimpleDateFormat;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.Date;
13
14import org.openstreetmap.josm.Main;
15import org.openstreetmap.josm.data.coor.LatLon;
16import org.openstreetmap.josm.data.gpx.GpxData;
17import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
18import org.openstreetmap.josm.data.gpx.WayPoint;
19import org.openstreetmap.josm.tools.DateUtils;
20import org.openstreetmap.josm.tools.Utils;
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 static final SimpleDateFormat GGATIMEFMT =
135 // new SimpleDateFormat("HHmmss.SSS");
136 private static final SimpleDateFormat RMCTIMEFMT =
137 new SimpleDateFormat("ddMMyyHHmmss.SSS");
138 private static final SimpleDateFormat RMCTIMEFMTSTD =
139 new SimpleDateFormat("ddMMyyHHmmss");
140
141 private Date readTime(String p) {
142 Date d = RMCTIMEFMT.parse(p, new ParsePosition(0));
143 if (d == null) {
144 d = RMCTIMEFMTSTD.parse(p, new ParsePosition(0));
145 }
146 if (d == null)
147 throw new RuntimeException("Date is malformed"); // malformed
148 return d;
149 }
150
151 // functons for reading the error stats
152 public NMEAParserState ps;
153
154 public int getParserUnknown() {
155 return ps.unknown;
156 }
157 public int getParserZeroCoordinates() {
158 return ps.zero_coord;
159 }
160 public int getParserChecksumErrors() {
161 return ps.checksum_errors+ps.no_checksum;
162 }
163 public int getParserMalformed() {
164 return ps.malformed;
165 }
166 public int getNumberOfCoordinates() {
167 return ps.success;
168 }
169
170 public NmeaReader(InputStream source) {
171
172 // create the data tree
173 data = new GpxData();
174 Collection<Collection<WayPoint>> currentTrack = new ArrayList<>();
175
176 try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, Utils.UTF_8))) {
177 StringBuilder sb = new StringBuilder(1024);
178 int loopstart_char = rd.read();
179 ps = new NMEAParserState();
180 if(loopstart_char == -1)
181 //TODO tell user about the problem?
182 return;
183 sb.append((char)loopstart_char);
184 ps.p_Date="010100"; // TODO date problem
185 while(true) {
186 // don't load unparsable files completely to memory
187 if(sb.length()>=1020) {
188 sb.delete(0, sb.length()-1);
189 }
190 int c = rd.read();
191 if(c=='$') {
192 parseNMEASentence(sb.toString(), ps);
193 sb.delete(0, sb.length());
194 sb.append('$');
195 } else if(c == -1) {
196 // EOF: add last WayPoint if it works out
197 parseNMEASentence(sb.toString(),ps);
198 break;
199 } else {
200 sb.append((char)c);
201 }
202 }
203 currentTrack.add(ps.waypoints);
204 data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
205
206 } catch (Exception e) {
207 Main.warn(e);
208 }
209 }
210
211 private static class NMEAParserState {
212 protected Collection<WayPoint> waypoints = new ArrayList<>();
213 protected String p_Time;
214 protected String p_Date;
215 protected WayPoint p_Wp;
216
217 protected int success = 0; // number of successfully parsend sentences
218 protected int malformed = 0;
219 protected int checksum_errors = 0;
220 protected int no_checksum = 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) throws IllegalDataException {
229 try {
230 if (s.isEmpty()) {
231 throw new IllegalArgumentException("s is empty");
232 }
233
234 // checksum check:
235 // the bytes between the $ and the * are xored
236 // if there is no * or other meanities it will throw
237 // and result in a malformed packet.
238 String[] chkstrings = s.split("\\*");
239 if(chkstrings.length > 1)
240 {
241 byte[] chb = chkstrings[0].getBytes(Utils.UTF_8);
242 int chk=0;
243 for (int i = 1; i < chb.length; i++) {
244 chk ^= chb[i];
245 }
246 if (Integer.parseInt(chkstrings[1].substring(0,2),16) != chk) {
247 ps.checksum_errors++;
248 ps.p_Wp=null;
249 return false;
250 }
251 } else {
252 ps.no_checksum++;
253 }
254 // now for the content
255 String[] e = chkstrings[0].split(",");
256 String accu;
257
258 WayPoint currentwp = ps.p_Wp;
259 String currentDate = ps.p_Date;
260
261 // handle the packet content
262 if("$GPGGA".equals(e[0]) || "$GNGGA".equals(e[0])) {
263 // Position
264 LatLon latLon = parseLatLon(
265 e[GPGGA.LATITUDE_NAME.position],
266 e[GPGGA.LONGITUDE_NAME.position],
267 e[GPGGA.LATITUDE.position],
268 e[GPGGA.LONGITUDE.position]
269 );
270 if (latLon==null) {
271 throw new IllegalDataException("Malformed lat/lon");
272 }
273
274 if ((latLon.lat()==0.0) && (latLon.lon()==0.0)) {
275 ps.zero_coord++;
276 return false;
277 }
278
279 // time
280 accu = e[GPGGA.TIME.position];
281 Date d = readTime(currentDate+accu);
282
283 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(accu)) {
284 // this node is newer than the previous, create a new waypoint.
285 // no matter if previous WayPoint was null, we got something
286 // better now.
287 ps.p_Time=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.attr.put("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.attr.put("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.attr.put("sat", accu);
315 }
316 // h-dilution
317 accu=e[GPGGA.HDOP.position];
318 if(!accu.isEmpty()) {
319 currentwp.attr.put("hdop", Float.parseFloat(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.attr.put("fix", "none");
328 break;
329 case 1:
330 if(sat < 4) {
331 currentwp.attr.put("fix", "2d");
332 } else {
333 currentwp.attr.put("fix", "3d");
334 }
335 break;
336 case 2:
337 currentwp.attr.put("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.attr.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.attr.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.attr.put("vdop", Float.parseFloat(accu));
369 }
370 // hdop
371 accu=e[GPGSA.HDOP.position];
372 if(!accu.isEmpty()) {
373 currentwp.attr.put("hdop", Float.parseFloat(accu));
374 }
375 // pdop
376 accu=e[GPGSA.PDOP.position];
377 if(!accu.isEmpty()) {
378 currentwp.attr.put("pdop", Float.parseFloat(accu));
379 }
380 }
381 else if("$GPRMC".equals(e[0]) || "$GNRMC".equals(e[0])) {
382 // coordinates
383 LatLon latLon = parseLatLon(
384 e[GPRMC.WIDTH_NORTH_NAME.position],
385 e[GPRMC.LENGTH_EAST_NAME.position],
386 e[GPRMC.WIDTH_NORTH.position],
387 e[GPRMC.LENGTH_EAST.position]
388 );
389 if((latLon.lat()==0.0) && (latLon.lon()==0.0)) {
390 ps.zero_coord++;
391 return false;
392 }
393 // time
394 currentDate = e[GPRMC.DATE.position];
395 String time = e[GPRMC.TIME.position];
396
397 Date d = readTime(currentDate+time);
398
399 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(time)) {
400 // this node is newer than the previous, create a new waypoint.
401 ps.p_Time=time;
402 currentwp = new WayPoint(latLon);
403 }
404 // time: this sentence has complete time so always use it.
405 currentwp.attr.put("time", DateUtils.fromDate(d));
406 // speed
407 accu = e[GPRMC.SPEED.position];
408 if(!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {
409 double speed = Double.parseDouble(accu);
410 speed *= 0.514444444; // to m/s
411 currentwp.attr.put("speed", Double.toString(speed));
412 }
413 // course
414 accu = e[GPRMC.COURSE.position];
415 if(!accu.isEmpty() && !currentwp.attr.containsKey("course")) {
416 Double.parseDouble(accu);
417 currentwp.attr.put("course", accu);
418 }
419
420 // TODO fix?
421 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S
422 // * = simulated)
423 // *
424 // * @since NMEA 2.3
425 //
426 //MODE(12);
427 } else {
428 ps.unknown++;
429 return false;
430 }
431 ps.p_Date = currentDate;
432 if(ps.p_Wp != currentwp) {
433 if(ps.p_Wp!=null) {
434 ps.p_Wp.setTime();
435 }
436 ps.p_Wp = currentwp;
437 ps.waypoints.add(currentwp);
438 ps.success++;
439 return true;
440 }
441 return true;
442
443 } catch (RuntimeException x) {
444 // out of bounds and such
445 ps.malformed++;
446 ps.p_Wp=null;
447 return false;
448 }
449 }
450
451 private LatLon parseLatLon(String ns, String ew, String dlat, String dlon)
452 throws NumberFormatException {
453 String widthNorth = dlat.trim();
454 String lengthEast = dlon.trim();
455
456 // return a zero latlon instead of null so it is logged as zero coordinate
457 // instead of malformed sentence
458 if(widthNorth.isEmpty() && lengthEast.isEmpty()) return new LatLon(0.0,0.0);
459
460 // The format is xxDDLL.LLLL
461 // xx optional whitespace
462 // DD (int) degres
463 // LL.LLLL (double) latidude
464 int latdegsep = widthNorth.indexOf('.') - 2;
465 if (latdegsep < 0) return null;
466
467 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));
468 double latmin = Double.parseDouble(widthNorth.substring(latdegsep));
469 if(latdeg < 0) {
470 latmin *= -1.0;
471 }
472 double lat = latdeg + latmin / 60;
473 if ("S".equals(ns)) {
474 lat = -lat;
475 }
476
477 int londegsep = lengthEast.indexOf('.') - 2;
478 if (londegsep < 0) return null;
479
480 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));
481 double lonmin = Double.parseDouble(lengthEast.substring(londegsep));
482 if(londeg < 0) {
483 lonmin *= -1.0;
484 }
485 double lon = londeg + lonmin / 60;
486 if ("W".equals(ew)) {
487 lon = -lon;
488 }
489 return new LatLon(lat, lon);
490 }
491}
Note: See TracBrowser for help on using the repository browser.