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