Changeset 18787 in josm for trunk/src/org/openstreetmap
- Timestamp:
- 2023-07-27T22:22:25+02:00 (18 months ago)
- Location:
- trunk/src/org/openstreetmap/josm/io/nmea
- Files:
-
- 1 added
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/io/nmea/NmeaReader.java
r18742 r18787 7 7 import java.io.InputStreamReader; 8 8 import java.nio.charset.StandardCharsets; 9 import java.text.ParsePosition;10 import java.text.SimpleDateFormat;11 import java.time.Instant;12 9 import java.util.ArrayList; 13 import java.util.Arrays;14 10 import java.util.Collection; 15 11 import java.util.Collections; 16 import java.util.Date;17 import java.util.Locale;18 12 import java.util.Objects; 19 import java.util.regex.Matcher;20 import java.util.regex.Pattern;21 13 22 import org.openstreetmap.josm.data.coor.LatLon;23 import org.openstreetmap.josm.data.gpx.GpxConstants;24 14 import org.openstreetmap.josm.data.gpx.GpxData; 25 15 import org.openstreetmap.josm.data.gpx.GpxTrack; … … 28 18 import org.openstreetmap.josm.io.IllegalDataException; 29 19 import org.openstreetmap.josm.tools.Logging; 30 import org.openstreetmap.josm.tools.date.DateUtils;31 import org.xml.sax.SAXException;32 20 33 21 /** … … 48 36 public class NmeaReader implements IGpxReader { 49 37 38 private final InputStream source; 39 GpxData data; 40 41 private NmeaParser ps; 42 43 /* functions for reading the error stats */ 44 50 45 /** 51 * Course Over Ground and Ground Speed. 52 * <p> 53 * The actual course and speed relative to the ground 46 * Number of unknown sentences 47 * @return return the number of unknown sentences encountered 54 48 */ 55 enum VTG { 56 COURSE(1), COURSE_REF(2), // true course 57 COURSE_M(3), COURSE_M_REF(4), // magnetic course 58 SPEED_KN(5), SPEED_KN_UNIT(6), // speed in knots 59 SPEED_KMH(7), SPEED_KMH_UNIT(8), // speed in km/h 60 REST(9); // version-specific rest 61 62 final int position; 63 64 VTG(int position) { 65 this.position = position; 66 } 49 public int getParserUnknown() { 50 return ps.getParserUnknown(); 67 51 } 68 52 69 53 /** 70 * Recommended Minimum Specific GNSS Data. 71 * <p> 72 * Time, date, position, course and speed data provided by a GNSS navigation receiver. 73 * This sentence is transmitted at intervals not exceeding 2-seconds. 74 * RMC is the recommended minimum data to be provided by a GNSS receiver. 75 * All data fields must be provided, null fields used only when data is temporarily unavailable. 54 * Number of empty coordinates 55 * @return return the number of coordinates which have been zero 76 56 */ 77 enum RMC {78 TIME(1),79 /** Warning from the receiver (A = data ok, V = warning) */80 RECEIVER_WARNING(2),81 WIDTH_NORTH(3), WIDTH_NORTH_NAME(4), // Latitude, NS82 LENGTH_EAST(5), LENGTH_EAST_NAME(6), // Longitude, EW83 SPEED(7), COURSE(8), DATE(9), // Speed in knots84 MAGNETIC_DECLINATION(10), UNKNOWN(11), // magnetic declination85 /**86 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)87 *88 * @since NMEA 2.389 */90 MODE(12);91 92 final int position;93 94 RMC(int position) {95 this.position = position;96 }97 }98 99 /**100 * Global Positioning System Fix Data.101 * <p>102 * Time, position and fix related data for a GPS receiver.103 */104 enum GGA {105 TIME(1), LATITUDE(2), LATITUDE_NAME(3), LONGITUDE(4), LONGITUDE_NAME(5),106 /**107 * Quality (0 = invalid, 1 = GPS, 2 = DGPS, 6 = estimanted (@since NMEA 2.3))108 */109 QUALITY(6), SATELLITE_COUNT(7),110 HDOP(8), // HDOP (horizontal dilution of precision)111 HEIGHT(9), HEIGHT_UNTIS(10), // height above NN (above geoid)112 HEIGHT_2(11), HEIGHT_2_UNTIS(12), // height geoid - height ellipsoid (WGS84)113 GPS_AGE(13), // Age of differential GPS data114 REF(14); // REF station115 116 final int position;117 GGA(int position) {118 this.position = position;119 }120 }121 122 /**123 * GNSS DOP and Active Satellites.124 * <p>125 * GNSS receiver operating mode, satellites used in the navigation solution reported by the GGA or GNS sentence,126 * and DOP values.127 * If only GPS, GLONASS, etc. is used for the reported position solution the talker ID is GP, GL, etc.128 * and the DOP values pertain to the individual system. If GPS, GLONASS, etc. are combined to obtain the129 * reported position solution multiple GSA sentences are produced, one with the GPS satellites, another with130 * the GLONASS satellites, etc. Each of these GSA sentences shall have talker ID GN, to indicate that the131 * satellites are used in a combined solution and each shall have the PDOP, HDOP and VDOP for the132 * combined satellites used in the position.133 */134 enum GSA {135 AUTOMATIC(1),136 FIX_TYPE(2), // 1 = not fixed, 2 = 2D fixed, 3 = 3D fixed)137 // PRN numbers for max 12 satellites138 PRN_1(3), PRN_2(4), PRN_3(5), PRN_4(6), PRN_5(7), PRN_6(8),139 PRN_7(9), PRN_8(10), PRN_9(11), PRN_10(12), PRN_11(13), PRN_12(14),140 PDOP(15), // PDOP (precision)141 HDOP(16), // HDOP (horizontal precision)142 VDOP(17); // VDOP (vertical precision)143 144 final int position;145 GSA(int position) {146 this.position = position;147 }148 }149 150 /**151 * Geographic Position - Latitude/Longitude.152 * <p>153 * Latitude and Longitude of vessel position, time of position fix and status.154 */155 enum GLL {156 LATITUDE(1), LATITUDE_NS(2), // Latitude, NS157 LONGITUDE(3), LONGITUDE_EW(4), // Latitude, EW158 UTC(5), // Universal Time Coordinated159 STATUS(6), // Status: A = Data valid, V = Data not valid160 /**161 * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)162 * @since NMEA 2.3163 */164 MODE(7);165 166 final int position;167 GLL(int position) {168 this.position = position;169 }170 }171 172 private final InputStream source;173 GpxData data;174 175 private static final Pattern DATE_TIME_PATTERN = Pattern.compile("(\\d{12})(\\.\\d+)?");176 177 private final SimpleDateFormat rmcTimeFmt = new SimpleDateFormat("ddMMyyHHmmss.SSS", Locale.ENGLISH);178 179 private Instant readTime(String p) throws IllegalDataException {180 // NMEA defines time with "a variable number of digits for decimal-fraction of seconds"181 // This variable decimal fraction cannot be parsed by SimpleDateFormat182 Matcher m = DATE_TIME_PATTERN.matcher(p);183 if (m.matches()) {184 String date = m.group(1);185 double milliseconds = 0d;186 if (m.groupCount() > 1 && m.group(2) != null) {187 milliseconds = 1000d * Double.parseDouble("0" + m.group(2));188 }189 // Add milliseconds on three digits to match SimpleDateFormat pattern190 date += String.format(".%03d", (int) milliseconds);191 Date d = rmcTimeFmt.parse(date, new ParsePosition(0));192 if (d != null)193 return d.toInstant();194 }195 throw new IllegalDataException("Date is malformed: '" + p + "'");196 }197 198 // functons for reading the error stats199 public NMEAParserState ps;200 201 public int getParserUnknown() {202 return ps.unknown;203 }204 205 57 public int getParserZeroCoordinates() { 206 58 return ps.zeroCoord; 207 59 } 208 60 61 /** 62 * Number of checksum errors 63 * @return return the number of sentences with checksum errors 64 */ 209 65 public int getParserChecksumErrors() { 210 66 return ps.checksumErrors+ps.noChecksum; 211 67 } 212 68 69 /** 70 * Number of malformed errors 71 * @return return the number of malformed sentences 72 */ 213 73 public int getParserMalformed() { 214 74 return ps.malformed; … … 217 77 @Override 218 78 public int getNumberOfCoordinates() { 219 return ps. success;79 return ps.getSuccess(); 220 80 } 221 81 … … 227 87 public NmeaReader(InputStream source) throws IOException { 228 88 this.source = Objects.requireNonNull(source); 229 rmcTimeFmt.setTimeZone(DateUtils.UTC);230 89 } 231 90 232 91 @Override 233 public boolean parse(boolean tryToFinish) throws SAXException,IOException {92 public boolean parse(boolean tryToFinish) throws IOException { 234 93 // create the data tree 235 94 data = new GpxData(); … … 239 98 StringBuilder sb = new StringBuilder(1024); 240 99 int loopstartChar = rd.read(); 241 ps = new N MEAParserState();100 ps = new NmeaParser(); 242 101 if (loopstartChar == -1) 243 102 //TODO tell user about the problem? 244 103 return false; 245 104 sb.append((char) loopstartChar); 246 ps.pDate = "010100"; // TODO date problem247 105 while (true) { 248 106 // don't load unparsable files completely to memory … … 252 110 int c = rd.read(); 253 111 if (c == '$') { 254 parseNMEASentence(sb.toString() , ps);112 ps.parseNMEASentence(sb.toString()); 255 113 sb.delete(0, sb.length()); 256 114 sb.append('$'); 257 115 } else if (c == -1) { 258 116 // EOF: add last WayPoint if it works out 259 parseNMEASentence(sb.toString() , ps);117 ps.parseNMEASentence(sb.toString()); 260 118 break; 261 119 } else { … … 263 121 } 264 122 } 265 currentTrack.add(ps. waypoints);123 currentTrack.add(ps.getWaypoints()); 266 124 data.tracks.add(new GpxTrack(currentTrack, Collections.<String, Object>emptyMap())); 267 125 … … 273 131 } 274 132 275 private static class NMEAParserState {276 protected Collection<WayPoint> waypoints = new ArrayList<>();277 protected String pTime;278 protected String pDate;279 protected WayPoint pWp;280 281 protected int success; // number of successfully parsed sentences282 protected int malformed;283 protected int checksumErrors;284 protected int noChecksum;285 protected int unknown;286 protected int zeroCoord;287 }288 289 /**290 * Determines if the given address denotes the given NMEA sentence formatter of a known talker.291 * @param address first tag of an NMEA sentence292 * @param formatter sentence formatter mnemonic code293 * @return {@code true} if the {@code address} denotes the given NMEA sentence formatter of a known talker294 */295 static boolean isSentence(String address, Sentence formatter) {296 return Arrays.stream(TalkerId.values())297 .anyMatch(talker -> address.equals('$' + talker.name() + formatter.name()));298 }299 300 // Parses split up sentences into WayPoints which are stored301 // in the collection in the NMEAParserState object.302 // Returns true if the input made sense, false otherwise.303 private boolean parseNMEASentence(String s, NMEAParserState ps) throws IllegalDataException {304 try {305 if (s.isEmpty()) {306 throw new IllegalArgumentException("s is empty");307 }308 309 // checksum check:310 // the bytes between the $ and the * are xored311 // if there is no * or other meanities it will throw312 // and result in a malformed packet.313 String[] chkstrings = s.split("\\*", -1);314 if (chkstrings.length > 1) {315 byte[] chb = chkstrings[0].getBytes(StandardCharsets.UTF_8);316 int chk = 0;317 for (int i = 1; i < chb.length; i++) {318 chk ^= chb[i];319 }320 if (Integer.parseInt(chkstrings[1].substring(0, 2), 16) != chk) {321 ps.checksumErrors++;322 ps.pWp = null;323 return false;324 }325 } else {326 ps.noChecksum++;327 }328 // now for the content329 String[] e = chkstrings[0].split(",", -1);330 String accu;331 332 WayPoint currentwp = ps.pWp;333 String currentDate = ps.pDate;334 335 // handle the packet content336 if (isSentence(e[0], Sentence.GGA)) {337 // Position338 LatLon latLon = parseLatLon(339 e[GGA.LATITUDE_NAME.position],340 e[GGA.LONGITUDE_NAME.position],341 e[GGA.LATITUDE.position],342 e[GGA.LONGITUDE.position]343 );344 if (latLon == null) {345 throw new IllegalDataException("Malformed lat/lon");346 }347 348 if (LatLon.ZERO.equals(latLon)) {349 ps.zeroCoord++;350 return false;351 }352 353 // time354 accu = e[GGA.TIME.position];355 Instant instant = readTime(currentDate+accu);356 357 if ((ps.pTime == null) || (currentwp == null) || !ps.pTime.equals(accu)) {358 // this node is newer than the previous, create a new waypoint.359 // no matter if previous WayPoint was null, we got something better now.360 ps.pTime = accu;361 currentwp = new WayPoint(latLon);362 }363 if (!currentwp.attr.containsKey("time")) {364 // As this sentence has no complete time only use it365 // if there is no time so far366 currentwp.setInstant(instant);367 }368 // elevation369 accu = e[GGA.HEIGHT_UNTIS.position];370 if ("M".equals(accu)) {371 // Ignore heights that are not in meters for now372 accu = e[GGA.HEIGHT.position];373 if (!accu.isEmpty()) {374 Double.parseDouble(accu);375 // if it throws it's malformed; this should only happen if the376 // device sends nonstandard data.377 if (!accu.isEmpty()) { // FIX ? same check378 currentwp.put(GpxConstants.PT_ELE, accu);379 }380 }381 }382 // number of satellites383 accu = e[GGA.SATELLITE_COUNT.position];384 int sat = 0;385 if (!accu.isEmpty()) {386 sat = Integer.parseInt(accu);387 currentwp.put(GpxConstants.PT_SAT, accu);388 }389 // h-dilution390 accu = e[GGA.HDOP.position];391 if (!accu.isEmpty()) {392 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));393 }394 // fix395 accu = e[GGA.QUALITY.position];396 if (!accu.isEmpty()) {397 int fixtype = Integer.parseInt(accu);398 switch(fixtype) {399 case 0:400 currentwp.put(GpxConstants.PT_FIX, "none");401 break;402 case 1:403 if (sat < 4) {404 currentwp.put(GpxConstants.PT_FIX, "2d");405 } else {406 currentwp.put(GpxConstants.PT_FIX, "3d");407 }408 break;409 case 2:410 currentwp.put(GpxConstants.PT_FIX, "dgps");411 break;412 case 3:413 currentwp.put(GpxConstants.PT_FIX, "pps");414 break;415 case 4:416 currentwp.put(GpxConstants.PT_FIX, "rtk");417 break;418 case 5:419 currentwp.put(GpxConstants.PT_FIX, "float rtk");420 break;421 case 6:422 currentwp.put(GpxConstants.PT_FIX, "estimated");423 break;424 case 7:425 currentwp.put(GpxConstants.PT_FIX, "manual");426 break;427 case 8:428 currentwp.put(GpxConstants.PT_FIX, "simulated");429 break;430 default:431 break;432 }433 }434 // reference ID435 if (GGA.REF.position < e.length) {436 accu = e[GGA.REF.position];437 if (!accu.isEmpty()) {438 currentwp.put(GpxConstants.PT_DGPSID, accu);439 }440 }441 } else if (isSentence(e[0], Sentence.VTG)) {442 // COURSE443 accu = e[VTG.COURSE_REF.position];444 if ("T".equals(accu)) {445 // other values than (T)rue are ignored446 accu = e[VTG.COURSE.position];447 if (!accu.isEmpty() && currentwp != null) {448 Double.parseDouble(accu);449 currentwp.put("course", accu);450 }451 }452 // SPEED453 accu = e[VTG.SPEED_KMH_UNIT.position];454 if (accu.startsWith("K")) {455 accu = e[VTG.SPEED_KMH.position];456 if (!accu.isEmpty() && currentwp != null) {457 double speed = Double.parseDouble(accu);458 currentwp.put("speed", Double.toString(speed)); // speed in km/h459 }460 }461 } else if (isSentence(e[0], Sentence.GSA)) {462 // vdop463 accu = e[GSA.VDOP.position];464 if (!accu.isEmpty() && currentwp != null) {465 currentwp.put(GpxConstants.PT_VDOP, Float.valueOf(accu));466 }467 // hdop468 accu = e[GSA.HDOP.position];469 if (!accu.isEmpty() && currentwp != null) {470 currentwp.put(GpxConstants.PT_HDOP, Float.valueOf(accu));471 }472 // pdop473 accu = e[GSA.PDOP.position];474 if (!accu.isEmpty() && currentwp != null) {475 currentwp.put(GpxConstants.PT_PDOP, Float.valueOf(accu));476 }477 } else if (isSentence(e[0], Sentence.RMC)) {478 // coordinates479 LatLon latLon = parseLatLon(480 e[RMC.WIDTH_NORTH_NAME.position],481 e[RMC.LENGTH_EAST_NAME.position],482 e[RMC.WIDTH_NORTH.position],483 e[RMC.LENGTH_EAST.position]484 );485 if (LatLon.ZERO.equals(latLon)) {486 ps.zeroCoord++;487 return false;488 }489 // time490 currentDate = e[RMC.DATE.position];491 String time = e[RMC.TIME.position];492 493 Instant instant = readTime(currentDate+time);494 495 if (ps.pTime == null || currentwp == null || !ps.pTime.equals(time)) {496 // this node is newer than the previous, create a new waypoint.497 ps.pTime = time;498 currentwp = new WayPoint(latLon);499 }500 // time: this sentence has complete time so always use it.501 currentwp.setInstant(instant);502 // speed503 accu = e[RMC.SPEED.position];504 if (!accu.isEmpty() && !currentwp.attr.containsKey("speed")) {505 double speed = Double.parseDouble(accu);506 speed *= 0.514444444 * 3.6; // to km/h507 currentwp.put("speed", Double.toString(speed));508 }509 // course510 accu = e[RMC.COURSE.position];511 if (!accu.isEmpty() && !currentwp.attr.containsKey("course")) {512 Double.parseDouble(accu);513 currentwp.put("course", accu);514 }515 516 // TODO fix?517 // * Mode (A = autonom; D = differential; E = estimated; N = not valid; S = simulated)518 // *519 // * @since NMEA 2.3520 //521 //MODE(12);522 } else if (isSentence(e[0], Sentence.GLL)) {523 // coordinates524 LatLon latLon = parseLatLon(525 e[GLL.LATITUDE_NS.position],526 e[GLL.LONGITUDE_EW.position],527 e[GLL.LATITUDE.position],528 e[GLL.LONGITUDE.position]529 );530 if (LatLon.ZERO.equals(latLon)) {531 ps.zeroCoord++;532 return false;533 }534 // only consider valid data535 if (!"A".equals(e[GLL.STATUS.position])) {536 return false;537 }538 539 // RMC sentences contain a full date while GLL sentences contain only time,540 // so create new waypoints only of the NMEA file does not contain RMC sentences541 if (ps.pTime == null || currentwp == null) {542 currentwp = new WayPoint(latLon);543 }544 } else {545 ps.unknown++;546 return false;547 }548 ps.pDate = currentDate;549 if (ps.pWp != currentwp) {550 if (ps.pWp != null) {551 ps.pWp.getInstant();552 }553 ps.pWp = currentwp;554 ps.waypoints.add(currentwp);555 ps.success++;556 return true;557 }558 return true;559 560 } catch (IllegalArgumentException | IndexOutOfBoundsException | IllegalDataException ex) {561 if (ps.malformed < 5) {562 Logging.warn(ex);563 } else {564 Logging.debug(ex);565 }566 ps.malformed++;567 ps.pWp = null;568 return false;569 }570 }571 572 private static LatLon parseLatLon(String ns, String ew, String dlat, String dlon) {573 String widthNorth = dlat.trim();574 String lengthEast = dlon.trim();575 576 // return a zero latlon instead of null so it is logged as zero coordinate577 // instead of malformed sentence578 if (widthNorth.isEmpty() && lengthEast.isEmpty()) return LatLon.ZERO;579 580 // The format is xxDDLL.LLLL581 // xx optional whitespace582 // DD (int) degres583 // LL.LLLL (double) latidude584 int latdegsep = widthNorth.indexOf('.') - 2;585 if (latdegsep < 0) return null;586 587 int latdeg = Integer.parseInt(widthNorth.substring(0, latdegsep));588 double latmin = Double.parseDouble(widthNorth.substring(latdegsep));589 if (latdeg < 0) {590 latmin *= -1.0;591 }592 double lat = latdeg + latmin / 60;593 if ("S".equals(ns)) {594 lat = -lat;595 }596 597 int londegsep = lengthEast.indexOf('.') - 2;598 if (londegsep < 0) return null;599 600 int londeg = Integer.parseInt(lengthEast.substring(0, londegsep));601 double lonmin = Double.parseDouble(lengthEast.substring(londegsep));602 if (londeg < 0) {603 lonmin *= -1.0;604 }605 double lon = londeg + lonmin / 60;606 if ("W".equals(ew)) {607 lon = -lon;608 }609 return new LatLon(lat, lon);610 }611 612 133 @Override 613 134 public GpxData getGpxData() {
Note:
See TracChangeset
for help on using the changeset viewer.