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 de.saring.util.unitcalc.CalculationUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
/**
* This implementation of an ExerciseParser is for reading PWX files of the
* Timex Race Trainer watch. It will likely work with other PWX files
* downloaded from the Training Peaks website. It works well and has been
* tested with Chrono PWX files but has not been tested with Interval PWX files.
* <br/>
* It is assumed that the exercise files have the extension ".pwx".
* <br/>
* This file has been completely rewritten from the initial version
* that was based on PolarHsrRawParser.java by Remco den Breeje
* which is based on PolarSRawParser.java by Stefan Saring
* <br/>
* TODO: This parser contains a lot of unused code (commented out),
* remove it when not needed anymore.
* <p/>
* 9/10/2010 Version 1.2
* Added support for Global Trainer Pwx Files
* Changed Lap Distance to Distance since beginning of exercise
* 01/03/2012 Version 1.3
* Added support for Timex Ironman Run Trainer Pwx Files
*
* @author Robert C. Schultz, Stefan Saring
* @version 1.3
*/
public class TimexPwxParser extends AbstractExerciseParser {
/**
* Informations about this parser.
*/
private final ExerciseParserInfo info = new ExerciseParserInfo("Timex PWX", new String[]{"pwx", "PWX"});
private static class MinMaxAvg {
private float min = 0;
private float max = 0;
private float avg = 0;
public void setMin(float in) {
min = in;
}
public float getMin() {
return min;
}
public void setMax(float in) {
max = in;
}
public float getMax() {
return max;
}
public void setAvg(float in) {
avg = in;
}
public float getAvg() {
return avg;
}
}
private MinMaxAvg node2MinMaxAvg(Node inNode) {
MinMaxAvg result = new MinMaxAvg();
NamedNodeMap attributes = inNode.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
if (attributes.item(i).getNodeName().equals("max")) {
result.setMax(Float.valueOf(attributes.item(i).getTextContent()));
} else if (attributes.item(i).getNodeName().equals("min")) {
result.setMin(Float.valueOf(attributes.item(i).getTextContent()));
} else if (attributes.item(i).getNodeName().equals("avg")) {
result.setAvg(Float.valueOf(attributes.item(i).getTextContent()));
}
}
return result;
}
private static class SummaryData {
private double beginning = 0;
private double duration = 0;
private int work = 0;
private MinMaxAvg hr;
// private double durationStopped = 0;
// private float tss = 0;
// private int normalizedPower = 0;
private MinMaxAvg speed;
// private MinMaxAvg power;
// private MinMaxAvg torque;
// private MinMaxAvg cadence;
private float distance = 0;
private MinMaxAvg altitude;
// private MinMaxAvg temperature;
// private int variabilityIndex = 0;
// private float climbingElevation = 0;
public void setBeginning(double in) {
beginning = in;
}
public double getBeginning() {
return beginning;
}
public void setDuration(double in) {
duration = in;
}
public double getDuration() {
return duration;
}
public void setWork(int in) {
work = in;
}
public int getWork() {
return work;
}
public void setHr(MinMaxAvg in) {
hr = in;
}
public MinMaxAvg getHr() {
return hr;
}
// public void setDurationStopped(double in){ durationStopped = in; }
// public double getDurationStopped(){ return durationStopped; }
// public void setTss(float in){ tss = in; }
// public float getTss(){ return tss; }
// public void setNormalizedPower(int in){ normalizedPower = in; }
// public int getNormalizedPower(){ return normalizedPower; }
public void setSpeed(MinMaxAvg in) {
speed = in;
}
public MinMaxAvg getSpeed() {
return speed;
}
// public void setPower(MinMaxAvg in){ power = in; }
// public MinMaxAvg getPower(){ return power ; }
// public void setTorque(MinMaxAvg in){ torque = in; }
// public MinMaxAvg getTorque(){ return torque; }
// public void setCadence(MinMaxAvg in){ cadence = in; }
// public MinMaxAvg getCadence(){ return cadence; }
public void setDistance(float in) {
distance = in;
}
public float getDistance() {
return distance;
}
public void setAltitude(MinMaxAvg in) {
altitude = in;
}
public MinMaxAvg getAltitude() {
return altitude;
}
// public void setTemperature(MinMaxAvg in){ temperature = in; }
// public MinMaxAvg getTemperature(){ return temperature; }
// public void setVariabilityIndex(int in){ variabilityIndex = in; }
// public int getVariabilityIndex(){ return variabilityIndex; }
// public void setClimbingElevation(float in){ climbingElevation = in; }
// public float getClimbingElevation(){ return climbingElevation; }
}
@Override
public ExerciseParserInfo getInfo() {
return info;
}
int countNodeItems(Node node, String string2count) {
// Given a Node and a Child Node Name, count the number of children with that node name
NodeList children = node.getChildNodes();
int numChildren = children.getLength();
String currentNodeName = null;
int numMatches = 0;
for (int i = 0; i < numChildren; i++) {
currentNodeName = children.item(i).getNodeName();
if (currentNodeName.equals(string2count)) {
numMatches++;
}
}
return numMatches;
}
private EVExercise parseWorkoutNode(EVExercise exercise, Node workoutNode) {
NodeList children = workoutNode.getChildNodes();
String childName;
for (int i = 0; i < children.getLength(); i++) {
childName = children.item(i).getNodeName();
switch (childName) {
case "athlete":
// Nothing to do with this yet...or is there?
break;
case "goal":
// Not in files downloaded directly from the Timex 843/844
// Probably is in the files downloaded from the online software
break;
case "sportType":
// obtain sportType
exercise.setType(children.item(i).getTextContent());
break;
case "cmt":
case "code":
// Not implemented
break;
case "device":
// parse device
exercise = parseWorkoutDeviceNode(exercise, children.item(i));
// The passing an object and then assigning the result of the method to the same object is akward to me.
// It seems like that could result in a lot of time moving data. My understanding is that is not the case
// in Java though.
case "time":
// obtain start time
try {
String strDateTime = children.item(i).getTextContent();
exercise.setDateTime(LocalDateTime.parse(strDateTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} catch (Exception e) {
exercise.setDateTime(null);
}
break;
case "summarydata":
// parse workout summary data
SummaryData workoutSummary = parseSummaryData(children.item(i));
exercise.setDuration((int) workoutSummary.getDuration() * 10);
exercise.setSumExerciseTime((int) workoutSummary.getDuration() / 60); // Not sure why these are different.
exercise.setSumRideTime((int) workoutSummary.getDuration() / 60); // Assume some watches keep track of bike specific time..This one doesn't
exercise.setEnergy((int) (workoutSummary.getWork() * (0.238845896627495939619))); // Convert to Calories first
//exercise.setEnergyTotal((int) (workoutSummary.getWork() * (0.238845896627495939619))); // Using the value in device/extensions
if (workoutSummary.getHr() != null) {
exercise.setHeartRateMax((short) workoutSummary.getHr().getMax());
// exercise.setHeartRateMin((short) workoutSummary.getHr().getMin()); // Not implemented in EVExercise
exercise.setHeartRateAVG((short) workoutSummary.getHr().getAvg());
}
exercise.setOdometer((int) workoutSummary.getDistance() / 1000);
if (workoutSummary.getSpeed() != null) {
ExerciseSpeed workoutSpeed = new ExerciseSpeed();
workoutSpeed.setDistance((int) workoutSummary.getDistance());
workoutSpeed.setSpeedAVG(workoutSummary.getSpeed().getAvg() * (float) 3.6);
workoutSpeed.setSpeedMax(workoutSummary.getSpeed().getMax() * (float) 3.6);
exercise.setSpeed(workoutSpeed);
}
if (workoutSummary.getAltitude() != null) {
ExerciseAltitude workoutAltitude = new ExerciseAltitude();
workoutAltitude.setAltitudeAVG((short) workoutSummary.getAltitude().getAvg());
workoutAltitude.setAltitudeMax((short) workoutSummary.getAltitude().getMax());
workoutAltitude.setAltitudeMin((short) workoutSummary.getAltitude().getMin());
exercise.setAltitude(workoutAltitude);
}
break;
case "segment":
case "sample":
// This is handled after parsing everything else
break;
case "extension":
// Used for Timex Global Trainer and possibly others.
exercise = parseWorkoutExtensionNode(exercise, children.item(i));
break;
}
}
// parse lap segments
exercise = parseWorkoutSegments(exercise, workoutNode);
// parse samples
exercise = parseWorkoutSamples(exercise, workoutNode);
return exercise;
}
private EVExercise parseWorkoutExtensionNode(EVExercise exercise, Node workoutExtensionNode) {
// Used for Global Trainer
NodeList children = workoutExtensionNode.getChildNodes();
String childName;
for (int i = 0; i < children.getLength(); i++) {
childName = children.item(i).getNodeName();//
if (childName.equals("ascent")) {
exercise.getAltitude().setAscent(Integer.valueOf(children.item(i).getTextContent()));
} else if (childName.equals("descent")) {
// obtain descent - not used in EVExercise
} else if (childName.equals("points")) {
// points - not used in EVExercise
}
}
return exercise;
}
private EVExercise parseWorkoutDeviceNode(EVExercise exercise, Node deviceNode) {
NodeList children = deviceNode.getChildNodes();
String childName;
String make = "";
String model = "";
for (int i = 0; i < children.getLength(); i++) {
childName = children.item(i).getNodeName();
if (childName.equals("extension")) {
// parse extension
exercise = parseDeviceExtensionNode(exercise, children.item(i));
} else if (childName.equals("make")) {
make = children.item(i).getTextContent();
} else if (childName.equals("model")) {
model = children.item(i).getTextContent();
if (model.equals("Global Trainer") || model.equals("Run Trainer")) {
exercise = setGlobalTrainerRecordingMode(exercise);
exercise = setGlobalTrainerZones(exercise);
}
} else if (childName.equals("stopdetectionsetting")) {
// obtain stopdetectionsetting
} else if (childName.equals("elevationchangesetting")) {
// obtain elevationchangesetting
}
}
exercise.setDeviceName((make.isEmpty() ? "" : make + " ") + model);
return exercise;
}
private EVExercise setGlobalTrainerRecordingMode(EVExercise exercise) {
RecordingMode recMode = new RecordingMode();
recMode.setPower(true);
recMode.setLocation(true);
recMode.setCadence(false);
recMode.setAltitude(true);
recMode.setSpeed(true);
recMode.setBikeNumber((byte) 0);
recMode.setIntervalExercise(false); //
exercise.setRecordingMode(recMode);
return exercise;
}
private EVExercise setGlobalTrainerZones(EVExercise exercise) {
HeartRateLimit Zones[] = new HeartRateLimit[6];
for (int i = 0; i < 6; i++) {
Zones[i] = new HeartRateLimit();
Zones[i].setUpperHeartRate((short) (50 + (i + 1) * 25));
Zones[i].setLowerHeartRate((short) (50 + i * 25));
Zones[i].setAbsoluteRange(true);
Zones[i].setTimeAbove(0);
Zones[i].setTimeBelow(0);
Zones[i].setTimeWithin(0);
}
exercise.setHeartRateLimits(new HeartRateLimit[6]);
System.arraycopy(Zones, 0, exercise.getHeartRateLimits(), 0, 6);
return exercise;
}
private EVExercise parseDeviceExtensionNode(EVExercise exercise, Node deviceExtensionNode) {
NodeList children = deviceExtensionNode.getChildNodes();
String childName;
for (int i = 0; i < children.getLength(); i++) {
childName = children.item(i).getNodeName();//
if (childName.equals("settings")) {
exercise = parseDeviceExtensionSettingsNode(exercise, children.item(i));
} else if (childName.equals("stoppage")) {
// obtain stoppage - not used in EVExercise
}
}
return exercise;
}
private EVExercise parseDeviceExtensionSettingsNode(EVExercise exercise, Node deviceExtensionSettingsNode) {
// None of this data is explicitly specified in the pwx.xsd.
// It is in the pwx files from the Timex watch though.
//------------------------------------------------------------
NodeList children = deviceExtensionSettingsNode.getChildNodes();
String childName;
// Create and Initialize Heart Rate Limits
HeartRateLimit Zones[] = new HeartRateLimit[6];
for (int i = 0; i < 6; i++) {
Zones[i] = new HeartRateLimit();
Zones[i].setUpperHeartRate((short) 0);
Zones[i].setLowerHeartRate((short) 0);
Zones[i].setAbsoluteRange(true);
Zones[i].setTimeAbove(0);
Zones[i].setTimeBelow(0);
Zones[i].setTimeWithin(0);
}
for (int i = 0; i < children.getLength(); i++) {
childName = children.item(i).getNodeName();//
if (childName.equals("CHRMaxHR") || childName.equals("HRMMaxHR")) {
// Obtain Max HR - This is basis for Timex Zones
short HRMMaxHR = Short.valueOf(children.item(i).getTextContent());
double HRZonesPercentages[] = {1, .9, .8, .7, .6, .5};
for (int k = 0; k < 5; k++) {
Zones[k] = new HeartRateLimit();
Zones[k].setUpperHeartRate((short) (HRZonesPercentages[k] * HRMMaxHR));
Zones[k].setLowerHeartRate((short) (1 + HRZonesPercentages[k + 1] * HRMMaxHR));
Zones[k].setAbsoluteRange(true);
Zones[k].setTimeAbove(0);
Zones[k].setTimeBelow(0);
Zones[k].setTimeWithin(0);
}
} else if (childName.equals("CHRManualZoneHigherLimit") || childName.equals("HRMBpmManHi")) {
// obtain Manual Zone Higher Limit
Zones[5].setUpperHeartRate(Short.valueOf(children.item(i).getTextContent()));
} else if (childName.equals("CHRManualZoneLowerLimit") || childName.equals("HRMBpmManLo")) {
// obtain Manual Zone Lower Limit
Zones[5].setLowerHeartRate(Short.valueOf(children.item(i).getTextContent()));
} else if (childName.equals("HasHRMData")) {
// does file have hrm data
} else if (childName.equals("KCalPerDevice")) {
// obtain kCalPerDevice
exercise.setEnergyTotal(Integer.valueOf(children.item(i).getTextContent()));
} else if (childName.equals("WorkoutType")) {
// obtain workout type - If not Chrono, then stop parsing since intervals aren't yet implemented
if (!children.item(i).getTextContent().equals("Chrono")) {
// Not sure how to handle this... I want it to stop parsing and report an unsupported file.
}
} else {
// obtain Alarm flags 1-3 (Enabled/Disabled)
// obtain Alarm Hours 1-3
// obtain Alarm Minutes 1-3
// obtain Alarm Type 1-3
// obtain Application that generated the file
// obtain AVG Lap time
// obtain Best Lap time
// obtain best lap number
// obtain average HRs for each lap (CHRDatabaseTable##)
// Don't Care about HRM display format
// obtain Time In Target Zone
// obtain recovery end bpm
// obtain recovery start bpm
// obtain CHRSplitDuration
// obtain CHRStatus
// obtain Target HR Zone
// obtain ManZone Percentage Hi (This is different then the Manual HR Zone in bpm)
// obtain ManZone Percentage Low (This is different then the Manual HR Zone in bpm)
// obtain weight
// obtain weight units
// does file have Recovery BPM
// obtain interval data --- Not implementing this yet ---
// obtain watch ID - This is a String
// obtain watch manufacturer
// obtain watch model
// obtain version number
// obtain workout number - Not sure what this number is
}
}
// don't care about Button Beep, Hourly Chime, Night Mode, Night Mode Duration, Display Format
// don't care about HRMAlertApp
// don't care about Display Units (need to check to see if changing to Percentage changes the way data is stored.)
// don't care about out of zone alert
// don't care about RCVYPresetIndex
// don't care about the Timer data
// don't care about the Time of Day format /Time Zone (Might care about the time zone if it was actual time zone but its not)
// don't care if - is file Locked
// do laps overflow - might care about this but not sure when
// don't care about some ucaddr# values
RecordingMode recMode = new RecordingMode();
recMode.setPower(false);
recMode.setCadence(false);
recMode.setAltitude(false);
recMode.setSpeed(false);
recMode.setBikeNumber((byte) 0);
recMode.setIntervalExercise(false); //
exercise.setRecordingMode(recMode);
exercise.setHeartRateLimits(new HeartRateLimit[6]);
System.arraycopy(Zones, 0, exercise.getHeartRateLimits(), 0, 6);
return exercise;
}
private SummaryData parseSummaryData(Node summaryDataNode) {
SummaryData nodeSummaryData = new SummaryData();
NodeList children = summaryDataNode.getChildNodes();
String childName;
for (int i = 0; i < children.getLength(); i++) {
childName = children.item(i).getNodeName();
if (childName.equals("beginning")) {
// obtain beginning time
nodeSummaryData.setBeginning(Double.valueOf(children.item(i).getTextContent()));
} else if (childName.equals("duration")) {
// obtain duration
nodeSummaryData.setDuration(Double.valueOf(children.item(i).getTextContent()));
} else if (childName.equals("hr")) {
// obtain hr (MinMaxAvg) (bpm)
nodeSummaryData.setHr(node2MinMaxAvg(children.item(i)));
} else if (childName.equals("work")) {
// obtain work (Apparently Not used in Laps) (kJ)
nodeSummaryData.setWork(Integer.valueOf(children.item(i).getTextContent()));
} else if (childName.equals("spd")) {
// obtain spd (MinMaxAvg) (meters/second)
nodeSummaryData.setSpeed(node2MinMaxAvg(children.item(i)));
} else if (childName.equals("alt")) {
// obtain altitude (MinMaxAvg) (meters)
nodeSummaryData.setAltitude(node2MinMaxAvg(children.item(i)));
} else if (childName.equals("dist")) {
// obtain distance (meters)
nodeSummaryData.setDistance(Float.valueOf(children.item(i).getTextContent()));
}
// 1st time its for the entire workout
// remaining times is for the Laps
// obtain duration stopped
// obtain tss
// obtain normalizedPower (watts)
// obtain pwr (MinMaxAvg) (watts)
// obtain torq (MinMaxAvg) (nM)
// obtain cadence (MinMaxAvg) (rpm)
// obtain temp (MinMaxAvg) (C)
// obtain variabilityIndex - Not sure what this is
// obtain climbingelevation
}
return nodeSummaryData; // Probably don't want to pass and return the Exercise itself.
}
private EVExercise parseWorkoutSegments(EVExercise exercise, Node workoutNode) {
ArrayList<Lap> laps = new ArrayList<>();
// obtain segment name ( Either laps or Workout Summary )
// parse segment summary data
// Create and initialize a holding Lap
// Finished Holding Lap
NodeList children = workoutNode.getChildNodes();
NodeList segmentChildren = null;
String childName;
float runningDistance = 0;
for (int i = 0; i < children.getLength(); i++) {
childName = children.item(i).getNodeName();
if (childName.equals("segment")) {
segmentChildren = children.item(i).getChildNodes();
Lap lap = new Lap();
LapAltitude lapAlt = new LapAltitude();
LapSpeed lapSpd = new LapSpeed();
LapTemperature lapTmp = new LapTemperature();
lapAlt.setAscent(0);
lapAlt.setAltitude((short) 0);
lap.setAltitude(lapAlt);
lapSpd.setCadence((short) 0);
lapSpd.setDistance(402); // I typically mark each lap at the 1/4 mile. A popup might be nice to fill in the rest.
lapSpd.setSpeedAVG((float) 0.0);
lapSpd.setSpeedEnd((float) 0.0);
lap.setSpeed(lapSpd);
lapTmp.setTemperature((short) 25);
lap.setTemperature(lapTmp);
lap.setHeartRateSplit((short) 0);
lap.setHeartRateMax((short) 0);
for (int j = 0; j < segmentChildren.getLength(); j++) {
childName = segmentChildren.item(j).getNodeName();
if (childName.equals("summarydata")) {
SummaryData segmentSummary = parseSummaryData(segmentChildren.item(j));
lap.setTimeSplit((int) ((segmentSummary.getDuration() + segmentSummary.getBeginning()) * 10));
if (segmentSummary.getDistance() != 0) {
runningDistance += segmentSummary.getDistance();
lapSpd.setDistance((int) runningDistance);
lapSpd.setSpeedAVG((float) (3.600 * segmentSummary.getDistance() / segmentSummary.getDuration())); // Assumes 1/4 Mile Lap
lapSpd.setSpeedEnd((float) 0.0);
} else {
runningDistance += 402.336;
lapSpd.setDistance((int) runningDistance);
lapSpd.setSpeedAVG((float) (3.6 * 402.336 / segmentSummary.getDuration())); // Assumes 1/4 Mile Lap
lapSpd.setSpeedEnd((float) 0.0);
}
lap.setSpeed(lapSpd);
if (segmentSummary.getHr() != null) {
lap.setHeartRateAVG((short) segmentSummary.getHr().getAvg());
lap.setHeartRateMax((short) segmentSummary.getHr().getMax());
}
if (segmentSummary.getAltitude() != null) {
lapAlt.setAltitude((short) segmentSummary.getAltitude().getMax());
lapAlt.setAscent((int) (segmentSummary.getAltitude().getMax() - segmentSummary.getAltitude().getMin()));
lap.setAltitude(lapAlt);
}
}
}
// sometimes there are laps (mostly the last one) with lap length 0 => ignore them
Lap previousLap = laps.isEmpty() ? null : laps.get(laps.size() - 1);
if (previousLap == null || lap.getTimeSplit() > previousLap.getTimeSplit()) {
laps.add(lap);
}
}
}
if (!laps.isEmpty()) {
exercise.setLapList(laps.toArray(new Lap[laps.size()]));
}
return exercise;
}
private static float getDistanceFromPositions(Position startPosition, Position stopPosition) { //float lat1, float lng1, float lat2, float lng2) {
double earthRadius = 6369.6; //3958.75;
double dLat = Math.toRadians(stopPosition.getLatitude() - startPosition.getLatitude());
double dLng = Math.toRadians(stopPosition.getLongitude() - startPosition.getLongitude());
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+ Math.cos(Math.toRadians(startPosition.getLatitude())) * Math.cos(Math.toRadians(stopPosition.getLatitude()))
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double dist = earthRadius * c;
int meterConversion = 1000; // 1609;
if (dist < 0) {
dist = 0 - dist;
}
return (float) (dist * meterConversion);
}
private EVExercise parseWorkoutSamples(EVExercise exercise, Node workoutNode) {
// obtain all the sample data.
int totalSamples = countNodeItems(workoutNode, "sample");
int currentSampleNumber = 0;
float lastDistance = 0;
boolean distanceinsample = false;
boolean firstsample = true;
exercise.setSampleList(new ExerciseSample[totalSamples]);
double lastOffset = 0;
double currentOffset = 0;
Position lastPosition = new Position(0, 0);
NodeList children = workoutNode.getChildNodes();
NodeList sampleChildren = null;
String childName;
ExerciseSample lastSample = new ExerciseSample(); // Stop the jitters... assumes no
Double latitude = 0.0, longitude = 0.0;
double belowZone[] = {0, 0, 0, 0, 0, 0};
double inZone[] = {0, 0, 0, 0, 0, 0};
double aboveZone[] = {0, 0, 0, 0, 0, 0};
int istop = children.getLength(); // getLength() is a slow function so keep it out of the loop.
for (int i = 0; i < istop; i++) {
childName = children.item(i).getNodeName();
if (childName.equals("sample")) {
ExerciseSample sample = new ExerciseSample();
sampleChildren = children.item(i).getChildNodes();
int jstop = sampleChildren.getLength();
for (int j = 0; j < jstop; j++) {
childName = sampleChildren.item(j).getNodeName();
if (childName.equals("timeoffset")) {
if (currentOffset != 0)
lastOffset = currentOffset;
currentOffset = Double.valueOf(sampleChildren.item(j).getTextContent());
sample.setTimestamp((long) (1000 * currentOffset));
} else if (childName.equals("hr")) {
sample.setHeartRate(Short.valueOf(sampleChildren.item(j).getTextContent()));
} else if (childName.equals("spd")) {
sample.setSpeed((float) 3.6 * Float.valueOf(sampleChildren.item(j).getTextContent()).floatValue());
} else if (childName.equals("pwr")) {
// Not implemented in ExerciseSample class
} else if (childName.equals("torq")) {
// Not implemented in ExerciseSample class
} else if (childName.equals("cad")) {
sample.setCadence(Short.valueOf(sampleChildren.item(j).getTextContent()));
exercise.getRecordingMode().setCadence(true);
} else if (childName.equals("dist")) {
double dist = Double.valueOf(sampleChildren.item(j).getTextContent());
sample.setDistance((int) Math.round(dist));
distanceinsample = true;
} else if (childName.equals("lat")) {
latitude = Double.valueOf(sampleChildren.item(j).getTextContent());
} else if (childName.equals("lon")) {
longitude = Double.valueOf(sampleChildren.item(j).getTextContent());
} else if (childName.equals("alt")) {
sample.setAltitude(Float.valueOf(sampleChildren.item(j).getTextContent()).shortValue());
} else if (childName.equals("temp")) {
sample.setTemperature(Float.valueOf(sampleChildren.item(j).getTextContent()).shortValue());
} else if (childName.equals("time")) {
// Not implemented in ExerciseSample
}
}
sample.setPosition(new Position(latitude, longitude));
if (firstsample) {
lastPosition = sample.getPosition();
firstsample = false;
}
if (!distanceinsample) {
lastDistance += getDistanceFromPositions(lastPosition, sample.getPosition());
sample.setDistance((int) lastDistance);
lastPosition = sample.getPosition();
}
// Eliminates the jitters of 0bpm samples... assumes that heart rate won't change instantiously by much and
// that there will only be the occasional missed heart beat. Also fixes the laps not adding up.
if (sample.getHeartRate() == 0)
sample.setHeartRate(lastSample.getHeartRate());
else
lastSample.setHeartRate(sample.getHeartRate());
exercise.getSampleList()[currentSampleNumber++] = sample;
// update Zone information
if (exercise.getHeartRateLimits() != null) {
for (int j = 0; j < 6; j++) {
if (sample.getHeartRate() > exercise.getHeartRateLimits()[j].getUpperHeartRate()) {
aboveZone[j] += (currentOffset - lastOffset);
} else if (sample.getHeartRate() < exercise.getHeartRateLimits()[j].getLowerHeartRate()) {
belowZone[j] += (currentOffset - lastOffset);
} else {
inZone[j] += (currentOffset - lastOffset);
}
}
}
}
}
// Store Zone Information in the exercise file
if (exercise.getHeartRateLimits() != null) {
for (int i = 0; i < 6; i++) {
exercise.getHeartRateLimits()[i].setTimeAbove((short) aboveZone[i]);
exercise.getHeartRateLimits()[i].setTimeBelow((short) belowZone[i]);
exercise.getHeartRateLimits()[i].setTimeWithin((short) inZone[i]);
}
}
exercise.setRecordingInterval((short) 2);
// some models (e.g. Timex Ironman Run Trainer) don't contain statistic date (avg, max, ...)
// => compute the missing data
if (exercise.getSampleList().length > 0) {
computeHeartrateStatisticIfMissing(exercise);
computeSpeedStatisticIfMissing(exercise);
computeAltitudeStatisticIfMissing(exercise);
}
return exercise;
}
private Node findFirstPwx(Document doc) {
// Find the first node of the document that is a pwx and then return it otherwise, return null
// Normally only expect one node at this level but who knows.
NodeList rootNodeList = doc.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
if (rootNodeList.item(i).getNodeName().equals("pwx")) {
return rootNodeList.item(i);
}
}
return null;
}
@Override
public EVExercise parseExercise(String filename) throws EVException {
// create an EVExercise object from this data and set file type
EVExercise exercise = new EVExercise();
exercise.setFileType(EVExercise.ExerciseFileType.TIMEX_PWX);
// Open Document and Get root
DocumentBuilderFactory dbf = null;
DocumentBuilder db = null;
Document doc = null;
Node root = null;
NodeList children = null;
// Open the pwx file
try {
dbf = DocumentBuilderFactory.newInstance(); // DocumentBuilderFactory
db = dbf.newDocumentBuilder(); // DocumentBuilder
doc = db.parse(filename); // Document
root = findFirstPwx(doc); // Node
} catch (Exception e) {
throw new EVException("Failed to open pwx exercise file '" + filename + "' ...", e);
}
if (root != null)
exercise.setFileType(EVExercise.ExerciseFileType.TIMEX_PWX);
else
throw new EVException("Failed to find a pwx node in file '" + filename + "'");
children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i).getNodeName().equals("workout")) {
exercise = parseWorkoutNode(exercise, children.item(i));
}
}
// done :-) ?
return exercise;
}
private void computeHeartrateStatisticIfMissing(EVExercise exercise) {
if (exercise.getHeartRateAVG() == 0) {
double sumHeartrate = 0;
for (ExerciseSample sample : exercise.getSampleList()) {
sumHeartrate += sample.getHeartRate();
exercise.setHeartRateMax((short) Math.max(exercise.getHeartRateMax(), sample.getHeartRate()));
}
exercise.setHeartRateAVG((short) Math.round(sumHeartrate / (double) exercise.getSampleList().length));
}
}
private void computeSpeedStatisticIfMissing(EVExercise exercise) {
if (exercise.getRecordingMode().isSpeed() && exercise.getSpeed() == null) {
ExerciseSpeed exSpeed = new ExerciseSpeed();
exSpeed.setSpeedMax(Float.MIN_VALUE);
exercise.setSpeed(exSpeed);
for (ExerciseSample sample : exercise.getSampleList()) {
exSpeed.setSpeedMax(Math.max(exSpeed.getSpeedMax(), sample.getSpeed()));
}
ExerciseSample lastSample = exercise.getSampleList()[exercise.getSampleList().length - 1];
exSpeed.setDistance(lastSample.getDistance());
exSpeed.setSpeedAVG(CalculationUtils.calculateAvgSpeed(
exSpeed.getDistance() / 1000f,
Math.round(exercise.getDuration() / 10f)));
}
}
private void computeAltitudeStatisticIfMissing(EVExercise exercise) {
if (exercise.getRecordingMode().isAltitude() && exercise.getAltitude() == null) {
ExerciseAltitude exAltitude = new ExerciseAltitude();
exAltitude.setAltitudeMin(Short.MAX_VALUE);
exAltitude.setAltitudeMax(Short.MIN_VALUE);
exercise.setAltitude(exAltitude);
double sumAltitude = 0;
short previousAltitude = Short.MAX_VALUE;
for (ExerciseSample sample : exercise.getSampleList()) {
sumAltitude += sample.getAltitude();
exAltitude.setAltitudeMin((short) Math.min(exAltitude.getAltitudeMin(), sample.getAltitude()));
exAltitude.setAltitudeMax((short) Math.max(exAltitude.getAltitudeMax(), sample.getAltitude()));
if (previousAltitude < sample.getAltitude()) {
exAltitude.setAscent(exAltitude.getAscent() + (sample.getAltitude() - previousAltitude));
}
previousAltitude = sample.getAltitude();
}
exAltitude.setAltitudeAVG((short) Math.round(sumAltitude / (double) exercise.getSampleList().length));
}
}
}