/*
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.columbus;
import slash.common.type.CompactCalendar;
import slash.navigation.base.ParserContext;
import slash.navigation.base.RouteCharacteristics;
import slash.navigation.base.SimpleFormat;
import slash.navigation.base.WaypointType;
import slash.navigation.base.Wgs84Position;
import slash.navigation.base.Wgs84Route;
import slash.navigation.common.NavigationPosition;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;
import static java.lang.Long.parseLong;
import static java.nio.ByteBuffer.allocate;
import static java.nio.ByteBuffer.wrap;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.util.Calendar.DAY_OF_MONTH;
import static java.util.Calendar.HOUR_OF_DAY;
import static java.util.Calendar.MILLISECOND;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.SECOND;
import static java.util.Calendar.YEAR;
import static slash.common.type.CompactCalendar.UTC;
import static slash.common.type.CompactCalendar.fromCalendar;
import static slash.navigation.base.RouteCharacteristics.Track;
import static slash.navigation.base.WaypointType.PointOfInterest;
import static slash.navigation.base.WaypointType.Waypoint;
import static slash.navigation.columbus.ColumbusV1000Device.getTimeZone;
import static slash.navigation.columbus.ColumbusV1000Device.getUseLocalTimeZone;
/**
* Reads Columbus GPS Binary (.gps) files.
*
* @author Christian Pesch
*/
public class ColumbusGpsBinaryFormat extends SimpleFormat<Wgs84Route> {
private static final int HEADER_SIZE = 2;
private static final short HEADER = 0x0707;
private static final double COORDINATE_FACTOR = 1000000.0;
private static final double ALTITUDE_FACTOR = 10.0; // 0.1m
private static final double SPEED_FACTOR = 10.0; // 0.1km/h
private static final double PRESSURE_FACTOR = 10.0; // 0.1hPa
private static final double TEMPERATURE_FACTOR = 10.0; // 0.1Degress Celsius
public String getName() {
return "Columbus GPS Binary (*" + getExtension() + ")";
}
public String getExtension() {
return ".gps";
}
public int getMaximumPositionCount() {
return UNLIMITED_MAXIMUM_POSITION_COUNT;
}
public boolean isSupportsReading() {
return true;
}
public boolean isSupportsWriting() {
return false;
}
public boolean isSupportsMultipleRoutes() {
return true;
}
@SuppressWarnings({"unchecked"})
public <P extends NavigationPosition> Wgs84Route createRoute(RouteCharacteristics characteristics, String name, List<P> positions) {
return new Wgs84Route(this, characteristics, (List<Wgs84Position>) positions);
}
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();
}
public void read(InputStream source, ParserContext<Wgs84Route> context) throws Exception {
if (isValidHeader(source)) {
int available = source.available();
ByteBuffer body = allocate(available);
byte[] data = new byte[available];
if (source.read(data) != available)
throw new IOException("Could not read " + available + " bytes");
body.put(data);
body.position(0);
List<Wgs84Position> positions = internalRead(body);
if (positions.size() > 0)
context.appendRoute(new Wgs84Route(this, Track, positions));
}
}
private boolean isValidHeader(InputStream inputStream) throws IOException {
byte[] header = new byte[HEADER_SIZE];
if (inputStream.read(header) == HEADER_SIZE) {
ByteBuffer buffer = wrap(header);
buffer.order(LITTLE_ENDIAN);
if (buffer.getShort() == HEADER)
return true;
}
return false;
}
private List<Wgs84Position> internalRead(ByteBuffer buffer) {
List<Wgs84Position> result = new ArrayList<>();
while (buffer.position() + 28 <= buffer.capacity()) {
byte index0 = buffer.get();
byte index1 = buffer.get();
byte index2 = buffer.get();
int index = (index2 & 0xFF) | ((index1 & 0xFF) << 8) | ((index0 & 0x0F) << 16);
byte byte3 = buffer.get();
WaypointType tag = parseTag(byte3);
CompactCalendar time = parseTime(buffer.getInt());
boolean isSouth = hasBitSet(byte3, 2);
boolean isWest = hasBitSet(byte3, 3);
double latitude = parseCoordinate(buffer.getInt(), isSouth);
double longitude = parseCoordinate(buffer.getInt(), isWest);
double altitude = buffer.getInt() / ALTITUDE_FACTOR;
double speed = buffer.getShort() / SPEED_FACTOR;
double heading = buffer.getShort();
double pressure = buffer.getShort() / PRESSURE_FACTOR;
double temperature = buffer.getShort() / TEMPERATURE_FACTOR;
Wgs84Position position = new Wgs84Position(longitude, latitude, altitude, speed, time, "Trackpoint " + String.valueOf(index));
position.setHeading(heading);
position.setPressure(pressure);
position.setTemperature(temperature);
position.setWaypointType(tag);
result.add(position);
}
return result;
}
boolean hasBitSet(byte aByte, int position) {
return ((aByte >> position) & 1) == 1;
}
private WaypointType parseTag(byte aByte) {
if (!hasBitSet(aByte, 0) && !hasBitSet(aByte, 1))
return Waypoint;
else if (hasBitSet(aByte, 0) && !hasBitSet(aByte, 1))
return PointOfInterest;
return Waypoint;
}
private static final long YEAR_MASK = parseLong("11111100000000000000000000000000", 2);
private static final long MONTH_MASK = parseLong("00000011110000000000000000000000", 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);
private CompactCalendar parseTime(int time) {
int year = (int) ((time & YEAR_MASK) >> 26);
int month = (int) ((time & MONTH_MASK) >> 22);
int day = (int) ((time & DAY_MASK) >> 17);
int hour = (int) ((time & HOUR_MASK) >> 12);
int minute = (int) ((time & MINUTE_MASK) >> 6);
int second = (int) ((time & SECOND_MASK));
Calendar calendar = Calendar.getInstance(UTC);
calendar.set(YEAR, 2016 + year);
calendar.set(MONTH, month - 1);
calendar.set(DAY_OF_MONTH, day);
calendar.set(HOUR_OF_DAY, hour);
calendar.set(MINUTE, minute);
calendar.set(SECOND, second);
calendar.set(MILLISECOND, 0);
CompactCalendar dateAndTime = fromCalendar(calendar);
if(getUseLocalTimeZone())
dateAndTime = dateAndTime.asUTCTimeInTimeZone(TimeZone.getTimeZone(getTimeZone()));
return dateAndTime;
}
double parseCoordinate(int integer, boolean isSouthOrWest) {
return integer / COORDINATE_FACTOR * (isSouthOrWest ? -1 : 1);
}
public void write(Wgs84Route route, PrintWriter writer, int startIndex, int endIndex) {
// this format parses the InputStream directly but wants to derive from SimpleFormat to use Wgs84Route
throw new UnsupportedOperationException();
}
public void write(Wgs84Route route, OutputStream target, int startIndex, int endIndex) throws IOException {
throw new UnsupportedOperationException();
}
}