/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.wattzap.model;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jfree.data.xy.XYSeries;
import com.gpxcreator.gpxpanel.GPXFile;
import com.gpxcreator.gpxpanel.Route;
import com.gpxcreator.gpxpanel.Waypoint;
import com.gpxcreator.gpxpanel.WaypointGroup;
import com.wattzap.model.dto.Point;
/**
* Tacx TTS file reader.
*
* @author JaroslawP
* @author David George
* @date 1 April 2015
*/
@RouteAnnotation
public class TTSReader extends RouteReader {
private static double frameRate;
private ArrayList<Point> pointList = null;
private ProgramPoint[] programList = null;
private List<byte[]> content;
private byte[] pre;
private static String currentFile;
private static int imageId;
private double maxSlope;
private double minSlope;
private XYSeries series;
private double totalDistance = 0.0;
private String fileName;
private String ttsName = "";
// Block Types
private final static int PROGRAM_DATA = 1032;
private final static int DISTANCE_FRAME = 5020;
private final static int GPS_DATA = 5050;
private final static int GENERAL_INFO = 1031;
private final static int TRAINING_TYPE = 5010;
private final static int SEGMENT_INFO = 1041;
private final static int SEGMENT_RANGE = 1050;
private static final int VIDEO_INFO = 2010;
private static Logger logger = LogManager.getLogger("TTS Reader");
private GPXFile gpxFile = null;
private static int[] key = { 0xD6, 0x9C, 0xD8, 0xBC, 0xDA, 0xA9, 0xDC,
0xB0, 0xDE, 0xB6, 0xE0, 0x95, 0xE2, 0xC3, 0xE4, 0x97, 0xE6, 0x92,
0xE8, 0x85, 0xEA, 0x8E, 0xEC, 0x9E, 0xEE, 0x91, 0xF0, 0xB1, 0xF2,
0xD2, 0xF4, 0xD4, 0xF6, 0xD7, 0xF8, 0xB1, 0xFA, 0x9E, 0xFC, 0xDD,
0xFE, 0x96, 0x00, 0x72, 0x02, 0x23, 0x04, 0x71, 0x06, 0x6F, 0x08,
0x6C, 0x0A, 0x2B, 0x0C, 0x7E, 0x0E, 0x4E, 0x10, 0x67, 0x12, 0x7A,
0x14, 0x7A, 0x16, 0x65, 0x18, 0x39, 0x1A, 0x74, 0x1C, 0x7B, 0x1E,
0x3F, 0x20, 0x44, 0x22, 0x75, 0x24, 0x40, 0x26, 0x55, 0x28, 0x50,
0x2A, 0x0B, 0x2C, 0x18, 0x2E, 0x19, 0x30, 0x08, 0x32, 0x0B, 0x34,
0x02, 0x36, 0x1F, 0x38, 0x10, 0x3A, 0x1F, 0x3C, 0x17, 0x3E, 0x17,
0x40, 0x62, 0x42, 0x65, 0x44, 0x65, 0x46, 0x6E, 0x48, 0x69, 0x4A,
0x25, 0x4C, 0x28, 0x4E, 0x2A, 0x50, 0x35, 0x52, 0x36, 0x54, 0x31,
0x56, 0x77, 0x58, 0x09, 0x5A, 0x29, 0x5C, 0x6D, 0x5E, 0x2B, 0x60,
0x52, 0x62, 0x00, 0x64, 0x31, 0x66, 0x2E, 0x68, 0x26, 0x6A, 0x25,
0x6C, 0x4D, 0x6E, 0x3C, 0x70, 0x28, 0x72, 0x20, 0x74, 0x21, 0x76,
0x32, 0x78, 0x34, 0x7A, 0x55, 0x7C, 0x37, 0x7E, 0x0A, 0x80, 0xF2,
0x82, 0xF7, 0x84, 0xC7, 0x86, 0xC2, 0x88, 0xDA, 0x8A, 0xDE, 0x8C,
0xDF, 0x8E, 0xCA, 0x90, 0xE5, 0x92, 0xFC, 0x94, 0xB0, 0x96, 0xC9,
0x98, 0xF4, 0x9A, 0xAF, 0x9C, 0xF6, 0x9E, 0xFA, 0xA0, 0xD5, 0xA2,
0xCB, 0xA4, 0xCC, 0xA6, 0xD4, 0xA8, 0x83, 0xAA, 0x8F, 0xAC, 0xD9,
0xAE, 0xDD, 0xB0, 0xD8, 0xB2, 0xDD, 0xB4, 0xD2, 0xB6, 0xDB, 0xB8,
0xE6, 0xBA, 0xE4, 0xBC, 0xE2, 0xBE, 0xE0, 0xC0, 0xAF, 0xC2, 0xA4,
0xC4, 0xE2, 0xC6, 0xA9, 0xC8, 0xBC, 0xCA, 0xAD, 0xCC, 0xAB, 0xCE,
0xEE };
@Override
public String getExtension() {
return "tts";
}
@Override
public String getFilename() {
// TODO Auto-generated method stub
return fileName;
}
@Override
public String getName() {
// TODO Auto-generated method stub
return ttsName;
}
@Override
public GPXFile getGpxFile() {
if (!gpxFile.isGPXFileWithNoRoutes()) {
return gpxFile;
}
return null;
}
@Override
public XYSeries getSeries() {
// TODO Auto-generated method stub
return series;
}
@Override
public Point[] getPoints() {
return points;
}
@Override
public void load(String fileName) {
this.fileName = fileName.substring(0, fileName.lastIndexOf('.'));
parseFile(fileName);
}
@Override
public void close() {
pointList = null;
}
@Override
public double getDistanceMeters() {
// TODO Auto-generated method stub
return totalDistance;
}
@Override
public int routeType() {
return SLOPE;
}
@Override
public double getMaxSlope() {
// TODO Auto-generated method stub
return maxSlope;
}
@Override
public double getMinSlope() {
// TODO Auto-generated method stub
return minSlope;
}
private void parseFile(String fileName) {
content = new ArrayList<byte[]>();
pre = new byte[2];
int lastSize = -1;
InputStream is = null;
try {
is = new FileInputStream(fileName);
for (;;) {
if (!readData(is, pre, false)) {
break;
}
if (isHeader(pre)) {
byte[] header = new byte[14];
if (readData(is, header, true)) {
content.add(header);
lastSize = getUInt(header, 6) * getUInt(header, 10);
} else {
throw new IllegalArgumentException("Cannot read header");
}
// one byte data.. unconditionally read as data, no-one is
// able
// to check it
if (lastSize < 2) {
byte[] data = new byte[lastSize];
if (readData(is, data, false)) {
content.add(data);
} else {
throw new IllegalArgumentException("Cannot read "
+ lastSize + "b data");
}
lastSize = -1;
}
} else {
if (lastSize < 2) {
throw new IllegalArgumentException(
"Data not allowed, header " + getUShort(pre, 0));
}
byte[] data = new byte[lastSize];
if (readData(is, data, true)) {
content.add(data);
} else {
throw new IllegalArgumentException("Cannot read "
+ lastSize + "b data");
}
lastSize = -1;
}
}// for
} catch (IOException ex) {
logger.error("Cannot read " + fileName + "::"
+ ex.getLocalizedMessage());
} catch (IllegalArgumentException ex) {
logger.error("Wrong file format " + fileName + "::"
+ ex.getLocalizedMessage());
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
logger.error("IOException : " + e);
}
}
loadHeaders();
}
private static String toHex(byte bb) {
int b = bb;
if (b < 0) {
b += 256;
}
String s = Integer.toHexString(b);
if (s.length() == 1) {
return "0" + s;
} else {
return s;
}
}
private static final Map<Integer, String> strings = new HashMap<Integer, String>();
static {
strings.put(1001, "route name");
strings.put(1002, "route description");
strings.put(1041, "segment name");
strings.put(1042, "segment description");
strings.put(2001, "company");
strings.put(2004, "serial");
strings.put(2005, "time");
strings.put(2007, "link");
strings.put(5001, "product");
strings.put(5002, "video name");
strings.put(6001, "infobox #1");
}
private enum StringType {
NONPRINTABLE, BLOCK, STRING, IMAGE, CRC
};
private void loadHeaders() {
pointList = new ArrayList<Point>();
gpxFile = new GPXFile();
series = new XYSeries("");
int[] key2 = rehashKey(key, 17);
int[] keyH = null;
int blockType = -1;
int version = -1;
int stringId = -1;
StringType stringType = StringType.BLOCK;
int fingerprint = 0;
int bytes = 0;
for (byte[] data : content) {
if (isHeader(data)) {
String hdr = bytes + " [" + Integer.toHexString(bytes) + "]: "
+ getUShort(data, 0) + "." + getUShort(data, 2) + " v"
+ getUShort(data, 4) + " " + getUInt(data, 6) + "x"
+ getUInt(data, 10);
logger.debug(hdr);
fingerprint = getUInt(data, 6);
keyH = encryptHeader(iarr(data), key2);
stringType = StringType.NONPRINTABLE;
switch (getUShort(data, 2)) {
case 10: // crc of the data?
// I don't know how to compute it.. and to which data it
// belongs..
// for sure I'm not going to check these, I assume file is
// not broken
// (why it can be?)
stringType = StringType.CRC;
break;
case 110: // UTF-16 string
stringType = StringType.STRING;
stringId = getUShort(data, 0);
break;
case 120: // image fingerprint
stringId = getUShort(data, 0) + 1000;
break;
case 121: // imageType? always 01
break;
case 122: // image bytes, name is present in previous string
// from the block
stringType = StringType.IMAGE;
break;
default:
stringType = StringType.BLOCK;
blockType = getUShort(data, 2);
version = getUShort(data, 4);
logger.debug("\nblock type " + blockType + " version "
+ version);
stringId = -1;
break;
}
} else {
int[] decrD = decryptData(iarr(data), keyH);
keyH = null;
logger.debug("::");
String result = null;
switch (stringType) {
case CRC:
break;
case IMAGE:
logger.debug("[image " + blockType + "."
+ (stringId - 1000) + "]");
/*
* try { result = currentFile + "." + (imageId++) + ".png";
* FileOutputStream file = new FileOutputStream(result);
* file.write(barr(decrD)); file.close(); } catch
* (IOException e) { result = "cannot create: " + e; }
*/
break;
case STRING:
if (strings.containsKey(blockType + stringId)) {
logger.debug("[" + strings.get(blockType + stringId)
+ "]");
} else {
logger.debug("[" + blockType + "." + stringId + "]");
}
StringBuilder str = new StringBuilder();
for (int i = 0; i < decrD.length / 2; i++) {
char c = (char) (decrD[2 * i] | (int) decrD[2 * i + 1] << 8);
str.append(c);
}
result = str.toString();
switch (blockType + stringId) {
case 5002: // Video Name
ttsName = result;
break;
default:
logger.debug("[" + result + "]");
}
break;
case BLOCK:
blockProcessing(blockType, version, barr(decrD));
break;
}
}
logger.debug("\n");
bytes += data.length;
}
// merge program points
int pointCount = 0;
long lastDistance = 0;
double lastSlope = 0.0;
Point lastPoint = pointList.get(0);
int programCount = programList.length;
for (int i = 0; i < programCount; i++) {
ProgramPoint pp = programList[i];
while (pointCount < pointList.size()) {
Point p = pointList.get(pointCount);
if (pp.distance > p.getDistanceFromStart()
|| i == programCount - 1) {
// see which point is closest
if ((p.getDistanceFromStart() - lastDistance) < (p
.getDistanceFromStart() - pp.distance)) {
p.setGradient(pp.slope);
} else {
p.setGradient(lastSlope);
}
// turn into meters
p.setDistanceFromStart(p.getDistanceFromStart() / 100);
// convert to mS
p.setTime(p.getTime() * 1000);
// speed = d/t
if (pointCount > 0) {
p.setSpeed((3600 * (p.getDistanceFromStart() - lastPoint
.getDistanceFromStart()))
/ ((p.getTime() - lastPoint.getTime())));
}
lastPoint = p;
// meters / meters
logger.debug(p);
series.add(p.getDistanceFromStart() / 1000,
p.getElevation());
} else {
break;
}
lastDistance = pp.distance;
lastSlope = pp.slope;
pointCount++;
}// while
}// for
// fill in speed for first point
pointList.get(0).setSpeed(pointList.get(1).getSpeed());
;
totalDistance = pointList.get(pointList.size() - 1)
.getDistanceFromStart();
points = pointList.toArray(new Point[pointList.size()]);
programList = null; // let the GC recover the memory
pointList = null;
}
private void blockProcessing(int blockType, int version, byte[] data) {
switch (blockType) {
case PROGRAM_DATA:
programData(version, data);
break;
case DISTANCE_FRAME:
distanceToFrame(version, data);
break;
case GPS_DATA:
GPSData(version, data);
break;
case GENERAL_INFO:
generalInfo(version, data);
break;
case TRAINING_TYPE:
trainingType(version, data);
break;
case SEGMENT_INFO:
segmentInfo(version, data);
break;
case SEGMENT_RANGE:
segmentRange(version, data);
break;
case VIDEO_INFO:
videoInfo(version, data);
default:
logger.info("Unidentified block type " + blockType);
}
}
/*
* Block routines
*/
private void videoInfo(int version, byte[] data) {
// first three values could be shorts and then?
}
private void segmentRange(int version, byte[] data) {
if (data.length % 10 != 0) {
logger.error("Segment Range data wrong length " + data.length);
}
StringBuilder b = new StringBuilder();
b.append("[segment range]");
for (int i = 0; i < data.length / 10; i++) {
b.append(" [" + i + "=" + (getUInt(data, i * 10 + 0) / 100000.0)
+ "-" + (getUInt(data, i * 10 + 4) / 100000.0));
if (getUShort(data, i * 10 + 6) != 0) {
b.append("/0x"
+ Integer.toHexString(getUShort(data, i * 10 + 6)));
}
b.append("]");
}
logger.debug(b.toString());
}
// segment range; 548300 is 5.483km. What is short value in "old" files?
private void segmentInfo(int version, byte[] data) {
if ((version == 1104) && (data.length == 8)) {
logger.debug("[segment range] " + (getUInt(data, 0) / 100000.0)
+ "-" + (getUInt(data, 4) / 100000.0));
}
if ((version == 1000) && (data.length == 10)) {
logger.debug("[segment range] " + (getUInt(data, 2) / 100000.0)
+ "-" + (getUInt(data, 6) / 100000.0) + "/"
+ getUShort(data, 0));
}
}
// 1 for "plain" RLV, 2 for ERGOs
private void trainingType(int version, byte[] data) {
if (version == 1004) {
switch (data[5]) {
case 1:
logger.debug("[video type] RLV");
case 2:
logger.debug("[video type] ERGO");
}
}
}
// it looks like part of GENERALINFO, definition of type is included:
// DWORD WattSlopePulse;//0 = Watt program, 1 = Slope program, 2 = Pulse
// (HR) program
// DWORD TimeDist; //0 = Time based program, 1 = distance based program
// I'm not sure about the order.. but only slope/distance (0/) and
// power/time (1/1) pairs are handled..
private void generalInfo(int version, byte[] data) {
String programType;
switch (data[0]) {
case 0:
programType = "slope";
break;
case 1:
programType = "watt";
throw new RuntimeException("Power files not currently supported");
// break;
case 2:
programType = "heartRate";
break;
default:
programType = "unknown program";
break;
}
String trainingType;
switch (data[1]) {
case 0:
trainingType = "distance";
break;
case 1:
trainingType = "time";
break;
default:
trainingType = "unknown training";
break;
}
logger.debug("[program type] " + programType + " -> " + trainingType);
return;
}
// It screams.. "I'm GPS position!". Distance followed by lat, lon,
// altitude
private void GPSData(int version, byte[] data) {
if (data.length % 16 != 0) {
logger.error("GPS Points Data wrong length " + data.length);
return;
}
gpxFile.addRoute();
List<Route> routes = gpxFile.getRoutes();
Route route = routes.get(0);
WaypointGroup path = route.getPath();
logger.debug("[" + (data.length / 16) + " gps points]");
int pointCount = 0;
int lastDistance = 0;
double lastLat = 0;
double lastLon = 0;
double lastAlt = 0;
int gpsCount = data.length / 16;
for (int i = 0; i < gpsCount; i++) {
int distance = getUInt(data, i * 16);
// out.print("[" + i + "] " + distance + " cm, ");
double lat = Float.intBitsToFloat(getUInt(data, i * 16 + 4));
double lon = Float.intBitsToFloat(getUInt(data, i * 16 + 8));
double altitude = Float.intBitsToFloat(getUInt(data, i * 16 + 12));
// GPS Data
Waypoint wayPoint = new Waypoint(lat, lon);
wayPoint.setEle(altitude);
path.addWaypoint(wayPoint);
while (pointCount < pointList.size()) {
Point p = pointList.get(pointCount);
/*
* we're straddling two points or we've run out of gps points to
* use
*/
if (distance > p.getDistanceFromStart() || i == gpsCount - 1) {
// see which point is closest
if ((p.getDistanceFromStart() - lastDistance) < (p
.getDistanceFromStart() - distance)) {
// System.out.println("[" + pointCount + "]"
// + p.getDistanceFromStart() + "/"
// + distance + " : " + p.getTime()
// + " secs " + lat + "/" + lon + ", "
// + altitude + " meters");
p.setElevation(altitude);
p.setLatitude(lat);
p.setLongitude(lon);
} else {
// System.out.println("[" + pointCount + "]"
// + (long) p.getDistanceFromStart() + "/"
// + lastDistance + " : " + p.getTime()
// + " secs " + lastLat + "/" + lastLon
// + ", " + lastAlt + " meters");
p.setElevation(lastAlt);
p.setLatitude(lastLat);
p.setLongitude(lastLon);
}
} else {
break;
}
pointCount++;
}// while
lastDistance = distance;
lastLat = lat;
lastLon = lon;
lastAlt = altitude;
}
gpxFile.updateAllProperties();
return;
}
/*
* Slope/Distance Program: Distance to Frame Mapping Format: 2102675
* (cm)/77531 (frames)
*
* Watt/Time Program:
*/
private void distanceToFrame(int version, byte[] data) {
if (data.length % 8 != 0) {
logger.error("Distance2Frame Data wrong length " + data.length);
return;
}
/*
* We can calculate frame rate by dividing number of frames by no. of
* data points. If value is less than or equal to 30 this is the
* framerate, if between 30 and 100 divided by 2 (one datapoint every 2
* seconds of film - typical with tacx TTS files), if the value is more
* than 100 divide by 10 (typical with RLV files converted to TTS).
*/
logger.debug("[" + (data.length / 8) + " video points][last frame "
+ getUInt(data, data.length - 4) + "]");
double dataPoints = (data.length / 8);
double frames = getUInt(data, data.length - 4);
frameRate = frames / dataPoints;
if (frameRate > 30 && frameRate < 100) {
frameRate = frameRate / 2.0;
} else if (frameRate >= 100) {
frameRate = frameRate / 10.0;
}
logger.info("Frame Rate " + frameRate);
// System.out.println("Frame Rate " + frameRate);
for (int i = 0; i < data.length / 8; i++) {
Point p = new Point();
p.setDistanceFromStart(getUInt(data, i * 8));
int frame = getUInt(data, i * 8 + 4);
// System.out.println("frame " + frame + " dist " +
// (p.getDistanceFromStart()/1000));
p.setTime((int) (frame / frameRate));
pointList.add(p);
}
}
/**
* PROGRAM data
*
*
* Format: slope/distance information or power/time
*
* short slope // FLOAT RollingFriction; // Usually 4.0 // Now it is integer
* (/100=>[m], /100=>[s]) and short (/100=>[%], [W], // probably HR as
* well..) // Value selector is in 1031.
*/
private void programData(int version, byte[] data) {
if (data.length % 6 != 0) {
logger.error("Program Data wrong length " + data.length);
return;
}
logger.debug("[" + (data.length / 6) + " program points]");
long distance = 0;
int pointCount = data.length / 6;
programList = new ProgramPoint[pointCount];
for (int i = 0; i < pointCount; i++) {
int slope = getUShort(data, i * 6);
if ((slope & 0x8000) != 0) {
slope -= 0x10000;
}
// slope %, distance meters
distance += (getUInt(data, i * 6 + 2));
// System.out.println("slope " + slope + " distance " + distance);
ProgramPoint p = new ProgramPoint();
p.slope = (double) slope / 100;
if (i == 0) {
// first time thru'
minSlope = p.slope;
maxSlope = p.slope;
}
if (p.slope < minSlope) {
minSlope = p.slope;
}
if (p.slope > maxSlope) {
maxSlope = p.slope;
}
p.distance = distance;
programList[i] = p;
}
}
/*
* Utility Routines
*/
private static int uint(byte b) {
if (b < 0) {
return (int) b + 256;
} else {
return (int) b;
}
}
private static int[] rehashKey(int[] A_0, int A_1) {
int i;
char[] chArray1 = new char[A_0.length / 2];
for (i = 0; i < chArray1.length; i++) {
chArray1[i] = (char) (A_0[2 * i] + 256 * A_0[2 * i + 1]);
}
int num1 = 1000170181 + A_1;
int num2 = 0;
int num3 = 1;
while (num2 < chArray1.length) {
int index1 = num2;
char[] chArray2 = chArray1;
int index2 = index1;
int num4 = (int) (short) chArray1[index1];
int num5 = (int) 255;
int num6 = num4 & num5;
int num7 = num1;
int num8 = 1;
int num9 = num7 + num8;
byte num10 = (byte) (num6 ^ num7);
int num11 = 8;
int num12 = num4 >> num11;
int num13 = num9;
int num14 = 1;
num1 = num13 + num14;
int num15 = (int) (byte) (num12 ^ num13);
int num16 = (int) (uint(num10) << 8 | uint((byte) num15)) & 0xffff;
chArray2[index2] = (char) num16;
int num17 = 1;
num2 += num17;
}
int[] ret = new int[chArray1.length];
for (i = 0; i < ret.length; i++) {
ret[i] = (int) chArray1[i];
}
return ret;
}
private static int[] encryptHeader(int[] A_0, int[] key2) {
int[] bytes = key2;
int[] numArray = new int[bytes.length];
int index1 = 0;
int index2 = 0;
int num = 6;
while (true) {
switch (num) {
case 0:
index1 = 0;
num = 2;
continue;
case 1:
if (index2 < bytes.length) {
numArray[index2] = (A_0[index1] ^ bytes[index2]);
num = 5;
continue;
} else {
num = 3;
continue;
}
case 2:
++index2;
num = 4;
continue;
case 3:
return numArray;
case 4:
case 6:
num = 1;
continue;
case 5:
if (index1++ >= A_0.length - 1) {
num = 0;
continue;
} else {
num = 2;
continue;
}
default:
throw new IllegalArgumentException("Restart function?");
}
}
}
private static int[] decryptData(int[] A_0, int[] A_1) {
int[] numArray = new int[A_0.length];
int index = 0;
int num1 = 5;
int num2 = -1000;
int e = 0; // set before each block
while (true) {
switch (num1) {
case 0:
return numArray;
case 1:
e = 0;
num1 = 4;
continue;
case 2:
if (index < A_0.length) {
numArray[index] = A_1[e] ^ A_0[index];
num2 = e++;
num1 = 6;
continue;
} else {
num1 = 0;
continue;
}
case 3:
case 5:
num1 = 2;
continue;
case 4:
++index;
num1 = 3;
continue;
case 6:
if (num2 >= A_1.length - 1) {
num1 = 1;
continue;
} else {
num1 = 4;
continue;
}
default:
throw new IllegalArgumentException("Restart function?");
}
}
}
private static int[] iarr(byte[] a) {
int[] r = new int[a.length];
for (int i = 0; i < a.length; i++) {
r[i] = (int) a[i];
}
return r;
}
private static byte[] barr(int[] a) {
byte[] r = new byte[a.length];
for (int i = 0; i < a.length; i++) {
r[i] = (byte) a[i];
}
return r;
}
private boolean readData(InputStream is, byte[] buffer, boolean copyPre)
throws IOException {
int first = 0;
if (copyPre) {
buffer[0] = pre[0];
buffer[1] = pre[1];
first = 2;
}
return (is.read(buffer, first, buffer.length - first) == buffer.length
- first);
}
private static int getUByte(byte[] buffer, int offset) {
int b = buffer[offset];
if (b < 0) {
b += 256;
}
return b;
}
private static int getUShort(byte[] buffer, int offset) {
return getUByte(buffer, offset) | (getUByte(buffer, offset + 1) << 8);
}
private static int getUInt(byte[] buffer, int offset) {
return getUShort(buffer, offset)
| (getUShort(buffer, offset + 2) << 16);
}
public static int readLEInt(byte[] buffer, int offset) {
return (buffer[offset + 3]) << 24 | (buffer[offset + 2] & 0xff) << 16
| (buffer[offset + 1] & 0xff) << 8
| (buffer[offset + 0] & 0xff);
}
public float readLittleFloat(byte[] buffer, int offset) {
return Float.intBitsToFloat(readLEInt(buffer, offset));
}
public long readLELong(byte[] buffer, int offset) {
return (buffer[offset + 7]) << 56 | (buffer[offset + 6] & 0xff) << 48
| (buffer[offset + 5] & 0xff) << 40
| (buffer[offset + 4] & 0xff) << 32
| (buffer[offset + 3] & 0xff) << 24
| (buffer[offset + 2] & 0xff) << 16
| (buffer[offset + 1] & 0xff) << 8
| (buffer[offset + 0] & 0xff);
}
public double readLittleDouble(byte[] buffer, int offset) {
return Double.longBitsToDouble(readLELong(buffer, offset));
}
private static String getHex(byte[] buffer, int offset) {
StringBuilder b = new StringBuilder();
for (int i = offset; i < buffer.length; i++) {
b.append(' ');
b.append(toHex(buffer[i]));
}
return b.toString();
}
private static boolean isHeader(byte[] buffer) {
if (buffer.length < 2) {
return false;
}
return getUShort(buffer, 0) <= 20;
}
class gpsPoint {
}
}
class ProgramPoint {
long distance;
double slope;
}