/*
This file is part of RouteConverter.
RouteConverter is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
RouteConverter is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with RouteConverter; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/
package slash.navigation.nmea;
import slash.common.type.CompactCalendar;
import slash.navigation.base.RouteCharacteristics;
import slash.navigation.common.NavigationPosition;
import slash.navigation.common.ValueAndOrientation;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.List;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Locale.US;
import static slash.common.io.Transfer.*;
import static slash.common.type.CompactCalendar.createDateFormat;
import static slash.navigation.common.UnitConversion.kiloMeterToNauticMiles;
import static slash.navigation.common.UnitConversion.nauticMilesToKiloMeter;
/**
* Reads and writes NMEA 0183 Sentences (.nmea) files.
*
* See http://aprs.gids.nl/nmea and http://www.kh-gps.de/nmea-faq.htm
*
* @author Christian Pesch
*/
public class NmeaFormat extends BaseNmeaFormat {
private static final Preferences preferences = Preferences.userNodeForPackage(NmeaFormat.class);
private static final NumberFormat ALTITUDE_AND_SPEED_NUMBER_FORMAT = DecimalFormat.getNumberInstance(US);
static {
int maximumFractionDigits = preferences.getInt("altitudeSpeedMaximumFractionDigits", 1);
ALTITUDE_AND_SPEED_NUMBER_FORMAT.setGroupingUsed(false);
ALTITUDE_AND_SPEED_NUMBER_FORMAT.setMinimumFractionDigits(1);
ALTITUDE_AND_SPEED_NUMBER_FORMAT.setMaximumFractionDigits(maximumFractionDigits);
ALTITUDE_AND_SPEED_NUMBER_FORMAT.setMinimumIntegerDigits(1);
ALTITUDE_AND_SPEED_NUMBER_FORMAT.setMaximumIntegerDigits(6);
}
private static final String DAY_FORMAT = "dd";
private static final String MONTH_FORMAT = "MM";
private static final String YEAR_FORMAT = "yyyy";
// $GPRMC,180114,A,4808.9490,N,00928.9610,E,000.0,000.0,160607,, ,A*76
// $GPRMC,140403.000,A,4837.5194,N,00903.4022,E,15.00,0.00,260707,, *3E
// $GPRMC,172103.38,V,4424.5358,N,06812.3754,W,0.000,0.000,101010,0,W,N*3A
// $GNRMC,162622.00,A,4857.29112,N,00850.57680,E,0.813,251.19,160217,,,D,V*0D
private static final Pattern RMC_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "RMC" + SEPARATOR +
"([\\d\\.]*)" + SEPARATOR + // UTC Time
"[AV]" + SEPARATOR + // Status, A=active, V=void
"([\\s\\d\\.]+)" + SEPARATOR + "([NS])" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([EW])" + SEPARATOR +
"([\\d\\.]*)" + SEPARATOR + // Speed over ground, knots
"([\\d\\.]*)" + SEPARATOR + // Course over ground, degrees
"(\\d*)" + SEPARATOR + // Date, ddmmyy
"[\\d\\.]*" + SEPARATOR +
"[\\d\\.]*" + SEPARATOR + "?" + // Magnetic variation
"[ADEW]?" + SEPARATOR + "?" + // E=East, W=West
"([ADEMNSV])?" + // Signal integrity, N=not valid
END_OF_LINE);
// $GPGGA,130441.89,5239.3154,N,00907.7011,E,1,08,1.25,16.76,M,46.79,M,,*6D
// $GPGGA,162611,3554.2367,N,10619.4966,W,1,03,06.7,02300.3,M,-022.4,M,,*7F
// $GPGGA,132713,5509.7861,N,00140.5854,W,1,07,1.0,98.9,M,,M,,*7d
private static final Pattern GGA_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "GGA" + SEPARATOR + "([\\d\\.]*)" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([NS])" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([WE])" + SEPARATOR +
"([\\d+])" + SEPARATOR + // Fix quality, 0=invalid
"([\\d]*)" + SEPARATOR + // Number of satellites in view, 00 - 12
"[\\d\\.]*" + SEPARATOR +
"(-?[\\d\\.]*)" + SEPARATOR + // Antenna Altitude above/below mean-sea-level (geoid)
"M" + SEPARATOR +
"[-?\\d\\.]*" + SEPARATOR +
"M?" + SEPARATOR +
".*" + SEPARATOR +
".*" + // Differential reference station ID, 0000-1023
END_OF_LINE);
// $GPGLL,4916.45,N,12311.12,W,220433.11,A*6D
private static final Pattern GLL_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "GLL" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([NS])" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([WE])" + SEPARATOR +
"([\\d\\.]+)" + SEPARATOR + // UTC Time
"([AVP])" + // Status
".*" +
END_OF_LINE);
// $GNGNS,184113.00,5215.46773,N,01021.80963,E,AAAN,17,0.73,73.9,45.8,,,V*21
private static final Pattern GNS_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "GNS" + SEPARATOR +
"([\\d\\.]+)" + SEPARATOR + // UTC Time
"([\\s\\d\\.]+)" + SEPARATOR + "([NS])" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([WE])" + SEPARATOR +
"([NADPRFEMS]+)" + SEPARATOR + // Mode indicator, N=no fix
"(\\d*)" + SEPARATOR + // Number of SVs in use, range 00?99
"([\\d\\.]*)" + SEPARATOR + // HDOP
"([\\d\\.]*)" + SEPARATOR + // Orthometric height in meters
"[\\d\\.]*" + SEPARATOR + // Geoidal separation in meters
"\\d*" + SEPARATOR + // Age of differential data
"\\d*" + SEPARATOR + // Reference station ID
".*" +
END_OF_LINE);
// $GPWPL,5334.169,N,01001.920,E,STATN1*22
// $GPWPL,3018.000,S,15309.000,E,Coffs Harbor (Sidney)
private static final Pattern WPL_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "WPL" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([NS])" + SEPARATOR +
"([\\s\\d\\.]+)" + SEPARATOR + "([WE])" + SEPARATOR +
"([^\\*]*)" +
"(\\*[0-9A-Fa-f][0-9A-Fa-f])?$");
// $GPZDA,032910.542,07,08,2004,00,00*48
// $GNZDA,184113.00,23,02,2017,00,00*71
private static final Pattern ZDA_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "ZDA" + SEPARATOR +
"([\\d\\.]*)" + SEPARATOR + // UTC Time
"(\\d*)" + SEPARATOR + // day
"(\\d*)" + SEPARATOR + // month
"(\\d*)" + SEPARATOR + // year
"\\d*" + SEPARATOR +
"\\d*" +
END_OF_LINE);
// $GPVTG,0.00,T,,M,1.531,N,2.835,K,A*37
// $GPVTG,138.7,T,,M,014.2,N,026.3,K,A*00
// $GNVTG,251.19,T,,M,0.813,N,1.506,K,D*20
private static final Pattern VTG_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "VTG" + SEPARATOR +
"([\\d\\.]*)" + SEPARATOR + // true course
"T" + SEPARATOR +
"[\\d\\.]*" + SEPARATOR + // magnetic course
"M" + SEPARATOR +
"([\\d\\.]*)" + SEPARATOR +
"N" + SEPARATOR +
"([\\d\\.]*)" + SEPARATOR +
"K" + SEPARATOR +
"([ADEN])" + // Mode indicator, N=not valid
END_OF_LINE);
// $GPGSA,A,3,,,,15,17,18,23,,,,,,4.7,4.4,1.5*3F
private static final Pattern GSA_PATTERN = Pattern.
compile(BEGIN_OF_LINE + "GSA" + SEPARATOR +
"[AM]" + SEPARATOR +
"([123])" + SEPARATOR + // Fix, 1=Fix not available
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"\\d*" + SEPARATOR +
"([\\d\\.]*)" + SEPARATOR + // PDOP
"([\\d\\.]*)" + SEPARATOR + // HDOP
"([\\d\\.]*)" + // VDOP
END_OF_LINE);
public String getExtension() {
return ".nmea";
}
public String getName() {
return "NMEA 0183 Sentences (*" + getExtension() + ")";
}
@SuppressWarnings({"unchecked"})
public <P extends NavigationPosition> NmeaRoute createRoute(RouteCharacteristics characteristics, String name, List<P> positions) {
return new NmeaRoute(this, characteristics, (List<NmeaPosition>) positions);
}
protected boolean isPosition(String line) {
Matcher rmcMatcher = RMC_PATTERN.matcher(line);
if (rmcMatcher.matches())
return hasValidChecksum(line) && hasValidFix(line, rmcMatcher.group(9), "N");
Matcher ggaMatcher = GGA_PATTERN.matcher(line);
if (ggaMatcher.matches())
return hasValidChecksum(line) && hasValidFix(line, ggaMatcher.group(6), "0");
Matcher gllMatcher = GLL_PATTERN.matcher(line);
if (gllMatcher.matches())
return hasValidChecksum(line) && hasValidFix(line, gllMatcher.group(6), "V");
Matcher gnsMatcher = GNS_PATTERN.matcher(line);
if (gnsMatcher.matches())
return hasValidChecksum(line) && hasValidFix(line, gnsMatcher.group(6), "X");
Matcher wplMatcher = WPL_PATTERN.matcher(line);
if (wplMatcher.matches())
return wplMatcher.group(6) == null || hasValidChecksum(line);
Matcher zdaMatcher = ZDA_PATTERN.matcher(line);
if (zdaMatcher.matches())
return hasValidChecksum(line);
Matcher vtgMatcher = VTG_PATTERN.matcher(line);
if (vtgMatcher.matches())
return hasValidChecksum(line) && hasValidFix(line, vtgMatcher.group(4), "N");
Matcher gsaMatcher = GSA_PATTERN.matcher(line);
return gsaMatcher.matches() && hasValidChecksum(line) && hasValidFix(line, gsaMatcher.group(1), "1");
}
protected NmeaPosition parsePosition(String line) {
Matcher rmcMatcher = RMC_PATTERN.matcher(line);
if (rmcMatcher.matches()) {
String time = rmcMatcher.group(1);
String latitude = rmcMatcher.group(2);
String northOrSouth = rmcMatcher.group(3);
String longitude = rmcMatcher.group(4);
String westOrEast = rmcMatcher.group(5);
Double speed = null;
String speedStr = rmcMatcher.group(6);
if (speedStr != null) {
Double miles = parseDouble(speedStr);
if (miles != null)
speed = nauticMilesToKiloMeter(miles);
}
Double heading = parseDouble(rmcMatcher.group(7));
String date = rmcMatcher.group(8);
return new NmeaPosition(parseDouble(longitude), westOrEast, parseDouble(latitude), northOrSouth,
null, speed, heading, parseDateAndTime(date, time), null);
}
Matcher ggaMatcher = GGA_PATTERN.matcher(line);
if (ggaMatcher.matches()) {
String time = ggaMatcher.group(1);
String latitude = ggaMatcher.group(2);
String northOrSouth = ggaMatcher.group(3);
String longitude = ggaMatcher.group(4);
String westOrEast = ggaMatcher.group(5);
String satellites = ggaMatcher.group(7);
String altitude = ggaMatcher.group(8);
NmeaPosition position = new NmeaPosition(parseDouble(longitude), westOrEast, parseDouble(latitude), northOrSouth,
parseDouble(altitude), null, null, parseTime(time), null);
position.setSatellites(parseInteger(satellites));
return position;
}
Matcher gllMatcher = GLL_PATTERN.matcher(line);
if (gllMatcher.matches()) {
String latitude = gllMatcher.group(1);
String northOrSouth = gllMatcher.group(2);
String longitude = gllMatcher.group(3);
String westOrEast = gllMatcher.group(4);
String time = gllMatcher.group(5);
return new NmeaPosition(parseDouble(longitude), westOrEast, parseDouble(latitude), northOrSouth,
null, null, null, parseTime(time), null);
}
Matcher gnsMatcher = GNS_PATTERN.matcher(line);
if (gnsMatcher.matches()) {
String time = gnsMatcher.group(1);
String latitude = gnsMatcher.group(2);
String northOrSouth = gnsMatcher.group(3);
String longitude = gnsMatcher.group(4);
String westOrEast = gnsMatcher.group(5);
String svs = gnsMatcher.group(7);
String hdop = gnsMatcher.group(8);
String orthometricHeight = gnsMatcher.group(9);
NmeaPosition position = new NmeaPosition(parseDouble(longitude), westOrEast, parseDouble(latitude), northOrSouth,
parseDouble(orthometricHeight), null, null, parseTime(time), null);
position.setHdop(parseDouble(hdop));
position.setSatellites(parseInt(svs));
return position;
}
Matcher wplMatcher = WPL_PATTERN.matcher(line);
if (wplMatcher.matches()) {
String latitude = wplMatcher.group(1);
String northOrSouth = wplMatcher.group(2);
String longitude = wplMatcher.group(3);
String westOrEast = wplMatcher.group(4);
String description = wplMatcher.group(5);
return new NmeaPosition(parseDouble(longitude), westOrEast, parseDouble(latitude), northOrSouth,
null, null, null, null, trim(description));
}
Matcher zdaMatcher = ZDA_PATTERN.matcher(line);
if (zdaMatcher.matches()) {
String time = zdaMatcher.group(1);
String day = trim(zdaMatcher.group(2));
String month = trim(zdaMatcher.group(3));
String year = trim(zdaMatcher.group(4));
String date = (day != null ? day : "") + (month != null ? month : "") + (year != null ? year : "");
return new NmeaPosition(null, null, null, null, null, null, null, parseDateAndTime(date, time), null);
}
Matcher vtgMatcher = VTG_PATTERN.matcher(line);
if (vtgMatcher.matches()) {
Double heading = parseDouble(vtgMatcher.group(1));
boolean miles = false;
String speedStr = trim(vtgMatcher.group(3));
if (speedStr == null) {
speedStr = trim(vtgMatcher.group(2));
miles = true;
}
Double speed = parseDouble(speedStr);
if (miles && speed != null)
speed = nauticMilesToKiloMeter(speed);
return new NmeaPosition(null, null, null, null, null, speed, heading, null, null);
}
Matcher gsaMatcher = GSA_PATTERN.matcher(line);
if (gsaMatcher.matches()) {
String pdop = gsaMatcher.group(2);
String hdop = gsaMatcher.group(3);
String vdop = gsaMatcher.group(4);
NmeaPosition position = new NmeaPosition(null, null, null, null, null, null, null, null, null);
position.setPdop(parseDouble(pdop));
position.setHdop(parseDouble(hdop));
position.setVdop(parseDouble(vdop));
return position;
}
throw new IllegalArgumentException("'" + line + "' does not match");
}
private String formatDay(CompactCalendar date) {
if (date == null)
return "";
return createDateFormat(DAY_FORMAT).format(date.getTime());
}
private String formatMonth(CompactCalendar date) {
if (date == null)
return "";
return createDateFormat(MONTH_FORMAT).format(date.getTime());
}
private String formatYear(CompactCalendar date) {
if (date == null)
return "";
return createDateFormat(YEAR_FORMAT).format(date.getTime());
}
private String formatAltitude(Double altitude) {
if (altitude == null)
return "";
return ALTITUDE_AND_SPEED_NUMBER_FORMAT.format(altitude);
}
private String formatSpeed(Double speed) {
if (speed == null)
return "";
return ALTITUDE_AND_SPEED_NUMBER_FORMAT.format(speed);
}
private String formatAccuracy(Double accuracy) {
if (accuracy == null)
return "";
return ALTITUDE_AND_SPEED_NUMBER_FORMAT.format(accuracy);
}
protected void writePosition(NmeaPosition position, PrintWriter writer) {
ValueAndOrientation longitudeAsValueAndOrientation = position.getLongitudeAsValueAndOrientation();
String longitude = formatLongitude(longitudeAsValueAndOrientation.getValue());
String westOrEast = longitudeAsValueAndOrientation.getOrientation().value();
ValueAndOrientation latitudeAsValueAndOrientation = position.getLatitudeAsValueAndOrientation();
String latitude = formatLatitude(latitudeAsValueAndOrientation.getValue());
String northOrSouth = latitudeAsValueAndOrientation.getOrientation().value();
String satellites = position.getSatellites() != null ? formatIntAsString(position.getSatellites()) : "";
String description = escape(position.getDescription(), SEPARATOR, ';');
String time = formatTime(position.getTime());
String date = formatDate(position.getTime());
String altitude = formatAltitude(position.getElevation());
String speedKnots = position.getSpeed() != null ? formatSpeed(kiloMeterToNauticMiles(position.getSpeed())) : "";
// $GPGGA,130441.89,5239.3154,N,00907.7011,E,1,08,1.25,16.76,M,46.79,M,,*6D
String gga = "GPGGA" + SEPARATOR + time + SEPARATOR +
latitude + SEPARATOR + northOrSouth + SEPARATOR + longitude + SEPARATOR + westOrEast + SEPARATOR +
"1" + SEPARATOR + satellites + SEPARATOR + SEPARATOR + altitude + SEPARATOR + "M" +
SEPARATOR + SEPARATOR + "M" + SEPARATOR + SEPARATOR;
writeSentence(writer, gga);
// $GPWPL,5334.169,N,01001.920,E,STATN1*22
String wpl = "GPWPL" + SEPARATOR +
latitude + SEPARATOR + northOrSouth + SEPARATOR + longitude + SEPARATOR + westOrEast + SEPARATOR +
description;
writeSentence(writer, wpl);
// $GPRMC,180114,A,4808.9490,N,00928.9610,E,000.0,000.0,160607,,A*76
String rmc = "GPRMC" + SEPARATOR + time + SEPARATOR + "A" + SEPARATOR +
latitude + SEPARATOR + northOrSouth + SEPARATOR + longitude + SEPARATOR + westOrEast + SEPARATOR +
speedKnots + SEPARATOR + SEPARATOR +
date + SEPARATOR + SEPARATOR + "A";
writeSentence(writer, rmc);
if(position.hasTime()) {
// $GPZDA,032910,07,08,2004,00,00*48
String day = formatDay(position.getTime());
String month = formatMonth(position.getTime());
String year = formatYear(position.getTime());
String zda = "GPZDA" + SEPARATOR + time + SEPARATOR + day + SEPARATOR + month + SEPARATOR + year + SEPARATOR + SEPARATOR;
writeSentence(writer, zda);
}
if(position.getHeading() != null || position.getSpeed() != null) {
String heading = formatAltitude(position.getHeading());
String speedKm = formatSpeed(position.getSpeed());
// $GPVTG,32.1,T,,M,1.531,N,2.835,K,A*37
String vtg = "GPVTG" + SEPARATOR + heading + SEPARATOR + "T" + SEPARATOR + SEPARATOR + "M" + SEPARATOR +
speedKnots + SEPARATOR + "N" + SEPARATOR + speedKm + SEPARATOR + "K" + SEPARATOR + "A";
writeSentence(writer, vtg);
}
if (position.getHdop() != null || position.getPdop() != null || position.getVdop() != null) {
String hdop = formatAccuracy(position.getHdop());
String pdop = formatAccuracy(position.getPdop());
String vdop = formatAccuracy(position.getVdop());
// $GPGSA,A,3,,,,15,17,18,23,,,,,,4.7,4.4,1.5*3F
String gsa = "GPGSA" + SEPARATOR + "A" + SEPARATOR + "3" + SEPARATOR + SEPARATOR + SEPARATOR + SEPARATOR +
SEPARATOR + SEPARATOR + SEPARATOR + SEPARATOR + SEPARATOR + SEPARATOR + SEPARATOR + SEPARATOR +
SEPARATOR + pdop + SEPARATOR + hdop + SEPARATOR + vdop;
writeSentence(writer, gsa);
}
}
}