/* * ShootOFF - Software for Laser Dry Fire Training * Copyright (C) 2016 phrack * * This program 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. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package com.shootoff.gui.controller; import java.io.File; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.config.Configuration; import com.shootoff.gui.SessionCanvasManager; import com.shootoff.session.Event; import com.shootoff.session.SessionRecorder; import com.shootoff.session.ShotEvent; import com.shootoff.session.io.SessionIO; import com.shootoff.util.NamedThreadFactory; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.control.Slider; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.stage.Stage; import marytts.util.io.FileFilter; public class SessionViewerController { @FXML private HBox sessionViewerPane; @FXML private ListView<File> sessionListView; @FXML private TabPane cameraTabPane; @FXML private Button togglePlaybackButton; @FXML private Slider timeSlider; @FXML private Label timeLabel; @FXML private ListView<Event> eventsListView; private static final int STEP_INTERVAL = 100; // ms private static final int CORE_POOL_SIZE = 2; private final Logger logger = LoggerFactory.getLogger(SessionViewerController.class); private ScheduledExecutorService executorService; private final ObservableList<File> sessionEntries = FXCollections.observableArrayList(); private final ObservableList<Event> eventEntries = FXCollections.observableArrayList(); private final Map<String, SessionCanvasManager> cameraGroups = new HashMap<>(); private final Map<Tab, Integer> eventSelectionsPerTab = new HashMap<>(); private boolean isPlaying = false; private boolean refreshFromSlider = true; private boolean refreshFromSelection = true; private SessionRecorder currentSession; private Configuration config; public void init(Configuration config) { this.config = config; sessionEntries.addAll(findSessions()); sessionListView.setItems(sessionEntries); togglePlaybackButton.setGraphic(new ImageView( new Image(VideoPlayerController.class.getResourceAsStream("/images/gnome_media_playback_start.png")))); sessionListView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<File>() { @Override public void changed(ObservableValue<? extends File> ov, File oldFile, File newFile) { if (isPlaying) togglePlaybackButton.fire(); final Optional<SessionRecorder> session = SessionIO.loadSession(new File(System.getProperty("shootoff.home") + File.separator + "sessions" + File.separator + newFile.getName())); if (session.isPresent()) { refreshFromSlider = false; timeSlider.setValue(0); refreshFromSlider = true; currentSession = session.get(); updateCameraTabs(); final Tab selectedTab = cameraTabPane.getSelectionModel().getSelectedItem(); if (selectedTab != null) { final String cameraName = selectedTab.getText(); listCameraEvents(cameraName); } else { eventEntries.clear(); } } } }); cameraTabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() { @Override public void changed(ObservableValue<? extends Tab> ot, Tab oldTab, Tab newTab) { if (newTab == null) return; if (isPlaying) togglePlaybackButton.fire(); eventSelectionsPerTab.put(oldTab, eventsListView.getSelectionModel().getSelectedIndex()); listCameraEvents(newTab.getText()); final List<Event> cameraEvents = currentSession.getCameraEvents(newTab.getText()); refreshFromSlider = false; timeSlider.setValue(0); timeSlider.setMax(cameraEvents.get(cameraEvents.size() - 1).getTimestamp()); refreshFromSlider = true; if (eventSelectionsPerTab.containsKey(newTab)) { refreshFromSelection = false; eventsListView.getSelectionModel().select(eventSelectionsPerTab.get(newTab)); refreshFromSelection = true; } } }); eventsListView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Event>() { @Override public void changed(ObservableValue<? extends Event> oe, Event oldEvent, Event newEvent) { if (newEvent == null) return; refreshFromSlider = false; if (!isPlaying) timeSlider.setValue(newEvent.getTimestamp()); refreshFromSlider = true; if (!refreshFromSelection) return; final int oldIndex = eventEntries.indexOf(oldEvent); final int newIndex = eventEntries.indexOf(newEvent); if (oldIndex <= newIndex) { updateEvents(oldIndex, newIndex, EventsUpdate.DO); } else { updateEvents(oldIndex, newIndex, EventsUpdate.UNDO); } } }); eventsListView.setOnMouseClicked((event) -> { if (event.getClickCount() < 2) return; final Event selectedEvent = eventEntries.get(eventsListView.getSelectionModel().getSelectedIndex()); if (selectedEvent instanceof ShotEvent) { final ShotEvent se = (ShotEvent) selectedEvent; if (!se.getVideoString().isPresent()) return; final FXMLLoader loader = new FXMLLoader( getClass().getClassLoader().getResource("com/shootoff/gui/VideoPlayer.fxml")); try { loader.load(); } catch (final IOException ioe) { ioe.printStackTrace(); } final Stage videoPlayerStage = new Stage(); final VideoPlayerController controller = (VideoPlayerController) loader.getController(); controller.init(se.getVideos()); videoPlayerStage.setTitle("Video Player"); videoPlayerStage.setScene(new Scene(loader.getRoot())); videoPlayerStage.show(); config.registerVideoPlayer(controller); controller.getStage().setOnCloseRequest((closeEvent) -> { config.unregisterVideoPlayer(controller); }); } }); eventsListView.setItems(eventEntries); timeSlider.setOnMouseClicked((event) -> { if (isPlaying) togglePlaybackButton.fire(); }); timeSlider.valueProperty().addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) { if (newValue == null) { timeLabel.setText(""); return; } setTime(newValue.longValue()); if (!refreshFromSlider) return; final List<Event> reversedEntries = new ArrayList<>(eventEntries); Collections.reverse(reversedEntries); for (final Event e : reversedEntries) { if (e.getTimestamp() <= newValue.longValue()) { eventsListView.getSelectionModel().select(e); break; } } } }); } private void setTime(long timestamp /* ms */) { final Date date = new Date(timestamp); final DateFormat formatter = new SimpleDateFormat("mm:ss:SSS"); timeLabel.setText(formatter.format(date)); } private List<File> findSessions() { final File sessionsFolder = new File(System.getProperty("shootoff.sessions")); final List<File> sessions = new ArrayList<>(); if (!sessionsFolder.exists()) { logger.debug("No sessions folder available"); return sessions; } final File[] sessionFiles = sessionsFolder.listFiles(new FileFilter("xml")); if (sessionFiles != null) { for (final File file : sessionFiles) { sessions.add(new File(file.getName())); } } else { logger.error("Failed to find session files because a list of files could not be retrieved: " + "sessionsFolder = {}", sessionsFolder.getPath()); } return sessions; } private void updateCameraTabs() { cameraTabPane.getTabs().clear(); cameraGroups.clear(); eventSelectionsPerTab.clear(); for (final String cameraName : currentSession.getEvents().keySet()) { final Group canvas = new Group(); final ScrollPane scrollPane = new ScrollPane(canvas); scrollPane.setPrefSize(cameraTabPane.getPrefWidth(), cameraTabPane.getPrefHeight()); scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED); scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); cameraGroups.put(cameraName, new SessionCanvasManager(canvas, config)); final Tab cameraTab = new Tab(cameraName); cameraTab.setContent(scrollPane); cameraTabPane.getTabs().add(cameraTab); } } private void listCameraEvents(String cameraName) { eventEntries.clear(); eventEntries.addAll(currentSession.getCameraEvents(cameraName)); } private enum EventsUpdate { DO, UNDO } private void updateEvents(int oldIndex, int newIndex, EventsUpdate updateType) { List<Event> events; if (updateType == EventsUpdate.DO) { events = new ArrayList<>(eventEntries.subList(oldIndex + 1, newIndex + 1)); } else { events = new ArrayList<>(eventEntries.subList(newIndex + 1, oldIndex + 1)); Collections.reverse(events); } final SessionCanvasManager currentCanvasManager = cameraGroups .get(cameraTabPane.getSelectionModel().getSelectedItem().getText()); for (final Event e : events) { if (updateType == EventsUpdate.DO) { currentCanvasManager.doEvent(e); } else { currentCanvasManager.undoEvent(e); } } } @FXML public void nextButtonClicked(ActionEvent event) { if (isPlaying) togglePlaybackButton.fire(); int selectedIndex = eventsListView.getSelectionModel().getSelectedIndex(); if (selectedIndex >= 0) { if (selectedIndex < eventEntries.size() - 1) { eventsListView.getSelectionModel().select(++selectedIndex); } else { eventsListView.getSelectionModel().select(0); } } else { eventsListView.getSelectionModel().select(0); } } @FXML public void previousButtonClicked(ActionEvent event) { if (isPlaying) togglePlaybackButton.fire(); int selectedIndex = eventsListView.getSelectionModel().getSelectedIndex(); if (selectedIndex >= 0) { if (selectedIndex == 0) { eventsListView.getSelectionModel().select(eventEntries.size() - 1); } else { eventsListView.getSelectionModel().select(--selectedIndex); } } else { eventsListView.getSelectionModel().select(eventEntries.size() - 1); } } private class AdvanceSlider implements Callable<Void> { @Override public Void call() throws Exception { if (isPlaying) { final double currentTime = timeSlider.getValue(); Platform.runLater(() -> { if (currentTime + 100 > timeSlider.getMax()) togglePlaybackButton.fire(); timeSlider.setValue(currentTime + STEP_INTERVAL); }); executorService.schedule(new AdvanceSlider(), STEP_INTERVAL, TimeUnit.MILLISECONDS); } return null; } } @FXML public void togglePlaybackButtonClicked(ActionEvent event) { isPlaying = !isPlaying; if (isPlaying) { togglePlaybackButton.setGraphic(new ImageView(new Image( SessionViewerController.class.getResourceAsStream("/images/gnome_media_playback_pause.png")))); executorService = Executors.newScheduledThreadPool(CORE_POOL_SIZE, new NamedThreadFactory("SessionPlayback")); executorService.schedule(new AdvanceSlider(), STEP_INTERVAL, TimeUnit.MILLISECONDS); } else { togglePlaybackButton.setGraphic(new ImageView(new Image( SessionViewerController.class.getResourceAsStream("/images/gnome_media_playback_start.png")))); executorService.shutdownNow(); } } public Node getPane() { return sessionViewerPane; } }