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

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

fix #10121 - Add a new look-and-feel preference to display ISO 8601 dates globally

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