001 //License: GPL. Copyright 2008 by Christoph Brill 002 003 package org.openstreetmap.josm.io; 004 005 import java.io.BufferedReader; 006 import java.io.File; 007 import java.io.IOException; 008 import java.io.InputStream; 009 import java.io.InputStreamReader; 010 import java.text.ParsePosition; 011 import java.text.SimpleDateFormat; 012 import java.util.ArrayList; 013 import java.util.Collection; 014 import java.util.Collections; 015 import java.util.Date; 016 017 import org.openstreetmap.josm.data.coor.LatLon; 018 import org.openstreetmap.josm.data.gpx.GpxData; 019 import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack; 020 import org.openstreetmap.josm.data.gpx.WayPoint; 021 import org.openstreetmap.josm.tools.DateUtils; 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 */ 029 public 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 try { 179 BufferedReader rd = 180 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 rd.close(); 209 currentTrack.add(ps.waypoints); 210 data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap())); 211 212 } catch (final IOException e) { 213 // TODO tell user about the problem? 214 } 215 } 216 private static class NMEAParserState { 217 protected Collection<WayPoint> waypoints = new ArrayList<WayPoint>(); 218 protected String p_Time; 219 protected String p_Date; 220 protected WayPoint p_Wp; 221 222 protected int success = 0; // number of successfully parsend sentences 223 protected int malformed = 0; 224 protected int checksum_errors = 0; 225 protected int no_checksum = 0; 226 protected int unknown = 0; 227 protected int zero_coord = 0; 228 } 229 230 // Parses split up sentences into WayPoints which are stored 231 // in the collection in the NMEAParserState object. 232 // Returns true if the input made sence, false otherwise. 233 private boolean ParseNMEASentence(String s, NMEAParserState ps) { 234 try { 235 if (s.equals("")) 236 throw new NullPointerException(); 237 238 // checksum check: 239 // the bytes between the $ and the * are xored; 240 // if there is no * or other meanities it will throw 241 // and result in a malformed packet. 242 String[] chkstrings = s.split("\\*"); 243 if(chkstrings.length > 1) 244 { 245 byte[] chb = chkstrings[0].getBytes(); 246 int chk=0; 247 for(int i = 1; i < chb.length; i++) { 248 chk ^= chb[i]; 249 } 250 if(Integer.parseInt(chkstrings[1].substring(0,2),16) != chk) { 251 //System.out.println("Checksum error"); 252 ps.checksum_errors++; 253 ps.p_Wp=null; 254 return false; 255 } 256 } else { 257 ps.no_checksum++; 258 } 259 // now for the content 260 String[] e = chkstrings[0].split(","); 261 String accu; 262 263 WayPoint currentwp = ps.p_Wp; 264 String currentDate = ps.p_Date; 265 266 // handle the packet content 267 if(e[0].equals("$GPGGA") || e[0].equals("$GNGGA")) { 268 // Position 269 LatLon latLon = parseLatLon( 270 e[GPGGA.LATITUDE_NAME.position], 271 e[GPGGA.LONGITUDE_NAME.position], 272 e[GPGGA.LATITUDE.position], 273 e[GPGGA.LONGITUDE.position] 274 ); 275 if(latLon==null) 276 throw new NullPointerException(); // malformed 277 278 if((latLon.lat()==0.0) && (latLon.lon()==0.0)) { 279 ps.zero_coord++; 280 return false; 281 } 282 283 // time 284 accu = e[GPGGA.TIME.position]; 285 Date d = readTime(currentDate+accu); 286 287 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(accu)) { 288 // this node is newer than the previous, create a new waypoint. 289 // no matter if previous WayPoint was null, we got something 290 // better now. 291 ps.p_Time=accu; 292 currentwp = new WayPoint(latLon); 293 } 294 if(!currentwp.attr.containsKey("time")) { 295 // As this sentence has no complete time only use it 296 // if there is no time so far 297 currentwp.attr.put("time", DateUtils.fromDate(d)); 298 } 299 // elevation 300 accu=e[GPGGA.HEIGHT_UNTIS.position]; 301 if(accu.equals("M")) { 302 // Ignore heights that are not in meters for now 303 accu=e[GPGGA.HEIGHT.position]; 304 if(!accu.equals("")) { 305 Double.parseDouble(accu); 306 // if it throws it's malformed; this should only happen if the 307 // device sends nonstandard data. 308 if(!accu.equals("")) { 309 currentwp.attr.put("ele", accu); 310 } 311 } 312 } 313 // number of sattelites 314 accu=e[GPGGA.SATELLITE_COUNT.position]; 315 int sat = 0; 316 if(!accu.equals("")) { 317 sat = Integer.parseInt(accu); 318 currentwp.attr.put("sat", accu); 319 } 320 // h-dilution 321 accu=e[GPGGA.HDOP.position]; 322 if(!accu.equals("")) { 323 currentwp.attr.put("hdop", Float.parseFloat(accu)); 324 } 325 // fix 326 accu=e[GPGGA.QUALITY.position]; 327 if(!accu.equals("")) { 328 int fixtype = Integer.parseInt(accu); 329 switch(fixtype) { 330 case 0: 331 currentwp.attr.put("fix", "none"); 332 break; 333 case 1: 334 if(sat < 4) { 335 currentwp.attr.put("fix", "2d"); 336 } else { 337 currentwp.attr.put("fix", "3d"); 338 } 339 break; 340 case 2: 341 currentwp.attr.put("fix", "dgps"); 342 break; 343 default: 344 break; 345 } 346 } 347 } else if(e[0].equals("$GPVTG") || e[0].equals("$GNVTG")) { 348 // COURSE 349 accu = e[GPVTG.COURSE_REF.position]; 350 if(accu.equals("T")) { 351 // other values than (T)rue are ignored 352 accu = e[GPVTG.COURSE.position]; 353 if(!accu.equals("")) { 354 Double.parseDouble(accu); 355 currentwp.attr.put("course", accu); 356 } 357 } 358 // SPEED 359 accu = e[GPVTG.SPEED_KMH_UNIT.position]; 360 if(accu.startsWith("K")) { 361 accu = e[GPVTG.SPEED_KMH.position]; 362 if(!accu.equals("")) { 363 double speed = Double.parseDouble(accu); 364 speed /= 3.6; // speed in m/s 365 currentwp.attr.put("speed", Double.toString(speed)); 366 } 367 } 368 } else if(e[0].equals("$GPGSA") || e[0].equals("$GNGSA")) { 369 // vdop 370 accu=e[GPGSA.VDOP.position]; 371 if(!accu.equals("")) { 372 currentwp.attr.put("vdop", Float.parseFloat(accu)); 373 } 374 // hdop 375 accu=e[GPGSA.HDOP.position]; 376 if(!accu.equals("")) { 377 currentwp.attr.put("hdop", Float.parseFloat(accu)); 378 } 379 // pdop 380 accu=e[GPGSA.PDOP.position]; 381 if(!accu.equals("")) { 382 currentwp.attr.put("pdop", Float.parseFloat(accu)); 383 } 384 } 385 else if(e[0].equals("$GPRMC") || e[0].equals("$GNRMC")) { 386 // coordinates 387 LatLon latLon = parseLatLon( 388 e[GPRMC.WIDTH_NORTH_NAME.position], 389 e[GPRMC.LENGTH_EAST_NAME.position], 390 e[GPRMC.WIDTH_NORTH.position], 391 e[GPRMC.LENGTH_EAST.position] 392 ); 393 if((latLon.lat()==0.0) && (latLon.lon()==0.0)) { 394 ps.zero_coord++; 395 return false; 396 } 397 // time 398 currentDate = e[GPRMC.DATE.position]; 399 String time = e[GPRMC.TIME.position]; 400 401 Date d = readTime(currentDate+time); 402 403 if((ps.p_Time==null) || (currentwp==null) || !ps.p_Time.equals(time)) { 404 // this node is newer than the previous, create a new waypoint. 405 ps.p_Time=time; 406 currentwp = new WayPoint(latLon); 407 } 408 // time: this sentence has complete time so always use it. 409 currentwp.attr.put("time", DateUtils.fromDate(d)); 410 // speed 411 accu = e[GPRMC.SPEED.position]; 412 if(!accu.equals("") && !currentwp.attr.containsKey("speed")) { 413 double speed = Double.parseDouble(accu); 414 speed *= 0.514444444; // to m/s 415 currentwp.attr.put("speed", Double.toString(speed)); 416 } 417 // course 418 accu = e[GPRMC.COURSE.position]; 419 if(!accu.equals("") && !currentwp.attr.containsKey("course")) { 420 Double.parseDouble(accu); 421 currentwp.attr.put("course", accu); 422 } 423 424 // TODO fix? 425 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S 426 // * = simulated) 427 // * 428 // * @since NMEA 2.3 429 // 430 //MODE(12); 431 } else { 432 ps.unknown++; 433 return false; 434 } 435 ps.p_Date = currentDate; 436 if(ps.p_Wp != currentwp) { 437 if(ps.p_Wp!=null) { 438 ps.p_Wp.setTime(); 439 } 440 ps.p_Wp = currentwp; 441 ps.waypoints.add(currentwp); 442 ps.success++; 443 return true; 444 } 445 return true; 446 447 } catch(RuntimeException x) { 448 // out of bounds and such 449 // x.printStackTrace(); 450 // System.out.println("Malformed line: "+s.toString().trim()); 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.equals("")&&lengthEast.equals("")) 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 }