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