diff --git a/src/main/java/es/kauron/estraba/App.java b/src/main/java/es/kauron/estraba/App.java index 9892d83..3df4f27 100644 --- a/src/main/java/es/kauron/estraba/App.java +++ b/src/main/java/es/kauron/estraba/App.java @@ -24,7 +24,6 @@ package es.kauron.estraba; -import es.kauron.estraba.controller.DashboardController; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -51,7 +50,7 @@ public class App extends Application { @Override public void start(Stage stage) throws Exception { FXMLLoader loader = new FXMLLoader( - App.class.getResource("fxml/Dashboard.fxml"), GENERAL_BUNDLE); + App.class.getResource("fxml/Splash.fxml"), GENERAL_BUNDLE); Parent root = loader.load(); stage.getIcons().add(new Image(App.class.getResource("img/icon.png").toString())); @@ -59,7 +58,11 @@ public class App extends Application { stage.setResizable(false); stage.setScene(new Scene(root)); + // Begin awesomewm code + stage.setMinHeight(500); + stage.setMinWidth(300); + stage.setResizable(false); + // End awesomewm code stage.show(); - loader.getController().postinit(); } } diff --git a/src/main/java/es/kauron/estraba/controller/DashboardController.java b/src/main/java/es/kauron/estraba/controller/DashboardController.java index a998fdc..bbcca17 100644 --- a/src/main/java/es/kauron/estraba/controller/DashboardController.java +++ b/src/main/java/es/kauron/estraba/controller/DashboardController.java @@ -2,12 +2,14 @@ package es.kauron.estraba.controller; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXSnackbar; +import com.jfoenix.controls.JFXSpinner; import com.lynden.gmapsfx.GoogleMapView; import com.lynden.gmapsfx.MapComponentInitializedListener; import com.lynden.gmapsfx.javascript.object.*; import com.lynden.gmapsfx.shapes.Polyline; import com.lynden.gmapsfx.shapes.PolylineOptions; import es.kauron.estraba.App; +import es.kauron.estraba.model.DataBundle; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -15,28 +17,14 @@ import javafx.fxml.Initializable; import javafx.scene.chart.AreaChart; import javafx.scene.chart.LineChart; import javafx.scene.chart.PieChart; -import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.control.Tab; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; -import javafx.stage.FileChooser; import jgpx.model.analysis.Chunk; -import jgpx.model.analysis.TrackData; -import jgpx.model.gpx.Track; -import jgpx.model.jaxb.GpxType; -import jgpx.model.jaxb.TrackPointExtensionT; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; -import java.io.File; import java.net.URL; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; import java.util.ResourceBundle; /** @@ -59,13 +47,15 @@ public class DashboardController implements Initializable, MapComponentInitializ private Label valueHRAvg, valueHRMin, valueHRMax, valueSpeedAvg, valueSpeedMax, valueCadenceAvg, valueCadenceMax, valueDate, valueTime, valueActiveTime, valueTotalTime, valueDistance, valueElevation, labelMotivationUpper, valueAscent, valueDescent, labelMotivatorLower; + @FXML + private JFXSpinner mapSpinner; @FXML private PieChart zoneChart; @FXML private GoogleMapView mapView; - private TrackData track; + private ObservableList chunks; @FXML private JFXButton elevationButton, speedButton, hrButton, cadenceButton; @@ -77,8 +67,6 @@ public class DashboardController implements Initializable, MapComponentInitializ private LineChart speedChart, hrChart, cadenceChart, mapChart; private JFXSnackbar snackbar; - private static final double DISTANCE_EPSILON = 1E-6; - private static final double KILOMETER_CUTOFF = 10000; @Override public void initialize(URL location, ResourceBundle resources) { @@ -117,123 +105,41 @@ public class DashboardController implements Initializable, MapComponentInitializ } } - public void postinit() { + void postInit(DataBundle bundle) { snackbar.registerSnackbarContainer(root); - try {load();} catch (JAXBException e) {e.printStackTrace();} + loadTrack(bundle); } - private void loadTrack(TrackData track) { - valueHRAvg.setText(track.getAverageHeartrate() - + App.GENERAL_BUNDLE.getString("unit.bpm")); - valueHRMax.setText(track.getMaxHeartrate() - + App.GENERAL_BUNDLE.getString("unit.bpm")); - valueHRMin.setText(track.getMinHeartRate() - + App.GENERAL_BUNDLE.getString("unit.bpm")); + private void loadTrack(DataBundle bundle) { + valueHRAvg.setText(bundle.HRAvg); + valueHRMax.setText(bundle.HRMax); + valueHRMin.setText(bundle.HRMin); + valueSpeedAvg.setText(bundle.speedAvg); + valueSpeedMax.setText(bundle.speedMax); + valueCadenceAvg.setText(bundle.cadenceAvg); + valueCadenceMax.setText(bundle.cadenceMax); + valueDate.setText(bundle.date); + valueTime.setText(bundle.time); + valueActiveTime.setText(bundle.activeTime); + valueTotalTime.setText(bundle.totalTime); + valueDistance.setText(bundle.distance); + valueElevation.setText(bundle.elevation); + valueAscent.setText(bundle.ascent); + valueDescent.setText(bundle.descent); - // speed is given as m/s - valueSpeedAvg.setText(String.format("%.2f", track.getAverageSpeed() * 3.6) - + App.GENERAL_BUNDLE.getString("unit.kmph")); - valueSpeedMax.setText(String.format("%.2f", track.getMaxSpeed() * 3.6) - + App.GENERAL_BUNDLE.getString("unit.kmph")); - - valueCadenceAvg.setText(track.getAverageCadence() - + App.GENERAL_BUNDLE.getString("unit.hz")); - valueCadenceMax.setText(track.getMaxCadence() - + App.GENERAL_BUNDLE.getString("unit.hz")); - - valueDate.setText(track.getStartTime().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL))); - valueTime.setText(track.getStartTime().format(DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM))); - valueActiveTime.setText(LocalTime.MIDNIGHT.plus(track.getMovingTime()) - .format(DateTimeFormatter.ofPattern("HH:mm:ss"))); - valueTotalTime.setText(App.GENERAL_BUNDLE.getString("time.of") - + LocalTime.MIDNIGHT.plus(track.getTotalDuration()) - .format(DateTimeFormatter.ofPattern("HH:mm:ss"))); - - if (track.getTotalDistance() > KILOMETER_CUTOFF) { - valueDistance.setText(String.format("%.2f", track.getTotalDistance() / 1000) - + App.GENERAL_BUNDLE.getString("unit.km")); - } else { - valueDistance.setText(String.format("%.2f", track.getTotalDistance()) - + App.GENERAL_BUNDLE.getString("unit.m")); - } - - valueElevation.setText((int)(track.getTotalAscent() - track.getTotalDescend()) - + App.GENERAL_BUNDLE.getString("unit.m")); - valueAscent.setText((int)track.getTotalAscent() - + App.GENERAL_BUNDLE.getString("unit.m")); - valueDescent.setText((int)track.getTotalDescend() - + App.GENERAL_BUNDLE.getString("unit.m")); - - // create charts data - XYChart.Series elevationChartData = new XYChart.Series<>(); - XYChart.Series speedChartData = new XYChart.Series<>(); - XYChart.Series hrChartData = new XYChart.Series<>(); - XYChart.Series cadenceChartData = new XYChart.Series<>(); - - // traverse the chunks - ObservableList chunks = track.getChunks(); - double currentDistance = 0.0; - double currentHeight = chunks.get(0).getFirstPoint().getElevation(); - for (Chunk chunk : chunks) { - currentDistance += chunk.getDistance(); - if (chunk.getDistance() < DISTANCE_EPSILON) continue; - currentHeight += chunk.getAscent() - chunk.getDescend(); - - elevationChartData.getData().add(new XYChart.Data<>(currentDistance, currentHeight)); - speedChartData.getData().add(new XYChart.Data<>(currentDistance, chunk.getSpeed()*3.6)); // m/s - hrChartData.getData().add(new XYChart.Data<>(currentDistance, chunk.getAvgHeartRate())); - cadenceChartData.getData().add(new XYChart.Data<>(currentDistance, chunk.getAvgCadence())); - - String zone; - if (chunk.getAvgHeartRate() > 170) zone = App.GENERAL_BUNDLE.getString("zone.anaerobic"); - else if (chunk.getAvgHeartRate() > 150) zone = App.GENERAL_BUNDLE.getString("zone.threshold"); - else if (chunk.getAvgHeartRate() > 130) zone = App.GENERAL_BUNDLE.getString("zone.tempo"); - else if (chunk.getAvgHeartRate() > 110) zone = App.GENERAL_BUNDLE.getString("zone.endurance"); - else zone = App.GENERAL_BUNDLE.getString("zone.recovery"); - - boolean pieFound = false; - for (PieChart.Data d : zoneChart.getData()){ - if (d.getName().equals(zone)) { - pieFound = true; - d.setPieValue(d.getPieValue() + 1); - } - } - if (!pieFound) zoneChart.getData().add( new PieChart.Data(zone, 1) ); - } + zoneChart.setData(bundle.pieData); // populate the charts - elevationChart.getData().add(elevationChartData); - speedChart.getData().add(speedChartData); - hrChart.getData().add(hrChartData); - cadenceChart.getData().add(cadenceChartData); + elevationChart.getData().add(bundle.elevationSeries); + speedChart.getData().add(bundle.speedSeries); + hrChart.getData().add(bundle.hrSeries); + cadenceChart.getData().add(bundle.cadenceSeries); //initialize map + chunks = bundle.chunks; mapView.addMapInializedListener(this); } - private void load() throws JAXBException { - FileChooser fileChooser = new FileChooser(); - fileChooser.getExtensionFilters().add( - new FileChooser.ExtensionFilter(App.GENERAL_BUNDLE.getString("app.extension.filter.name"), "*.gpx")); - File file = fileChooser.showOpenDialog(root.getScene().getWindow()); - if (file == null) return; - - String name = file.getName(); - JAXBContext jaxbContext = JAXBContext.newInstance(GpxType.class, TrackPointExtensionT.class); - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - @SuppressWarnings("unchecked") - JAXBElement jaxbElement = (JAXBElement) unmarshaller.unmarshal(file); - GpxType gpx = (GpxType) jaxbElement.getValue(); - - if (gpx != null) { - track = new TrackData(new Track(gpx.getTrk().get(0))); - loadTrack(track); - snackbar.show("GPX file: " + name + "successfully loaded", 3000); - } else { - snackbar.show("Error loading GPX file: " + name, 3000); - } - } - @Override public void mapInitialized() { // When the JS init is done @@ -260,10 +166,10 @@ public class DashboardController implements Initializable, MapComponentInitializ // Prepare an array with LatLong objects MVCArray pathArray = new MVCArray(); pathArray.push(new LatLong( // first step of the route - track.getChunks().get(0).getFirstPoint().getLatitude(), - track.getChunks().get(0).getFirstPoint().getLongitude() + chunks.get(0).getFirstPoint().getLatitude(), + chunks.get(0).getFirstPoint().getLongitude() )); - track.getChunks().forEach(chunk -> { + chunks.forEach(chunk -> { double lat = chunk.getLastPoint().getLatitude(); double lon = chunk.getLastPoint().getLongitude(); coord[N] = Math.max(lat, coord[N]); @@ -283,9 +189,8 @@ public class DashboardController implements Initializable, MapComponentInitializ new LatLong(coord[N], coord[E]) )); map.setZoom(getBoundsZoomLevel(coord, mapView.getHeight(), mapView.getWidth())); - // Print some debug info - System.err.printf("Bound to coords: %.2fN, %.2S, %.2fE, %.2fW\n", coord[N], coord[S], coord[E], coord[W]); - System.err.printf("Selected zoom: %d\n", map.getZoom()); + mapView.setVisible(true); + mapSpinner.setVisible(false); } /** diff --git a/src/main/java/es/kauron/estraba/controller/SplashController.java b/src/main/java/es/kauron/estraba/controller/SplashController.java index 1c489e7..2150abf 100644 --- a/src/main/java/es/kauron/estraba/controller/SplashController.java +++ b/src/main/java/es/kauron/estraba/controller/SplashController.java @@ -1,15 +1,27 @@ package es.kauron.estraba.controller; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXSnackbar; import com.jfoenix.controls.JFXSpinner; import es.kauron.estraba.App; +import es.kauron.estraba.model.DataBundle; +import javafx.application.Platform; +import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; +import javafx.scene.Parent; +import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import java.io.File; +import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; @@ -20,6 +32,9 @@ import java.util.ResourceBundle; public class SplashController implements Initializable{ + @FXML + private AnchorPane root; + @FXML private ImageView imgLogo; @@ -32,17 +47,70 @@ public class SplashController implements Initializable{ @FXML private JFXButton buttonLoad; + private JFXSnackbar snackbar; + private File file; + @FXML - void loadGPXFile(ActionEvent event) { + private void loadGPXFile(ActionEvent event) { buttonLoad.setVisible(false); labelWelcome.setVisible(false); spinner.setVisible(true); + snackbar.registerSnackbarContainer(root); + snackbar.show("Loading file", 5000); + FileChooser fileChooser = new FileChooser(); + fileChooser.getExtensionFilters().add( + new FileChooser.ExtensionFilter(App.GENERAL_BUNDLE.getString("app.extension.filter.name"), "*.gpx")); + file = fileChooser.showOpenDialog(root.getScene().getWindow()); + if (file == null) { + errorLoading(); + return; + } + Task task = new Task() { + @Override + protected Void call() throws Exception { + try { + DataBundle bundle = DataBundle.loadFrom(file); + FXMLLoader loader = new FXMLLoader( + App.class.getResource("fxml/Dashboard.fxml"), App.GENERAL_BUNDLE); + Parent root = loader.load(); + + Stage stage = new Stage(); + stage.getIcons().add(new Image(App.class.getResource("img/icon.png").toString())); + stage.setTitle(App.GENERAL_BUNDLE.getString("app.title")); + stage.setResizable(false); + stage.setScene(new Scene(root)); + + // Begin awesomewm code + stage.setMinHeight(500); + stage.setMinWidth(800); + stage.setResizable(false); + // End awesomewm code + stage.show(); + loader.getController().postInit(bundle); + Platform.runLater(() -> ((Stage) root.getScene().getWindow()).close()); + } catch (IOException e) { + errorLoading(); + } + return null; + } + }; + Thread t = new Thread(task); + t.setDaemon(true); + t.start(); } @Override public void initialize(URL location, ResourceBundle resources) { - imgLogo.setImage(new Image(App.class.getResourceAsStream("img/splash.png"))); + imgLogo.setImage(new Image(App.class.getResourceAsStream("img/strava-transparent.png"))); + snackbar = new JFXSnackbar(); + } + + private void errorLoading() { + buttonLoad.setVisible(true); + labelWelcome.setVisible(true); + spinner.setVisible(false); + snackbar.show("Error loading file", 3000); } } diff --git a/src/main/java/es/kauron/estraba/model/DataBundle.java b/src/main/java/es/kauron/estraba/model/DataBundle.java new file mode 100644 index 0000000..367059e --- /dev/null +++ b/src/main/java/es/kauron/estraba/model/DataBundle.java @@ -0,0 +1,107 @@ +package es.kauron.estraba.model; + +import es.kauron.estraba.App; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.PieChart; +import javafx.scene.chart.XYChart; +import jgpx.model.analysis.Chunk; +import jgpx.model.analysis.TrackData; +import jgpx.model.gpx.Track; +import jgpx.model.jaxb.GpxType; +import jgpx.model.jaxb.TrackPointExtensionT; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.Unmarshaller; +import java.io.File; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +public class DataBundle { + public static final int N = 0, S = 1, E = 2, W = 3; + private static final double DISTANCE_EPSILON = 1E-6; + private static final double KILOMETER_CUTOFF = 10000; + + public String HRAvg, HRMax, HRMin, speedAvg, speedMax, cadenceAvg, cadenceMax; + public String date, time, activeTime, totalTime, distance, elevation, ascent, descent; + public XYChart.Series elevationSeries = new XYChart.Series<>(), + speedSeries = new XYChart.Series<>(), + hrSeries = new XYChart.Series<>(), + cadenceSeries = new XYChart.Series<>(); + public ObservableList pieData = FXCollections.emptyObservableList(); + public ObservableList chunks; + + public static DataBundle loadFrom(File file) throws Exception { + String name = file.getName(); + JAXBElement jaxbElement; + JAXBContext jaxbContext = JAXBContext.newInstance(GpxType.class, TrackPointExtensionT.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + jaxbElement = (JAXBElement) unmarshaller.unmarshal(file); + GpxType gpx = (GpxType) jaxbElement.getValue(); + + if (gpx == null) throw new Exception(); + return new DataBundle(new TrackData(new Track(gpx.getTrk().get(0)))); + } + + private DataBundle(TrackData track) { + HRAvg = track.getAverageHeartrate() + App.GENERAL_BUNDLE.getString("unit.bpm"); + HRMax = (track.getMaxHeartrate() + App.GENERAL_BUNDLE.getString("unit.bpm")); + HRMin = track.getMinHeartRate() + App.GENERAL_BUNDLE.getString("unit.bpm"); + + // speed is given as m/s + speedAvg = String.format("%.2f", track.getAverageSpeed() * 3.6) + App.GENERAL_BUNDLE.getString("unit.kmph"); + speedMax = String.format("%.2f", track.getMaxSpeed() * 3.6) + App.GENERAL_BUNDLE.getString("unit.kmph"); + + cadenceAvg = track.getAverageCadence() + App.GENERAL_BUNDLE.getString("unit.hz"); + cadenceMax = track.getMaxCadence() + App.GENERAL_BUNDLE.getString("unit.hz"); + + date = track.getStartTime().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)); + time = track.getStartTime().format(DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)); + activeTime = LocalTime.MIDNIGHT.plus(track.getMovingTime()).format(DateTimeFormatter.ofPattern("HH:mm:ss")); + totalTime = App.GENERAL_BUNDLE.getString("time.of") + + LocalTime.MIDNIGHT.plus(track.getTotalDuration()).format(DateTimeFormatter.ofPattern("HH:mm:ss")); + + if (track.getTotalDistance() > KILOMETER_CUTOFF) { + distance = String.format("%.2f", track.getTotalDistance() / 1000) + App.GENERAL_BUNDLE.getString("unit.km"); + } else { + distance = String.format("%.2f", track.getTotalDistance()) + App.GENERAL_BUNDLE.getString("unit.m"); + } + + elevation = (int)(track.getTotalAscent() - track.getTotalDescend()) + App.GENERAL_BUNDLE.getString("unit.m"); + ascent = (int)track.getTotalAscent() + App.GENERAL_BUNDLE.getString("unit.m"); + descent = (int)track.getTotalDescend() + App.GENERAL_BUNDLE.getString("unit.m"); + + // traverse the chunks + chunks = track.getChunks(); + double currentDistance = 0.0; + double currentHeight = chunks.get(0).getFirstPoint().getElevation(); + for (Chunk chunk : chunks) { + currentDistance += chunk.getDistance(); + if (chunk.getDistance() < DISTANCE_EPSILON) continue; + currentHeight += chunk.getAscent() - chunk.getDescend(); + + elevationSeries.getData().add(new XYChart.Data<>(currentDistance, currentHeight)); + speedSeries.getData().add(new XYChart.Data<>(currentDistance, chunk.getSpeed()*3.6)); // m/s + hrSeries.getData().add(new XYChart.Data<>(currentDistance, chunk.getAvgHeartRate())); + cadenceSeries.getData().add(new XYChart.Data<>(currentDistance, chunk.getAvgCadence())); + + String zone; + if (chunk.getAvgHeartRate() > 170) zone = App.GENERAL_BUNDLE.getString("zone.anaerobic"); + else if (chunk.getAvgHeartRate() > 150) zone = App.GENERAL_BUNDLE.getString("zone.threshold"); + else if (chunk.getAvgHeartRate() > 130) zone = App.GENERAL_BUNDLE.getString("zone.tempo"); + else if (chunk.getAvgHeartRate() > 110) zone = App.GENERAL_BUNDLE.getString("zone.endurance"); + else zone = App.GENERAL_BUNDLE.getString("zone.recovery"); + + boolean pieFound = false; + for (PieChart.Data d : pieData){ + if (d.getName().equals(zone)) { + pieFound = true; + d.setPieValue(d.getPieValue() + 1); + } + } + if (!pieFound) pieData.add( new PieChart.Data(zone, 1) ); + } + } +} diff --git a/src/main/resources/es/kauron/estraba/fxml/Splash.fxml b/src/main/resources/es/kauron/estraba/fxml/Splash.fxml index 57b7392..edc5b7d 100644 --- a/src/main/resources/es/kauron/estraba/fxml/Splash.fxml +++ b/src/main/resources/es/kauron/estraba/fxml/Splash.fxml @@ -8,7 +8,7 @@ - + diff --git a/src/main/resources/general.properties b/src/main/resources/general.properties index 5578318..3e06b64 100644 --- a/src/main/resources/general.properties +++ b/src/main/resources/general.properties @@ -4,8 +4,10 @@ label.cadence=Cadence label.distance=Distance label.elevation=Elevation label.hr=Heart rate +label.loadGPX=Load GPX file label.motivation=Welcome! label.speed=Speed +label.welcome=Welcome! tab.dashboard=Dashboard tab.graph=Stats tab.map=Your Route diff --git a/src/main/resources/general_ca.properties b/src/main/resources/general_ca.properties index 00152d0..573d01b 100644 --- a/src/main/resources/general_ca.properties +++ b/src/main/resources/general_ca.properties @@ -4,8 +4,10 @@ label.cadence=Cadencia label.distance=Distancia label.elevation=Elevacion label.hr=YOLO +label.loadGPX=Obrir arxiu GPX label.motivation=\u00a1Benvinguts! label.speed=Speed +label.welcome=Benvingut! tab.dashboard=Sumari tab.graph=Estad\u00edstiques tab.map=La teva ruta diff --git a/src/main/resources/general_es.properties b/src/main/resources/general_es.properties index 0cbca19..51e5794 100644 --- a/src/main/resources/general_es.properties +++ b/src/main/resources/general_es.properties @@ -4,8 +4,10 @@ label.cadence=Cadencia label.distance=Distancia label.elevation=Elevacion label.hr=Pulsaci\u00f3nes +label.loadGPX=Abrir archivo GPX label.motivation=\u00a1Bienvenido! label.speed=Velocidad +label.welcome=¡Bienvenido! tab.dashboard=Res\u00famen tab.graph=Estad\u00edsticas tab.map=Tu ruta