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

Last change on this file since 11456 was 11374, checked in by Don-vip, 7 years ago

sonar - squid:S00112 - Generic exceptions should never be thrown: define JosmRuntimeException

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