/* * 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; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.camera.CameraManager; import com.shootoff.camera.CameraView; import com.shootoff.camera.Shot; import com.shootoff.camera.processors.MalfunctionsProcessor; import com.shootoff.camera.processors.ShotProcessor; import com.shootoff.camera.processors.VirtualMagazineProcessor; import com.shootoff.camera.recorders.ShotRecorder; import com.shootoff.camera.shot.ArenaShot; import com.shootoff.camera.shot.DisplayShot; import com.shootoff.camera.shot.ShotColor; import com.shootoff.config.Configuration; import com.shootoff.gui.pane.ProjectorArenaPane; import com.shootoff.gui.targets.MirroredTarget; import com.shootoff.gui.targets.TargetCommands; import com.shootoff.gui.targets.TargetView; import com.shootoff.plugins.TrainingExercise; import com.shootoff.plugins.TrainingExerciseBase; import com.shootoff.targets.Hit; import com.shootoff.targets.ImageRegion; import com.shootoff.targets.RegionType; import com.shootoff.targets.Target; import com.shootoff.targets.TargetRegion; import com.shootoff.targets.io.TargetIO; import com.shootoff.targets.io.TargetIO.TargetComponents; import javafx.application.Platform; import javafx.collections.ObservableList; import com.shootoff.util.SwingFXUtils; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.util.Pair; public class CanvasManager implements CameraView { private final Logger logger = LoggerFactory.getLogger(CanvasManager.class); private final Group canvasGroup; private final Configuration config; protected CameraManager cameraManager; private final VBox diagnosticsVBox = new VBox(); private static final int DIAGNOSTIC_POOL_SIZE = 10; private static final int DIAGNOSTIC_CHIME_DELAY = 5000; // ms private final ScheduledExecutorService diagnosticExecutorService = Executors .newScheduledThreadPool(DIAGNOSTIC_POOL_SIZE); private final Map<Label, ScheduledFuture<Void>> diagnosticFutures = new HashMap<>(); private final Image muteImage = new Image(CanvasManager.class.getResourceAsStream("/images/mute.png")); private final Image soundImage = new Image(CanvasManager.class.getResourceAsStream("/images/sound.png")); private final Resetter resetter; private final String cameraName; private final ObservableList<ShotEntry> shotEntries; private final ImageView background = new ImageView(); private final List<DisplayShot> shots = Collections.synchronizedList(new ArrayList<DisplayShot>()); private final List<Target> targets = new ArrayList<>(); private ProgressIndicator progress; private Optional<ContextMenu> contextMenu = Optional.empty(); private Optional<TargetView> selectedTarget = Optional.empty(); private boolean showShots = true; private boolean hadMalfunction = false; private boolean hadReload = false; private static final int MAX_FEED_FPS = 15; private static final int MINIMUM_FRAME_DELTA = 1000 / MAX_FEED_FPS; // ms private long lastFrameTime = 0; protected Optional<ProjectorArenaPane> arenaPane = Optional.empty(); private Optional<Bounds> projectionBounds = Optional.empty(); public CanvasManager(Group canvasGroup, Resetter resetter, String cameraName, ObservableList<ShotEntry> shotEntries) { this.canvasGroup = canvasGroup; config = Configuration.getConfig(); this.resetter = resetter; this.cameraName = cameraName; this.shotEntries = shotEntries; background.setOnMouseClicked((event) -> { toggleTargetSelection(Optional.empty()); }); if (Platform.isFxApplicationThread()) { progress = new ProgressIndicator(ProgressIndicator.INDETERMINATE_PROGRESS); progress.setPrefHeight(config.getDisplayHeight()); progress.setPrefWidth(config.getDisplayWidth()); canvasGroup.getChildren().add(progress); canvasGroup.getChildren().add(diagnosticsVBox); diagnosticsVBox.setAlignment(Pos.CENTER); diagnosticsVBox.setFillWidth(true); diagnosticsVBox.setPrefWidth(config.getDisplayWidth()); } canvasGroup.setOnMouseClicked((event) -> { if (contextMenu.isPresent() && contextMenu.get().isShowing()) contextMenu.get().hide(); if (config.inDebugMode() && event.getButton() == MouseButton.PRIMARY) { // Click to shoot final ShotColor shotColor; if (event.isShiftDown()) { shotColor = ShotColor.RED; } else if (event.isControlDown()) { shotColor = ShotColor.GREEN; } else { return; } // Skip the camera manager for injected shots made from the // arena tab otherwise they get scaled before the call to // addArenaShot when they go through the arena camera feed's // canvas manager if (this instanceof MirroredCanvasManager) { final long shotTimestamp = System.currentTimeMillis(); addShot(new DisplayShot(new Shot(shotColor, event.getX(), event.getY(), shotTimestamp), config.getMarkerRadius()), false); } else { cameraManager.injectShot(shotColor, event.getX(), event.getY(), false); } return; } else if (contextMenu.isPresent() && event.getButton() == MouseButton.SECONDARY) { contextMenu.get().show(canvasGroup, event.getScreenX(), event.getScreenY()); } }); } @Override public void close() { diagnosticExecutorService.shutdownNow(); } @Override public void setCameraManager(CameraManager cameraManager) { this.cameraManager = cameraManager; } public CameraManager getCameraManager() { return cameraManager; } @Override public boolean addChild(Node c) { return getCanvasGroup().getChildren().add(c); } @Override public boolean removeChild(Node c) { return getCanvasGroup().getChildren().remove(c); } public Label addDiagnosticMessage(final String message, final long chimeDelay, final Color backgroundColor) { final Label diagnosticLabel = new Label(message); diagnosticLabel.setStyle("-fx-background-color: " + colorToWebCode(backgroundColor)); final ImageView muteView = new ImageView(); muteView.setFitHeight(20); muteView.setFitWidth(muteView.getFitHeight()); if (config.isChimeMuted(message)) { muteView.setImage(muteImage); } else { muteView.setImage(soundImage); } diagnosticLabel.setContentDisplay(ContentDisplay.RIGHT); diagnosticLabel.setGraphic(muteView); diagnosticLabel.setOnMouseClicked((event) -> { if (config.isChimeMuted(message)) { muteView.setImage(soundImage); config.unmuteMessageChime(message); } else { muteView.setImage(muteImage); config.muteMessageChime(message); } try { config.writeConfigurationFile(); } catch (final Exception e) { logger.error("Failed persisting message's (" + message + ") chime mute settings.", e); } }); Platform.runLater(() -> diagnosticsVBox.getChildren().add(diagnosticLabel)); if (chimeDelay > 0 && !config.isChimeMuted(message) && !diagnosticExecutorService.isShutdown()) { @SuppressWarnings("unchecked") final ScheduledFuture<Void> chimeFuture = (ScheduledFuture<Void>) diagnosticExecutorService.schedule( () -> TrainingExerciseBase.playSound("sounds/chime.wav"), chimeDelay, TimeUnit.MILLISECONDS); diagnosticFutures.put(diagnosticLabel, chimeFuture); } return diagnosticLabel; } @Override public Label addDiagnosticMessage(String message, Color backgroundColor) { return addDiagnosticMessage(message, DIAGNOSTIC_CHIME_DELAY, backgroundColor); } @Override public void removeDiagnosticMessage(Label diagnosticLabel) { if (diagnosticFutures.containsKey(diagnosticLabel)) { diagnosticFutures.get(diagnosticLabel).cancel(false); diagnosticFutures.remove(diagnosticLabel); } Platform.runLater(() -> diagnosticsVBox.getChildren().remove(diagnosticLabel)); } public static String colorToWebCode(Color color) { return String.format("#%02X%02X%02X", (int) (color.getRed() * 255), (int) (color.getGreen() * 255), (int) (color.getBlue() * 255)); } private void jdk8094135Warning() { Platform.runLater(() -> { final Alert cameraAlert = new Alert(AlertType.ERROR); cameraAlert.setTitle("Internal Error"); cameraAlert.setHeaderText("Internal Error -- Likely Too Many false Shots"); cameraAlert.setResizable(true); cameraAlert.setContentText("An internal error due to JDK bug 8094135 occured in Java that will cause all " + "of your shots to be lost. This error is most likely to occur when you are getting a lot of false " + "shots due to poor lighting conditions and/or a poor camera setup. Please put the camera in front " + "of the shooter and turn off any bright lights in front of the camera that are the same height as " + "the shooter. If problems persist you may need to restart ShootOFF."); cameraAlert.show(); shots.clear(); shotEntries.clear(); }); } public String getCameraName() { return cameraName; } public void setContextMenu(ContextMenu menu) { contextMenu = Optional.of(menu); } public void setBackgroundFit(double width, double height) { background.setFitWidth(width); background.setFitHeight(height); } @Override public void updateBackground(BufferedImage frame, Optional<Bounds> projectionBounds) { updateCanvasGroup(); if (frame == null) { background.setX(0); background.setY(0); background.setImage(null); return; } // Prevent the webcam feed from being refreshed faster than some maximum // FPS otherwise we waste CPU cycles converting a frames to show the // user and these are cycles we could spend detecting shots. A lower // FPS (e.g. ~15) looks perfect fine to a person if (System.currentTimeMillis() - lastFrameTime < MINIMUM_FRAME_DELTA) return; else lastFrameTime = System.currentTimeMillis(); Image img; if (projectionBounds.isPresent()) { final Bounds translatedBounds = translateCameraToCanvas(projectionBounds.get()); background.setX(translatedBounds.getMinX()); background.setY(translatedBounds.getMinY()); img = SwingFXUtils.toFXImage( resize(frame, (int) translatedBounds.getWidth(), (int) translatedBounds.getHeight()), null); } else { background.setX(0); background.setY(0); img = SwingFXUtils.toFXImage(resize(frame, config.getDisplayWidth(), config.getDisplayHeight()), null); } Platform.runLater(() -> background.setImage(img)); } public void updateBackground(Image img) { updateCanvasGroup(); background.setX(0); background.setY(0); Platform.runLater(() -> background.setImage(img)); } private void updateCanvasGroup() { if (!canvasGroup.getChildren().contains(background)) { if (canvasGroup.getChildren().isEmpty()) { canvasGroup.getChildren().add(background); } else { // Remove the wait spinner and replace it // with the background Platform.runLater(() -> canvasGroup.getChildren().set(0, background)); } } } private BufferedImage resize(BufferedImage source, int width, int height) { if (source.getWidth() == width && source.getHeight() == height) return source; final BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); final Graphics2D g2 = tmp.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2.drawImage(source, 0, 0, width, height, null); g2.dispose(); return tmp; } public BufferedImage getBufferedImage() { final BufferedImage projectedScene = SwingFXUtils.fromFXImage(canvasGroup.getScene().snapshot(null), null); return projectedScene; } public Bounds translateCameraToCanvas(Bounds bounds) { if (config.getDisplayWidth() == cameraManager.getFeedWidth() && config.getDisplayHeight() == cameraManager.getFeedHeight()) return bounds; final double scaleX = (double) config.getDisplayWidth() / (double) cameraManager.getFeedWidth(); final double scaleY = (double) config.getDisplayHeight() / (double) cameraManager.getFeedHeight(); final double minX = (bounds.getMinX() * scaleX); final double minY = (bounds.getMinY() * scaleY); final double width = (bounds.getWidth() * scaleX); final double height = (bounds.getHeight() * scaleY); logger.trace("translateCameraToCanvas {} {} {} {} - {} {} {} {}", bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight(), minX, minY, width, height); return new BoundingBox(minX, minY, width, height); } public Bounds translateCanvasToCamera(Bounds bounds) { if (config.getDisplayWidth() == cameraManager.getFeedWidth() && config.getDisplayHeight() == cameraManager.getFeedHeight()) return bounds; final double scaleX = (double) cameraManager.getFeedWidth() / (double) config.getDisplayWidth(); final double scaleY = (double) cameraManager.getFeedHeight() / (double) config.getDisplayHeight(); final double minX = (bounds.getMinX() * scaleX); final double minY = (bounds.getMinY() * scaleY); final double width = (bounds.getWidth() * scaleX); final double height = (bounds.getHeight() * scaleY); logger.trace("translateCanvasToCamera {} {} {} {} - {} {} {} {}", bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight(), minX, minY, width, height); return new BoundingBox(minX, minY, width, height); } /* Takes a point x,y and translates it from an arena canvas to a camera (feed) point. * * This is hackish because it will break if any of the math for these calculations changes in other places. */ public Pair<Double, Double> translateCanvasToCameraPoint(double x, double y) { if (cameraManager == null) { logger.error("Called when cameraManager == null"); return new Pair<Double, Double>(x,y); } if (!cameraManager.getProjectionBounds().isPresent() || !arenaPane.isPresent()) { logger.error("Called when projectionBounds is not available"); return new Pair<Double, Double>(x,y); } final Bounds b = cameraManager.getProjectionBounds().get(); final double x_scale = b.getWidth() / arenaPane.get().getWidth(); final double y_scale = b.getHeight() / arenaPane.get().getHeight(); logger.trace("translateCanvasToCameraPoint scale x {} y {}", x_scale, y_scale); return new Pair<Double, Double>(b.getMinX() + (x * x_scale), b.getMinY() + (y*y_scale)); } public Group getCanvasGroup() { return canvasGroup; } @Override public void clearShots() { final Runnable clearShotsAction = () -> { for (final DisplayShot shot : shots) { canvasGroup.getChildren().remove(shot.getMarker()); } shots.clear(); try { if (shotEntries != null) shotEntries.clear(); } catch (final NullPointerException npe) { logger.error("JDK 8094135 exception", npe); jdk8094135Warning(); } if (arenaPane.isPresent() && !(this instanceof MirroredCanvasManager)) arenaPane.get().getCanvasManager().clearShots(); }; if (Platform.isFxApplicationThread()) { clearShotsAction.run(); } else { Platform.runLater(clearShotsAction); } } @Override public void reset() { // Reset animations for (final Target target : targets) { for (final TargetRegion region : target.getRegions()) { if (region.getType() == RegionType.IMAGE) ((ImageRegion) region).reset(); } } if (arenaPane.isPresent() && !(this instanceof MirroredCanvasManager)) { arenaPane.get().getCanvasManager().reset(); } clearShots(); } public void setProjectorArena(ProjectorArenaPane arenaPane, Bounds projectionBounds) { this.arenaPane = Optional.ofNullable(arenaPane); this.projectionBounds = Optional.ofNullable(projectionBounds); } public void setShowShots(boolean showShots) { if (this.showShots != showShots) { for (final DisplayShot shot : shots) shot.getMarker().setVisible(showShots); } this.showShots = showShots; } private void notifyShot(Shot shot) { if (config.getSessionRecorder().isPresent()) { for (final CameraManager cm : config.getRecordingManagers()) cm.notifyShot(shot); } } private Optional<String> createVideoString(Shot shot) { if (config.getSessionRecorder().isPresent() && !config.getRecordingManagers().isEmpty()) { final StringBuilder sb = new StringBuilder(); for (final CameraManager cm : config.getRecordingManagers()) { final ShotRecorder r = cm.getRevelantRecorder(shot); if (sb.length() > 0) { sb.append(","); } sb.append(r.getCameraName().replaceAll(":", "-")); sb.append(":"); sb.append(r.getRelativeVideoFile().getPath()); } return Optional.of(sb.toString()); } return Optional.empty(); } private Optional<ShotProcessor> processShot(Shot shot) { Optional<ShotProcessor> rejectingProcessor = Optional.empty(); for (final ShotProcessor processor : config.getShotProcessors()) { if (!processor.processShot(shot)) { if (processor instanceof MalfunctionsProcessor) { hadMalfunction = true; } else if (processor instanceof VirtualMagazineProcessor) { hadReload = true; } rejectingProcessor = Optional.of(processor); logger.debug("Processing Shot: Shot Rejected By {}", processor.getClass().getName()); break; } } return rejectingProcessor; } private void recordRejectedShot(DisplayShot shot, ShotProcessor rejectingProcessor) { if (!config.getSessionRecorder().isPresent()) return; notifyShot(shot); final Optional<String> videoString = createVideoString(shot); if (rejectingProcessor instanceof MalfunctionsProcessor) { config.getSessionRecorder().get().recordShot(cameraName, shot, true, false, Optional.empty(), Optional.empty(), videoString); } else if (rejectingProcessor instanceof VirtualMagazineProcessor) { config.getSessionRecorder().get().recordShot(cameraName, shot, false, true, Optional.empty(), Optional.empty(), videoString); } } // For testing protected List<DisplayShot> getShots() { return shots; } @Override public void addShot(DisplayShot shot, boolean isMirroredShot) { if (!isMirroredShot) { final Optional<ShotProcessor> rejectingProcessor = processShot(shot); if (rejectingProcessor.isPresent()) { recordRejectedShot(shot, rejectingProcessor.get()); return; } else { notifyShot(shot); } // TODO: Add separate infrared sound or switch config to read // "red/infrared" if (config.useRedLaserSound() && (ShotColor.RED.equals(shot.getColor()) || ShotColor.INFRARED.equals(shot.getColor()))) { TrainingExerciseBase.playSound(config.getRedLaserSound()); } else if (config.useGreenLaserSound() && ShotColor.GREEN.equals(shot.getColor())) { TrainingExerciseBase.playSound(config.getGreenLaserSound()); } } // Create a shot entry to show the shot's data // in the shot timer table if the shot timer // table is in use if (shotEntries != null) { final Optional<Shot> lastShot; if (shotEntries.isEmpty()) { lastShot = Optional.empty(); } else { lastShot = Optional.of(shotEntries.get(shotEntries.size() - 1).getShot()); } final ShotEntry shotEntry; if (hadMalfunction || hadReload) { shotEntry = new ShotEntry(shot, lastShot, config.getShotTimerRowColor(), hadMalfunction, hadReload); hadMalfunction = false; hadReload = false; } else { shotEntry = new ShotEntry(shot, lastShot, config.getShotTimerRowColor(), false, false); } try { shotEntries.add(shotEntry); } catch (final NullPointerException npe) { logger.error("JDK 8094135 exception", npe); jdk8094135Warning(); } } shots.add(shot); drawShot(shot); final Optional<String> videoString = createVideoString(shot); boolean passedToArena = false; boolean processedShot = false; if (arenaPane.isPresent() && !(this instanceof MirroredCanvasManager) && projectionBounds.isPresent()) { final Bounds b = projectionBounds.get(); if (b.contains(shot.getX(), shot.getY())) { passedToArena = true; final ArenaShot arenaShot = new ArenaShot(shot); scaleShotToArenaBounds(arenaShot); processedShot = arenaPane.get().getCanvasManager().addArenaShot(arenaShot, videoString, isMirroredShot); } } // If the arena canvas handled the shot, we don't need to do anything // else if (passedToArena || processedShot) return; final Optional<TrainingExercise> currentExercise = config.getExercise(); final Optional<Hit> hit = checkHit(shot, videoString, isMirroredShot); if (hit.isPresent() && hit.get().getHitRegion().tagExists("command")) executeRegionCommands(hit.get(), isMirroredShot); if (currentExercise.isPresent() && !processedShot) { // If the canvas is mirrored, use the one without the camera manager // for exercises because that is the one for the arena window. // If we use the arena tab canvas manager the targets will be // copies and will not be the versions of the targets added // by exercises. if ((this instanceof MirroredCanvasManager) && cameraManager == null) { currentExercise.get().shotListener(shot, hit); } else if (!(this instanceof MirroredCanvasManager)) { currentExercise.get().shotListener(shot, hit); } } } public void scaleShotToArenaBounds(ArenaShot shot) { if (!projectionBounds.isPresent()) { logger.error("scaleShotToArenaBounds called when projectionBounds not present"); return; } final double x_scale = arenaPane.get().getWidth() / projectionBounds.get().getWidth(); final double y_scale = arenaPane.get().getHeight() / projectionBounds.get().getHeight(); logger.trace("scaleShotToArenaBounds pre x {} y {}", shot.getX(), shot.getY()); shot.setArenaCoords((shot.getX() - projectionBounds.get().getMinX()) * x_scale, (shot.getY() - projectionBounds.get().getMinY()) * y_scale); logger.trace("scaleShotToArenaBounds post x {} y {}", shot.getX(), shot.getY()); } public boolean addArenaShot(ArenaShot shot, Optional<String> videoString, boolean isMirroredShot) { shots.add(shot); drawShot(shot); final Optional<TrainingExercise> currentExercise = config.getExercise(); final Optional<Hit> hit = checkHit(shot, videoString, isMirroredShot); if (hit.isPresent() && hit.get().getHitRegion().tagExists("command")) { executeRegionCommands(hit.get(), isMirroredShot); } if (!isMirroredShot) { if (currentExercise.isPresent()) { currentExercise.get().shotListener(shot, hit); return true; } } return false; } private void drawShot(DisplayShot shot) { final Runnable drawShotAction = () -> { canvasGroup.getChildren().add(shot.getMarker()); shot.getMarker().setVisible(showShots); }; if (Platform.isFxApplicationThread()) { drawShotAction.run(); } else { Platform.runLater(drawShotAction); } } protected Optional<Hit> checkHit(DisplayShot shot, Optional<String> videoString, boolean isMirroredShot) { // Targets are in order of when they were added, thus we must search in // reverse to ensure shots register for the top target when targets // overlap for (final ListIterator<Target> li = targets.listIterator(targets.size()); li.hasPrevious();) { final Target target = li.previous(); final Optional<Hit> hit; if (shot instanceof ArenaShot) { hit = target.isHit(((ArenaShot)shot).getX(), ((ArenaShot)shot).getY()); } else { hit = target.isHit(shot.getX(), shot.getY()); } if (hit.isPresent()) { hit.get().setShot(shot); final TargetRegion region = hit.get().getHitRegion(); if (config.inDebugMode()) { final Map<String, String> tags = region.getAllTags(); final StringBuilder tagList = new StringBuilder(); for (final Iterator<Entry<String, String>> it = tags.entrySet().iterator(); it.hasNext();) { final Entry<String, String> entry = it.next(); tagList.append(entry.getKey()); tagList.append(":"); tagList.append(entry.getValue()); if (it.hasNext()) tagList.append(", "); } logger.debug("Processing Shot: Found Hit Region For Shot ({}, {}), Type ({}), Tags ({})", shot.getX(), shot.getY(), region.getType(), tagList.toString()); } if (!isMirroredShot && config.getSessionRecorder().isPresent()) { config.getSessionRecorder().get().recordShot(cameraName, shot, false, false, Optional.of(target), Optional.of(target.getRegions().indexOf(region)), videoString); } return hit; } } logger.debug("Processing Shot: Did Not Find Hit For Shot ({}, {})", shot.getX(), shot.getY()); if (!isMirroredShot && config.getSessionRecorder().isPresent()) { config.getSessionRecorder().get().recordShot(cameraName, shot, false, false, Optional.empty(), Optional.empty(), videoString); } return Optional.empty(); } private void executeRegionCommands(Hit hit, boolean isMirroredShot) { if (arenaPane.isPresent()) TargetView.parseCommandTag(hit.getHitRegion(), new TargetCommands(arenaPane.get().getCanvasManager(), targets, resetter, hit, isMirroredShot)); else TargetView.parseCommandTag(hit.getHitRegion(), new TargetCommands(this, targets, resetter, hit, isMirroredShot)); } protected Optional<TargetComponents> loadTarget(File targetFile, boolean playAnimations) { Optional<TargetComponents> targetComponents; if ('@' == targetFile.toString().charAt(0)) { if (!config.getPlugin().isPresent()) { throw new AssertionError("Loaded target from training exercise resources, but a plugin does not " + "exist for the target."); } final ClassLoader loader = config.getPlugin().get().getLoader(); final InputStream resourceTargetStream = loader .getResourceAsStream(targetFile.toString().substring(1).replace("\\", "/")); if (resourceTargetStream != null) { targetComponents = TargetIO.loadTarget(resourceTargetStream, playAnimations, loader); } else { targetComponents = Optional.empty(); logger.error("Error adding target from stream created from resource {}", targetFile.toString().substring(1).replace("\\", "/")); } } else { targetComponents = TargetIO.loadTarget(targetFile, playAnimations); } return targetComponents; } public Optional<Target> addTarget(File targetFile, boolean playAnimations) { final Optional<TargetComponents> targetComponents = loadTarget(targetFile, playAnimations); if (targetComponents.isPresent()) { final TargetComponents tc = targetComponents.get(); final Optional<Target> target = Optional .of(addTarget(targetFile, tc.getTargetGroup(), tc.getTargetTags(), true)); if (config.getSessionRecorder().isPresent() && target.isPresent()) { config.getSessionRecorder().get().recordTargetAdded(cameraName, target.get()); } return target; } return Optional.empty(); } @Override public Optional<Target> addTarget(File targetFile) { return addTarget(targetFile, true); } public Target addTarget(File targetFile, Group targetGroup, Map<String, String> targetTags, boolean userDeletable) { final TargetView newTarget; if (this instanceof MirroredCanvasManager) { newTarget = new MirroredTarget(targetFile, targetGroup, targetTags, config, this, userDeletable); } else { newTarget = new TargetView(targetFile, targetGroup, targetTags, this, userDeletable); } return addTarget(newTarget); } @Override public Target addTarget(Target newTarget) { final Runnable addTargetAction = () -> canvasGroup.getChildren().add(((TargetView) newTarget).getTargetGroup()); if (Platform.isFxApplicationThread()) { addTargetAction.run(); } else { Platform.runLater(addTargetAction); } targets.add(newTarget); // If this is a mirrored canvas, only alert exercises of target updates // from the arena window, not the tab. There is no arena tab if we are in // headless mode, thus there is also no MirroredCanvasManager. Always // perform target update when the canvas isn't mirrored for that reason. if (!(this instanceof MirroredCanvasManager) || ((this instanceof MirroredCanvasManager) && arenaPane.isPresent())) { final Optional<TrainingExercise> enabledExercise = config.getExercise(); if (enabledExercise.isPresent()) enabledExercise.get().targetUpdate(newTarget, TrainingExercise.TargetChange.ADDED); } return newTarget; } public void removeTarget(Target target) { final Runnable removeTargetAction = () -> canvasGroup.getChildren() .remove(((TargetView) target).getTargetGroup()); if (Platform.isFxApplicationThread()) { removeTargetAction.run(); } else { Platform.runLater(removeTargetAction); } if (config.getSessionRecorder().isPresent()) { config.getSessionRecorder().get().recordTargetRemoved(cameraName, target); } targets.remove(target); // If this is a mirrored canvas, only alert exercises of target updates // from the arena window, not the tab. There is no arena tab if we are in // headless mode, thus there is also no MirroredCanvasManager. Always // perform target update when the canvas isn't mirrored for that reason. if (!(this instanceof MirroredCanvasManager) || ((this instanceof MirroredCanvasManager) && arenaPane.isPresent())) { final Optional<TrainingExercise> enabledExercise = config.getExercise(); if (enabledExercise.isPresent()) enabledExercise.get().targetUpdate(target, TrainingExercise.TargetChange.REMOVED); } } public void clearTargets() { for (final Target t : new ArrayList<>(targets)) { removeTarget(t); } } public List<Target> getTargets() { return targets; } public void toggleTargetSelection(Optional<TargetView> newSelection) { if (selectedTarget.isPresent()) selectedTarget.get().toggleSelected(); if (newSelection.isPresent()) { newSelection.get().toggleSelected(); selectedTarget = newSelection; } else { selectedTarget = Optional.empty(); canvasGroup.requestFocus(); } } }