package de.saring.exerciseviewer.parser.impl;
import de.saring.exerciseviewer.core.EVException;
import de.saring.exerciseviewer.data.*;
import de.saring.exerciseviewer.parser.AbstractExerciseParser;
import de.saring.exerciseviewer.parser.ExerciseParserInfo;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This class implements a parser for the .TUR files which are created with
* CicloSport HACTronic software.
*
* @author Stefan Saring (the C# version was done by Ilja Booij)
*/
public class HAC4TURParser extends AbstractExerciseParser {
/**
* Informations about this parser.
*/
private final ExerciseParserInfo info = new ExerciseParserInfo("HACTronic", new String[]{"tur", "TUR"});
/**
* String that's always on top of TUR files.
*/
private static final String VERSION_HEADER_STRING = "HACtronic - Tour";
/**
* Most interesting values can be found on fixed lines (counting from 0.)
*/
private interface FilePosition {
public static final int VERSION_HEADER = 0;
public static final int START_DATE = 7;
public static final int START_TIME = 8;
public static final int NR_OF_LINES_IN_NOTE = 9;
public static final int DURATION = 11;
public static final int AVERAGE_SPEED = 15;
public static final int AVERAGE_HEART_RATE = 19;
public static final int HR_LIMIT_UPPER = 35;
public static final int HR_LIMIT_LOWER = 36;
public static final int ODOMETER = 39;
public static final int TOTAL_EXERCISE_TIME = 40;
public static final int NR_SAMPLES = 53;
public static final int BEGIN_SAMPLES = 54;
}
/**
* Private inner class which is used for reading samples from the file.
*/
private static class Sample {
private int[] contents = new int[20];
private int distance = 0;
private int altitude;
private int heartRate;
private int cadence;
private int temperature;
private long time;
/**
* Construct a new sample with the sample bytes. This will initialize
* all sample values.
*/
public Sample(int[] contents) throws EVException {
if (contents.length != 20) {
throw new EVException("Length of a sample should be 20 bytes!");
}
this.contents = contents;
initializeValues();
}
/**
* distance in kilometers
*/
public int getDistance() {
return distance;
}
/**
* altitude in meters
*/
public int getAltitude() {
return altitude;
}
/**
* heart rate
*/
public int getHeartRate() {
return heartRate;
}
/**
* cadence
*/
public int getCadence() {
return cadence;
}
/**
* time
*/
public long getTime() {
return time;
}
/**
* temperature in degrees celcius
*/
public int getTemperature() {
return temperature;
}
private void initializeValues() {
this.distance = calculateDistance();
this.altitude = calculateAltitude();
this.heartRate = calculateHeartRate();
this.cadence = calculateCadence();
this.temperature = calculateTemperature();
this.time = calculateTime();
}
private long calculateTime() {
int[] timeBytes = Arrays.copyOfRange(contents, 0, 0 + 4);
return (timeBytes[0] & 0xff)
+ ((timeBytes[1] << 8) & 0xff00)
+ ((timeBytes[2] << 16) & 0xff0000)
+ ((timeBytes[3] << 24) & 0xff000000);
}
private int calculateDistance() {
int[] distanceBytes = Arrays.copyOfRange(contents, 8, 8 + 4);
int distance = 0;
for (int theByte = 0; theByte < distanceBytes.length; theByte++) {
distance += distanceBytes[theByte] * (int) Math.pow(2, theByte * 8);
}
return distance;
}
private int calculateAltitude() {
int[] altitudeBytes = Arrays.copyOfRange(contents, 12, 12 + 2);
int altitude = 0;
for (int theByte = 0; theByte < altitudeBytes.length; theByte++) {
altitude += altitudeBytes[theByte] * (int) Math.pow(2, theByte * 8);
}
return altitude;
}
private int calculateHeartRate() {
return contents[14];
}
private int calculateCadence() {
return contents[15];
}
private int calculateTemperature() {
return contents[16];
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("distance = ").append(distance).append(", ");
builder.append("altitude = ").append(altitude).append(", ");
builder.append("HR = ").append(heartRate).append(", ");
builder.append("Cadence = ").append(cadence).append(", ");
builder.append("Temp = ").append(temperature).append(", ");
builder.append("Time (seconds) = ").append(time).append(", ");
return builder.toString();
}
}
private String[] fileContents;
private int[] fileContentsBytes;
private int nrOfLinesInNote;
private int sampleInterval;
@Override
public ExerciseParserInfo getInfo() {
return info;
}
@Override
public EVExercise parseExercise(String filename) throws EVException {
// read file to array of strings and to array of bytes.
// we'll need both as the tur file contains both text (header)
// and binary (samples) information.
fileContents = readFileToStringArray(filename);
fileContentsBytes = readFileToByteArray(filename);
// Create a new exercise file and give it the right type
EVExercise exercise = new EVExercise();
exercise.setFileType(EVExercise.ExerciseFileType.HAC4TUR);
// check the first line to see if we're really dealing with a HAC4 TUR file
String strVersion = fileContents[FilePosition.VERSION_HEADER];
if (strVersion == null || !strVersion.equals(VERSION_HEADER_STRING)) {
throw new EVException("Failed to read HAC4 TUR File. Can't find correct header in file");
}
exercise.setDeviceName(strVersion);
// get the nr of lines there are in the note. This is important,
// as this decides were all the following data can be found.
nrOfLinesInNote = readInteger(FilePosition.NR_OF_LINES_IN_NOTE);
// set recording mode. For now, just set it to always having recorded
// speed, cadence, altitude, but not Power.
RecordingMode recMode = new RecordingMode();
exercise.setRecordingMode(recMode);
recMode.setSpeed(readFloat(FilePosition.AVERAGE_SPEED + nrOfLinesInNote) > 0.0);
recMode.setAltitude(true);
recMode.setPower(false);
recMode.setTemperature(true);
// get date and time
String strDateAndTime = readLine(FilePosition.START_DATE) + "-" + readLine(FilePosition.START_TIME);
try {
exercise.setDateTime(LocalDateTime.parse(strDateAndTime, DateTimeFormatter.ofPattern("dd.MM.yyy-HH:mm")));
} catch (Exception e) {
throw new EVException("Failed to read exercise date and time from string '" + strDateAndTime + "'...", e);
}
// get duration of exercise. The duration is recorded in seconds, so this one is easy.
exercise.setDuration(readInteger(FilePosition.DURATION + nrOfLinesInNote) * 10);
// get average heart rate
exercise.setHeartRateAVG((short) Math.round(readFloat(FilePosition.AVERAGE_HEART_RATE + nrOfLinesInNote)));
// odometer
exercise.setOdometer(readInteger(FilePosition.ODOMETER + nrOfLinesInNote));
// total exercise time, the total exercise time is recorded in seconds, so this needs to be calculated to minutes.
exercise.setSumExerciseTime(readInteger(FilePosition.TOTAL_EXERCISE_TIME + nrOfLinesInNote) / 60);
// set recording interval
sampleInterval = readSampleInterval(FilePosition.BEGIN_SAMPLES + nrOfLinesInNote);
exercise.setRecordingInterval((short) sampleInterval);
// read all samples
exercise.setSampleList(readSamples(
FilePosition.NR_SAMPLES + nrOfLinesInNote,
FilePosition.BEGIN_SAMPLES + nrOfLinesInNote));
// now that we have the samples, other values can be calculated.
exercise.setAltitude(calculateAltitudes(exercise));
exercise.setCadence(calculateCadence(exercise));
recMode.setCadence(exercise.getCadence().getCadenceMax() > 0);
exercise.setSpeed(calculateSpeed(exercise));
exercise.setTemperature(calculateTemperature(exercise));
// calculate heartrate limits (only one available in HAC files)
exercise.setHeartRateLimits(new HeartRateLimit[1]);
exercise.getHeartRateLimits()[0] = calculateHeartRate(exercise);
// get lap data
exercise.setLapList(getLaps(exercise));
// we're done :-)
return exercise;
}
/**
* Calculate the altitude information.
*/
private ExerciseAltitude calculateAltitudes(EVExercise exercise) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
int ascent = 0;
long total = 0;
short prevAlt = Short.MAX_VALUE;
for (ExerciseSample sample : exercise.getSampleList()) {
short alt = sample.getAltitude();
// update values
min = Math.min(alt, min);
max = Math.max(alt, max);
if (alt > prevAlt) {
ascent += (alt - prevAlt);
}
prevAlt = alt;
total += alt;
}
ExerciseAltitude ea = new ExerciseAltitude();
ea.setAltitudeMin((short) min);
ea.setAltitudeMax((short) max);
ea.setAltitudeAVG((short) (total / exercise.getSampleList().length));
ea.setAscent(ascent);
return ea;
}
/**
* Calculates the cadence information.
*/
private ExerciseCadence calculateCadence(EVExercise exercise) {
int maximum = Integer.MIN_VALUE;
long total = 0;
for (ExerciseSample sample : exercise.getSampleList()) {
short cadence = sample.getCadence();
maximum = Math.max(cadence, maximum);
total += cadence;
}
ExerciseCadence ec = new ExerciseCadence();
ec.setCadenceMax((short) maximum);
ec.setCadenceAVG((short) (total / exercise.getSampleList().length));
return ec;
}
/**
* Calculates the speed information.
*/
private ExerciseSpeed calculateSpeed(EVExercise exercise) {
float max = Float.MIN_VALUE;
int nrMovingIntervals = 0; // nr of intervals bike was moving
float speedAVG = 0.0f;
int previousDistance = 0;
for (ExerciseSample sample : exercise.getSampleList()) {
if (sample.getDistance() > previousDistance) {
nrMovingIntervals++;
speedAVG = speedAVG + ((sample.getSpeed() - speedAVG) / nrMovingIntervals);
}
max = Math.max(max, sample.getSpeed());
previousDistance = sample.getDistance();
}
ExerciseSpeed es = new ExerciseSpeed();
int sampleCount = exercise.getSampleList().length;
es.setDistance(exercise.getSampleList()[sampleCount - 1].getDistance());
es.setSpeedAVG(speedAVG);
es.setSpeedMax(max);
return es;
}
/**
* Calculates the temperature information.
*/
private ExerciseTemperature calculateTemperature(EVExercise exercise) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
long total = 0;
for (ExerciseSample sample : exercise.getSampleList()) {
min = Math.min(min, sample.getTemperature());
max = Math.max(max, sample.getTemperature());
total += sample.getTemperature();
}
ExerciseTemperature et = new ExerciseTemperature();
et.setTemperatureMax((short) max);
et.setTemperatureMin((short) min);
et.setTemperatureAVG((short) (total / exercise.getSampleList().length));
return et;
}
/**
* Calculates the heartrate range information.
*/
private HeartRateLimit calculateHeartRate(EVExercise exercise) throws EVException {
short upper = (short) readInteger(FilePosition.HR_LIMIT_UPPER + nrOfLinesInNote);
short lower = (short) readInteger(FilePosition.HR_LIMIT_LOWER + nrOfLinesInNote);
int max = Integer.MIN_VALUE;
int intervalsBelow = 0;
int intervalsBetween = 0;
int intervalsAbove = 0;
for (ExerciseSample sample : exercise.getSampleList()) {
short hr = sample.getHeartRate();
max = Math.max(max, hr);
if (hr < lower) {
intervalsBelow += 1;
} else if (hr >= lower && hr <= upper) {
intervalsBetween += 1;
} else {// above upper
intervalsAbove += 1;
}
}
exercise.setHeartRateMax((short) max);
HeartRateLimit hrl = new HeartRateLimit();
hrl.setLowerHeartRate(lower);
hrl.setUpperHeartRate(upper);
hrl.setTimeBelow(intervalsBelow * sampleInterval);
hrl.setTimeWithin(intervalsBetween * sampleInterval);
hrl.setTimeAbove(intervalsAbove * sampleInterval);
return hrl;
}
/**
* Gets the laps from the file. As I don't have any tur files with lap info,
* I cannot do anything with this yet. Maybe later.. For now I'll just fill
* a Lap object with values from the complete exercise.
*/
private Lap[] getLaps(EVExercise exercise) {
ExerciseSample lastSample = exercise.getSampleList()[exercise.getSampleList().length - 1];
Lap lap = new Lap();
lap.setTimeSplit(exercise.getDuration());
lap.setHeartRateSplit(lastSample.getHeartRate());
lap.setHeartRateMax(exercise.getHeartRateMax());
lap.setHeartRateAVG(exercise.getHeartRateAVG());
lap.setAltitude(new LapAltitude());
lap.getAltitude().setAltitude(lastSample.getAltitude());
lap.getAltitude().setAscent(exercise.getAltitude().getAscent());
lap.setSpeed(new LapSpeed());
lap.getSpeed().setCadence(lastSample.getCadence());
lap.getSpeed().setDistance(exercise.getSpeed().getDistance());
lap.getSpeed().setSpeedEnd(lastSample.getSpeed());
lap.getSpeed().setSpeedAVG(exercise.getSpeed().getSpeedAVG());
lap.setTemperature(new LapTemperature());
lap.getTemperature().setTemperature(lastSample.getTemperature());
return new Lap[]{lap};
}
/**
* Returns the sample interval in seconds.
*
* @param fpBeginSamples file position at which the sample data begins
*/
private int readSampleInterval(int fpBeginSamples) throws EVException {
// find length of all strings to this point
int lengthUntilSamples = 0;
for (int i = 0; i < fpBeginSamples; i++)
lengthUntilSamples += fileContents[i].length() + 1;
// start reading samples
int[] firstSampleBytes = Arrays.copyOfRange(fileContentsBytes, lengthUntilSamples, lengthUntilSamples + 20);
int[] secondSampleBytes = Arrays.copyOfRange(fileContentsBytes, lengthUntilSamples + 20, lengthUntilSamples + 40);
Sample firstSample = new Sample(firstSampleBytes);
Sample secondSample = new Sample(secondSampleBytes);
return (int) (secondSample.getTime() - firstSample.getTime());
}
/**
* Reads the raw sample data. This uses the private inner Sample class for
* temporary storing the samples. It parses the number of samples and the sample
* data from the specified file positions.
*
* @param fpNrSamples file position for the number of samples
* @param fpBeginSamples file position for the begin of sample data
* @return the created array of exercise samples
*/
private ExerciseSample[] readSamples(int fpNrSamples, int fpBeginSamples) throws EVException {
int nrSamples = readInteger(fpNrSamples);
// find length of all strings to this point
int lengthUntilSamples = 0;
for (int i = 0; i < fpBeginSamples; i++) {
lengthUntilSamples += fileContents[i].length() + 1;
}
// start reading samples
List<Sample> samples = new ArrayList<>();
for (int i = 0; i < nrSamples; i++) {
int startFrom = lengthUntilSamples + (i * 20);
int[] sampleBytes = Arrays.copyOfRange(fileContentsBytes, startFrom, startFrom + 20);
samples.add(new Sample(sampleBytes));
}
ExerciseSample[] eSamples = new ExerciseSample[nrSamples];
int previousDistance = 0;
long firstTimestamp = nrSamples > 0 ? samples.get(0).getTime() : 0;
for (int i = 0; i < nrSamples; i++) {
Sample sample = samples.get(i);
eSamples[i] = new ExerciseSample();
eSamples[i].setTimestamp((sample.getTime() - firstTimestamp) * 1000L);
eSamples[i].setHeartRate((short) sample.getHeartRate());
eSamples[i].setAltitude((short) sample.getAltitude());
eSamples[i].setCadence((short) sample.getCadence());
int distanceDiff = sample.getDistance() - previousDistance;
previousDistance = sample.getDistance();
eSamples[i].setDistance(sample.getDistance() * 10);
eSamples[i].setSpeed(((float) distanceDiff / (float) sampleInterval) * (float) 3.6 * 10f);
eSamples[i].setTemperature((short) sample.getTemperature());
}
return eSamples;
}
/**
* Read a float from the file contents.
*/
private float readFloat(int pos) throws EVException {
try {
return Float.parseFloat(readLine(pos));
} catch (Exception e) {
throw new EVException("Invalid value for float at position " + pos, e);
}
}
/**
* Read an integer from the file contents.
*/
private int readInteger(int pos) throws EVException {
try {
return Integer.parseInt(readLine(pos));
} catch (Exception e) {
throw new EVException("Invalid value for integer at position " + pos, e);
}
}
/**
* Read a line from the file contents.
*/
private String readLine(int pos) {
return fileContents[pos];
}
}