/* This file is part of Wattzap Community Edition. * * Wattzap Community Edtion 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 3 of the License, or * (at your option) any later version. * * Wattzap Community Edition 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 Wattzap. If not, see <http://www.gnu.org/licenses/>. */ package com.wattzap.view.training; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel; import net.miginfocom.swing.MigLayout; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import com.wattzap.model.UserPreferences; import com.wattzap.model.dto.Telemetry; import com.wattzap.model.dto.WorkoutData; /** * Displays a summary analysis of a workout. * * (c) 2014 David George / Wattzap.com * * @author David George * @date 1 January 2014 */ public class TrainingAnalysis extends JFrame { private static final long serialVersionUID = -7939830514817673972L; private JLabel fiveSecondPower; private JLabel fiveSecondWKG; private JLabel oneMinutePower; private JLabel oneMinuteWKG; private JLabel fiveMinutePower; private JLabel fiveMinuteWKG; private JLabel twentyMinutePower; private JLabel twentyMinuteWKG; // Cadence // Heart Rate private JLabel maxHeartRate; private JLabel aveHeartRate; private JLabel minHeartRate; private JLabel fTHR; private JLabel distance; // Power private JLabel totalPower; private JLabel avePower; private JLabel maxPower; private JLabel qPower; private JLabel ftPower; private JLabel ft1Power; private JLabel ft20Power; private JLabel load; private JLabel stress; SimpleDateFormat df; DecimalFormat decimalFormat = new DecimalFormat("#.##"); private JLabel time; private static Logger logger = LogManager.getLogger("Training Analysis"); private final UserPreferences userPrefs = UserPreferences.INSTANCE; public TrainingAnalysis() { super(); setTitle("Analysis"); ImageIcon img = new ImageIcon("icons/preferences.jpg"); setIconImage(img.getImage()); df = new SimpleDateFormat("H'h' m'm' s's'"); df.setTimeZone(TimeZone.getTimeZone("GMT")); MigLayout layout = new MigLayout(); this.setLayout(layout); setBackground(Color.LIGHT_GRAY); int style1 = Font.CENTER_BASELINE; Font font1 = new Font("Arial", style1, 13); JLabel fiveSecondPowerLabel = new JLabel(); fiveSecondPowerLabel.setFont(font1); fiveSecondPowerLabel.setText(userPrefs.getString("5secpow")); add(fiveSecondPowerLabel); fiveSecondPower = new JLabel(); fiveSecondPower.setFont(font1); fiveSecondWKG = new JLabel(); fiveSecondWKG.setFont(font1); add(fiveSecondPower); add(fiveSecondWKG, "wrap"); JLabel oneMinutePowerLabel = new JLabel(); oneMinutePowerLabel.setFont(font1); oneMinutePowerLabel.setText(userPrefs.getString("1minpow")); add(oneMinutePowerLabel); oneMinutePower = new JLabel(); oneMinutePower.setFont(font1); oneMinuteWKG = new JLabel(); oneMinuteWKG.setFont(font1); add(oneMinutePower); add(oneMinuteWKG, "wrap"); JLabel fiveMinutePowerLabel = new JLabel(); fiveMinutePowerLabel.setFont(font1); fiveMinutePowerLabel.setText(userPrefs.getString("5minpow")); add(fiveMinutePowerLabel); fiveMinutePower = new JLabel(); fiveMinutePower.setFont(font1); fiveMinuteWKG = new JLabel(); fiveMinuteWKG.setFont(font1); add(fiveMinutePower); add(fiveMinuteWKG, "wrap"); JLabel twentyMinutePowerLabel = new JLabel(); twentyMinutePowerLabel.setFont(font1); twentyMinutePowerLabel .setText(userPrefs.getString("20minpow")); add(twentyMinutePowerLabel); twentyMinutePower = new JLabel(); twentyMinutePower.setFont(font1); twentyMinuteWKG = new JLabel(); twentyMinuteWKG.setFont(font1); add(twentyMinutePower); add(twentyMinuteWKG, "wrap"); if (userPrefs.isAntEnabled()) { JLabel maxHeartRateLabel = new JLabel(); maxHeartRateLabel.setFont(font1); maxHeartRateLabel.setText(userPrefs.getString("maxhr")); add(maxHeartRateLabel); maxHeartRate = new JLabel(); maxHeartRate.setFont(font1); add(maxHeartRate, "wrap"); JLabel aveHeartRateLabel = new JLabel(); aveHeartRateLabel.setFont(font1); aveHeartRateLabel.setText(userPrefs.getString("avehr")); add(aveHeartRateLabel); aveHeartRate = new JLabel(); aveHeartRate.setFont(font1); add(aveHeartRate, "wrap"); JLabel fTHRLabel = new JLabel(); fTHRLabel.setFont(font1); fTHRLabel.setText(userPrefs.getString("fthr")); add(fTHRLabel); fTHR = new JLabel(); fTHR.setFont(font1); add(fTHR, "wrap"); } JLabel timeLabel = new JLabel(); timeLabel.setFont(font1); timeLabel.setText("Time"); add(timeLabel); time = new JLabel(); time.setFont(font1); add(time, "wrap"); JLabel distanceLabel = new JLabel(); distanceLabel.setFont(font1); distanceLabel.setText(userPrefs.getString("distance")); add(distanceLabel); distance = new JLabel(); distance.setFont(font1); add(distance, "wrap"); JLabel powerLabel = new JLabel(); powerLabel.setFont(font1); powerLabel.setText(userPrefs.getString("power")); add(powerLabel); totalPower = new JLabel(); totalPower.setFont(font1); add(totalPower, "wrap"); JLabel aveLabel = new JLabel(); aveLabel.setFont(font1); aveLabel.setText(userPrefs.getString("avepow")); add(aveLabel); avePower = new JLabel(); avePower.setFont(font1); add(avePower, "wrap"); JLabel maxLabel = new JLabel(); maxLabel.setFont(font1); maxLabel.setText(userPrefs.getString("maxpow")); add(maxLabel); maxPower = new JLabel(); maxPower.setFont(font1); add(maxPower, "wrap"); JLabel qLabel = new JLabel(); qLabel.setFont(font1); qLabel.setText(userPrefs.getString("qpow")); add(qLabel); qPower = new JLabel(); qPower.setFont(font1); add(qPower, "wrap"); JLabel ftpLabel = new JLabel(); ftpLabel.setFont(font1); ftpLabel.setText(userPrefs.getString("cftp")); add(ftpLabel); ftPower = new JLabel(); ftPower.setFont(font1); add(ftPower, "wrap"); JLabel ftp1Label = new JLabel(); ftp1Label.setFont(font1); ftp1Label.setText(userPrefs.getString("1minftp")); add(ftp1Label); ft1Power = new JLabel(); ft1Power.setFont(font1); add(ft1Power, "wrap"); JLabel ftp2Label = new JLabel(); ftp2Label.setFont(font1); ftp2Label.setText(userPrefs.getString("20minftp")); add(ftp2Label); ft20Power = new JLabel(); ft20Power.setFont(font1); add(ft20Power, "wrap"); JLabel loadLabel = new JLabel(); loadLabel.setFont(font1); loadLabel.setText(userPrefs.getString("load")); add(loadLabel); load = new JLabel(); load.setFont(font1); add(load, "wrap"); JLabel stressLabel = new JLabel(); stressLabel.setFont(font1); stressLabel.setText(userPrefs.getString("stress")); add(stressLabel); stress = new JLabel(); stress.setFont(font1); add(stress, "wrap"); Dimension d = new Dimension(500, 400); this.setSize(d); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); } public void show(WorkoutData workoutData) { if (workoutData == null) { return; } time.setText(df.format(workoutData.getTime()) + " "); distance.setText(decimalFormat.format(workoutData.getDistanceMeters()/1000) + " km"); qPower.setText(workoutData.getQuadraticPower() + " Watts"); double weight = workoutData.getWeight(); fiveSecondPower.setText(workoutData.getFiveSecondPwr() + " Watts"); fiveSecondWKG.setText(String.format("%.2f", workoutData.getFiveSecondPwr() / weight) + " W/kg"); avePower.setText(workoutData.getAvePower() + " Watts"); maxPower.setText(workoutData.getMaxPower() + " Watts"); totalPower.setText(workoutData.getTotalPower() + " Watts"); oneMinutePower.setText(workoutData.getOneMinutePwr() + " Watts"); oneMinuteWKG.setText(String.format("%.2f", workoutData.getOneMinutePwr() / weight) + " W/kg"); fiveMinutePower.setText(workoutData.getFiveMinutePwr() + " Watts"); fiveMinuteWKG.setText(String.format("%.2f", workoutData.getFiveMinutePwr() / weight) + " W/kg"); twentyMinutePower.setText(workoutData.getTwentyMinutePwr() + " Watts"); twentyMinuteWKG.setText(String.format("%.2f", workoutData.getTwentyMinutePwr() / weight) + " W/kg"); ftPower.setText(UserPreferences.INSTANCE.getMaxPower() + " Watts"); ft1Power.setText(String.format("%.2f", workoutData.getOneMinutePwr() * 0.75) + " Watts"); ft20Power.setText(String.format("%.2f", workoutData.getTwentyMinutePwr() * 0.95) + " Watts"); if (userPrefs.isAntEnabled()) { fTHR.setText(workoutData.getFtHR() + " bpm"); maxHeartRate.setText(workoutData.getMaxHR() + " bpm"); aveHeartRate.setText(workoutData.getAveHR() + " bpm"); } load.setText(String.format("%.2f", workoutData.getIntensity() * 100)); stress.setText("" + workoutData.getStress()); setVisible(true); } public static WorkoutData analyze(ArrayList<Telemetry> data) { WorkoutData workoutData = new WorkoutData(); if (data == null || data.size() == 0) { logger.info("No training data to analyze"); return null; } Telemetry firstPoint = data.get(0); Telemetry lastPoint = data.get(data.size() - 1); long len = (lastPoint.getTime() - firstPoint.getTime()); workoutData.setTime(len); workoutData.setDate(firstPoint.getTime()); workoutData.setDistanceMeters(lastPoint.getDistanceMeters() - firstPoint.getDistanceMeters()); int maxCad = 0; long aveCad = 0; int maxHR = 0; long aveHR = 0; int minHR = 220; double tPower = 0; // five second power Telemetry last = null; int maxPwr = 0; double qPwr = 0; TreeMap<Integer, Long> pow = new TreeMap<Integer, Long>(); TreeMap<Integer, Long> hr = new TreeMap<Integer, Long>(); Telemetry first = null; for (Telemetry t : data) { /* * Produces a map of power buckets and their times. e.g. * 12w:5s,10w:7s,8w:9s,6w:7s,3w:5s */ if (first == null) { // first time through first = t; } else { // Power Values if (pow.containsKey(t.getPower())) { long time = pow.get(t.getPower()); pow.put(t.getPower(), time + (t.getTime() - first.getTime())); } else { pow.put(t.getPower(), t.getTime() - first.getTime()); } // Heart Rate Values if (hr.containsKey(t.getHeartRate())) { long time = hr.get(t.getHeartRate()); hr.put(t.getHeartRate(), time + (t.getTime() - first.getTime())); } else { hr.put(t.getHeartRate(), t.getTime() - first.getTime()); } first = t; } if (maxPwr < t.getPower()) { maxPwr = t.getPower(); } qPwr += t.getPower() * t.getPower(); if (t.getHeartRate() > maxHR) { maxHR = t.getHeartRate(); } if (t.getCadence() > maxCad) { maxCad = t.getCadence(); } if (t.getHeartRate() != -1 && t.getHeartRate() < minHR) { minHR = t.getHeartRate(); } if (last != null) { /* * if data is recovered after a crash we need to take into account the time * gap, so we check to see if T > T' by more than 2 seconds and * then we adjust last time */ tPower += t.getPower() * (t.getTime() - last.getTime()); if (t.getHeartRate() > 0) { aveHR += t.getHeartRate() * (t.getTime() - last.getTime()); } aveCad += t.getCadence() * (t.getTime() - last.getTime()); } last = t; }// for /* * Calculate five second, 1 minute, five minute and twenty minute power. We order power from highest to lowest. */ int fiveSecPwr = 0; int oneMinPwr = 0; int fiveMinPwr = 0; int twentyMinPwr = 0; long timeInMillis = 0; for (Map.Entry<Integer, Long> entry : pow.descendingMap().entrySet()) { timeInMillis += entry.getValue(); if (timeInMillis >= 5000 && fiveSecPwr == 0) { fiveSecPwr = entry.getKey(); } if (timeInMillis >= 60000 && oneMinPwr == 0) { oneMinPwr = entry.getKey(); } if (timeInMillis >= 300000 && fiveMinPwr == 0) { fiveMinPwr = entry.getKey(); } if (timeInMillis >= 1200000 && twentyMinPwr == 0) { twentyMinPwr = entry.getKey(); } }// for workoutData.setFiveSecondPwr(fiveSecPwr); workoutData.setFiveMinutePwr(fiveMinPwr); workoutData.setOneMinutePwr(oneMinPwr); workoutData.setTwentyMinutePwr(twentyMinPwr); qPwr /= data.size(); qPwr = Math.sqrt(qPwr); workoutData.setQuadraticPower((int) qPwr); // Calculate 20 minute heart rate int twentyMinHR = 0; timeInMillis = 0; for (Map.Entry<Integer, Long> entry : hr.descendingMap().entrySet()) { timeInMillis += entry.getValue(); if (timeInMillis >= 1200000 && twentyMinHR == 0) { twentyMinHR = entry.getKey(); } }// for workoutData.setFtHR(twentyMinHR); // always save as Kilograms workoutData.setWeight(UserPreferences.INSTANCE.getWeightKG()); workoutData.setMaxHR(maxHR); workoutData.setMinHR(minHR); workoutData.setMaxCadence(maxCad); if (len > 0) { workoutData.setAveCadence((int) (aveCad / len)); workoutData.setAveHR((int) (aveHR / len)); workoutData.setAvePower((int) (tPower / len)); } workoutData.setMaxPower(maxPwr); workoutData.setTotalPower((int) (tPower / (3600000))); return workoutData; } }