/* * 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.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.camera.CameraCalibrationListener; import com.shootoff.camera.CameraManager; import com.shootoff.camera.CameraView; import com.shootoff.camera.perspective.PerspectiveManager; import com.shootoff.config.Configuration; import com.shootoff.gui.pane.ProjectorArenaPane; import com.shootoff.gui.targets.TargetView; import com.shootoff.plugins.ProjectorTrainingExerciseBase; import com.shootoff.plugins.TrainingExercise; import com.shootoff.targets.CameraViews; import com.shootoff.targets.RectangleRegion; import com.shootoff.targets.io.TargetIO; import com.shootoff.util.TimerPool; import javafx.application.Platform; import javafx.geometry.Bounds; import javafx.geometry.Dimension2D; import javafx.scene.Group; import javafx.scene.control.Label; import javafx.scene.paint.Color; import javafx.stage.WindowEvent; public class CalibrationManager implements CameraCalibrationListener { private static final int MAX_AUTO_CALIBRATION_TIME = 12 * 1000; private static final int MAX_AUTO_CALIBRATION_TIME_HEADLESS = 45 * 1000; private static final Logger logger = LoggerFactory.getLogger(CalibrationManager.class); private final CalibrationConfigurator calibrationConfigurator; private final CameraManager calibratingCameraManager; private final CanvasManager calibratingCanvasManager; private final CameraViews cameraViews; private final Configuration config; private final Optional<AutocalibrationListener> autocalibrationListener; private final ExerciseListener exerciseListener; private final List<CalibrationListener> calibrationListeners = new ArrayList<>(); private final ProjectorArenaPane arenaPane; private ScheduledFuture<?> autoCalibrationFuture = null; private Optional<TrainingExercise> savedExercise = Optional.empty(); private Optional<TargetView> calibrationTarget = Optional.empty(); private Optional<CameraView> originalView = Optional.empty(); private Optional<Dimension2D> perspectivePaperDims = Optional.empty(); private final AtomicBoolean isCalibrating = new AtomicBoolean(false); private final AtomicBoolean isShowingPattern = new AtomicBoolean(false); public CalibrationManager(CalibrationConfigurator calibrationConfigurator, CameraManager calibratingCameraManager, ProjectorArenaPane arenaPane, CameraViews cameraViews, AutocalibrationListener autocalibrationListener, ExerciseListener exerciseListener) { this.calibrationConfigurator = calibrationConfigurator; this.calibratingCameraManager = calibratingCameraManager; calibratingCanvasManager = (CanvasManager) calibratingCameraManager.getCameraView(); calibrationListeners.add(arenaPane); this.arenaPane = arenaPane; this.cameraViews = cameraViews; config = Configuration.getConfig(); this.autocalibrationListener = Optional.ofNullable(autocalibrationListener); this.exerciseListener = exerciseListener; arenaPane.setFeedCanvasManager(calibratingCanvasManager); calibratingCameraManager.setCalibrationManager(this); calibratingCameraManager.setOnCloseListener(() -> Platform .runLater(() -> arenaPane.fireEvent(new WindowEvent(null, WindowEvent.WINDOW_CLOSE_REQUEST)))); } public void addCalibrationListener(CalibrationListener calibrationListener) { calibrationListeners.add(calibrationListener); } public void enableCalibration() { // Projector exercises can alter what is on the arena, thereby // interfearing with calibration. Thus, if an projector exercise // is set, we unset it for calibration, and reset it afterwards. if (config.getExercise().isPresent() && config.getExercise().get() instanceof ProjectorTrainingExerciseBase) { savedExercise = config.getExercise(); exerciseListener.setExercise(null); } else { // CalibrationManager is re-used when the user hits the // calibration button on the projector slide, thus we need // be sure to have clean state savedExercise = Optional.empty(); } arenaPane.getCanvasManager().setShowShots(false); isCalibrating.set(true); calibrationConfigurator.toggleCalibrating(true); // Sets calibrating and not detecting calibratingCameraManager.setCalibrating(true); calibratingCameraManager.setProjectionBounds(null); if (arenaPane.isFullScreen()) { enableAutoCalibration(); } else { showFullScreenRequest(); } } public void stopCalibration() { isCalibrating.set(false); if (calibrationTarget.isPresent()) calibrate(calibrationTarget.get().getTargetGroup().getBoundsInParent(), Optional.empty(), true, -1); calibratingCameraManager.disableAutoCalibration(); TimerPool.cancelTimer(autoCalibrationFuture); calibrationConfigurator.toggleCalibrating(false); removeFullScreenRequest(); removeAutoCalibrationMessage(); removeManualCalibrationRequestMessage(); removeCalibrationTargetIfPresent(); if (originalView.isPresent()) { cameraViews.selectCameraView(originalView.get()); } PerspectiveManager pm = null; final Dimension2D feedDim = new Dimension2D(calibratingCameraManager.getFeedWidth(), calibratingCameraManager.getFeedHeight()); if (calibratingCameraManager.getProjectionBounds().isPresent()) { if (PerspectiveManager.isCameraSupported(calibratingCameraManager.getName(), feedDim)) { if (perspectivePaperDims.isPresent()) { pm = new PerspectiveManager(calibratingCameraManager.getName(), calibratingCameraManager.getProjectionBounds().get(), feedDim, perspectivePaperDims.get(), arenaPane.getArenaStageResolution()); } else { pm = new PerspectiveManager(calibratingCameraManager.getName(), calibratingCameraManager.getProjectionBounds().get(), feedDim, arenaPane.getArenaStageResolution()); } } else { if (perspectivePaperDims.isPresent()) { pm = new PerspectiveManager(calibratingCameraManager.getProjectionBounds().get(), feedDim, perspectivePaperDims.get(), arenaPane.getArenaStageResolution()); } else { logger.debug("Too many perspective parameters are unknown to create a perspective manager."); } } } for (final CalibrationListener c : calibrationListeners) c.calibrated(Optional.ofNullable(pm)); arenaPane.restoreCurrentBackground(); calibratingCameraManager.setCalibrating(false); isShowingPattern.set(false); // We disable shot detection briefly because the pattern going away can // cause false shots. This statement applies to all the cam feeds rather // than just the arena. I don't think that should be a problem? calibratingCameraManager.setDetecting(false); TimerPool.schedule(() -> calibratingCameraManager.setDetecting(true), 600); arenaPane.getCanvasManager().setShowShots(config.showArenaShotMarkers()); if (savedExercise.isPresent()) exerciseListener.setProjectorExercise(savedExercise.get()); } @Override public void calibrate(Bounds arenaBounds, Optional<Dimension2D> perspectivePaperDims, boolean calibratedFromCanvas, long delay) { removeCalibrationTargetIfPresent(); if (!calibratedFromCanvas) arenaBounds = calibratingCanvasManager.translateCameraToCanvas(arenaBounds); configureArenaCamera(calibrationConfigurator.getCalibratedFeedBehavior(), arenaBounds); logger.debug("calibrate {} {} {}", arenaBounds, perspectivePaperDims, calibratedFromCanvas); this.perspectivePaperDims = perspectivePaperDims; if (isCalibrating()) stopCalibration(); } public void configureArenaCamera(CalibrationOption option) { calibratingCameraManager.setCropFeedToProjection(CalibrationOption.CROP.equals(option)); calibratingCameraManager.setLimitDetectProjection(CalibrationOption.ONLY_IN_BOUNDS.equals(option)); } public void arenaClosing() { calibratingCameraManager.setProjectionBounds(null); } private void createCalibrationTarget(double x, double y, double width, double height) { final RectangleRegion calibrationRectangle = new RectangleRegion(x, y, width, height); calibrationRectangle.setFill(Color.PURPLE); calibrationRectangle.setOpacity(TargetIO.DEFAULT_OPACITY); final Group calibrationGroup = new Group(); calibrationGroup.setOnMouseClicked((e) -> { calibrationGroup.requestFocus(); }); calibrationGroup.getChildren().add(calibrationRectangle); calibrationTarget = Optional.of((TargetView) calibratingCanvasManager.addTarget(null, calibrationGroup, new HashMap<String, String>(), false)); calibrationTarget.get().setKeepInBounds(true); } private void removeCalibrationTargetIfPresent() { if (calibrationTarget.isPresent()) { calibratingCanvasManager.removeTarget(calibrationTarget.get()); calibrationTarget = Optional.empty(); } } private void configureArenaCamera(CalibrationOption option, Bounds bounds) { final Bounds translatedToCameraBounds = calibratingCanvasManager.translateCanvasToCamera(bounds); calibratingCanvasManager.setProjectorArena(arenaPane, bounds); configureArenaCamera(option); calibratingCameraManager.setProjectionBounds(translatedToCameraBounds); } private void enableManualCalibration() { logger.trace("enableManualCalibration"); final int DEFAULT_DIM = 75; final int DEFAULT_POS = 150; removeAutoCalibrationMessage(); originalView = Optional.of(cameraViews.getSelectedCameraView()); cameraViews.selectCameraView(calibratingCanvasManager); showManualCalibrationRequestMessage(); if (!calibrationTarget.isPresent()) { createCalibrationTarget(DEFAULT_DIM, DEFAULT_DIM, DEFAULT_POS, DEFAULT_POS); } else { calibratingCameraManager.getCameraView().addTarget(calibrationTarget.get()); } } private void disableManualCalibration() { removeCalibrationTargetIfPresent(); removeManualCalibrationRequestMessage(); } private Label manualCalibrationRequestMessage = null; private volatile boolean showingManualCalibrationRequestMessage = false; private void showManualCalibrationRequestMessage() { if (showingManualCalibrationRequestMessage) return; showingManualCalibrationRequestMessage = true; manualCalibrationRequestMessage = calibratingCanvasManager .addDiagnosticMessage("Please manually calibrate the projection region", 20000, Color.ORANGE); } private void removeManualCalibrationRequestMessage() { logger.trace("removeFullScreenRequest {}", manualCalibrationRequestMessage); if (showingManualCalibrationRequestMessage) { showingManualCalibrationRequestMessage = false; calibratingCanvasManager.removeDiagnosticMessage(manualCalibrationRequestMessage); manualCalibrationRequestMessage = null; } } private Label fullScreenRequestMessage = null; private volatile boolean showingFullScreenRequestMessage = false; private void showFullScreenRequest() { if (showingFullScreenRequestMessage) return; showingFullScreenRequestMessage = true; fullScreenRequestMessage = calibratingCanvasManager .addDiagnosticMessage("Please move the arena to your projector and hit F11", Color.YELLOW); } private void removeFullScreenRequest() { logger.trace("removeFullScreenRequest {}", fullScreenRequestMessage); if (showingFullScreenRequestMessage) { showingFullScreenRequestMessage = false; calibratingCanvasManager.removeDiagnosticMessage(fullScreenRequestMessage); fullScreenRequestMessage = null; } } private void enableAutoCalibration() { logger.trace("enableAutoCalibration"); for (final CalibrationListener c : calibrationListeners) c.startCalibration(); arenaPane.setCalibrationMessageVisible(false); // We may already be calibrating if the user decided to move the arena // to another screen while calibrating. If we save the background in // that case we are saving the calibration pattern as the background. if (!isShowingPattern.get()) arenaPane.saveCurrentBackground(); setArenaBackground("pattern.png"); isShowingPattern.set(true); calibratingCameraManager.enableAutoCalibration(false); showAutoCalibrationMessage(); launchAutoCalibrationTimer(); } private void launchAutoCalibrationTimer() { TimerPool.cancelTimer(autoCalibrationFuture); autoCalibrationFuture = TimerPool.schedule(() -> { Platform.runLater(() -> { if (isCalibrating.get() && isFullScreen) { if (autocalibrationListener.isPresent()) { autocalibrationListener.get().autocalibrationTimedOut(); stopCalibration(); } else { calibratingCameraManager.disableAutoCalibration(); enableManualCalibration(); } } // Keep waiting else if (!isFullScreen) launchAutoCalibrationTimer(); }); }, autocalibrationListener.isPresent() ? MAX_AUTO_CALIBRATION_TIME_HEADLESS : MAX_AUTO_CALIBRATION_TIME); } @Override public void setArenaBackground(String resourceFilename) { if (resourceFilename != null) { final InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceFilename); final LocatedImage img = new LocatedImage(is, resourceFilename); arenaPane.setArenaBackground(img); } else { arenaPane.setArenaBackground(null); } } private Label autoCalibrationMessage = null; private volatile boolean showingAutoCalibrationMessage = false; private void showAutoCalibrationMessage() { logger.trace("showAutoCalibrationMessage - showingAutoCalibrationMessage {} autoCalibrationMessage {}", showingAutoCalibrationMessage, autoCalibrationMessage); if (showingAutoCalibrationMessage) return; showingAutoCalibrationMessage = true; autoCalibrationMessage = calibratingCanvasManager.addDiagnosticMessage("Attempting autocalibration", 11000, Color.CYAN); } private void removeAutoCalibrationMessage() { logger.trace("removeAutoCalibrationMessage - showingAutoCalibrationMessage {} autoCalibrationMessage {}", showingAutoCalibrationMessage, autoCalibrationMessage); if (showingAutoCalibrationMessage) { showingAutoCalibrationMessage = false; if (logger.isTraceEnabled()) logger.trace("removeAutoCalibrationMessage {} ", autoCalibrationMessage); calibratingCanvasManager.removeDiagnosticMessage(autoCalibrationMessage); autoCalibrationMessage = null; } } private boolean isFullScreen = false; public void setFullScreenStatus(boolean fullScreen) { isFullScreen = fullScreen; logger.trace("setFullScreenStatus - {} {}", fullScreen, isCalibrating); if (!isCalibrating.get()) { enableCalibration(); } else if (!fullScreen) { calibratingCameraManager.disableAutoCalibration(); removeCalibrationTargetIfPresent(); removeAutoCalibrationMessage(); disableManualCalibration(); showFullScreenRequest(); } else { removeFullScreenRequest(); // Delay slightly to prevent #444 bug TimerPool.schedule(() -> { Platform.runLater(() -> { if (isCalibrating.get()) enableAutoCalibration(); }); }, 100); } } public boolean isCalibrating() { return isCalibrating.get(); } }