/* 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.ParserContext; import slash.navigation.base.RouteCharacteristics; import slash.navigation.base.SimpleFormat; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.logging.Logger; 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.isEmpty; import static slash.common.io.Transfer.trim; import static slash.common.type.CompactCalendar.*; import static slash.common.type.HexadecimalNumber.decodeBytes; import static slash.common.type.HexadecimalNumber.encodeByte; import static slash.navigation.base.RouteCharacteristics.Track; /** * The base of all NMEA-like formats. * * @author Christian Pesch */ public abstract class BaseNmeaFormat extends SimpleFormat<NmeaRoute> { private static final Preferences preferences = Preferences.userNodeForPackage(BaseNmeaFormat.class); protected final Logger log; static final char SEPARATOR = ','; static final String BEGIN_OF_LINE = "^\\$G[NP]"; static final String END_OF_LINE = "\\*[0-9A-Fa-f][0-9A-Fa-f]$"; private static final Pattern LINE_PATTERN = Pattern.compile("(^@.*|^\\$.*|" + BEGIN_OF_LINE + ".*" + END_OF_LINE + ")"); private static final String DATE_AND_PRECISE_TIME_FORMAT = "ddMMyy HHmmss.SSS"; private static final String PRECISE_DATE_AND_TIME_FORMAT = "ddMMyyyy HHmmss"; private static final String DATE_AND_TIME_FORMAT = "ddMMyy HHmmss"; private static final String DATE_FORMAT = "ddMMyy"; private static final String PRECISE_TIME_FORMAT = "HHmmss.SSS"; private static final String TIME_FORMAT = "HHmmss"; private static final NumberFormat LONGITUDE_NUMBER_FORMAT = DecimalFormat.getNumberInstance(US); private static final NumberFormat LATITUDE_NUMBER_FORMAT = DecimalFormat.getNumberInstance(US); static { int maximumFractionDigits = preferences.getInt("positionMaximumFractionDigits", 4); LONGITUDE_NUMBER_FORMAT.setGroupingUsed(false); LONGITUDE_NUMBER_FORMAT.setMinimumFractionDigits(4); LONGITUDE_NUMBER_FORMAT.setMaximumFractionDigits(maximumFractionDigits); LONGITUDE_NUMBER_FORMAT.setMinimumIntegerDigits(5); LONGITUDE_NUMBER_FORMAT.setMaximumIntegerDigits(5); LATITUDE_NUMBER_FORMAT.setGroupingUsed(false); LATITUDE_NUMBER_FORMAT.setMinimumFractionDigits(4); LATITUDE_NUMBER_FORMAT.setMaximumFractionDigits(maximumFractionDigits); LATITUDE_NUMBER_FORMAT.setMinimumIntegerDigits(4); LATITUDE_NUMBER_FORMAT.setMaximumIntegerDigits(4); } public BaseNmeaFormat() { this.log = Logger.getLogger(getClass().getName()); } public int getMaximumPositionCount() { return UNLIMITED_MAXIMUM_POSITION_COUNT; } protected int getGarbleCount() { return 0; } protected RouteCharacteristics getCharacteristics() { return Track; } public void read(BufferedReader reader, String encoding, ParserContext<NmeaRoute> context) throws IOException { List<NmeaPosition> positions = new ArrayList<>(); CompactCalendar startDate = context.getStartDate(); CompactCalendar originalStartDate = startDate; int lineCount = 0; NmeaPosition previous = null; while (true) { String line = reader.readLine(); if (line == null) break; if (trim(line) == null) continue; if (isValidLine(line)) { if (isPosition(line)) { NmeaPosition position = parsePosition(line); boolean validStartDate = isValidStartDate(position.getTime()); if (validStartDate) startDate = position.getTime(); else position.setStartDate(startDate); if (haveDifferentLongitudeAndLatitude(previous, position) || haveDifferentTime(previous, position) && !validStartDate) { positions.add(position); previous = position; } else { mergePositions(previous, position, originalStartDate); } } } else { // exception for Mobile Navigator 6: accept that the first line may be garbled if (lineCount++ > getGarbleCount()) return; } } if (positions.size() > 0) context.appendRoute(createRoute(getCharacteristics(), null, positions)); } boolean haveDifferentLongitudeAndLatitude(NmeaPosition predecessor, NmeaPosition successor) { return predecessor == null || (predecessor.hasCoordinates() && successor.hasCoordinates() && !(predecessor.getLongitudeAsValueAndOrientation().equals(successor.getLongitudeAsValueAndOrientation()) && predecessor.getLatitudeAsValueAndOrientation().equals(successor.getLatitudeAsValueAndOrientation()))); } boolean haveDifferentTime(NmeaPosition predecessor, NmeaPosition successor) { if(predecessor == null) return true; if(!predecessor.hasTime() || !successor.hasTime()) return false; CompactCalendar predecessorTime = predecessor.getTime(); CompactCalendar successorTime = successor.getTime(); return predecessorTime.hasDateDefined() && successorTime.hasDateDefined() && !predecessorTime.equals(successorTime); } private void mergePositions(NmeaPosition position, NmeaPosition toBeMergedInto, CompactCalendar originalStartDate) { if (isEmpty(position.getDescription()) && !isEmpty(toBeMergedInto.getDescription())) position.setDescription(toBeMergedInto.getDescription()); if (isEmpty(position.getElevation()) && !isEmpty(toBeMergedInto.getElevation())) position.setElevation(toBeMergedInto.getElevation()); if (isEmpty(position.getSpeed()) && !isEmpty(toBeMergedInto.getSpeed())) position.setSpeed(toBeMergedInto.getSpeed()); if (isEmpty(position.getHeading()) && !isEmpty(toBeMergedInto.getHeading())) position.setHeading(toBeMergedInto.getHeading()); if (isEmpty(position.getLatitude()) && !isEmpty(toBeMergedInto.getLatitude())) position.setLatitudeAsValueAndOrientation(toBeMergedInto.getLatitudeAsValueAndOrientation()); if (isEmpty(position.getLongitude()) && !isEmpty(toBeMergedInto.getLongitude())) position.setLongitudeAsValueAndOrientation(toBeMergedInto.getLongitudeAsValueAndOrientation()); if (toBeMergedInto.hasTime() && (!position.hasTime() || isStartDateEqual(position.getTime(), originalStartDate) || position.getTime().getCalendar().before(toBeMergedInto.getTime().getCalendar()))) position.setTime(toBeMergedInto.getTime()); if (isEmpty(position.getHdop()) && !isEmpty(toBeMergedInto.getHdop())) position.setHdop(toBeMergedInto.getHdop()); if (isEmpty(position.getPdop()) && !isEmpty(toBeMergedInto.getPdop())) position.setPdop(toBeMergedInto.getPdop()); if (isEmpty(position.getVdop()) && !isEmpty(toBeMergedInto.getVdop())) position.setVdop(toBeMergedInto.getVdop()); if (isEmpty(position.getSatellites()) && !isEmpty(toBeMergedInto.getSatellites())) position.setSatellites(toBeMergedInto.getSatellites()); } private boolean isStartDateEqual(CompactCalendar compactCalendar1, CompactCalendar compactCalendar2) { if (compactCalendar1 == null || compactCalendar2 == null) return false; Calendar calendar1 = compactCalendar1.getCalendar(); Calendar calendar2 = compactCalendar2.getCalendar(); return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR) && calendar1.get(Calendar.MONTH) == calendar2.get(Calendar.MONTH) && calendar1.get(Calendar.DAY_OF_MONTH) == calendar2.get(Calendar.DAY_OF_MONTH); } protected boolean isValidLine(String line) { Matcher matcher = LINE_PATTERN.matcher(line); return matcher.matches(); } private byte computeChecksum(String line) { byte result = 0; for (int i = 0; i < line.length(); i++) { result ^= line.charAt(i); } return result; } protected boolean hasValidChecksum(String line) { String lineForChecksum = line.substring(1, line.length() - 3); byte expected = computeChecksum(lineForChecksum); String actualStr = line.substring(line.length() - 2); byte[] actual = decodeBytes(actualStr); if (actual.length != 1 || actual[0] != expected) { String expectedStr = encodeByte(expected); log.severe("Checksum of '" + line + "' is invalid. Expected '" + expectedStr + "' but found '" + actualStr + "'"); return preferences.getBoolean("ignoreInvalidChecksum", false); } return true; } protected boolean hasValidFix(String line, String field, String valueThatIndicatesNoFix) { if (field != null && field.equals(valueThatIndicatesNoFix)) { log.severe("Fix for '" + line + "' is invalid. Contains '" + valueThatIndicatesNoFix + "'"); return preferences.getBoolean("ignoreInvalidFix", false); } return true; } protected abstract boolean isPosition(String line); protected abstract NmeaPosition parsePosition(String line); protected CompactCalendar parseTime(String time) { time = trim(time); if (time == null) return null; // 130441.89 try { Date parsed = createDateFormat(PRECISE_TIME_FORMAT).parse(time); return fromDate(parsed); } catch (ParseException e) { // intentionally left empty } // 130441 return parseDate(time, TIME_FORMAT); } protected CompactCalendar parseDateAndTime(String date, String time) { time = trim(time); date = trim(date); if (date == null) return parseTime(time); String dateAndTime = date + " " + time; // date: 160607 time: 130441.89 try { Date parsed = createDateFormat(DATE_AND_PRECISE_TIME_FORMAT).parse(dateAndTime); return fromDate(parsed); } catch (ParseException e) { // intentionally left empty } // date: 160607 time: 130441 try { Date parsed = createDateFormat(DATE_AND_TIME_FORMAT).parse(dateAndTime); return fromDate(parsed); } catch (ParseException e) { // intentionally left empty } // date: 16062007 time: 130441 return parseDate(dateAndTime, PRECISE_DATE_AND_TIME_FORMAT); } protected String formatTime(CompactCalendar time) { if (time == null) return ""; return createDateFormat(PRECISE_TIME_FORMAT).format(time.getTime()); } protected String formatDate(CompactCalendar date) { if (date == null) return ""; return createDateFormat(DATE_FORMAT).format(date.getTime()); } protected String formatLongitude(Double longitude) { if (longitude == null) return ""; return LONGITUDE_NUMBER_FORMAT.format(longitude); } protected String formatLatitude(Double latitude) { if (latitude == null) return ""; return LATITUDE_NUMBER_FORMAT.format(latitude); } protected void writeSentence(PrintWriter writer, String sentence) { String ggaChecksum = encodeByte(computeChecksum(sentence)); writer.println("$" + sentence + "*" + ggaChecksum); } protected abstract void writePosition(NmeaPosition position, PrintWriter writer); protected void writeHeader(PrintWriter writer) { } public void write(NmeaRoute route, PrintWriter writer, int startIndex, int endIndex) { writeHeader(writer); List<NmeaPosition> positions = route.getPositions(); for (int i = startIndex; i < endIndex; i++) { NmeaPosition position = positions.get(i); writePosition(position, writer); } writeFooter(writer); } protected void writeFooter(PrintWriter writer) { } }