package de.saring.exerciseviewer.gui.panels; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import de.saring.leafletmap.ColorMarker; import de.saring.leafletmap.ControlPosition; import de.saring.leafletmap.LatLong; import de.saring.leafletmap.LeafletMapView; import de.saring.leafletmap.MapConfig; import de.saring.leafletmap.MapLayer; import de.saring.leafletmap.ScaleControlConfig; import de.saring.leafletmap.ZoomControlConfig; import javafx.concurrent.Worker; import javafx.fxml.FXML; import javafx.geometry.Point2D; import javafx.scene.Scene; import javafx.scene.control.Slider; import javafx.scene.control.Tooltip; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.Window; import de.saring.exerciseviewer.data.EVExercise; import de.saring.exerciseviewer.data.ExerciseSample; import de.saring.exerciseviewer.data.Lap; import de.saring.exerciseviewer.data.Position; import de.saring.exerciseviewer.gui.EVContext; import de.saring.exerciseviewer.gui.EVDocument; import de.saring.util.unitcalc.FormatUtils; /** * Controller (MVC) class of the "Track" panel, which displays the recorded location data of the exercise (if * available) in a map.<br/> * The map component is LeafletMap which is based on the Leaflet Javascript library, the data provider is OpenStreetMap. * * @author Stefan Saring */ public class TrackPanelController extends AbstractPanelController { private static final Logger LOGGER = Logger.getLogger(TrackPanelController.class.getName()); private static final int TRACKPOINT_TOOLTIP_DISTANCE_BUFFER = 4; @FXML private StackPane spTrackPanel; @FXML private VBox vbTrackViewer; @FXML private StackPane spMapViewer; @FXML private Slider slPosition; private LeafletMapView mapView; private MapConfig mapConfig; private Tooltip spMapViewerTooltip; private String positionMarkerName; /** Flag whether the exercise track has already been shown. */ private boolean showTrackExecuted = false; /** * Standard c'tor for dependency injection. * * @param context the ExerciseViewer UI context * @param document the ExerciseViewer document / model */ public TrackPanelController(final EVContext context, final EVDocument document) { super(context, document); } @Override protected String getFxmlFilename() { return "/fxml/panels/TrackPanel.fxml"; } @Override protected void setupPanel() { // setup the map viewer if track data is available final EVExercise exercise = getDocument().getExercise(); if (exercise.getRecordingMode().isLocation()) { setupMapView(); setupMapViewerTooltip(); setupTrackPositionSlider(); } else { // remove the track viewer VBox, the StackPane now displays the label "No track data available") spTrackPanel.getChildren().remove(vbTrackViewer); } } private void setupMapView() { mapView = new LeafletMapView(); spMapViewer.getChildren().add(mapView); final boolean metric = getDocument().getOptions().getUnitSystem() == FormatUtils.UnitSystem.Metric; mapConfig = new MapConfig( Arrays.asList(MapLayer.OPENSTREETMAP, MapLayer.OPENCYCLEMAP, MapLayer.HIKE_BIKE_MAP, MapLayer.MTB_MAP), new ZoomControlConfig(true, ControlPosition.BOTTOM_LEFT), new ScaleControlConfig(true, ControlPosition.BOTTOM_LEFT, metric)); } private void setupMapViewerTooltip() { spMapViewerTooltip = new Tooltip(); spMapViewerTooltip.setAutoHide(true); } private void setupTrackPositionSlider() { // on position slider changes: update position marker in the map viewer and display tooltip with details // (slider uses a double value, make sure the int value has changed) slPosition.valueProperty().addListener((observable, oldValue, newValue) -> { if (oldValue.intValue() != newValue.intValue()) { movePositionMarker(newValue.intValue()); } }); } private void movePositionMarker(final int positionIndex) { final ExerciseSample sample = getDocument().getExercise().getSampleList()[positionIndex]; // some samples could have no position if (sample.getPosition() != null) { final LatLong position = new LatLong(sample.getPosition().getLatitude(), sample.getPosition().getLongitude()); if (positionMarkerName == null) { positionMarkerName = mapView.addMarker(position, "", ColorMarker.BLUE_MARKER, 0); } else { mapView.moveMarker(positionMarkerName, position); } final String tooltipText = createToolTipText(positionIndex); spMapViewerTooltip.setText(tooltipText); // display position tooltip in the upper left corner of the map viewer container Point2D tooltipPos = spMapViewer.localToScene(8d, 8d); tooltipPos = tooltipPos.add(getMapViewerScreenPosition()); spMapViewerTooltip.show(spMapViewer, tooltipPos.getX(), tooltipPos.getY()); } } /** * Displays map and the track of the current exercise, if available. This method will be executed only once and * should be called when the user wants to see the track (to prevent long startup delays). */ public void showMapAndTrack() { if (!showTrackExecuted) { showTrackExecuted = true; EVExercise exercise = getDocument().getExercise(); if (exercise.getRecordingMode().isLocation()) { // display map, on success display the track and laps mapView.displayMap(mapConfig).whenComplete((workerState, throwable) -> { if (workerState == Worker.State.SUCCEEDED) { showTrackAndLaps(); // enable position slider by setting max. sample count slPosition.setMax(exercise.getSampleList().length - 1); } else if (throwable != null) { LOGGER.log(Level.SEVERE, "Failed to display map!", throwable); } }); } } } private void showTrackAndLaps() { EVExercise exercise = getDocument().getExercise(); List<LatLong> samplePositions = createSamplePositionList(exercise); if (!samplePositions.isEmpty()) { mapView.addTrack(samplePositions); // display lap markers first, start and end needs to be displayed on top List<LatLong> lapPositions = createLapPositionList(exercise); for (int i = 0; i < lapPositions.size(); i++) { mapView.addMarker(lapPositions.get(i), getContext().getResources().getString("pv.track.maptooltip.lap", i + 1), ColorMarker.GREY_MARKER, 0); } mapView.addMarker(samplePositions.get(0), getContext().getResources().getString("pv.track.maptooltip.start"), ColorMarker.GREEN_MARKER, 1000); mapView.addMarker(samplePositions.get(samplePositions.size() - 1), getContext().getResources().getString("pv.track.maptooltip.end"), ColorMarker.RED_MARKER, 2000); } } private List<LatLong> createSamplePositionList(final EVExercise exercise) { final List<LatLong> positions = new ArrayList<>(); for (ExerciseSample sample : exercise.getSampleList()) { final Position pos = sample.getPosition(); if (pos != null) { positions.add(new LatLong(pos.getLatitude(), pos.getLongitude())); } } return positions; } private List<LatLong> createLapPositionList(final EVExercise exercise) { final List<LatLong> lapPositions = new ArrayList<>(); // ignore last lap split position, it's the exercise end position for (int i = 0; i < exercise.getLapList().length - 1; i++) { final Lap lap = exercise.getLapList()[i]; final Position pos = lap.getPositionSplit(); if (pos != null) { lapPositions.add(new LatLong(pos.getLatitude(), pos.getLongitude())); } } return lapPositions; } private Point2D getMapViewerScreenPosition() { final Scene scene = spMapViewer.getScene(); final Window window = scene.getWindow(); return new Point2D(scene.getX() + window.getX(), scene.getY() + window.getY()); } /** * Creates the tool tip text for the specified exercise sample to be shown on the map. * * @param sampleIndex index of the exercise sample * @return text */ private String createToolTipText(int sampleIndex) { EVExercise exercise = getDocument().getExercise(); ExerciseSample sample = exercise.getSampleList()[sampleIndex]; FormatUtils formatUtils = getContext().getFormatUtils(); StringBuilder sb = new StringBuilder(); appendToolTipLine(sb, "pv.track.tooltip.trackpoint", String.valueOf(sampleIndex + 1)); appendToolTipLine(sb, "pv.track.tooltip.time", formatUtils.seconds2TimeString((int) (sample.getTimestamp() / 1000))); appendToolTipLine(sb, "pv.track.tooltip.distance", formatUtils.distanceToString(sample.getDistance() / 1000f, 3)); if (exercise.getRecordingMode().isAltitude()) { appendToolTipLine(sb, "pv.track.tooltip.altitude", // formatUtils.heightToString(sample.getAltitude())); } appendToolTipLine(sb, "pv.track.tooltip.heartrate", // formatUtils.heartRateToString(sample.getHeartRate())); if (exercise.getRecordingMode().isSpeed()) { appendToolTipLine(sb, "pv.track.tooltip.speed", // formatUtils.speedToString(sample.getSpeed(), 2)); } if (exercise.getRecordingMode().isTemperature()) { appendToolTipLine(sb, "pv.track.tooltip.temperature", // formatUtils.temperatureToString(sample.getTemperature())); } return sb.toString(); } private void appendToolTipLine(StringBuilder sb, String resourceKey, String value) { sb.append(getContext().getResources().getString(resourceKey)); sb.append(": ").append(value).append("\n"); } }