package com.wattzap.model; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import lt.overdrive.trackparser.domain.Track; import lt.overdrive.trackparser.domain.TrackPoint; import lt.overdrive.trackparser.domain.Trail; import lt.overdrive.trackparser.parsing.ParserException; import lt.overdrive.trackparser.parsing.tcx.TcxParser; 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.controller.MessageBus; import com.wattzap.controller.Messages; import com.wattzap.model.dto.Point; import com.wattzap.model.dto.TrainingData; import com.wattzap.model.dto.TrainingItem; /* * Wrapper class for Tacx Real Life Video Routes * * @author David George (c) Copyright 2013-2015 * @author PiR43 * @date 19 November 2013 */ @RouteAnnotation public class RLVReader extends RouteReader { public static final int RLV_VIDEO_INFO = 2010; public static final int RLV_FRAME_DISTANCE_MAPPING = 2020; public static final int RLV_INFOBOX = 2030; public static final int RLV_COURSE_INFO = 2040; public static final int RLV_FINGERPRINT = 2000; // RLV (.rlv) public static final int PGMF_FINGERPRINT = 1000; public static final int PGMF_INFORMATION = 1010; public static final int PGMF_PROGRAM = 1020; private Charset iso88591charset = Charset.forName("ISO-8859-1"); private double totalDistance = 0.0; private String fileName; private String courseName; private float frameRate; private float orgRunWeight; private long frameOffset; private float altitudeStart; private double maxSlope; private double minSlope; private XYSeries series; private float maxPower; private int programType; private GPXFile gpxFile; // Time / Distance based int timeDist; public final static int TIME = 0; public final static int DISTANCE = 1; private static Logger logger = LogManager.getLogger("RLV Reader"); @Override public String getExtension() { return "rlv"; } @Override public int routeType() { return programType; } @Override public String getFilename() { // TODO Auto-generated method stub return fileName; } @Override public String getName() { // TODO Auto-generated method stub return courseName; } @Override public GPXFile getGpxFile() { // TODO Auto-generated method stub return gpxFile; } @Override public XYSeries getSeries() { return series; } @Override public Point[] getPoints() { return points; } @Override public double getDistanceMeters() { return totalDistance; } @Override public double getMaxSlope() { return maxSlope; } @Override public double getMinSlope() { return minSlope; } @Override public void load(String filename) { // logger.setLevel(Level.DEBUG); totalDistance = 0.0; maxSlope = 0; minSlope = 0; maxPower = 0; filename = filename.substring(0, filename.lastIndexOf('.')); this.fileName = filename; this.series = new XYSeries(""); ArrayList<Point> pgmfSegment = readPGMF(filename + ".pgmf"); ArrayList<Point> rlvSegment = readRLV(filename + ".rlv"); ArrayList<Point> tcxSegment = readTCX(filename + ".tcx"); gpxFile = null; WaypointGroup path = null; if (tcxSegment != null && tcxSegment.size() > 0) { double pgmfDist = pgmfSegment.get(pgmfSegment.size() - 1) .getDistanceFromStart(); double tcxDist = tcxSegment.get(tcxSegment.size() - 1) .getDistanceFromStart(); logger.debug("pgmd Dist: " + pgmfDist + " tcx distance: " + tcxDist); for (int i = 0; i < tcxSegment.size(); i++) { tcxSegment.get(i).setDistanceFromStart( tcxSegment.get(i).getDistanceFromStart() * pgmfDist / tcxDist); } tcxDist = tcxSegment.get(tcxSegment.size() - 1) .getDistanceFromStart(); logger.debug("pgmd Dist: " + pgmfDist + " tcx distance: " + tcxDist); // create mapping data gpxFile = new GPXFile(); gpxFile.addRoute(); List<Route> routes = gpxFile.getRoutes(); Route route = routes.get(0); path = route.getPath(); } /* * merge arrays, we basically go through the frame/distance array and * add speed and time data to the slope/distance array. */ long lastFrame = 0; double runningDistance = 0.0; double speed = 0.0; long startTime = 0; switch (timeDist) { case TIME: logger.debug("RLV mode TIME " + timeDist); ArrayList<Point> mergedPoints = new ArrayList<Point>(); TrainingData tData = new TrainingData(); tData.setPwr(true); Iterator<Point> rlvI = rlvSegment.iterator(); Point rlvP = null; if (rlvI.hasNext()) { rlvP = rlvI.next(); } // RLV Record is: frame No and distance per Frame long frame = rlvP.getTime(); long framesInRecord = frame - lastFrame; double distance = rlvP.getDistanceFromStart() * framesInRecord; speed = (distance * 3.6 * frameRate) / framesInRecord; startTime += framesInRecord * 1000 / frameRate; double normPower = 1; if (UserPreferences.INSTANCE.isVirtualPower()) { // set max power level to top of Level 5, VO2Max normPower = 1.2 * (UserPreferences.INSTANCE.getMaxPower() / maxPower); } // 40% of FTP int recoveryPower = (int) (UserPreferences.INSTANCE.getMaxPower() / 0.4); if (recoveryPower == 0) { recoveryPower = 75; } double interDistance = 0; long interTime = 0; for (int c1 = 0; c1 < pgmfSegment.size();) { // Duration Seconds // Power Watts Point p = pgmfSegment.get(c1); if ((p.getDistanceFromStart() * 1000) > startTime) { if (rlvI.hasNext()) { runningDistance += distance; // Normalize power rlvP.setPower((int) (p.getPower() * normPower)); if (rlvP.getPower() < recoveryPower) { // put a floor on minimum power rlvP.setPower(recoveryPower); } rlvP.setSpeed(speed); // interDistance = (speed * (startTime - interTime)) / // (3600); rlvP.setDistanceFromStart(runningDistance); rlvP.setTime(startTime); interTime = startTime; rlvP.setLongitude(90); mergedPoints.add(rlvP); rlvP = rlvI.next(); frame = rlvP.getTime(); framesInRecord = frame - lastFrame; lastFrame = frame; startTime += framesInRecord * 1000 / frameRate; distance = rlvP.getDistanceFromStart() * framesInRecord; speed = (distance * 3.6 * frameRate) / framesInRecord; continue; } } // Normalize power p.setPower((int) (p.getPower() * normPower)); if (p.getPower() < recoveryPower) { // put a floor on minimum power p.setPower(recoveryPower); } p.setTime((long) (p.getDistanceFromStart() * 1000)); interDistance = (speed * (p.getTime() - interTime)) / (3600); p.setDistanceFromStart(runningDistance + interDistance); p.setSpeed(speed); p.setLatitude(90); p.setElevation(0); mergedPoints.add(p); c1++; } totalDistance = mergedPoints.get(mergedPoints.size() - 1) .getDistanceFromStart(); points = mergedPoints.toArray(new Point[mergedPoints.size()]); // Create Training Data from merged segments TrainingItem item = new TrainingItem(); item.setPower(-1); for (Point p : points) { if (item.getPower() != ((int) p.getGradient())) { if (item.getPower() == -1) { item.setPower(p.getGradient()); } else { // item.setDescription("To " // + String.format("%.2f", // p.getDistanceFromStart() / 1000) // + " km"); item.setDistanceMeters(p.getDistanceFromStart()); tData.addItem(item); item = new TrainingItem(); item.setPower(p.getGradient()); } } } if (tData.getTraining().size() > 0) { MessageBus.INSTANCE.send(Messages.TRAINING, tData); } break; case DISTANCE: logger.debug("RLV mode DISTANCE " + timeDist); int c1 = 0; Point tcxPoint = null; Point lastTcxPoint = null; int c3 = 0; // for each RLV Point for (int c2 = 0; c2 < rlvSegment.size(); c2++) { Point rlvPoint = rlvSegment.get(c2); // point.time contains the frame number frame = rlvPoint.getTime(); framesInRecord = frame - lastFrame; distance = rlvPoint.getDistanceFromStart() * framesInRecord; speed = (distance * 3.6 * frameRate) / framesInRecord; // Merge with PGMF Points double d = 0; while (c1 < pgmfSegment.size()) { Point pgmfPoint = pgmfSegment.get(c1); if (pgmfPoint.getDistanceFromStart() > (runningDistance + distance) && c2 != rlvSegment.size() - 1) { // if we are not the last point and we are passed the // current slope point distance break; } pgmfPoint.setSpeed(speed); // t = d / s d = pgmfPoint.getDistanceFromStart() - runningDistance; long time = (long) (d * 3600 / speed); time += startTime; pgmfPoint.setTime(time); // put position from tcx if (tcxSegment != null && tcxSegment.size() > 0) { if (tcxPoint == null) tcxPoint = tcxSegment.get(0); while (tcxPoint.getDistanceFromStart() < pgmfPoint .getDistanceFromStart() && c3 < tcxSegment.size() - 1) { lastTcxPoint = tcxPoint; tcxPoint = tcxSegment.get(++c3); } if (lastTcxPoint == null || tcxPoint.getDistanceFromStart() == lastTcxPoint .getDistanceFromStart()) { pgmfPoint.setLatitude(tcxPoint.getLatitude()); pgmfPoint.setLongitude(tcxPoint.getLongitude()); } else { double dist = tcxPoint.getDistanceFromStart() - lastTcxPoint.getDistanceFromStart(); logger.debug("last: " + lastTcxPoint.getDistanceFromStart() + " pgmf: " + pgmfPoint.getDistanceFromStart() + " point:" + tcxPoint.getDistanceFromStart()); pgmfPoint .setLatitude((tcxPoint.getLatitude() * (dist - (tcxPoint .getDistanceFromStart() - pgmfPoint .getDistanceFromStart())) + lastTcxPoint .getLatitude() * (dist - (pgmfPoint .getDistanceFromStart() - lastTcxPoint .getDistanceFromStart()))) / dist); pgmfPoint .setLongitude((tcxPoint.getLongitude() * (dist - (tcxPoint .getDistanceFromStart() - pgmfPoint .getDistanceFromStart())) + lastTcxPoint .getLongitude() * (dist - (pgmfPoint .getDistanceFromStart() - lastTcxPoint .getDistanceFromStart()))) / dist); } // GPX Data Waypoint wayPoint = new Waypoint( pgmfPoint.getLatitude(), pgmfPoint.getLongitude()); wayPoint.setEle(pgmfPoint.getElevation()); path.addWaypoint(wayPoint); } logger.debug("c2:" + c2 + " c1:" + c1 + " frame: " + frame + " distanceFromstart: " + pgmfPoint.getDistanceFromStart() + " time: " + time + " distance (frameInRecord): " + distance + " framesInRecord: " + framesInRecord + " speed: " + speed + " lat:" + pgmfPoint.getLatitude() + " lon:" + pgmfPoint.getLongitude()); c1++; }// while if (gpxFile != null) { gpxFile.updateAllProperties(); } runningDistance += distance; startTime = (long) ((float) (frame * 1000) / frameRate); lastFrame = frame; } points = pgmfSegment.toArray(new Point[pgmfSegment.size()]); break; }// switch } public void close() { } private final ArrayList<Point> readTCX(String fileName) { ArrayList<Point> pl = new ArrayList<Point>(); try { TcxParser parser = new TcxParser(); Trail trail = parser.parse(new File(fileName)); logger.debug("tcx tracks: " + trail.getTracks().size()); List<Track> tracks = trail.getTracks(); Point last = null; for (Track track : tracks) { List<TrackPoint> points = track.getPoints(); for (TrackPoint point : points) { Point p = new Point(); p.setLatitude(point.getLatitude()); p.setLongitude(point.getLongitude()); p.setElevation(point.getAltitude()); if (last != null) { double leg = GPXReader.distance(p.getLatitude(), last.getLatitude(), p.getLongitude(), last.getLongitude(), last.getElevation(), p.getElevation()); p.setDistanceFromStart(last.getDistanceFromStart() + leg); } else { p.setDistanceFromStart(0); } pl.add(p); last = p; } } } catch (ParserException pe) { StringWriter sw = new StringWriter(); try { pe.getCause().printStackTrace(new PrintWriter(sw)); } catch (Exception e) { } String exceptionAsString = sw.toString(); logger.error("ParserException : " + pe + ":" + exceptionAsString + " file:" + fileName); } return pl; } private final ArrayList<Point> readPGMF(String fileName) { ArrayList<Point> p = new ArrayList<Point>(); maxPower = 0f; DataInputStream din = null; try { din = new DataInputStream(new FileInputStream(fileName)); /* * To read a Java short primitive from file, use byte readShort() * method of Java DataInputStream class. * * This method reads 2 bytes and returns it as a short value. */ int v = readLEShort(din); if (v != PGMF_FINGERPRINT) { throw new RuntimeException("Not an PGMF file " + v); } v = readLEShort(din); // record size v = readLEShort(din); // number of records v = readLEShort(din); while (din.available() > 0) { int recordType = readLEShort(din); switch (recordType) { case PGMF_INFORMATION: readPGMFInfo(din); break; case PGMF_PROGRAM: List<Point> segment = readPGMFProgram(din); p.addAll(segment); break; default: logger.error("Wrong record type for offset " + recordType); } } din.close(); } catch (FileNotFoundException fe) { logger.error("FileNotFoundException : " + fe); throw new RuntimeException("No PGMF file found for "); } catch (IOException ioe) { logger.error("IOException : " + ioe); } finally { try { if (din != null) { din.close(); } } catch (IOException e) { logger.error("IOException : " + e); } } return p; } /* * 12 byte record has 3 variables : * * Slope (Integer) 0 = Watt program, 1 = Slope program, 2 = Pulse (HR) * program * * TimeDist (Integer) 0 = Time based program, 1 = distance based program * * TotalTimeDist Double Total time (in seconds) or distance (in metres) of * complete program * * Note: for RLV runs, the TotalTimeDist is the distance for the entire RLV, * not just the section that this run relates to. * http://www.whitepeak.org/FortiusFile.aspx?file=.pgmf */ private void readPGMFInfo(DataInputStream din) throws IOException { int version = readLEShort(din); logger.debug("*** Read Course Info Version : " + version); long rib = readLEInt(din); logger.debug("Number of records in the block: " + rib); long sob = readLEInt(din); logger.debug("Size of Records in block: " + sob); for (int r = 0; r < rib; r++) { long cSum = readLEInt(din); courseName = readTacxString(din, 34); logger.debug("Course name " + courseName); programType = readLEInt(din); switch (programType) { case POWER: logger.debug("Watt Program"); break; case SLOPE: logger.debug("Slope Program"); break; case HEARTRATE: logger.debug("Pulse(HR) Program"); break; default: logger.error("Error in PGFM Program Type: " + programType); } timeDist = readLEInt(din); double totalTimeDist = readLittleDouble(din); switch (timeDist) { case TIME: logger.debug("Time Based " + totalTimeDist); break; case DISTANCE: logger.debug("Distance Based " + totalTimeDist); break; default: logger.error("Error in PGFM Base Type: " + timeDist); } double energyCons = readLittleDouble(din); logger.debug("EnergyCons: " + energyCons); altitudeStart = readLittleFloat(din); logger.debug("AltitudeStart: " + altitudeStart); long brakeCategory = readLEInt(din); logger.debug("BrakeCategory: " + brakeCategory); } } /* * @value DurationDistance Float Seconds or metres, depending on program * type * * @value PulseSlopeWatts Float Pulse, slope or watts data, depending on * program type * * @value RollingFriction Float */ private final List<Point> readPGMFProgram(DataInputStream din) throws IOException { int version = readLEShort(din); logger.debug("*** Read Course Info Version : " + version); long rib = readLEInt(din); long sob = readLEInt(din); float altitudeMin = 0; float altitude = 0.0f; ArrayList<Point> segment = new ArrayList<Point>(); Point p = new Point(); p.setElevation(altitudeStart); p.setDistanceFromStart(0); segment.add(p); series.add(0, altitudeStart); float totalClimbing = 0.0f; float totalDescent = 0.0f; for (int r = 0; r < rib; r++) { // distance or duration in seconds depending on the program float distanceMeters = readLittleFloat(din); // slope or power float slope = 0f; slope = readLittleFloat(din); switch (programType) { case SLOPE: // slope program totalDistance += distanceMeters; break; case POWER: // WATTS // time!!!! totalDistance += distanceMeters; if (slope > maxPower) { maxPower = slope; } break; } float rise = slope * (distanceMeters / 100.0f); altitude += rise; if (rise > 0) { totalClimbing += rise; } else { totalDescent += rise; } if (altitude < altitudeMin) { altitudeMin = altitude; // dunno what to do with this. } float rollingFriction = readLittleFloat(din); /* * logger.debug("distanceMeters: " + distanceMeters + " slope: " + * slope + " rise: " + rise + " altitude " + altitude + * " RollingFriction: " + rollingFriction); */ series.add(totalDistance / 1000, altitudeStart + altitude); if (r == 0) { // first time thru' set up initial point p.setGradient(slope); } p = new Point(); p.setGradient(slope); if (slope > maxSlope) { maxSlope = slope; } if (slope < minSlope) { minSlope = slope; } p.setElevation(altitudeStart + altitude); p.setDistanceFromStart(totalDistance); segment.add(p); }// for logger.info("PGFM Total Records " + rib + " Total Distance " + totalDistance + " Start Altitude " + altitudeStart + " Climb " + totalClimbing + " Descent " + totalDescent); return segment; } /*** Real Life Video ***/ private final ArrayList<Point> readRLV(String file) { ArrayList<Point> p = new ArrayList<Point>(); DataInputStream din = null; try { din = new DataInputStream(new FileInputStream(file)); /* * To read a Java short primitive from file, use byte readShort() * method of Java DataInputStream class. * * This method reads 2 bytes and returns it as a short value. */ int v = readLEShort(din); if (v != RLV_FINGERPRINT) { throw new RuntimeException("Not an RLV file"); } v = readLEShort(din); v = readLEShort(din); v = readLEShort(din); while (din.available() > 0) { int recordType = readLEShort(din); switch (recordType) { case RLV_VIDEO_INFO: readRLVInfo(din); break; case RLV_FRAME_DISTANCE_MAPPING: ArrayList<Point> segment = readFrameDistanceMapping(din); p.addAll(segment); break; case RLV_INFOBOX: readInfoBox(din); break; case RLV_COURSE_INFO: readCourseInfo(din); break; default: logger.error("something wrong here " + recordType); } } } catch (FileNotFoundException fe) { logger.error("FileNotFoundException : " + fe); } catch (IOException ioe) { logger.error("IOException : " + ioe); } finally { try { if (din != null) { din.close(); } } catch (IOException e) { logger.error("IOException : " + e); } } return p; } private void readRLVInfo(DataInputStream din) throws IOException { int version = readLEShort(din); logger.info("Version : " + version); long rib = readLEInt(din); logger.info("Number of records in the block: " + rib); long sob = readLEInt(din); logger.info("Size of Records in block: " + sob); // descriptive name String fileName = readTacxString(din, 522); logger.info("filename " + fileName); frameRate = readLittleFloat(din); System.out.println("frame rate " + frameRate); logger.info("Frame rate: " + frameRate); orgRunWeight = readLittleFloat(din); logger.info("OrgRunWeight: " + orgRunWeight); frameOffset = readLEInt(din); logger.info("FrameOffset: " + frameOffset); } /* * RLV Record contains: frame No and distance per Frame */ private final ArrayList<Point> readFrameDistanceMapping(DataInputStream din) throws IOException { int version = readLEShort(din); long rib = readLEInt(din); long sob = readLEInt(din); double distance = 0; long lastFrame = 0; ArrayList<Point> segment = new ArrayList<Point>(); for (int r = 0; r < rib; r++) { long frame = readLEInt(din); float distancePerFrame = readLittleFloat(din); if (frame == 0) { continue; // no useful information } Point p = new Point(); p.setDistanceFromStart(distancePerFrame); distance += (frame - lastFrame) * distancePerFrame; lastFrame = frame; p.setTime(frame); segment.add(p); } logger.debug("### RLV No of records " + rib + " version " + version + " total distance " + distance); return segment; } /* * RLV InfoBox info (2030) * * This block looks like it may contain the infobox information, i.e. the * frame number at which to popup the appropriate infobox from the * CmdList.txt file. * * Record format: Contents Version 100 Field uShort Number of Records in * Block (uInt) Size of Records in Block (uInt) * * [Frame Int32, Command Int32,...] */ private void readInfoBox(DataInputStream din) throws IOException { int version = readLEShort(din); logger.info("Read Info Box Version : " + version); long rib = readLEInt(din); logger.info("Number of records in the block: " + rib); long sob = readLEInt(din); logger.info("Size of Records in block: " + sob); for (int r = 0; r < rib; r++) { long frame = readLEInt(din); logger.info("Frame: " + frame); long command = readLEInt(din); logger.info("Cmd: " + command); } } /* * Course information (2040) * * Record format: Contents Version 100 Field uShort Number of Records in * Block (uInt) Size of Records in Block (uInt) * * Start (float) Start of route section (time (s) or distance (m), depending * on program type) End (float) End of route section (time (s) or distance * (m), depending on program type) CourseSegmentName 66 Char[] TextFile 522 * Char[] */ private void readCourseInfo(DataInputStream din) throws IOException { int version = readLEShort(din); logger.info("*** Read Course Info Version : " + version); long rib = readLEInt(din); logger.info("Number of records in the block: " + rib); long sob = readLEInt(din); logger.info("Size of Records in block: " + sob); for (int r = 0; r < rib; r++) { float start = readLittleFloat(din); float end = readLittleFloat(din); String courseSegmentName = readTacxString(din, 66); logger.info("name " + courseSegmentName); String textFile = readTacxString(din, 522); logger.info("textFile " + textFile); } } private int readLEShort(DataInputStream din) throws IOException { int b1 = din.readUnsignedByte(); int b2 = din.readUnsignedByte(); return (b2 << 8) + b1; } public static int readLEInt(DataInputStream dataInStream) throws IOException { byte[] byteBuffer = new byte[4]; dataInStream.readFully(byteBuffer, 0, 4); return (byteBuffer[3]) << 24 | (byteBuffer[2] & 0xff) << 16 | (byteBuffer[1] & 0xff) << 8 | (byteBuffer[0] & 0xff); } public float readLittleFloat(DataInputStream dis) throws IOException { return Float.intBitsToFloat(readLEInt(dis)); } public long readLELong(DataInputStream dataInStream) throws IOException { byte[] byteBuffer = new byte[8]; dataInStream.readFully(byteBuffer, 0, 8); return (byteBuffer[7]) << 56 | (byteBuffer[6] & 0xff) << 48 | (byteBuffer[5] & 0xff) << 40 | (byteBuffer[4] & 0xff) << 32 | (byteBuffer[3] & 0xff) << 24 | (byteBuffer[2] & 0xff) << 16 | (byteBuffer[1] & 0xff) << 8 | (byteBuffer[0] & 0xff); } public double readLittleDouble(DataInputStream dis) throws IOException { return Double.longBitsToDouble(readLELong(dis)); } /* * Input is ISO-8859-1 in 2 bytes */ private String readTacxString(DataInputStream dis, int len) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i += 2) { int b1 = dis.readUnsignedByte(); int b2 = dis.readUnsignedByte(); if (b1 == 0 && b2 == 0) { continue; } ByteBuffer inputBuffer = ByteBuffer.wrap(new byte[] { (byte) b1, (byte) b2 }); CharBuffer cb = iso88591charset.decode(inputBuffer); sb.append(cb.get(0)); } return sb.toString(); } }