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}