001//License: GPL. See README for details. 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 static 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 static 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 static 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 static 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 static 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 public int getParserZeroCoordinates() { 156 return ps.zero_coord; 157 } 158 public int getParserChecksumErrors() { 159 return ps.checksum_errors+ps.no_checksum; 160 } 161 public int getParserMalformed() { 162 return ps.malformed; 163 } 164 public int getNumberOfCoordinates() { 165 return ps.success; 166 } 167 168 public NmeaReader(InputStream source) throws IOException { 169 170 // create the data tree 171 data = new GpxData(); 172 Collection<Collection<WayPoint>> currentTrack = new ArrayList<>(); 173 174 try (BufferedReader rd = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8))) { 175 StringBuilder sb = new StringBuilder(1024); 176 int loopstart_char = rd.read(); 177 ps = new NMEAParserState(); 178 if(loopstart_char == -1) 179 //TODO tell user about the problem? 180 return; 181 sb.append((char)loopstart_char); 182 ps.p_Date="010100"; // TODO date problem 183 while(true) { 184 // don't load unparsable files completely to memory 185 if(sb.length()>=1020) { 186 sb.delete(0, sb.length()-1); 187 } 188 int c = rd.read(); 189 if(c=='$') { 190 parseNMEASentence(sb.toString(), ps); 191 sb.delete(0, sb.length()); 192 sb.append('$'); 193 } else if(c == -1) { 194 // EOF: add last WayPoint if it works out 195 parseNMEASentence(sb.toString(),ps); 196 break; 197 } else { 198 sb.append((char)c); 199 } 200 } 201 currentTrack.add(ps.waypoints); 202 data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap())); 203 204 } catch (IllegalDataException e) { 205 Main.warn(e); 206 } 207 } 208 209 private static class NMEAParserState { 210 protected Collection<WayPoint> waypoints = new ArrayList<>(); 211 protected String p_Time; 212 protected String p_Date; 213 protected WayPoint p_Wp; 214 215 protected int success = 0; // number of successfully parsend sentences 216 protected int malformed = 0; 217 protected int checksum_errors = 0; 218 protected int no_checksum = 0; 219 protected int unknown = 0; 220 protected int zero_coord = 0; 221 } 222 223 // Parses split up sentences into WayPoints which are stored 224 // in the collection in the NMEAParserState object. 225 // Returns true if the input made sence, false otherwise. 226 private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException { 227 try { 228 if (s.isEmpty()) { 229 throw new IllegalArgumentException("s is empty"); 230 } 231 232 // checksum check: 233 // the bytes between the $ and the * are xored 234 // if there is no * or other meanities it will throw 235 // and result in a malformed packet. 236 String[] chkstrings = s.split("\\*"); 237 if(chkstrings.length > 1) 238 { 239 byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8); 240 int chk=0; 241 for (int i = 1; i < chb.length; i++) { 242 chk ^= chb[i]; 243 } 244 if (Integer.parseInt(chkstrings[1].substring(0,2),16) != chk) { 245 ps.checksum_errors++; 246 ps.p_Wp=null; 247 return false; 248 } 249 } else { 250 ps.no_checksum++; 251 } 252 // now for the content 253 String[] e = chkstrings[0].split(","); 254 String accu; 255 256 WayPoint currentwp = ps.p_Wp; 257 String currentDate = ps.p_Date; 258 259 // handle the packet content 260 if("$GPGGA".equals(e[0]) || "$GNGGA".equals(e[0])) { 261 // Position 262 LatLon latLon = parseLatLon( 263 e[GPGGA.LATITUDE_NAME.position], 264 e[GPGGA.LONGITUDE_NAME.position], 265 e[GPGGA.LATITUDE.position], 266 e[GPGGA.LONGITUDE.position] 267 ); 268 if (latLon==null) { 269 throw new IllegalDataException("Malformed lat/lon"); 270 } 271 272 if ((latLon.lat()==0.0) && (latLon.lon()==0.0)) { 273 ps.zero_coord++; 274 return false; 275 } 276 277 // time 278 accu = e[GPGGA.TIME.position]; 279 Date d = readTime(currentDate+accu); 280 281 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(accu)) { 282 // this node is newer than the previous, create a new waypoint. 283 // no matter if previous WayPoint was null, we got something 284 // better now. 285 ps.p_Time=accu; 286 currentwp = new WayPoint(latLon); 287 } 288 if(!currentwp.attr.containsKey("time")) { 289 // As this sentence has no complete time only use it 290 // if there is no time so far 291 currentwp.put(GpxConstants.PT_TIME, DateUtils.fromDate(d)); 292 } 293 // elevation 294 accu=e[GPGGA.HEIGHT_UNTIS.position]; 295 if("M".equals(accu)) { 296 // Ignore heights that are not in meters for now 297 accu=e[GPGGA.HEIGHT.position]; 298 if(!accu.isEmpty()) { 299 Double.parseDouble(accu); 300 // if it throws it's malformed; this should only happen if the 301 // device sends nonstandard data. 302 if(!accu.isEmpty()) { // FIX ? same check 303 currentwp.put(GpxConstants.PT_ELE, accu); 304 } 305 } 306 } 307 // number of sattelites 308 accu=e[GPGGA.SATELLITE_COUNT.position]; 309 int sat = 0; 310 if(!accu.isEmpty()) { 311 sat = Integer.parseInt(accu); 312 currentwp.put(GpxConstants.PT_SAT, accu); 313 } 314 // h-dilution 315 accu=e[GPGGA.HDOP.position]; 316 if(!accu.isEmpty()) { 317 currentwp.put(GpxConstants.PT_HDOP, Float.parseFloat(accu)); 318 } 319 // fix 320 accu=e[GPGGA.QUALITY.position]; 321 if(!accu.isEmpty()) { 322 int fixtype = Integer.parseInt(accu); 323 switch(fixtype) { 324 case 0: 325 currentwp.put(GpxConstants.PT_FIX, "none"); 326 break; 327 case 1: 328 if(sat < 4) { 329 currentwp.put(GpxConstants.PT_FIX, "2d"); 330 } else { 331 currentwp.put(GpxConstants.PT_FIX, "3d"); 332 } 333 break; 334 case 2: 335 currentwp.put(GpxConstants.PT_FIX, "dgps"); 336 break; 337 default: 338 break; 339 } 340 } 341 } else if("$GPVTG".equals(e[0]) || "$GNVTG".equals(e[0])) { 342 // COURSE 343 accu = e[GPVTG.COURSE_REF.position]; 344 if("T".equals(accu)) { 345 // other values than (T)rue are ignored 346 accu = e[GPVTG.COURSE.position]; 347 if(!accu.isEmpty()) { 348 Double.parseDouble(accu); 349 currentwp.put("course", accu); 350 } 351 } 352 // SPEED 353 accu = e[GPVTG.SPEED_KMH_UNIT.position]; 354 if(accu.startsWith("K")) { 355 accu = e[GPVTG.SPEED_KMH.position]; 356 if(!accu.isEmpty()) { 357 double speed = Double.parseDouble(accu); 358 speed /= 3.6; // speed in m/s 359 currentwp.put("speed", Double.toString(speed)); 360 } 361 } 362 } else if("$GPGSA".equals(e[0]) || "$GNGSA".equals(e[0])) { 363 // vdop 364 accu=e[GPGSA.VDOP.position]; 365 if(!accu.isEmpty()) { 366 currentwp.put(GpxConstants.PT_VDOP, Float.parseFloat(accu)); 367 } 368 // hdop 369 accu=e[GPGSA.HDOP.position]; 370 if(!accu.isEmpty()) { 371 currentwp.put(GpxConstants.PT_HDOP, Float.parseFloat(accu)); 372 } 373 // pdop 374 accu=e[GPGSA.PDOP.position]; 375 if(!accu.isEmpty()) { 376 currentwp.put(GpxConstants.PT_PDOP, Float.parseFloat(accu)); 377 } 378 } 379 else if("$GPRMC".equals(e[0]) || "$GNRMC".equals(e[0])) { 380 // coordinates 381 LatLon latLon = parseLatLon( 382 e[GPRMC.WIDTH_NORTH_NAME.position], 383 e[GPRMC.LENGTH_EAST_NAME.position], 384 e[GPRMC.WIDTH_NORTH.position], 385 e[GPRMC.LENGTH_EAST.position] 386 ); 387 if((latLon.lat()==0.0) && (latLon.lon()==0.0)) { 388 ps.zero_coord++; 389 return false; 390 } 391 // time 392 currentDate = e[GPRMC.DATE.position]; 393 String time = e[GPRMC.TIME.position]; 394 395 Date d = readTime(currentDate+time); 396 397 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(time)) { 398 // this node is newer than the previous, create a new waypoint. 399 ps.p_Time=time; 400 currentwp = new WayPoint(latLon); 401 } 402 // time: this sentence has complete time so always use it. 403 currentwp.put(GpxConstants.PT_TIME, DateUtils.fromDate(d)); 404 // speed 405 accu = e[GPRMC.SPEED.position]; 406 if(!accu.isEmpty() && !currentwp.attr.containsKey("speed")) { 407 double speed = Double.parseDouble(accu); 408 speed *= 0.514444444; // to m/s 409 currentwp.put("speed", Double.toString(speed)); 410 } 411 // course 412 accu = e[GPRMC.COURSE.position]; 413 if(!accu.isEmpty() && !currentwp.attr.containsKey("course")) { 414 Double.parseDouble(accu); 415 currentwp.put("course", accu); 416 } 417 418 // TODO fix? 419 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S 420 // * = simulated) 421 // * 422 // * @since NMEA 2.3 423 // 424 //MODE(12); 425 } else { 426 ps.unknown++; 427 return false; 428 } 429 ps.p_Date = currentDate; 430 if(ps.p_Wp != currentwp) { 431 if(ps.p_Wp!=null) { 432 ps.p_Wp.setTime(); 433 } 434 ps.p_Wp = currentwp; 435 ps.waypoints.add(currentwp); 436 ps.success++; 437 return true; 438 } 439 return true; 440 441 } catch (RuntimeException x) { 442 // out of bounds and such 443 ps.malformed++; 444 ps.p_Wp=null; 445 return false; 446 } 447 } 448 449 private LatLon parseLatLon(String ns, String ew, String dlat, String dlon) 450 throws NumberFormatException { 451 String widthNorth = dlat.trim(); 452 String lengthEast = dlon.trim(); 453 454 // return a zero latlon instead of null so it is logged as zero coordinate 455 // instead of malformed sentence 456 if(widthNorth.isEmpty() && lengthEast.isEmpty()) return new LatLon(0.0,0.0); 457 458 // The format is xxDDLL.LLLL 459 // xx optional whitespace 460 // DD (int) degres 461 // LL.LLLL (double) latidude 462 int latdegsep = widthNorth.indexOf('.') - 2; 463 if (latdegsep < 0) return null; 464 465 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep)); 466 double latmin = Double.parseDouble(widthNorth.substring(latdegsep)); 467 if(latdeg < 0) { 468 latmin *= -1.0; 469 } 470 double lat = latdeg + latmin / 60; 471 if ("S".equals(ns)) { 472 lat = -lat; 473 } 474 475 int londegsep = lengthEast.indexOf('.') - 2; 476 if (londegsep < 0) return null; 477 478 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep)); 479 double lonmin = Double.parseDouble(lengthEast.substring(londegsep)); 480 if(londeg < 0) { 481 lonmin *= -1.0; 482 } 483 double lon = londeg + lonmin / 60; 484 if ("W".equals(ew)) { 485 lon = -lon; 486 } 487 return new LatLon(lat, lon); 488 } 489}