/* 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.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Toolkit; import java.io.EOFException; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Iterator; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import com.sun.tools.visualvm.charts.ChartFactory; import com.sun.tools.visualvm.charts.SimpleXYChartDescriptor; import com.sun.tools.visualvm.charts.SimpleXYChartSupport; import com.wattzap.controller.MessageBus; import com.wattzap.controller.MessageCallback; import com.wattzap.controller.Messages; import com.wattzap.model.UserPreferences; import com.wattzap.model.dto.Telemetry; import com.wattzap.model.dto.TrainingData; import com.wattzap.model.dto.TrainingItem; /** * (c) 2013 David George / TrainingLoops.com * * Displays training data. Shows target power/hr/cadence based on training * programme with real time data coming from sensors. * * @author David George * @date 1 September 2013 */ public class TrainingDisplay extends JPanel implements MessageCallback { private static final long serialVersionUID = 1L; private SimpleXYChartSupport support = null; int cadence; int heartRate = 0; long aggregateTime = 0; long startTime; long time; Iterator<TrainingItem> training; TrainingData tData; TrainingItem current; private ArrayList<Telemetry> data; int numElements; JComponent chart = null; ObjectOutputStream oos = null; boolean antEnabled = true; private static final long MILLISECSMINUTE = 60000; private final UserPreferences userPrefs = UserPreferences.INSTANCE; private static Logger logger = LogManager.getLogger("Training Display"); public TrainingDisplay(Dimension screenSize) { setPreferredSize(new Dimension(screenSize.width / 2, 400)); setLayout(new BorderLayout()); MessageBus.INSTANCE.register(Messages.SPEED, this); MessageBus.INSTANCE.register(Messages.CADENCE, this); MessageBus.INSTANCE.register(Messages.HEARTRATE, this); MessageBus.INSTANCE.register(Messages.START, this); MessageBus.INSTANCE.register(Messages.STARTPOS, this); MessageBus.INSTANCE.register(Messages.STOP, this); MessageBus.INSTANCE.register(Messages.TRAINING, this); MessageBus.INSTANCE.register(Messages.CLOSE, this); antEnabled = userPrefs.isAntEnabled(); } private void createModels(TrainingData tData) { if (chart != null) { remove(chart); chart = null; } SimpleXYChartDescriptor descriptor = SimpleXYChartDescriptor.decimal(0, 200, 300, 1d, true, 600); Color darkOrange = new Color(246, 46, 00); descriptor.addItem(userPrefs.getString("power"), darkOrange, 1.0f, Color.red, null, null); numElements = 1; if (antEnabled) { Color green = new Color(28, 237, 00); descriptor.addItem(userPrefs.getString("heartrate"), green, 1.0f, Color.green, null, null); descriptor.addItem(userPrefs.getString("cadence"), Color.blue, 1.0f, Color.blue, null, null); numElements += 2; } if (tData != null) { if (tData.isPwr()) { Color lightOrange = new Color(255, 47, 19); descriptor.addItem("Target Power", lightOrange, 2.5f, lightOrange, null, null); numElements++; } if (antEnabled) { if (tData.isHr()) { Color darkGreen = new Color(0, 110, 8); descriptor.addItem("Target Heartrate", darkGreen, 2.5f, darkGreen, null, null); numElements++; } if (tData.isCdc()) { Color lightBlue = new Color(64, 96, 255); descriptor.addItem("Target Cadence", lightBlue, 2.5f, lightBlue, null, null); numElements++; } } descriptor .setDetailsItems(new String[] { "<html><font size='+2'><b>Info" }); } support = ChartFactory.createSimpleXYChart(descriptor); chart = support.getChart(); add(chart, BorderLayout.CENTER); chart.setVisible(true); chart.revalidate(); } private void update(Telemetry telemetry) { if (time == telemetry.getTime()) { // no change return; } time = telemetry.getTime(); if (startTime == 0) { startTime = time; // start time } long[] values = new long[numElements]; values[0] = telemetry.getPower(); if (antEnabled) { values[1] = telemetry.getHeartRate(); values[2] = telemetry.getCadence(); } // training if (current != null && antEnabled) { long ct = current.getTime(); if (ct > 0) { // time based training if (aggregateTime + (time - startTime) > current.getTime()) { if (training.hasNext()) { current = training.next(); MessageBus.INSTANCE .send(Messages.TRAININGITEM, current); // Sound beep on training change Toolkit.getDefaultToolkit().beep(); } } } else { // distance based training if (tData.isNext(telemetry.getDistanceMeters())) { current = tData.getNext(telemetry.getDistanceMeters()); MessageBus.INSTANCE.send(Messages.TRAININGITEM, current); // Sound beep on training change Toolkit.getDefaultToolkit().beep(); } } if (tData != null) { int index = 3; if (tData.isPwr()) { values[index++] = current.getPower(); } if (tData.isHr()) { values[index++] = current.getHr(); } if (tData.isCdc()) { values[index] = current.getCadence(); } String[] details = { current.getDescription() + current.getPowerMsg() + current.getHRMsg() + current.getCadenceMsg() + "</b></font></html>" }; support.updateDetails(details); } } // use telemetry time support.addValues(time, values); add(telemetry); } /* * Save every one point for every second TODO: move this to data acquisition * so we don't even send these points * * @param t */ private void add(Telemetry t) { if (data == null) { // not yet initialized return; } int index = data.size(); if (index == 0) { // empty, first time through data.add(t); } else { Telemetry tn = data.get(index - 1); if (t.getTime() > tn.getTime() + 1000) { data.add(t); try { oos.writeObject(t); } catch (Exception e) { // TODO Auto-generated catch block logger.error("Can't write telemetry data to journal " + e.getLocalizedMessage()); } } } } public ArrayList<Telemetry> getData() { return data; } public void loadJournal() { ObjectInputStream objectInputStream = null; data = new ArrayList<Telemetry>(); Telemetry t = null; try { FileInputStream streamIn = new FileInputStream(userPrefs.getWD() + "/journal.ser"); objectInputStream = new ObjectInputStream(streamIn); while ((t = (Telemetry) objectInputStream.readObject()) != null) { data.add(t); }// while } catch (EOFException ex) { logger.info("Journal file read " + data.size() + " records"); } catch (Exception e) { // data = null; logger.error("Cannot read journal file " + e.getLocalizedMessage() + " at position " + data.size()); } finally { JOptionPane.showMessageDialog(this, "Recovered " + data.size() + " records", "Info", JOptionPane.INFORMATION_MESSAGE); if (t != null) { MessageBus.INSTANCE.send(Messages.STARTPOS, t.getDistanceKM()); } if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException e) { logger.error("Cannot close journal file " + e.getLocalizedMessage()); } } } // Now rewrite journal file (end might be corrupt) ObjectOutputStream objectOutputStream = null; // existing data, append to journal file try { FileOutputStream fout = new FileOutputStream(userPrefs.getWD() + "/journal.ser", true); objectOutputStream = new ObjectOutputStream(fout); for (Telemetry telemetry : data) { oos.writeObject(telemetry); } } catch (Exception e) { // TODO Auto-generated catch block logger.error("Can't write telemetry data to journal " + e.getLocalizedMessage()); } finally { try { if (objectOutputStream != null) { objectOutputStream.close(); } } catch (IOException e) { logger.error("Cannot close journal file " + e.getLocalizedMessage()); } } } @Override public void callback(Messages message, Object o) { switch (message) { case SPEED: if (numElements > 0) { // TODO: this is a race hazard, this method can be called before // setup, hence this test. // get a clone Telemetry t = new Telemetry((Telemetry) o); // recover last heart rate data t.setHeartRate(heartRate); t.setCadence(cadence); update(t); } break; case CADENCE: cadence = (Integer) o; break; case HEARTRATE: heartRate = (Integer) o; break; case STOP: if (data != null && !data.isEmpty()) { Telemetry lastPoint = data.get(data.size() - 1); long split = lastPoint.getTime() - startTime; int minutes = userPrefs.getEvalTime(); minutes -= (split / MILLISECSMINUTE); userPrefs.setEvalTime(minutes); aggregateTime += split; } break; case START: if (chart == null) { createModels(null); } try { if (oos == null) { // oos is closed if (data == null) { // new training, truncate the journal file data = new ArrayList<Telemetry>(); FileOutputStream fout = new FileOutputStream( userPrefs.getWD() + "/journal.ser", false); oos = new ObjectOutputStream(fout); } else { // existing data, append to journal file FileOutputStream fout = new FileOutputStream( userPrefs.getWD() + "/journal.ser", true); oos = new ObjectOutputStream(fout); } } } catch (Exception e) { logger.error("Can't create journal file " + e.getLocalizedMessage()); } startTime = 0; MessageBus.INSTANCE.send(Messages.TRAININGITEM, current); break; case STARTPOS: double distance = (Double) o; if (current != null && current.getTime() == 0 && tData != null) { current = tData.getNext(distance*1000); // MessageBus.INSTANCE.send(Messages.TRAININGITEM, current); String[] details = { current.getDescription() + current.getPowerMsg() + current.getHRMsg() + current.getCadenceMsg() + "</b></font></html>" }; support.updateDetails(details); } break; case TRAINING: tData = (TrainingData) o; training = tData.getTraining().iterator(); if (training.hasNext()) { current = training.next(); MessageBus.INSTANCE.send(Messages.TRAININGITEM, current); } createModels(tData); aggregateTime = 0; break; case CLOSE: current = null; if (chart != null) { remove(chart); chart = null; } tData = null; if (oos != null) { try { oos.close(); oos = null; } catch (IOException e) { logger.error("Can't close journal file " + e.getLocalizedMessage()); } } data = null; break; } } }