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