/*
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) 2010 Christian Pesch. All Rights Reserved.
*/
package slash.navigation.simple;
import slash.common.type.CompactCalendar;
import slash.navigation.base.*;
import slash.navigation.common.NavigationPosition;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import static java.lang.Long.parseLong;
import static java.util.Calendar.*;
import static slash.common.type.CompactCalendar.createDateFormat;
import static slash.common.type.CompactCalendar.fromCalendar;
import static slash.navigation.base.RouteCharacteristics.Track;
/**
* Reads Navilink (.sbp) files.
*
* Format is documented at:
* http://notes.splitbrain.org/navilink
* http://www.SteffenSiebert.de/soft/python/locosys_tools.html
* gpsbabel source navilink.c, sbp.c
*
* Devices are:
* Locosys BGT-31 http://www.locosystech.com/product.php?zln=en&id=30
*
* @author Malte Neumann
*/
public class NavilinkFormat extends SimpleFormat<Wgs84Route> {
protected static final int HEADER_SIZE = 64;
protected static final int SBP_RECORD_LENGTH = 32;
protected static final String TRACK_NAME_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
public String getExtension() {
return ".sbp";
}
public String getName() {
return "Navilink (*" + getExtension() + ")";
}
public int getMaximumPositionCount() {
return UNLIMITED_MAXIMUM_POSITION_COUNT;
}
public boolean isSupportsReading() {
return true;
}
public boolean isSupportsMultipleRoutes() {
return true;
}
public boolean isSupportsWriting() {
return false;
}
@SuppressWarnings("unchecked")
public <P extends NavigationPosition> Wgs84Route createRoute(RouteCharacteristics characteristics, String name, List<P> positions) {
Wgs84Route newRoute = new Wgs84Route(this, characteristics, (List<Wgs84Position>) positions);
newRoute.setName(name);
return newRoute;
}
public void read(BufferedReader reader, String encoding, ParserContext<Wgs84Route> context) throws IOException {
// this format parses the InputStream directly but wants to derive from SimpleFormat to use Wgs84Route
throw new UnsupportedOperationException();
}
private boolean checkHeader(byte[] header) {
/* gpbsbabel sbp.c:
* A complete SBP file contains 64 bytes header,
*
* Here is the definition of the SBP header
* BYTE 0 ~1 : true SBP header length
* BYTE 2~63: MID_FILE_ID(0xa0 0xa2 2 byte len 0xfd) with following payload :
* User Name, Serial Number, Log Rate, Firmware Version
* >field separator:","
* >User Name : MAX CHAR(13)
* >Serial Number : MAX CHAR(8)
* >Log Rate : MAX CHAR 3, 0..255 in seconds
* >Firmware Version : MAX CHAR (13)
* // will stuff 0xff for remaining bytes
*/
return ((header[2] == (byte) 0xA0) && (header[3] == (byte) 0xA2) && (header[6] == (byte) 0xFD));
}
private static final long MONTH_MASK = parseLong("11111111110000000000000000000000", 2);
private static final long DAY_MASK = parseLong("00000000001111100000000000000000", 2);
private static final long HOUR_MASK = parseLong("00000000000000011111000000000000", 2);
private static final long MINUTE_MASK = parseLong("00000000000000000000111111000000", 2);
private static final long SECOND_MASK = parseLong("00000000000000000000000000111111", 2);
protected CompactCalendar decodeDateTime(long dateTime) {
/*
Packed_Date_Time_UTC:
bit 31..22: year*12+month (10 bits) : real year= year+2000
bit 17..21: day (5bits)
bit 12..16: hour (5bits)
bit 6..11: min (6bits)
bit 0..5 : sec (6bits)
0 1 2 3
01234567 01234567 01234567 01234567
........ ........ ........ ........
SSSSSSMM MMMMHHHH Hdddddmm mmmmmmmm
*/
int second = (int) ((dateTime & SECOND_MASK));
int minute = (int) ((dateTime & MINUTE_MASK) >> 6);
int hour = (int) ((dateTime & HOUR_MASK) >> 12);
int day = (int) ((dateTime & DAY_MASK) >> 17);
int months = (int) ((dateTime & MONTH_MASK) >> 22);
int month = months % 12;
int year = 2000 + months / 12;
Calendar calendar = Calendar.getInstance(CompactCalendar.UTC);
calendar.set(YEAR, year);
calendar.set(MONTH, month - 1); // Java month starts with 0
calendar.set(DAY_OF_MONTH, day);
calendar.set(HOUR_OF_DAY, hour);
calendar.set(MINUTE, minute);
calendar.set(SECOND, second);
return fromCalendar(calendar);
}
protected boolean isTrackStart(ByteBuffer buffer) {
short bitFlags = buffer.get(30);
return (bitFlags & 0x01) == 1;
}
protected Wgs84Position decodePosition(ByteBuffer buffer) {
buffer.position(0);
/*
typedef __packed struct
{
UINT8 HDOP; // HDOP [0..51] with resolution 0.2
UINT8 SVIDCnt; // Number of SVs in solution [0 to 12]
UINT16 UtcSec; // UTC Second [0 to 59] in seconds with resolution 0.001
UINT32 date_time_UTC_packed; // refer to protocol doc
UINT32 SVIDList; // SVs in solution: Bit 0=1: SV1, Bit 1=1: SV2, ... , Bit 31=1: SV32
INT32 Lat; // Latitude [-90 to 90] in degrees with resolution 0.0000001
INT32 Lon; // Longitude [-180 to 180] in degrees with resolution 0.0000001
INT32 AltCM; // Altitude from Mean Sea Level in centi meters
UINT16 Sog; // Speed Over Ground in m/sec with resolution 0.01
UINT16 Cog; // Course Over Ground [0 to 360] in degrees with resolution 0.01
INT16 ClmbRte; // Climb rate in m/sec with resolution 0.01
UINT8 bitFlags; // bitFlags, default 0x00, bit 0=1 indicate the first point after power on
UINT8 reserved; //Malte: It seems that is also Flag: If bitFlags bit 0 = 1 and reserved = 0x14 = First point of new Track. This is used in gpsbabel for a new track.
} T_SBP;
*/
byte hdop = buffer.get();
byte satellites = buffer.get();
buffer.getShort(); //Second resolution 0.001 --> ignore
CompactCalendar dateTime = decodeDateTime(buffer.getInt());
buffer.getInt(); //SVs in solution --> ignore
int latitude = buffer.getInt();
int longitude = buffer.getInt();
int altitudeCm = buffer.getInt();
short speedMeterPerSecond = buffer.getShort();
int heading = buffer.getShort() & 0xFFFF;
Wgs84Position position = new Wgs84Position(longitude / 10000000.0,
latitude / 10000000.0,
altitudeCm / 100.0,
speedMeterPerSecond * 0.01 * 3.6,
dateTime,
null);
position.setHdop(hdop * 0.2);
position.setSatellites((int) satellites);
position.setHeading(heading * 0.01);
return position;
}
public void read(InputStream source, ParserContext<Wgs84Route> context) throws Exception {
byte[] header = new byte[HEADER_SIZE];
if ((source.read(header) == HEADER_SIZE) && checkHeader(header)) {
ByteBuffer sbpRecordByteBuffer = ByteBuffer.allocate(SBP_RECORD_LENGTH);
sbpRecordByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
Wgs84Route activeRoute = null;
byte[] record = new byte[SBP_RECORD_LENGTH];
while (source.read(record) == SBP_RECORD_LENGTH) {
sbpRecordByteBuffer.position(0);
sbpRecordByteBuffer.put(record);
Wgs84Position position = decodePosition(sbpRecordByteBuffer);
if ((activeRoute == null) || (isTrackStart(sbpRecordByteBuffer))) {
activeRoute = createRoute(Track,
createDateFormat(TRACK_NAME_DATE_FORMAT).format(position.getTime().getTime()),
new ArrayList<BaseNavigationPosition>());
context.appendRoute(activeRoute);
}
activeRoute.getPositions().add(position);
}
}
}
public void write(Wgs84Route route, PrintWriter writer, int startIndex, int endIndex) throws IOException {
throw new UnsupportedOperationException();
}
}