/* * 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.headless; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.camera.CameraErrorView; import com.shootoff.camera.CameraFactory; import com.shootoff.camera.CameraManager; import com.shootoff.camera.CameraView; import com.shootoff.camera.CamerasSupervisor; import com.shootoff.camera.Shot; import com.shootoff.camera.cameratypes.Camera; import com.shootoff.config.Configuration; import com.shootoff.config.ConfigurationException; import com.shootoff.courses.Course; import com.shootoff.courses.io.CourseIO; import com.shootoff.gui.AutocalibrationListener; import com.shootoff.gui.CalibrationConfigurator; import com.shootoff.gui.CalibrationManager; import com.shootoff.gui.CalibrationOption; import com.shootoff.gui.CanvasManager; import com.shootoff.gui.ExerciseListener; import com.shootoff.gui.LocatedImage; import com.shootoff.gui.Resetter; import com.shootoff.gui.ShotEntry; import com.shootoff.gui.pane.ArenaBackgroundsSlide; import com.shootoff.gui.pane.ProjectorArenaPane; import com.shootoff.gui.targets.TargetView; import com.shootoff.headless.protocol.AddTargetMessage; import com.shootoff.headless.protocol.AddedTargetMessage; import com.shootoff.headless.protocol.ClearCourseMessage; import com.shootoff.headless.protocol.ConfigurationData; import com.shootoff.headless.protocol.CurrentArenaSnapshotMessage; import com.shootoff.headless.protocol.CurrentBackgroundsMessage; import com.shootoff.headless.protocol.CurrentConfigurationMessage; import com.shootoff.headless.protocol.CurrentCoursesMessage; import com.shootoff.headless.protocol.CurrentExercisesMessage; import com.shootoff.headless.protocol.CurrentTargetsMessage; import com.shootoff.headless.protocol.ErrorMessage; import com.shootoff.headless.protocol.GetArenaSnapshotMessage; import com.shootoff.headless.protocol.ErrorMessage.ErrorType; import com.shootoff.headless.protocol.GetBackgroundsMessage; import com.shootoff.headless.protocol.GetConfigurationMessage; import com.shootoff.headless.protocol.GetCoursesMessage; import com.shootoff.headless.protocol.GetExercisesMessage; import com.shootoff.headless.protocol.GetTargetsMessage; import com.shootoff.headless.protocol.Message; import com.shootoff.headless.protocol.MessageListener; import com.shootoff.headless.protocol.MoveTargetMessage; import com.shootoff.headless.protocol.NewShotMessage; import com.shootoff.headless.protocol.RemoveTargetMessage; import com.shootoff.headless.protocol.ResetMessage; import com.shootoff.headless.protocol.ResizeTargetMessage; import com.shootoff.headless.protocol.SaveCourseMessage; import com.shootoff.headless.protocol.SetBackgroundMessage; import com.shootoff.headless.protocol.SetConfigurationMessage; import com.shootoff.headless.protocol.SetCourseMessage; import com.shootoff.headless.protocol.SetExerciseMessage; import com.shootoff.headless.protocol.StartCalibrationMessage; import com.shootoff.headless.protocol.StopCalibrationMessage; import com.shootoff.headless.protocol.TargetMessage; import com.shootoff.plugins.ExerciseMetadata; import com.shootoff.plugins.ProjectorTrainingExerciseBase; import com.shootoff.plugins.TrainingExercise; import com.shootoff.plugins.TrainingExerciseBase; import com.shootoff.plugins.TrainingExerciseView; import com.shootoff.plugins.engine.Plugin; import com.shootoff.plugins.engine.PluginEngine; import com.shootoff.plugins.engine.PluginListener; import com.shootoff.targets.ImageRegion; import com.shootoff.targets.Target; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Dimension2D; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.SnapshotParameters; import javafx.scene.control.Label; import javafx.scene.control.TableView; import javafx.scene.image.Image; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.stage.Stage; import marytts.util.io.FileFilter; public class HeadlessController implements AutocalibrationListener, CameraErrorView, Resetter, ExerciseListener, CalibrationConfigurator, QRCodeListener, ConnectionListener, MessageListener, TrainingExerciseView { private static final Logger logger = LoggerFactory.getLogger(HeadlessController.class); private final Configuration config; private final ProjectorArenaPane arenaPane; private final CamerasSupervisor camerasSupervisor; private final CanvasManager arenaCanvasManager; private final Map<UUID, Target> targets = new HashMap<>(); private final Set<TrainingExercise> trainingExercises = new HashSet<>(); private final Set<TrainingExercise> projectorTrainingExercises = new HashSet<>(); private PluginEngine pluginEngine; private CalibrationManager calibrationManager; private boolean calibrated = true; private Target qrCodeTarget; private Optional<HeadlessServer> server = Optional.empty(); public HeadlessController() { config = Configuration.getConfig(); // Configuring cameras may cause us to bail out before doing anything // actually useful but we initialize these points first so that we can // make more fields immutable (initializing them after a guarded return // will make it so the can't be final unless we initialize them all to // null). final ObservableList<ShotEntry> shotEntries = FXCollections.observableArrayList(); shotEntries.addListener(new ListChangeListener<ShotEntry>() { @Override public void onChanged(Change<? extends ShotEntry> change) { if (!server.isPresent() || !change.next() || change.getAddedSize() < 1) return; for (ShotEntry entry : change.getAddedSubList()) { final Shot shot = entry.getShot(); final Dimension2D d = arenaPane.getArenaStageResolution(); server.get().sendMessage(new NewShotMessage(shot.getColor(), shot.getX(), shot.getY(), shot.getTimestamp(), d.getWidth(), d.getHeight())); } } }); final CanvasManager canvasManager = new CanvasManager(new Group(), this, "Default", shotEntries); final Stage arenaStage = new Stage(); // TODO: Pass controls added to this pane to the device controlling // SBC final Pane trainingExerciseContainer = new Pane(); arenaPane = new ProjectorArenaPane(arenaStage, null, trainingExerciseContainer, this, shotEntries); arenaCanvasManager = arenaPane.getCanvasManager(); arenaStage.setTitle("Projector Arena"); arenaStage.setScene(new Scene(arenaPane)); arenaStage.setFullScreenExitHint(""); camerasSupervisor = new CamerasSupervisor(config); final Map<String, Camera> configuredCameras = config.getWebcams(); final Optional<Camera> camera; if (configuredCameras.isEmpty()) { camera = CameraFactory.getDefault(); } else { camera = Optional.of(configuredCameras.values().iterator().next()); } if (!camera.isPresent()) { logger.error("There are no cameras attached to the computer."); return; } final Camera c = camera.get(); if (c.isLocked() && !c.isOpen()) { logger.error("Default camera is locked, cannot proceed"); return; } initializePluginEngine(); final Optional<CameraManager> manager = camerasSupervisor.addCameraManager(c, this, canvasManager); if (manager.isPresent()) { final CameraManager cameraManager = manager.get(); // TODO: Camera views to non-null value to handle calibration issues calibrationManager = new CalibrationManager(this, cameraManager, arenaPane, null, this, this); arenaPane.setCalibrationManager(calibrationManager); arenaPane.toggleArena(); arenaPane.autoPlaceArena(); calibrationManager.enableCalibration(); } else { logger.error("Failed to start camera {}", c.getName()); } } private void initializePluginEngine() { try { pluginEngine = new PluginEngine(new PluginListener() { @Override public void registerExercise(TrainingExercise exercise) { trainingExercises.add(exercise); } @Override public void registerProjectorExercise(TrainingExercise exercise) { projectorTrainingExercises.add(exercise); } @Override public void unregisterExercise(TrainingExercise exercise) { if (exercise instanceof ProjectorTrainingExerciseBase) { projectorTrainingExercises.remove(exercise); } else { trainingExercises.remove(exercise); } } }); pluginEngine.startWatching(); } catch (IOException e) { logger.error("Failed to start plugin engine", e); } } @Override public void reset() { camerasSupervisor.reset(); } @Override public void showCameraLockError(Camera webcam, boolean allCamerasFailed) { if (!server.isPresent()) return; final String messageFormat; if (allCamerasFailed) { messageFormat = "Cannot open the webcam %s. It is being " + "used by another program or it is an IPCam with the wrong credentials. This " + "is the only configured camera, thus ShootOFF must close."; } else { messageFormat = "Cannot open the webcam %s. It is being " + "used by another program, it is an IPCam with the wrong credentials, or you " + "have ShootOFF open more than once."; } final Optional<String> webcamName = config.getWebcamsUserName(webcam); final String message = String.format(messageFormat, webcamName.isPresent() ? webcamName.get() : webcam.getName()); server.get().sendMessage(new ErrorMessage(message, ErrorType.TARGET)); } @Override public void autocalibrationTimedOut() { calibrated = false; final Label calibrationLabel = new Label("Calibration Failed!"); calibrationLabel.setFont(Font.font(48)); calibrationLabel.setTextFill(Color.web("#f5a807")); calibrationLabel.setLayoutX(6); calibrationLabel.setLayoutY(6); calibrationLabel.setPrefSize(628, 90); calibrationLabel.setAlignment(Pos.CENTER); arenaPane.getCanvasManager().getCanvasGroup().getChildren().add(calibrationLabel); } @Override public void showMissingCameraError(Camera webcam) { sendCameraError(webcam, CameraErrorView.MISSING_ERROR); } @Override public void showFPSWarning(Camera webcam, double fps) { sendCameraError(webcam, CameraErrorView.FPS_WARNING, fps); } @Override public void showBrightnessWarning(Camera webcam) { sendCameraError(webcam, CameraErrorView.BRIGHTNESS_WARNING); } private void sendCameraError(Camera webcam, String format, Object... args) { if (server.isPresent()) { final Optional<String> cameraUserName = config.getWebcamsUserName(webcam); final String cameraName; if (cameraUserName.isPresent()) { cameraName = cameraUserName.get(); } else { cameraName = webcam.getName(); } final List<Object> argsList = new ArrayList<Object>(Arrays.asList(args)); argsList.add(0, cameraName); server.get().sendMessage(new ErrorMessage(String.format(format, argsList.toArray()), ErrorType.CAMERA)); } } @Override public void setProjectorExercise(TrainingExercise exercise) { try { config.setExercise(null); final Constructor<?> ctor = exercise.getClass().getConstructor(List.class); final TrainingExercise newExercise = (TrainingExercise) ctor.newInstance(arenaCanvasManager.getTargets()); final Optional<Plugin> plugin = pluginEngine.getPlugin(newExercise); if (plugin.isPresent()) { config.setPlugin(plugin.get()); } else { config.setPlugin(null); } config.setExercise(newExercise); final Runnable initExercise = () -> { ((ProjectorTrainingExerciseBase) newExercise).init(camerasSupervisor, this, arenaPane); newExercise.init(); }; if (Platform.isFxApplicationThread()) { initExercise.run(); } else { Platform.runLater(initExercise); } } catch (final ReflectiveOperationException e) { final ExerciseMetadata metadata = exercise.getInfo(); logger.error("Failed to start projector exercise " + metadata.getName() + " " + metadata.getVersion(), e); } } @Override public void setExercise(TrainingExercise exercise) { try { // If there is a current exercise, ensure it is destroyed // before starting an new one in case it's a projector // exercise that added targets that need to be removed. config.setExercise(null); if (exercise == null) return; final Constructor<?> ctor = exercise.getClass().getConstructor(List.class); final TrainingExercise newExercise = (TrainingExercise) ctor.newInstance(arenaCanvasManager.getTargets()); final Optional<Plugin> plugin = pluginEngine.getPlugin(newExercise); if (plugin.isPresent()) { config.setPlugin(plugin.get()); } else { config.setPlugin(null); } config.setExercise(newExercise); final Runnable initExercise = () -> { ((TrainingExerciseBase) newExercise).init(camerasSupervisor, this); newExercise.init(); }; if (Platform.isFxApplicationThread()) { initExercise.run(); } else { Platform.runLater(initExercise); } } catch (final ReflectiveOperationException e) { final ExerciseMetadata metadata = exercise.getInfo(); logger.error("Failed to start exercise " + metadata.getName() + " " + metadata.getVersion(), e); } } @Override public PluginEngine getPluginEngine() { return pluginEngine; } @Override public CalibrationOption getCalibratedFeedBehavior() { return CalibrationOption.ONLY_IN_BOUNDS; } @Override public void calibratedFeedBehaviorsChanged() {} @Override public void toggleCalibrating(boolean isCalibrating) { if (!isCalibrating) { if (!calibrated) return; if (server.isPresent()) { server.get().sendMessage(new StopCalibrationMessage()); } else { startBluetooth(); } } } @Override public void qrCodeCreated(Image qrCodeImage) { final Group targetGroup = new Group(); final ImageRegion qrCodeRegion = new ImageRegion(qrCodeImage); qrCodeRegion.getAllTags().put(TargetView.TAG_IGNORE_HIT, "true"); targetGroup.getChildren().add(qrCodeRegion); qrCodeTarget = arenaCanvasManager.addTarget(null, targetGroup, new HashMap<String, String>(), false); } private void startBluetooth() { final HeadlessServer headlessServer = new BluetoothServer(this); server = Optional.of(headlessServer); headlessServer.startReading(this, this); } @Override public void connectionEstablished() { if (qrCodeTarget != null) { arenaCanvasManager.removeTarget(qrCodeTarget); qrCodeTarget = null; } } @Override public void bluetoothDisconnected() { server = Optional.empty(); startBluetooth(); } @Override public void messageReceived(Message message) { if (message instanceof ClearCourseMessage) { arenaPane.getCanvasManager().clearTargets(); } else if (message instanceof GetArenaSnapshotMessage) { Platform.runLater(() -> { if (server.isPresent()) { final WritableImage snapshot = arenaPane.getCanvasManager().getCanvasGroup() .snapshot(new SnapshotParameters(), null); final int width = (int) snapshot.getWidth(); final int height = (int) snapshot.getHeight(); final int[] snapshotPixels = new int[width * height]; snapshot.getPixelReader().getPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), snapshotPixels, 0, width); server.get().sendMessage(new CurrentArenaSnapshotMessage(snapshotPixels, width, height)); } }); } else if (message instanceof GetBackgroundsMessage) { if (server.isPresent()) server.get().sendMessage(new CurrentBackgroundsMessage(ArenaBackgroundsSlide.DEFAULT_BACKGROUNDS)); } else if (message instanceof GetConfigurationMessage) { sendConfiguration(); } else if (message instanceof GetCoursesMessage) { sendCourses(); } else if (message instanceof GetExercisesMessage) { sendExercises(); } else if (message instanceof GetTargetsMessage) { sendTargets(); } else if (message instanceof ResetMessage) { reset(); } else if (message instanceof SaveCourseMessage) { final File courseDirectory = new File(System.getProperty("shootoff.courses")); final File courseFile = ((SaveCourseMessage) message).getFile(); if (courseFile.getParent() != null) { final File parentDirectory = new File( courseDirectory.toString() + File.separator + courseFile.getParent()); if (!parentDirectory.exists() && !parentDirectory.mkdirs()) { logger.error("Failed to make directory for course {}", courseFile); } } CourseIO.saveCourse(arenaPane, new File(courseDirectory.toString() + File.separator + courseFile.toString())); } else if (message instanceof SetBackgroundMessage) { final SetBackgroundMessage backgroundMessage = (SetBackgroundMessage) message; if (backgroundMessage.getName().isEmpty() && backgroundMessage.getResourceName().isEmpty()) { arenaPane.setArenaBackground(null); } final String resourceName = backgroundMessage.getResourceName(); if (ArenaBackgroundsSlide.DEFAULT_BACKGROUNDS.containsKey(backgroundMessage.getName()) && ArenaBackgroundsSlide.DEFAULT_BACKGROUNDS.get(backgroundMessage.getName()) .equals(resourceName)) { final InputStream is = ArenaBackgroundsSlide.class.getResourceAsStream(resourceName); arenaPane.setArenaBackground(new LocatedImage(is, resourceName)); } else { if (server.isPresent()) { server.get().sendMessage(new ErrorMessage( "Background " + backgroundMessage.getName() + " " + resourceName + " does not exist.", ErrorType.BACKGROUND)); } } } else if (message instanceof SetConfigurationMessage) { final SetConfigurationMessage configMessage = (SetConfigurationMessage) message; setConfiguration(configMessage.getConfigurationData()); } else if (message instanceof SetCourseMessage) { final SetCourseMessage courseMessage = (SetCourseMessage) message; final File courseFile = new File( System.getProperty("shootoff.courses") + File.separator + courseMessage.getCourse().toString()); if (courseFile.exists()) { final Optional<Course> course = CourseIO.loadCourse(arenaPane, courseFile); if (course.isPresent()) { arenaPane.setCourse(course.get()); for (Target t : course.get().getTargets()) { final UUID targetUuid = UUID.randomUUID(); sendAddedTargetMessage(targetUuid, t); targets.put(targetUuid, t); } } } else { if (server.isPresent()) server.get().sendMessage( new ErrorMessage("Course " + courseMessage.getCourse() + " does not exist.", ErrorType.COURSE)); } } else if (message instanceof SetExerciseMessage) { final SetExerciseMessage exerciseMessage = (SetExerciseMessage) message; setExercise(exerciseMessage.getNewExercise()); } else if (message instanceof StartCalibrationMessage) { if (!calibrationManager.isCalibrating()) { calibrationManager.enableCalibration(); } } else if (message instanceof StopCalibrationMessage) { if (calibrationManager.isCalibrating()) { calibrationManager.stopCalibration(); } } else if (message instanceof TargetMessage) { handleTargetMessage((TargetMessage) message); } } private void sendConfiguration() { if (server.isPresent()) { final ConfigurationData configurationData = new ConfigurationData(config.getMarkerRadius(), config.ignoreLaserColor(), config.getIgnoreLaserColorName(), config.useVirtualMagazine(), config.getVirtualMagazineCapacity(), config.useMalfunctions(), config.getMalfunctionsProbability(), config.showArenaShotMarkers()); server.get().sendMessage(new CurrentConfigurationMessage(configurationData)); } } private void sendCourses() { if (!server.isPresent()) return; final java.io.FileFilter folderFilter = new java.io.FileFilter() { @Override public boolean accept(File path) { return path.isDirectory(); } }; final File coursesDirectory = new File(System.getProperty("shootoff.courses")); final File[] courseFolders = coursesDirectory.listFiles(folderFilter); final FilenameFilter courseFilter = new FilenameFilter() { @Override public boolean accept(File directory, String fileName) { return fileName.endsWith(".course"); } }; final Map<String, List<String>> courses = new HashMap<>(); for (File courseFolder : courseFolders) { final List<String> courseFileNames = new ArrayList<>(); for (File courseFile : courseFolder.listFiles(courseFilter)) { courseFileNames.add(courseFile.getName()); } courses.put(courseFolder.getName(), courseFileNames); } server.get().sendMessage(new CurrentCoursesMessage(courses)); } private void sendExercises() { if (server.isPresent()) { final Set<ExerciseMetadata> trainingExercisesMetadata = new HashSet<ExerciseMetadata>(); final Set<ExerciseMetadata> projectorTrainingExercisesMetadata = new HashSet<ExerciseMetadata>(); trainingExercises.stream().map(TrainingExercise::getInfo).forEach(trainingExercisesMetadata::add); projectorTrainingExercises.stream().map(TrainingExercise::getInfo) .forEach(projectorTrainingExercisesMetadata::add); final Optional<TrainingExercise> enabledExercise = config.getExercise(); final ExerciseMetadata enabledExerciseMetadata = enabledExercise.isPresent() ? enabledExercise.get().getInfo() : null; server.get().sendMessage(new CurrentExercisesMessage(enabledExerciseMetadata, trainingExercisesMetadata, projectorTrainingExercisesMetadata)); } } private void sendTargets() { if (!server.isPresent()) return; final String shootOffHome = System.getProperty("shootoff.home") + File.separator; final File targetsFolder = new File(shootOffHome + "targets"); final File[] targetFiles = targetsFolder.listFiles(new FileFilter("target")); if (targetFiles != null) { final File[] relativeTargetFiles = new File[targetFiles.length]; for (int i = 0; i < targetFiles.length; i++) { relativeTargetFiles[i] = new File(targetFiles[i].toString().replaceAll(shootOffHome, "")); } Arrays.sort(relativeTargetFiles); server.get().sendMessage(new CurrentTargetsMessage(Arrays.asList(relativeTargetFiles))); } else { server.get().sendMessage(new ErrorMessage("Failed to find target files", ErrorType.TARGET)); } } private void setConfiguration(ConfigurationData configurationData) { config.setMarkerRadius(configurationData.getMarkerRadius()); config.setIgnoreLaserColor(configurationData.isIgnoreLaserColor()); config.setIgnoreLaserColorName(configurationData.getIgnoreLaserColorName()); config.setUseVirtualMagazine(configurationData.useVirtualMagazine()); config.setVirtualMagazineCapacity(configurationData.getVirtualMagazineCapacity()); config.setMalfunctions(configurationData.useMalfunctions()); config.setMalfunctionsProbability(configurationData.getMalfunctionsProbability()); config.setShowArenaShotMarkers(configurationData.showArenaShotMarkers()); try { config.writeConfigurationFile(); } catch (ConfigurationException | IOException e) { logger.error("Failed to save headless configuration", e); } } private void setExercise(ExerciseMetadata exerciseMetadata) { logger.debug("Setting exercise with metadata {}", exerciseMetadata); if (exerciseMetadata.getCreator().isEmpty() && exerciseMetadata.getDescription().isEmpty() && exerciseMetadata.getName().isEmpty() && exerciseMetadata.getVersion().isEmpty()) { setExercise((TrainingExercise) null); logger.trace("Exercise unset"); return; } for (TrainingExercise exercise : trainingExercises) { if (exercise.getInfo().equals(exerciseMetadata)) { logger.trace("Setting exercise to {}", exercise.getInfo().toString()); setExercise(exercise); return; } } for (TrainingExercise exercise : projectorTrainingExercises) { if (exercise.getInfo().equals(exerciseMetadata)) { logger.trace("Setting projector exercise to {}", exercise.getInfo().toString()); setProjectorExercise(exercise); return; } } } private void handleTargetMessage(TargetMessage message) { if (message instanceof AddTargetMessage) { final AddTargetMessage addTarget = (AddTargetMessage) message; final Optional<Target> target = arenaCanvasManager.addTarget(addTarget.getTargetFile()); if (target.isPresent()) { final Target t = target.get(); targets.put(addTarget.getUuid(), t); sendAddedTargetMessage(addTarget.getUuid(), t); } } else { final UUID targetUuid = message.getUuid(); if (!targets.containsKey(targetUuid)) { final String errorMessage = String.format( "A target with UUID %s does not exist to perform operation %s", targetUuid, message.getClass().getName()); if (server.isPresent()) { server.get().sendMessage(new ErrorMessage(errorMessage, ErrorType.TARGET)); } logger.error(errorMessage); return; } final Target t = targets.get(targetUuid); if (message instanceof MoveTargetMessage) { final MoveTargetMessage moveTarget = (MoveTargetMessage) message; t.setPosition(moveTarget.getNewX(), moveTarget.getNewY()); } else if (message instanceof ResizeTargetMessage) { final ResizeTargetMessage resizeTarget = (ResizeTargetMessage) message; t.setDimensions(resizeTarget.getNewWidth(), resizeTarget.getNewHeight()); } else if (message instanceof RemoveTargetMessage) { arenaCanvasManager.removeTarget(t); targets.remove(targetUuid); } } } private void sendAddedTargetMessage(UUID uuid, Target t) { if (server.isPresent()) { final Point2D p = t.getPosition(); final Dimension2D d = t.getDimension(); final Dimension2D arenaD = arenaPane.getArenaStageResolution(); server.get().sendMessage(new AddedTargetMessage(uuid, t.getTargetFile(), p.getX(), p.getY(), d.getWidth(), d.getHeight(), arenaD.getWidth(), arenaD.getWidth())); } } @Override public Pane getTrainingExerciseContainer() { // TODO Use a stub that sends controls to tablet return new Pane(); } @Override public TableView<ShotEntry> getShotEntryTable() { // TODO Use a stub that sends column data to tablet return new TableView<ShotEntry>(); } @Override public VBox getButtonsPane() { // TODO Use a stub that sends controls to tablet return new VBox(); } @Override public Optional<CameraView> getArenaView() { return Optional.empty(); } @Override public List<Target> getTargets() { return arenaCanvasManager.getTargets(); } }