de.perdoctus.ebikeconnect.gui.ActivitiesOverviewController.java Source code

Java tutorial

Introduction

Here is the source code for de.perdoctus.ebikeconnect.gui.ActivitiesOverviewController.java

Source

package de.perdoctus.ebikeconnect.gui;

/*
 * #%L
 * ebikeconnect-gui
 * %%
 * Copyright (C) 2016 Christoph Giesche
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.perdoctus.ebikeconnect.gui.components.table.DurationCellFactory;
import de.perdoctus.ebikeconnect.gui.components.table.LocalDateCellFactory;
import de.perdoctus.ebikeconnect.gui.components.table.NumberCellFactory;
import de.perdoctus.ebikeconnect.gui.models.*;
import de.perdoctus.ebikeconnect.gui.models.json.LatLng;
import de.perdoctus.ebikeconnect.gui.services.ActivityDaysHeaderService;
import de.perdoctus.ebikeconnect.gui.services.ActivityDetailsGroupService;
import de.perdoctus.ebikeconnect.gui.services.export.ExportService;
import de.perdoctus.ebikeconnect.gui.services.export.GpxExportService;
import de.perdoctus.ebikeconnect.gui.services.export.TcxExportService;
import de.perdoctus.ebikeconnect.gui.util.DurationFormatter;
import de.perdoctus.fx.Bundle;
import de.perdoctus.fx.ToggleableSeriesChart;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.util.StringConverter;
import org.controlsfx.control.CheckListView;
import org.controlsfx.control.RangeSlider;
import org.controlsfx.dialog.ProgressDialog;
import org.slf4j.Logger;

import javax.inject.Inject;
import java.io.File;
import java.text.NumberFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.ResourceBundle;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toList;

public class ActivitiesOverviewController {

    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter
            .ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.MEDIUM);
    public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
    public static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance();

    @Inject
    private Logger logger;
    @Inject
    private ActivityDaysHeaderService activityDaysHeaderService;
    @Inject
    private ActivityDetailsGroupService activityDetailsGroupService;
    @Inject
    private GpxExportService gpxExportService;
    @Inject
    private TcxExportService tcxExportService;
    @Inject
    private ObjectMapper objectMapper;

    @Inject
    @Bundle("bundles/General")
    private ResourceBundle rb;
    // Activities Overview
    @FXML
    private TableView<ActivityHeaderGroup> activitiesTable;

    @FXML
    private TableColumn<ActivityHeaderGroup, Number> tcDistance;
    @FXML
    private TableColumn<ActivityHeaderGroup, LocalDate> tcDate;

    @FXML
    private TableColumn<ActivityHeaderGroup, Duration> tcDuration;

    // Activity Segments
    @FXML
    public CheckListView<ActivityHeader> lstSegments;
    // Webview
    @FXML
    private WebView webView;

    private WebEngine webEngine;
    // Chart
    @FXML
    private ToggleableSeriesChart<Number, Number> chart;

    @FXML
    private NumberAxis xAxis;
    @FXML
    private RangeSlider chartRangeSlider;
    // Properties
    private ObjectProperty<ActivityDetailsGroup> currentActivityDetailsGroup = new SimpleObjectProperty<>();

    @FXML
    public void initialize() {
        logger.info("Init!");

        NUMBER_FORMAT.setMaximumFractionDigits(2);

        webEngine = webView.getEngine();
        webEngine.load(getClass().getResource("/html/googleMap.html").toExternalForm());

        // Activity Headers
        activityDaysHeaderService.setOnSucceeded(event -> {
            activitiesTable.setItems(FXCollections.observableArrayList(activityDaysHeaderService.getValue()));
            activitiesTable.getSortOrder().add(tcDate);
            tcDate.setSortable(true);
        });
        activityDaysHeaderService.setOnFailed(
                event -> logger.error("Failed to obtain ActivityList!", activityDaysHeaderService.getException()));
        final ProgressDialog activityHeadersProgressDialog = new ProgressDialog(activityDaysHeaderService);
        activityHeadersProgressDialog.initModality(Modality.APPLICATION_MODAL);

        // Activity Details
        activityDetailsGroupService.setOnSucceeded(
                event -> this.currentActivityDetailsGroup.setValue(activityDetailsGroupService.getValue()));
        activityDetailsGroupService.setOnFailed(event -> logger.error("Failed to obtain ActivityDetails!",
                activityDaysHeaderService.getException()));
        final ProgressDialog activityDetailsProgressDialog = new ProgressDialog(activityDetailsGroupService);
        activityDetailsProgressDialog.initModality(Modality.APPLICATION_MODAL);

        // Gpx Export
        gpxExportService.setOnSucceeded(event -> gpxExportFinished());
        gpxExportService
                .setOnFailed(event -> handleError("Failed to generate GPX File", gpxExportService.getException()));

        tcxExportService.setOnSucceeded(event -> gpxExportFinished());
        tcxExportService
                .setOnFailed(event -> handleError("Failed to generate TCX File", tcxExportService.getException()));

        // ActivityTable
        tcDate.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getDate()));
        tcDate.setCellFactory(param -> new LocalDateCellFactory());
        tcDate.setSortType(TableColumn.SortType.DESCENDING);

        tcDistance.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getDistance() / 1000));
        tcDistance.setCellFactory(param -> new NumberCellFactory(1, "km"));

        tcDuration.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getDrivingTime()));
        tcDuration.setCellFactory(param -> new DurationCellFactory());

        activitiesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        activitiesTable.getSelectionModel().getSelectedItems()
                .addListener((ListChangeListener<ActivityHeaderGroup>) c -> {
                    while (c.next()) {
                        if (c.wasRemoved()) {
                            for (ActivityHeaderGroup activityHeaderGroup : c.getRemoved()) {
                                lstSegments.getItems().removeAll(activityHeaderGroup.getActivityHeaders());
                            }

                        }
                        if (c.wasAdded()) {
                            for (ActivityHeaderGroup activityHeaderGroup : c.getAddedSubList()) {
                                if (activityHeaderGroup != null) { // WTF? Why can this be null!?
                                    lstSegments.getItems().addAll(activityHeaderGroup.getActivityHeaders());
                                }
                            }
                        }

                    }
                    lstSegments.getItems().sort((o1, o2) -> o1.getStartTime().isAfter(o2.getStartTime()) ? 1 : 0);
                });

        activitiesTable.setOnMouseClicked(event -> {
            if (event.getClickCount() == 2) {
                lstSegments.getCheckModel().checkAll();
                openSelectedSections();
            }
        });

        // Segment List
        lstSegments
                .setCellFactory(listView -> new CheckBoxListCell<>(item -> lstSegments.getItemBooleanProperty(item),
                        new StringConverter<ActivityHeader>() {

                            @Override
                            public ActivityHeader fromString(String arg0) {
                                return null;
                            }

                            @Override
                            public String toString(ActivityHeader activityHeader) {
                                final String startTime = activityHeader.getStartTime().format(DATE_TIME_FORMATTER);
                                final String endTime = activityHeader.getEndTime().format(TIME_FORMATTER);
                                final double distance = activityHeader.getDistance() / 1000;
                                return startTime + " - " + endTime + " (" + NUMBER_FORMAT.format(distance) + " km)";
                            }

                        }));

        // -- Chart
        chartRangeSlider.setLowValue(0);
        chartRangeSlider.setHighValue(chartRangeSlider.getMax());

        xAxis.setAutoRanging(false);
        xAxis.lowerBoundProperty().bind(chartRangeSlider.lowValueProperty());
        xAxis.upperBoundProperty().bind(chartRangeSlider.highValueProperty());
        xAxis.tickUnitProperty().bind(
                chartRangeSlider.highValueProperty().subtract(chartRangeSlider.lowValueProperty()).divide(20));
        xAxis.setTickLabelFormatter(new StringConverter<Number>() {
            @Override
            public String toString(Number object) {
                final Duration duration = Duration.of(object.intValue(), ChronoUnit.SECONDS);
                return String.valueOf(DurationFormatter.formatHhMmSs(duration));
            }

            @Override
            public Number fromString(String string) {
                return null;
            }
        });

        chart.getChart().setOnScroll(event -> {
            final double scrollAmount = event.getDeltaY();
            chartRangeSlider.setLowValue(chartRangeSlider.getLowValue() + scrollAmount);
            chartRangeSlider.setHighValue(chartRangeSlider.getHighValue() - scrollAmount);
        });

        xAxis.setOnMouseMoved(event -> {
            if (getCurrentActivityDetailsGroup() == null) {
                return;
            }

            final Number valueForDisplay = xAxis.getValueForDisplay(event.getX());
            final List<Coordinate> trackpoints = getCurrentActivityDetailsGroup().getJoinedTrackpoints();
            final int index = valueForDisplay.intValue();
            if (index >= 0 && index < trackpoints.size()) {
                final Coordinate coordinate = trackpoints.get(index);
                if (coordinate.isValid()) {
                    final LatLng latLng = new LatLng(coordinate);
                    try {
                        webEngine.executeScript(
                                "updateMarkerPosition(" + objectMapper.writeValueAsString(latLng) + ");");
                    } catch (JsonProcessingException e) {
                        e.printStackTrace(); //TODO clean up ugly code!!!!--------------
                    }
                }
            }
        });

        // -- Current ActivityDetails
        this.currentActivityDetailsGroup.addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                activityGroupChanged(newValue);
            }
        });
    }

    private void handleError(final String message, final Throwable exception) {
        logger.error(exception.getMessage(), exception);
        final Alert alert = new Alert(Alert.AlertType.ERROR, exception.getMessage(), ButtonType.OK);
        alert.setTitle(message);
        alert.setHeaderText(rb.getString("error-header"));
        alert.show();
    }

    private void gpxExportFinished() {
        final Alert info = new Alert(Alert.AlertType.INFORMATION, "", ButtonType.OK);
        info.setTitle(rb.getString("gpx-export-finished"));
        info.setHeaderText(rb.getString("gpx-export-finished"));
        info.show();
    }

    private void activityGroupChanged(final ActivityDetailsGroup activityDetailsGroup) {
        refreshChart(activityDetailsGroup);
        refreshMap(activityDetailsGroup);
    }

    private void refreshMap(final ActivityDetailsGroup activityDetailsGroup) {
        final List<ActivityDetails> activityDaySegments = activityDetailsGroup.getActivitySegments();

        final ObjectMapper objectMapper = new ObjectMapper();

        webEngine.executeScript("clearPolylines();");

        for (ActivityDetails activityDaySegment : activityDaySegments) {
            final List<Coordinate> trackPoints = activityDaySegment.getTrackPoints();
            final List<LatLng> latLngs = trackPoints.stream().filter(Coordinate::isValid).map(LatLng::new)
                    .collect(toList());

            try {
                webEngine.executeScript("var bounds = new google.maps.LatLngBounds();");
                latLngs.forEach(e -> {
                    try {
                        webEngine.executeScript("bounds.extend(new google.maps.LatLng("
                                + objectMapper.writeValueAsString(e) + "))");
                    } catch (JsonProcessingException e1) {
                        logger.error("Failed to serialize LatLngs", e);
                    }
                });

                webEngine.executeScript("var track = " + objectMapper.writeValueAsString(latLngs) + ";");
                webEngine.executeScript("addTrackSegment(track);");
                webEngine.executeScript("googleMap.setCenter(bounds.getCenter())");
                webEngine.executeScript("googleMap.setZoom(12)");
            } catch (JsonProcessingException e) {
                logger.error("Failed to serialize LatLngs", e);
            }
        }

    }

    private void refreshChart(final ActivityDetailsGroup activityDetailsGroup) {
        final List<ActivityDetails> activityDaySegments = activityDetailsGroup.getActivitySegments();

        chart.getData().clear();
        addChartSeries(rb.getString("altitude"),
                activityDaySegments.stream().flatMap(ad -> ad.getAltitudes().stream()).collect(toList()));
        addChartSeries(rb.getString("speed"),
                activityDaySegments.stream().flatMap(ad -> ad.getSpeeds().stream()).collect(toList()));
        addChartSeries(rb.getString("heart-rate"),
                activityDaySegments.stream().flatMap(ad -> ad.getHeartRate().stream()).collect(toList()));
        addChartSeries(rb.getString("cadence"),
                activityDaySegments.stream().flatMap(ad -> ad.getCadences().stream()).collect(toList()));
        addChartSeries(rb.getString("driver-torque"),
                activityDaySegments.stream().flatMap(ad -> ad.getDriverTorques().stream()).collect(toList()));
        addChartSeries(rb.getString("motor-torque"),
                activityDaySegments.stream().flatMap(ad -> ad.getMotorTorques().stream()).collect(toList()));
        addChartSeries(rb.getString("motor-revolutions"), activityDaySegments.stream()
                .flatMap(ad -> ad.getMotorRevolutionRates().stream()).collect(toList()));
        addChartSeries(rb.getString("energy-economy"),
                activityDaySegments.stream().flatMap(ad -> ad.getEnergyEconomies().stream()).collect(toList()));
        chartRangeSlider.setLowValue(0);
        chartRangeSlider.setHighValue(chartRangeSlider.getMax());
    }

    private void addChartSeries(final String title, final List<? extends Number> samples) {
        logger.info(title + ": " + samples.size() + " samples.");

        final XYChart.Series<Number, Number> series = new XYChart.Series<>();
        series.setName(title);

        final ObservableList<XYChart.Data<Number, Number>> data = series.getData();

        for (int i = 0; i < samples.size(); i += 4) {
            final Number number = samples.get(i);
            if (number != null) {
                final XYChart.Data<Number, Number> dataPoint = new XYChart.Data<>(i, number);
                data.add(dataPoint);
            }
        }

        chartRangeSlider.setMax(samples.size());

        if (data.size() > 0) {
            chart.getData().add(series);
        }
    }

    @FXML
    private void openSelectedSections() {
        if (activityDetailsGroupService.isRunning()) {
            activityDetailsGroupService.cancel();
        }
        activityDetailsGroupService.reset();
        activityDetailsGroupService.setActivityIds(lstSegments.getCheckModel().getCheckedItems().stream()
                .filter(activityHeader -> activityHeader != null).map(ActivityHeader::getActivityId)
                .collect(Collectors.toList()));
        activityDetailsGroupService.start();
    }

    public void reloadHeaders() {
        logger.info("Reloading Headers!");
        if (!activityDaysHeaderService.isRunning()) {
            activitiesTable.getSelectionModel().clearSelection();
            activityDaysHeaderService.restart();
        }
    }

    public ActivityDetailsGroup getCurrentActivityDetailsGroup() {
        return currentActivityDetailsGroup.get();
    }

    public ObjectProperty<ActivityDetailsGroup> currentActivityDetailsGroupProperty() {
        return currentActivityDetailsGroup;
    }

    public void exportCurrentActivityAsGPX() {
        exportCurrentActivity(gpxExportService);
    }

    public void exportCurrentActivityAsTCX() {
        exportCurrentActivity(tcxExportService);
    }

    private void exportCurrentActivity(final ExportService exportService) {
        final FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle(exportService.getFileTypeDescription());
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
                exportService.getFileTypeDescription(), "*." + exportService.getFileExtension()));

        final File file = fileChooser.showSaveDialog(chart.getScene().getWindow());

        if (file != null) {
            exportService.setActivityDetails(this.currentActivityDetailsGroup.get().getActivitySegments());
            exportService.setFile(file);
            exportService.restart();
        }
    }
}