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