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    }