/*
* 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.plugins;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import com.shootoff.camera.CamerasSupervisor;
import com.shootoff.config.Configuration;
import com.shootoff.courses.Course;
import com.shootoff.courses.io.CourseIO;
import com.shootoff.gui.LocatedImage;
import com.shootoff.gui.ShotEntry;
import com.shootoff.gui.controller.ShootOFFController;
import com.shootoff.gui.pane.ProjectorArenaPane;
import com.shootoff.targets.Target;
import javafx.application.Platform;
import javafx.geometry.Dimension2D;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
/**
* The API for training exercises that only work on the projector arena.
* Training exercises that are intended to be used with the projector arena only
* (i.e. those that require a projector) should extend this class. If the
* exercise is not intended to only work with a projector, extend
* {@link TrainingExerciseBase} instead.
*
* @author phrack
*/
public class ProjectorTrainingExerciseBase extends TrainingExerciseBase {
private CamerasSupervisor camerasSupervisor;
private ProjectorArenaPane arenaPane;
private final List<Target> targets = new ArrayList<>();
private final Label exerciseLabel = new Label();
// Only exists to make it easy to call getInfo without having
// to do a bunch of unnecessary setup
public ProjectorTrainingExerciseBase() {}
public ProjectorTrainingExerciseBase(List<Target> targets) {
super(targets);
}
public void init(CamerasSupervisor camerasSupervisor, TrainingExerciseView exerciseView,
ProjectorArenaPane arenaPane) {
super.init(camerasSupervisor, exerciseView);
this.camerasSupervisor = camerasSupervisor;
this.arenaPane = arenaPane;
exerciseLabel.setTextFill(Color.WHITE);
Platform.runLater(() -> arenaPane.getCanvasManager().getCanvasGroup().getChildren().add(exerciseLabel));
}
// For unit tests
public void init(Configuration config, CamerasSupervisor camerasSupervisor, VBox buttonsContainer,
TableView<ShotEntry> shotEntryTable, ProjectorArenaPane arenaPane) {
super.init(config, camerasSupervisor, buttonsContainer, shotEntryTable);
this.config = config;
this.camerasSupervisor = camerasSupervisor;
this.arenaPane = arenaPane;
}
@Override
public void reset() {
camerasSupervisor.reset();
if (config.getExercise().isPresent())
config.getExercise().get().reset(arenaPane.getCanvasManager().getTargets());
}
/**
* Add a target to the projector arena at specific coordinates.
*
* @param target
* the file to load the target from
* @param x
* the top left x coordinate of the target
* @param y
* the top left y coordinate of the target
*
* @return the group that was loaded from the target file
*
* @since 2.1
*/
public Optional<Target> addTarget(File target, final double x, final double y) {
if ('@' != target.toString().charAt(0) && !target.isAbsolute())
target = new File(System.getProperty("shootoff.home") + File.separator + target.getPath());
final Optional<Target> newTarget = arenaPane.getCanvasManager().addTarget(target, false);
if (newTarget.isPresent()) {
final Target t = newTarget.get();
t.setPosition(x, y);
if (isPerspectiveInitialized()) {
arenaPane.resizeTargetToDefaultPerspective(t);
}
targets.add(t);
}
return newTarget;
}
public void removeTarget(Target target) {
arenaPane.getCanvasManager().removeTarget(target);
targets.remove(target);
}
/**
* Get the width of the arena in pixels.
*
* @return the arena's width in pixels
*
* @since 2.1
*/
public double getArenaWidth() {
return arenaPane.getWidth();
}
/**
* Get the height of the arena in pixels.
*
* @return the arena's height in pixels
*
* @since 2.1
*/
public double getArenaHeight() {
return arenaPane.getHeight();
}
/**
* Get the coordinates of the origin for the display the arena is currently
* located on. This is useful if you need to know what display the arena is
* on.
*
* @return the origin coordinates for the JavaFX screen the arena is located
* on, relative to other displays.
*
* @since 3.8
*/
public Point2D getArenaScreenOrigin() {
return arenaPane.getArenaScreenOrigin();
}
/**
* This function corrects coordinates in the arena area for the DPI of the
* arena screen and relative to the origin. This is for creating mouse
* events and other javafx functions that need true coordinates rather
* than scaled ones.
*
* @param point
* The coordinates to be corrected
*
* @return Translated coordinates
*
* @since 3.8
*/
public Point2D translateToTrueArenaCoords(Point2D point)
{
final double dpiScaleFactor = ShootOFFController.getDpiScaleFactorForScreen();
final Point2D origin = arenaPane.getArenaScreenOrigin();
return new Point2D(origin.getX() + (point.getX() * dpiScaleFactor),
origin.getY() + (point.getY() * dpiScaleFactor));
}
@Override
public void showTextOnFeed(String message) {
super.showTextOnFeed(message);
Platform.runLater(() -> exerciseLabel.setText(message));
}
/**
* Show a message on all webcam feeds, but optionally do not show the
* message on the arena itself.
*
* @param message
* the message to show
* @param showOnArena
* <tt>false</tt> if the message should not show on the arena
*/
public void showTextOnFeed(String message, boolean showOnArena) {
if (showOnArena) {
showTextOnFeed(message);
} else {
super.showTextOnFeed(message);
}
}
/**
* Show a message on all webcam feeds and the arena, but customize the
* location, font, and colors used to display the message.
*
* @param message
* the message to show
* @param x
* the x coordinate of the top left of the message
* @param y
* the y coordinate of the top left of the message
* @param backgroundColor
* the background color for the message
* @param textColor
* the color of the letters in the message
* @param font
* the font to use to display the message
*
* @since 3.7
*/
public void showTextOnFeed(String message, int x, int y, Color backgroundColor, Color textColor, Font font) {
showTextOnFeed(message);
Platform.runLater(() -> {
exerciseLabel.setLayoutX(x);
exerciseLabel.setLayoutY(y);
exerciseLabel.setBackground(
new Background(new BackgroundFill(backgroundColor, CornerRadii.EMPTY, Insets.EMPTY)));
exerciseLabel.setTextFill(textColor);
exerciseLabel.setFont(font);
});
}
/**
* Returns the current instance of this class. This method exists so that we
* can call methods in this class when in an internal class (e.g. to
* implement Callable) that doesn't have access to super.
*
* @return the current instance of this class
*/
@Override
public ProjectorTrainingExerciseBase getInstance() {
return this;
}
/**
* Set the projector arena's background image.
*
* @param background
* a file on the filesystem or a resource to set as the projector
* arena's background.
*
* @since 3.7
*/
public void setArenaBackground(LocatedImage background) {
arenaPane.setArenaBackground(background);
}
/**
* Set the projector arena's background image to one of the default
* backgrounds using its resource path.
*
* @param defaultResourcePath
* the resource path to the default ShootOFF background to set.
*
* @since 3.10
*/
public void setArenaBackground(String defaultResourcePath) {
final InputStream is = ProjectorTrainingExerciseBase.class.getResourceAsStream(defaultResourcePath);
final LocatedImage img = new LocatedImage(is, defaultResourcePath);
setArenaBackground(img);
}
/**
* Remove all targets on the arena and replace them with the course
* specified in the file <code>courseFile</code>.
*
* @param courseFile
* a file specifying the course to use
* @return a list of the targets loaded from <code>courseFile</code>
*
* @since 3.8
*/
public List<Target> setCourse(File courseFile) {
final Optional<Course> newCourse = CourseIO.loadCourse(arenaPane, courseFile);
arenaPane.setCourse(newCourse.get());
return newCourse.get().getTargets();
}
/**
* Determines whether or not ShootOFF has sufficient information to set a
* target's distance to a real world distance.
*
* @return <code>true</code> if ShootOFF can set a target to a real world
* distance
*
* @since 3.8
*/
public boolean isPerspectiveInitialized() {
return arenaPane.getPerspectiveManager().isPresent()
&& arenaPane.getPerspectiveManager().get().isInitialized();
}
/**
* Changes the size of a target to real world dimensions at a particular
* distance (e.g. to simulate a target that is 36"x24" at 10 yards). To do
* this, you must already know the size of the target and ShootOFF must be
* initialized with the distance and camera specification information
* required to measure distances in the real world. Look at the USPSA target
* for an example of setting a default real world width, height, and
* distance for a target.
*
* Distance and camera specification information is typically determined
* during the auto-calibration process. However, a user will have to
* manually enter at least the camera distance by manually setting a
* target's distance if they did not auto-calibrate or if they
* auto-calibrated without the perspective calibration pattern present.
*
* @param target
* the target to resize
* @param currentRealWidth
* the current width of the target as it appears on the
* projection in mm
* @param currentRealHeight
* the current height of the target as it appears on the
* projection in mm
* @param currentRealDistance
* the distance the target is currently sized to appear at (e.g.
* the target appears to be currentRealWidth x currentRealHeight
* because it is currentRealDistance away in mm)
* @param desiredDistance
* the new distance the target should appear at in mm (i.e.
* resize the target so it appears to be desiredDistance away
* based on its current size and distance)
* @return <code>true</code> if ShootOFF has the data to resize a target and
* successfully calculated new target dimensions
*/
public boolean setTargetDistance(Target target, int currentRealWidth, int currentRealHeight,
int desiredDistance) {
if (!isPerspectiveInitialized()) return false;
final Optional<Dimension2D> targetDimensions = arenaPane.getPerspectiveManager().get()
.calculateObjectSize(currentRealWidth, currentRealHeight, desiredDistance);
if (targetDimensions.isPresent()) {
final Dimension2D d = targetDimensions.get();
target.setDimensions(d.getWidth(), d.getHeight());
return true;
}
return false;
}
@Override
public void destroy() {
for (final Target target : targets)
arenaPane.getCanvasManager().removeTarget(target);
targets.clear();
Platform.runLater(() -> {
if (arenaPane != null)
arenaPane.getCanvasManager().getCanvasGroup().getChildren().remove(exerciseLabel);
});
super.destroy();
}
}