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