001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.nmea; 003 004import java.io.BufferedReader; 005import java.io.IOException; 006import java.io.InputStream; 007import java.io.InputStreamReader; 008import java.nio.charset.StandardCharsets; 009import java.text.ParsePosition; 010import java.text.SimpleDateFormat; 011import java.util.ArrayList; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.Date; 015import java.util.Locale; 016import java.util.Objects; 017import java.util.Optional; 018 019import org.openstreetmap.josm.data.coor.LatLon; 020import org.openstreetmap.josm.data.gpx.GpxConstants; 021import org.openstreetmap.josm.data.gpx.GpxData; 022import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack; 023import org.openstreetmap.josm.data.gpx.WayPoint; 024import org.openstreetmap.josm.io.IGpxReader; 025import org.openstreetmap.josm.io.IllegalDataException; 026import org.openstreetmap.josm.tools.Logging; 027import org.openstreetmap.josm.tools.date.DateUtils; 028import org.xml.sax.SAXException; 029 030/** 031 * Reads a NMEA 0183 file. Based on information from 032 * <a href="http://www.catb.org/gpsd/NMEA.html">http://www.catb.org/gpsd</a>. 033 * 034 * NMEA files are in printable ASCII form and may include information such as position, 035 * speed, depth, frequency allocation, etc. 036 * Typical messages might be 11 to a maximum of 79 characters in length. 037 * 038 * NMEA standard aims to support one-way serial data transmission from a single "talker" 039 * to one or more "listeners". The type of talker is identified by a 2-character mnemonic. 040 * 041 * NMEA information is encoded through a list of "sentences". 042 * 043 * @author cbrill 044 */ 045public class NmeaReader implements IGpxReader { 046 047 enum VTG { 048 COURSE(1), COURSE_REF(2), // true course 049 COURSE_M(3), COURSE_M_REF(4), // magnetic course 050 SPEED_KN(5), SPEED_KN_UNIT(6), // speed in knots 051 SPEED_KMH(7), SPEED_KMH_UNIT(8), // speed in km/h 052 REST(9); // version-specific rest 053 054 final int position; 055 056 VTG(int position) { 057 this.position = position; 058 } 059 } 060 061 enum RMC { 062 TIME(1), 063 /** Warning from the receiver (A = data ok, V = warning) */ 064 RECEIVER_WARNING(2), 065 WIDTH_NORTH(3), WIDTH_NORTH_NAME(4), // Latitude, NS 066 LENGTH_EAST(5), LENGTH_EAST_NAME(6), // Longitude, EW 067 SPEED(7), COURSE(8), DATE(9), // Speed in knots 068 MAGNETIC_DECLINATION(10), UNKNOWN(11), // magnetic declination 069 /** 070 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated) 071 * 072 * @since NMEA 2.3 073 */ 074 MODE(12); 075 076 final int position; 077 078 RMC(int position) { 079 this.position = position; 080 } 081 } 082 083 enum GGA { 084 TIME(1), LATITUDE(2), LATITUDE_NAME(3), LONGITUDE(4), LONGITUDE_NAME(5), 085 /** 086 * Quality (0 = invalid, 1 = GPS, 2 = DGPS, 6 = estimanted (@since NMEA 2.3)) 087 */ 088 QUALITY(6), SATELLITE_COUNT(7), 089 HDOP(8), // HDOP (horizontal dilution of precision) 090 HEIGHT(9), HEIGHT_UNTIS(10), // height above NN (above geoid) 091 HEIGHT_2(11), HEIGHT_2_UNTIS(12), // height geoid - height ellipsoid (WGS84) 092 GPS_AGE(13), // Age of differential GPS data 093 REF(14); // REF station 094 095 final int position; 096 GGA(int position) { 097 this.position = position; 098 } 099 } 100 101 enum GSA { 102 AUTOMATIC(1), 103 FIX_TYPE(2), // 1 = not fixed, 2 = 2D fixed, 3 = 3D fixed) 104 // PRN numbers for max 12 satellites 105 PRN_1(3), PRN_2(4), PRN_3(5), PRN_4(6), PRN_5(7), PRN_6(8), 106 PRN_7(9), PRN_8(10), PRN_9(11), PRN_10(12), PRN_11(13), PRN_12(14), 107 PDOP(15), // PDOP (precision) 108 HDOP(16), // HDOP (horizontal precision) 109 VDOP(17); // VDOP (vertical precision) 110 111 final int position; 112 GSA(int position) { 113 this.position = position; 114 } 115 } 116 117 enum GLL { 118 LATITUDE(1), LATITUDE_NS(2), // Latitude, NS 119 LONGITUDE(3), LONGITUDE_EW(4), // Latitude, EW 120 UTC(5), // Universal Time Coordinated 121 STATUS(6), // Status: A = Data valid, V = Data not valid 122 /** 123 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated) 124 * @since NMEA 2.3 125 */ 126 MODE(7); 127 128 final int position; 129 GLL(int position) { 130 this.position = position; 131 } 132 } 133 134 private final InputStream source; 135 GpxData data; 136 137 private final SimpleDateFormat rmcTimeFmt = new SimpleDateFormat("ddMMyyHHmmss.SSS", Locale.ENGLISH); 138 private final SimpleDateFormat rmcTimeFmtStd = new SimpleDateFormat("ddMMyyHHmmss", Locale.ENGLISH); 139 140 private Date readTime(String p) throws IllegalDataException { 141 Date d = Optional.ofNullable(rmcTimeFmt.parse(p, new ParsePosition(0))) 142 .orElseGet(() -> rmcTimeFmtStd.parse(p, new ParsePosition(0))); 143 if (d == null) 144 throw new IllegalDataException("Date is malformed: '" + p + "'"); 145 return d; 146 } 147 148 // functons for reading the error stats 149 public NMEAParserState ps; 150 151 public int getParserUnknown() { 152 return ps.unknown; 153 } 154 155 public int getParserZeroCoordinates() { 156 return ps.zeroCoord; 157 } 158 159 public int getParserChecksumErrors() { 160 return ps.checksumErrors+ps.noChecksum; 161 } 162 163 public int getParserMalformed() { 164 return ps.malformed; 165 } 166 167 public int getNumberOfCoordinates() { 168 return ps.success; 169 } 170 171 /** 172 * Constructs a new {@code NmeaReader} 173 * @param source NMEA file input stream 174 * @throws IOException if an I/O error occurs 175 */ 176 public NmeaReader(InputStream source) throws IOException { 177 this.source = Objects.requireNonNull(source); 178 rmcTimeFmt.setTimeZone(DateUtils.UTC); 179 rmcTimeFmtStd.setTimeZone(DateUtils.UTC); 180 } 181 182 @Override 183 public boolean parse(boolean tryToFinish) throws SAXException, IOException { 184 // create the data tree 185 data = new GpxData(); 186 Collection<Collection<WayPoint>> currentTrack = new ArrayList<>(); 187 188 try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) { 189 StringBuilder sb = new StringBuilder(1024); 190 int loopstartChar = rd.read(); 191 ps = new NMEAParserState(); 192 if (loopstartChar == -1) 193 //TODO tell user about the problem? 194 return false; 195 sb.append((char) loopstartChar); 196 ps.pDate = "010100"; // TODO date problem 197 while (true) { 198 // don't load unparsable files completely to memory 199 if (sb.length() >= 1020) { 200 sb.delete(0, sb.length()-1); 201 } 202 int c = rd.read(); 203 if (c == '$') { 204 parseNMEASentence(sb.toString(), ps); 205 sb.delete(0, sb.length()); 206 sb.append('$'); 207 } else if (c == -1) { 208 // EOF: add last WayPoint if it works out 209 parseNMEASentence(sb.toString(), ps); 210 break; 211 } else { 212 sb.append((char) c); 213 } 214 } 215 currentTrack.add(ps.waypoints); 216 data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap())); 217 218 } catch (IllegalDataException e) { 219 Logging.warn(e); 220 return false; 221 } 222 return true; 223 } 224 225 private static class NMEAParserState { 226 protected Collection<WayPoint> waypoints = new ArrayList<>(); 227 protected String pTime; 228 protected String pDate; 229 protected WayPoint pWp; 230 231 protected int success; // number of successfully parsed sentences 232 protected int malformed; 233 protected int checksumErrors; 234 protected int noChecksum; 235 protected int unknown; 236 protected int zeroCoord; 237 } 238 239 /** 240 * Determines if the given address denotes the given NMEA sentence formatter of a known talker. 241 * @param address first tag of an NMEA sentence 242 * @param formatter sentence formatter mnemonic code 243 * @return {@code true} if the {@code address} denotes the given NMEA sentence formatter of a known talker 244 */ 245 static boolean isSentence(String address, Sentence formatter) { 246 for (TalkerId talker : TalkerId.values()) { 247 if (address.equals('$' + talker.name() + formatter.name())) { 248 return true; 249 } 250 } 251 return false; 252 } 253 254 // Parses split up sentences into WayPoints which are stored 255 // in the collection in the NMEAParserState object. 256 // Returns true if the input made sense, false otherwise. 257 private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException { 258 try { 259 if (s.isEmpty()) { 260 throw new IllegalArgumentException("s is empty"); 261 } 262 263 // checksum check: 264 // the bytes between the $ and the * are xored 265 // if there is no * or other meanities it will throw 266 // and result in a malformed packet. 267 String[] chkstrings = s.split("\\*"); 268 if (chkstrings.length > 1) { 269 byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8); 270 int chk = 0; 271 for (int i = 1; i < chb.length; i++) { 272 chk ^= chb[i]; 273 } 274 if (Integer.parseInt(chkstrings[1].substring(0, 2), 16) != chk) { 275 ps.checksumErrors++; 276 ps.pWp = null; 277 return false; 278 } 279 } else { 280 ps.noChecksum++; 281 } 282 // now for the content 283 String[] e = chkstrings[0].split(","); 284 String accu; 285 286 WayPoint currentwp = ps.pWp; 287 String currentDate = ps.pDate; 288 289 // handle the packet content 290 if (isSentence(e[0], Sentence.GGA)) { 291 // Position 292 LatLon latLon = parseLatLon( 293 e[GGA.LATITUDE_NAME.position], 294 e[GGA.LONGITUDE_NAME.position], 295 e[GGA.LATITUDE.position], 296 e[GGA.LONGITUDE.position] 297 ); 298 if (latLon == null) { 299 throw new IllegalDataException("Malformed lat/lon"); 300 } 301 302 if (LatLon.ZERO.equals(latLon)) { 303 ps.zeroCoord++; 304 return false; 305 } 306 307 // time 308 accu = e[GGA.TIME.position]; 309 Date d = readTime(currentDate+accu); 310 311 if ((ps.pTime == null) || (currentwp == null) || !ps.pTime.equals(accu)) { 312 // this node is newer than the previous, create a new waypoint. 313 // no matter if previous WayPoint was null, we got something better now. 314 ps.pTime = accu; 315 currentwp = new WayPoint(latLon); 316 } 317 if (!currentwp.attr.containsKey("time")) { 318 // As this sentence has no complete time only use it 319 // if there is no time so far 320 currentwp.setTime(d); 321 } 322 // elevation 323 accu = e[GGA.HEIGHT_UNTIS.position]; 324 if ("M".equals(accu)) { 325 // Ignore heights that are not in meters for now 326 accu = e[GGA.HEIGHT.position]; 327 if (!accu.isEmpty()) { 328 Double.parseDouble(accu); 329 // if it throws it's malformed; this should only happen if the 330 // device sends nonstandard data. 331 if (!accu.isEmpty()) { // FIX ? same check 332 currentwp.put(GpxConstants.PT_ELE, accu); 333 } 334 } 335 } 336 // number of satellites 337 accu = e[GGA.SATELLITE_COUNT.position]; 338 int sat = 0; 339 if (!accu.isEmpty()) { 340 sat = Integer.parseInt(accu); 341 currentwp.put(GpxConstants.PT_SAT, accu); 342 } 343 // h-dilution 344 accu = e[GGA.HDOP.position]; 345 if (!accu.isEmpty()) { 346 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu)); 347 } 348 // fix 349 accu = e[GGA.QUALITY.position]; 350 if (!accu.isEmpty()) { 351 int fixtype = Integer.parseInt(accu); 352 switch(fixtype) { 353 case 0: 354 currentwp.put(GpxConstants.PT_FIX, "none"); 355 break; 356 case 1: 357 if (sat < 4) { 358 currentwp.put(GpxConstants.PT_FIX, "2d"); 359 } else { 360 currentwp.put(GpxConstants.PT_FIX, "3d"); 361 } 362 break; 363 case 2: 364 currentwp.put(GpxConstants.PT_FIX, "dgps"); 365 break; 366 default: 367 break; 368 } 369 } 370 } else if (isSentence(e[0], Sentence.VTG)) { 371 // COURSE 372 accu = e[VTG.COURSE_REF.position]; 373 if ("T".equals(accu)) { 374 // other values than (T)rue are ignored 375 accu = e[VTG.COURSE.position]; 376 if (!accu.isEmpty() && currentwp != null) { 377 Double.parseDouble(accu); 378 currentwp.put("course", accu); 379 } 380 } 381 // SPEED 382 accu = e[VTG.SPEED_KMH_UNIT.position]; 383 if (accu.startsWith("K")) { 384 accu = e[VTG.SPEED_KMH.position]; 385 if (!accu.isEmpty() && currentwp != null) { 386 double speed = Double.parseDouble(accu); 387 speed /= 3.6; // speed in m/s 388 currentwp.put("speed", Double.toString(speed)); 389 } 390 } 391 } else if (isSentence(e[0], Sentence.GSA)) { 392 // vdop 393 accu = e[GSA.VDOP.position]; 394 if (!accu.isEmpty() && currentwp != null) { 395 currentwp.put(GpxConstants.PT_VDOP, Float.valueOf(accu)); 396 } 397 // hdop 398 accu = e[GSA.HDOP.position]; 399 if (!accu.isEmpty() && currentwp != null) { 400 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu)); 401 } 402 // pdop 403 accu = e[GSA.PDOP.position]; 404 if (!accu.isEmpty() && currentwp != null) { 405 currentwp.put(GpxConstants.PT_PDOP, Float.valueOf(accu)); 406 } 407 } else if (isSentence(e[0], Sentence.RMC)) { 408 // coordinates 409 LatLon latLon = parseLatLon( 410 e[RMC.WIDTH_NORTH_NAME.position], 411 e[RMC.LENGTH_EAST_NAME.position], 412 e[RMC.WIDTH_NORTH.position], 413 e[RMC.LENGTH_EAST.position] 414 ); 415 if (LatLon.ZERO.equals(latLon)) { 416 ps.zeroCoord++; 417 return false; 418 } 419 // time 420 currentDate = e[RMC.DATE.position]; 421 String time = e[RMC.TIME.position]; 422 423 Date d = readTime(currentDate+time); 424 425 if (ps.pTime == null || currentwp == null || !ps.pTime.equals(time)) { 426 // this node is newer than the previous, create a new waypoint. 427 ps.pTime = time; 428 currentwp = new WayPoint(latLon); 429 } 430 // time: this sentence has complete time so always use it. 431 currentwp.setTime(d); 432 // speed 433 accu = e[RMC.SPEED.position]; 434 if (!accu.isEmpty() && !currentwp.attr.containsKey("speed")) { 435 double speed = Double.parseDouble(accu); 436 speed *= 0.514444444; // to m/s 437 currentwp.put("speed", Double.toString(speed)); 438 } 439 // course 440 accu = e[RMC.COURSE.position]; 441 if (!accu.isEmpty() && !currentwp.attr.containsKey("course")) { 442 Double.parseDouble(accu); 443 currentwp.put("course", accu); 444 } 445 446 // TODO fix? 447 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated) 448 // * 449 // * @since NMEA 2.3 450 // 451 //MODE(12); 452 } else if (isSentence(e[0], Sentence.GLL)) { 453 // coordinates 454 LatLon latLon = parseLatLon( 455 e[GLL.LATITUDE_NS.position], 456 e[GLL.LONGITUDE_EW.position], 457 e[GLL.LATITUDE.position], 458 e[GLL.LONGITUDE.position] 459 ); 460 if (LatLon.ZERO.equals(latLon)) { 461 ps.zeroCoord++; 462 return false; 463 } 464 // only consider valid data 465 if (!"A".equals(e[GLL.STATUS.position])) { 466 return false; 467 } 468 469 // RMC sentences contain a full date while GLL sentences contain only time, 470 // so create new waypoints only of the NMEA file does not contain RMC sentences 471 if (ps.pTime == null || currentwp == null) { 472 currentwp = new WayPoint(latLon); 473 } 474 } else { 475 ps.unknown++; 476 return false; 477 } 478 ps.pDate = currentDate; 479 if (ps.pWp != currentwp) { 480 if (ps.pWp != null) { 481 ps.pWp.setTime(); 482 } 483 ps.pWp = currentwp; 484 ps.waypoints.add(currentwp); 485 ps.success++; 486 return true; 487 } 488 return true; 489 490 } catch (IllegalArgumentException | IndexOutOfBoundsException | IllegalDataException ex) { 491 if (ps.malformed < 5) { 492 Logging.warn(ex); 493 } else { 494 Logging.debug(ex); 495 } 496 ps.malformed++; 497 ps.pWp = null; 498 return false; 499 } 500 } 501 502 private static LatLon parseLatLon(String ns, String ew, String dlat, String dlon) { 503 String widthNorth = dlat.trim(); 504 String lengthEast = dlon.trim(); 505 506 // return a zero latlon instead of null so it is logged as zero coordinate 507 // instead of malformed sentence 508 if (widthNorth.isEmpty() && lengthEast.isEmpty()) return LatLon.ZERO; 509 510 // The format is xxDDLL.LLLL 511 // xx optional whitespace 512 // DD (int) degres 513 // LL.LLLL (double) latidude 514 int latdegsep = widthNorth.indexOf('.') - 2; 515 if (latdegsep < 0) return null; 516 517 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep)); 518 double latmin = Double.parseDouble(widthNorth.substring(latdegsep)); 519 if (latdeg < 0) { 520 latmin *= -1.0; 521 } 522 double lat = latdeg + latmin / 60; 523 if ("S".equals(ns)) { 524 lat = -lat; 525 } 526 527 int londegsep = lengthEast.indexOf('.') - 2; 528 if (londegsep < 0) return null; 529 530 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep)); 531 double lonmin = Double.parseDouble(lengthEast.substring(londegsep)); 532 if (londeg < 0) { 533 lonmin *= -1.0; 534 } 535 double lon = londeg + lonmin / 60; 536 if ("W".equals(ew)) { 537 lon = -lon; 538 } 539 return new LatLon(lat, lon); 540 } 541 542 @Override 543 public GpxData getGpxData() { 544 return data; 545 } 546}