package edu.cmu.cs.eyetrack.gui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Shape;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import org.jdesktop.core.animation.timing.Animator;
import org.jdesktop.core.animation.timing.Animator.RepeatBehavior;
import org.jdesktop.core.animation.timing.AnimatorBuilder;
import org.jdesktop.core.animation.timing.KeyFrames;
import org.jdesktop.core.animation.timing.TimingSource;
import org.jdesktop.core.animation.timing.TimingTarget;
import org.jdesktop.core.animation.timing.TimingTargetAdapter;
import org.jdesktop.swing.animation.timing.sources.SwingTimerTimingSource;
import edu.cmu.cs.eyetrack.gui.shapes.Stimulus;
import edu.cmu.cs.eyetrack.gui.shapes.StimulusFactory;
import edu.cmu.cs.eyetrack.gui.shapes.StimulusFactory.StimulusType;
import edu.cmu.cs.eyetrack.gui.trial.Block;
import edu.cmu.cs.eyetrack.gui.trial.GameScreenBG;
import edu.cmu.cs.eyetrack.helper.Coordinate;
import edu.cmu.cs.eyetrack.helper.Util;
import edu.cmu.cs.eyetrack.helper.Util.PanelID;
import edu.cmu.cs.eyetrack.state.RandomGen;
import edu.cmu.cs.eyetrack.state.Settings;
import edu.cmu.cs.eyetrack.state.Settings.Experiment;
import edu.cmu.cs.eyetrack.state.Settings.TrialType;
import edu.cmu.cs.eyetrack.state.Trial;
@SuppressWarnings("serial")
public class GameScreen extends Screen {
private enum Status { UNSTARTED, ANIM_STARTED, WAITING_FOR_CLICK, DONE };
private static int MIN_JUMP_LENGTH = 500;
private static int MAX_JUMP_LENGTH = 1500;
private Experiment experiment;
// We need to boot up the basic GameScreen only once---the grid, the background
// images, etc.
private boolean initialized = false;
private Status status;
private long trialLength = 0; // in milliseconds
private int trialCount = 0;
// Blocks
private int numColumns=3, numRows=3;
private int blockWidth, blockHeight;
private ArrayList<ArrayList<Block>> blocks;
// Background Image (create once, use always)
BufferedImage bgImage;
// Stimuli
private HashSet<Stimulus> stimEverything = null;
private ArrayList<Stimulus> stimDistractors = null;
private Stimulus stimTarget = null;
// Controls the FPS of the game
private Thread animUpdater;
private long updateRate;
// The exact timestamp when the distractors and targets disappear. This can't
// be just (trial state time + trial length) due to FPS
private long disappearanceTime;
// Records all relevant information for this Trial screen
private Trial trial;
// We care about the frequencies/ending location of the target,
// so compute this with care once and keep across all trials
private List<Coordinate<Integer>> stimTargetEndingPos;
private Coordinate<Integer> stimTargetStartPos;
public GameScreen(EyeTrack owner, PanelID nextScreen) {
super(owner, nextScreen);
//layers = new JLayeredPane();
//setLayout(new BorderLayout());
//add(layers);
}
private void startNewGame() {
status = Status.ANIM_STARTED;
// Start animated, switching frames at a rate of once per 1/FPS ms
//animTask = new AnimationTask(this);
//timer.scheduleAtFixedRate(animTask, 0L, updateRate);
animUpdater = new Thread(new AnimatorThread(this));
animUpdater.start();
Util.dPrintln("Started trial ###, length is " + trialLength + "ms.");
for(Stimulus stim : stimEverything ) {
stim.getAnimator().start();
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static boolean enableOSXFullscreen(Window window) {
if(null == window) { return false; }
try {
Class util = Class.forName("com.apple.eawt.FullScreenUtilities");
Class params[] = new Class[]{Window.class, Boolean.TYPE};
Method method = util.getMethod("setWindowCanFullScreen", params);
method.invoke(util, window, true);
return true;
} catch (ClassNotFoundException e1) {
Util.dPrintln("OS X 10.7+ fullscreen API not supported or failed.");
} catch (Exception e) {
Util.dPrintln("OS X 10.7+ fullscreen API not supported or failed.");
}
return false;
}
@Override
protected void initialize() {
this.status = Status.UNSTARTED;
// Create the experiment from the user's desired settings
Settings settings = owner.getGameState().getSettings();
experiment = owner.getGameState().getSettings().getExperiment();
// Only want to initialize the background grid, load the distractors, etc once---not
// each time the user sees the screen
if(!initialized) {
// Kill the titlebar and fullscreen for the experiment
if( experiment.getUsesFullscreen() ) {
Util.dPrintln("Attempting to switch to fullscreen mode for trials.");
boolean keepTrying = true;
if(Util.isRunningOnMacOSX()) {
Util.dPrintln("Attempting Mac OS X native fullscreen API.");
keepTrying = !GameScreen.enableOSXFullscreen(owner);
}
if(keepTrying && GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().isFullScreenSupported() ) {
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow(owner);
experiment.updateInsets(owner.getSize());
} else {
Util.dPrintln("getDefaultScreenDevice().isFullScreenSupported() returned False; cannot use fullscreen mode.");
}
}
// CHECK ME!!!!
TimingSource ts = new SwingTimerTimingSource();//1, TimeUnit.MILLISECONDS);
AnimatorBuilder.setDefaultTimingSource(ts);
ts.init();
//
trialCount = 0;
// First, layout the grid (width x height, etc)
numColumns = settings.getExperiment().getGridXSize();
numRows = settings.getExperiment().getGridYSize();
bgImage = GameScreenBG.drawBG(owner);
//JPanel blocksPanel = new JPanel();
//blocksPanel.
setLayout(new GridLayout(numColumns, numRows));
// Get the expected size of a single block, based on the size of the parent JFrame
blockWidth = (int) (experiment.getPixelWidth() / (double) numColumns);
blockHeight = (int) (experiment.getPixelHeight() / (double) numRows);
// If we're using background images, keep track of which ones we've used
int bgIdx = 0;
List<ImageIcon> bgImages = owner.getGameState().getBackgroundImages();
// Now, create a separate block object for each block on the grid
blocks = new ArrayList<ArrayList<Block>>();
for(int colIdx=0; colIdx<numColumns; colIdx++) {
ArrayList<Block> row = new ArrayList<Block>();
for(int rowIdx=0; rowIdx<numRows; rowIdx++) {
Block block = null;
if(experiment.getUsesBGImages()) {
block = new Block(blockWidth, blockHeight, bgImages.get(bgIdx));
bgIdx = (bgIdx+1) % bgImages.size();
} else {
block = new Block(blockWidth, blockHeight);
}
// J2SE Polygon
row.add(block);
//blocksPanel.add(block);
//add(block);
}
blocks.add(row);
}
//layers.add(blocksPanel, 0);
// Set up the animation to occur at whatever user-specified FPS
updateRate = (long) (1000.0 / settings.getExperiment().getFPS());
if(1000 % settings.getExperiment().getFPS() != 0) {
Util.dPrintln("Frames per second unable to be split evenly across one second.");
}
Util.dPrintln("Setting animation to update once per " + updateRate + " milliseconds.");
// Add a click listener to report where (what grid box) the user clicks
addMouseListener(new GridBoxMouseListener());
// The user presses the Spacebar to begin the trial
setFocusable(true);
requestFocusInWindow();
addKeyListener(new BasicTrialKeyListener());
// Register real stimuli
owner.getGameState().registerStimuli(blockWidth, blockHeight, settings.getExperiment().getStimulusClass());
// Compute the ending location for the target (across all trials)
stimTargetEndingPos = owner.getGameState().getRandomGen().getRandomEndPositions(
owner.getGameState().getSettings().getExperiment().getTrialCount() );
initialized = true;
}
// Set up the mutable parts of the GUI---the distractors, movement vectors,
// initial positions, timing elements, etc.
setupNewTrial(settings.getExperiment().getNumDistractors());
requestFocusInWindow();
}
private void setupNewTrial(int numDistractors) {
// Make a new record for this trial
trialCount++;
trial = new Trial(trialCount);
owner.getGameState().addTrial(trial);
stimDistractors = new ArrayList<Stimulus>();
stimEverything = new HashSet<Stimulus>();
// If we are randomizing colors per-trial (instead of per-experiment), pick some random colors
List<Color> stimColors = owner.getGameState().getRandomGen().getRandomColors();
int colorIdx=0;
//trial.setStimColors(stimColors);
Set<Stimulus> previousStimTargets = owner.getGameState().getPreviousStimTargets();
// Create the target, place it first---we have the requirement
// that the ENDING grid position of the target must be uniformly
// distributed across all grid positions, as well as the BEGINNING position
Experiment exp = owner.getGameState().getSettings().getExperiment();
if(exp.getUsesColoradoTypedTrial()) {
// get the stimuls corresponding to this trial ID from the fixed UC list
stimTarget = StimulusFactory.getInstance().getRegisteredExample(StimulusType.TARGET, exp.getColoradoTypedTrial().getStimulus(trialCount) );
} else {
// Standard non-UColorado trials -- do random selection, etc
if(exp.getUsesRandomTarget()) {
// If we're sampling without replacement, make sure we sample a new target; otherwise,
// just sample any target from StimulusType.TARGET
if(!exp.getUsesRandomWithReplacement()) {
boolean legalStimTarget = true;
do {
stimTarget = StimulusFactory.getInstance().create(StimulusType.TARGET, stimColors.get(colorIdx));
legalStimTarget = true;
// Make sure we haven't sampled this target before
for(Stimulus previousStimTarget : previousStimTargets) {
if(previousStimTarget.isEquivalent(stimTarget)) {
legalStimTarget = false;
break;
}
}
} while(!legalStimTarget);
colorIdx++;
} else {
stimTarget = StimulusFactory.getInstance().create(StimulusType.TARGET, stimColors.get(colorIdx++));
}
} else {
stimTarget = exp.getCanonicalTarget().factoryClone();
// Adjust colors so that the target's color is first in line, skipped by distractors below.
for(int idx=0; idx<stimColors.size(); idx++) {
if(stimColors.get(idx).equals(stimTarget.getColor())) {
stimColors.set(idx, stimColors.get(0));
stimColors.set(0, stimTarget.getColor());
colorIdx++;
break;
}
}
}
}
previousStimTargets.add(stimTarget);
// Create #distractors Distractors, according to user's preferences
TrialType trialType = owner.getGameState().getSettings().getExperiment().getTrialType();
if(trialType.equals(TrialType.ALL_SAME) || trialType.equals(TrialType.SAME_AS_TARGET)) {
Stimulus distractorParent = null;
if(trialType.equals(TrialType.SAME_AS_TARGET)) {
// Distractors all look same as target
distractorParent = stimTarget;
} else if(trialType.equals(TrialType.ALL_SAME)) {
// Distractors all look the same, but different than target
do {
distractorParent = StimulusFactory.getInstance().create(StimulusType.DISTRACTOR, stimColors.get(colorIdx));
} while(distractorParent.isEquivalent(stimTarget));
}
for(int disIdx=0; disIdx < numDistractors; disIdx++) {
stimDistractors.add(distractorParent.factoryClone());
}
} else if(trialType.equals(TrialType.ALL_DIFF) && numDistractors > 0) {
// Distractors all look the same as target
List<Stimulus> allDiff = StimulusFactory.getInstance().createOneOfEach(StimulusType.DISTRACTOR);
int disIdx = 0;
do {
if( !allDiff.get(disIdx).isEquivalent(stimTarget) ) {
stimDistractors.add( allDiff.get(disIdx).factoryClone( stimColors.get(colorIdx) ) );
colorIdx = (colorIdx + 1) % stimColors.size();
}
disIdx = (disIdx + 1) % allDiff.size();
} while(stimDistractors.size() < numDistractors);
}
// Sometimes, we'd like to iterate over all the Stimuli together
stimEverything.addAll(stimDistractors);
stimEverything.add(stimTarget);
// Keep track of stimulus for the I/O
trial.pushStims(stimEverything);
trial.setTarget(stimTarget);
RandomGen rGen = owner.getGameState().getRandomGen();
List<Coordinate<Integer>> initialPlacements = rGen.genRandomGridPositions(stimEverything.size());
trialLength = ((long) owner.getGameState().getSettings().getExperiment().getTrialLength());
// Place the stimulus in random boxes
// Also initialize their first movement instructions
int idx=0;
// First, place the target stimulus and figure out how long this trial will last
placeAndPlanStimulus(stimTarget, initialPlacements.get(idx++));
// Second, place all the other distractors
for( final Stimulus stim : stimDistractors ) {
Coordinate<Integer> gridPlacement = initialPlacements.get(idx++);
placeAndPlanStimulus(stim, gridPlacement);
}
// Wait for the user to hit the spacebar
status = Status.UNSTARTED;
}
private void placeAndPlanStimulus(final Stimulus stim, Coordinate<Integer> startingGrid) {
// Place stimulus in its original location
Coordinate<Integer> gridCenter = getGridCenter(startingGrid);
moveStimulus(stim, gridCenter);
// Length of the trial is the minimum length specified by the user
// plus some random small number of milliseconds---this is defined by the target's movement
List<KeyFrames<Integer>> keyFramesList = new ArrayList<KeyFrames<Integer>>();
double timeUsed = owner.getGameState().getRandomGen().calcKeyFrames(keyFramesList,
MIN_JUMP_LENGTH,
MAX_JUMP_LENGTH,
trialLength,
stim.getWidth()/2,
stim.getHeight()/2,
stim.getCenter(),
stim == stimTarget ? getGridCenter(stimTargetEndingPos.get(trialCount-1)) : null); // stimTarget must end in specific grid center
if(stim == stimTarget) {
// Keep track of where the target started, for the red circle
stimTargetStartPos = startingGrid;
// If the target used more time than allocated, update this
trialLength = (long) timeUsed;
trial.setLength(trialLength);
}
final KeyFrames<Integer> keyFramesX = keyFramesList.get(0);
final KeyFrames<Integer> keyFramesY = keyFramesList.get(1);
final TimingTarget randomMovement = new TimingTargetAdapter() {
@Override
public void timingEvent(Animator source, double fraction) {
stim.move( keyFramesX.getInterpolatedValueAt(fraction),
keyFramesY.getInterpolatedValueAt(fraction) );
}
};
stim.setAnimator( new AnimatorBuilder()
.setDuration(trialLength, TimeUnit.MILLISECONDS)
.addTarget(randomMovement)
.setRepeatCount(1)
.setRepeatBehavior(RepeatBehavior.LOOP)
//.setInterpolator(new AccelerationInterpolator(0.4, 0.4))
.build() );
}
// Remove the stimuli, maybe call
@Override
protected void tearDown() {
// All stimuli will be recreated come next trial
stimDistractors = null;
stimTarget = null;
stimEverything = null;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// stimTargetEndingPos
g.drawImage(bgImage, 0, 0, null);
Graphics2D g2d = (Graphics2D) g;
//g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Draw each of our stimuli
if( status == Status.UNSTARTED || status == Status.ANIM_STARTED ) {
for(Stimulus stimulus : stimDistractors) {
stimulus.draw(g2d);
}
stimTarget.draw(g2d);
}
// Draw a red circle around the target stimulus
if( status == Status.UNSTARTED) {
float halfStroke = 2.0f;
Shape circle = new Ellipse2D.Float(experiment.getInsetX() + stimTargetStartPos.getX()*blockWidth + halfStroke,
experiment.getInsetY() + stimTargetStartPos.getY()*blockHeight + halfStroke,
blockWidth - halfStroke,
blockHeight - halfStroke);
g2d.setColor(Color.RED);
g2d.setStroke(new BasicStroke(2.0f * halfStroke));
g2d.draw(circle);
g2d.setColor(Color.BLACK);
}
}
private void processFinalClick(int x, int y) {
// Figure out which grid box the user selected
int gridX = xToGrid(x), gridY = yToGrid(y);
trial.setUserClickPos(new Coordinate<Double>(Double.valueOf(x),Double.valueOf(y)));
trial.setUserClickGridPos(new Coordinate<Integer>(gridX, gridY));
// Figure out if the user selected the *right* grid box
boolean clickCorrect = trial.getUserClickGridPos().equals(trial.getTargetFinalGridPos());
trial.setGridClickCorrect(clickCorrect);
// How long did it take the user to click on a box, once the distractors and target disappeared?
long responseTime = System.currentTimeMillis() - disappearanceTime;
trial.setGridClickRT(responseTime);
Util.dPrintln("User " + (clickCorrect ? "correctly" : "incorrectly") + " selected grid[" + gridX + ", " + gridY + "] in " + responseTime + " ms.");
// Done with this segment; send user to a cool-down screen
status = Status.DONE;
owner.switchContext(PanelID.BUFFER1);
}
class GridBoxMouseListener implements MouseListener {
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void mouseClicked(MouseEvent e) {
// If the animation is done and we are waiting for the participant to select
// the final grid box in which the target ended, process
if( status == Status.WAITING_FOR_CLICK ) {
processFinalClick(e.getX(), e.getY());
}
}
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void mouseEntered(MouseEvent e) {}
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void mouseExited(MouseEvent e) {}
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void mousePressed(MouseEvent e) {}
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void mouseReleased(MouseEvent e) {}
}
class BasicTrialKeyListener implements KeyListener {
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void keyPressed(KeyEvent e) {
// If the user presses the Spacebar, start the trial
if(e.getKeyCode() == KeyEvent.VK_SPACE && status == Status.UNSTARTED) {
Util.dPrintln("Spacebar pressed; starting trial.");
startNewGame();
} else if(e.getKeyCode() == KeyEvent.VK_SPACE && status == Status.WAITING_FOR_CLICK) {
processFinalClick(-1,-1);
}
}
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void keyReleased(KeyEvent e) {}
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void keyTyped(KeyEvent e) {}
}
class AnimatorThread implements Runnable {
private JPanel mainPanel;
private long trialStart;
public AnimatorThread(JPanel mainPanel) {
super();
this.mainPanel = mainPanel;
this.trialStart = 0L;
}
//@Override //TODO Java 1.5 screams about this; remove when not caring about Java 1.5
public void run() {
while(Thread.currentThread() == animUpdater) {
long frameStartTime = System.currentTimeMillis();
if(trialStart == 0) {
//trialStart = this.scheduledExecutionTime();
trialStart = frameStartTime;
trial.setStartTime(trialStart);
}
// Call .update() on each of the Shape objects
for(Stimulus stimulus : stimEverything) {
trial.pushPos(stimulus, new Coordinate<Double>(stimulus.getCenter(), frameStartTime));
}
trial.incFrameCount();
//System.out.println("Target center: " + stimTarget.getCenter());
//System.out.println("Grid center: " + getGridCenter( xToGrid(stimTarget.getCenter().getX()), yToGrid(stimTarget.getCenter().getY())));
// If we've run for at least the length of the trial,
// quit out to the part where the user clicks on the grid
if( trialLength < (frameStartTime - trialStart) ) {
//this.cancel();
// Record where the target stopped
trial.setTargetFinalPos(stimTarget.getCenter());
trial.setTargetFinalGridPos(new Coordinate<Integer>(
xToGrid( stimTarget.getCenter().getX()),
yToGrid( stimTarget.getCenter().getY())));
disappearanceTime = frameStartTime;
status = Status.WAITING_FOR_CLICK;
repaint();
return;
}
mainPanel.repaint();
// Tick again in "exactly" 40fps
try {
Thread.sleep(Math.max(0, updateRate - (System.currentTimeMillis() - frameStartTime)));
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
protected int xToGrid(int x) {
return xToGrid((double) x);
}
protected int yToGrid(int y) {
return yToGrid((double) y);
}
// Make these double versions more precise if we ever need it
protected int xToGrid(double x) {
return (int) ( (x - experiment.getInsetX() ) / blockWidth);
}
protected int yToGrid(double y) {
return (int) ( (y - experiment.getInsetY() ) / blockHeight);
}
protected Coordinate<Integer> getGridCenter(Coordinate<Integer> gridBlock) {
return getGridCenter(gridBlock.getX(), gridBlock.getY());
}
protected Coordinate<Integer> getGridCenter(int gridX, int gridY) {
return new Coordinate<Integer>(
(int) (experiment.getInsetX() + (gridX * blockWidth) + (0.5*blockWidth)),
(int) (experiment.getInsetY() + (gridY * blockHeight) + (0.5*blockHeight)));
}
protected void moveStimulus(Stimulus stimulus, Coordinate<Integer> coordinate) {
stimulus.move(coordinate.getX(), coordinate.getY());
//stimulus.getPolygon().translate(
// ((int) -stimulus.getPolygon().getBounds().getX()) + coordinate.getX() - (int) (0.5 * stimulus.getPolygon().getBounds().getWidth()),
// ((int) -stimulus.getPolygon().getBounds().getY()) + coordinate.getY() - (int) (0.5 * stimulus.getPolygon().getBounds().getHeight()));
}
}