MazeCanvas.java

/*
 * Copyright © 2012 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */

package com.nokia.example.amaze.ui;

import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.m3g.Appearance;
import javax.microedition.m3g.Background;
import javax.microedition.m3g.Camera;
import javax.microedition.m3g.Graphics3D;
import javax.microedition.m3g.Mesh;
import javax.microedition.m3g.Node;
import javax.microedition.m3g.RayIntersection;
import javax.microedition.m3g.Transform;
import javax.microedition.m3g.World;

import com.nokia.mid.ui.DeviceControl;
import com.nokia.mid.ui.orientation.Orientation;
import com.nokia.mid.ui.orientation.OrientationListener;

import com.nokia.example.amaze.Main;
import com.nokia.example.amaze.model.GameModel;
import com.nokia.example.amaze.model.MarbleModel;
import com.nokia.example.amaze.model.Maze;
import com.nokia.example.amaze.model.MyTimer;

/**
 * The main UI class.
 */
public class MazeCanvas
	extends GameCanvas
	implements Runnable,
			   OrientationListener,
			   CameraAnimator.Listener
{
	// Constants
	private final static int MILLIS_PER_TICK = 5; // Milliseconds to wait in the main loop
	private final static float DEFAULT_TOP_CAMERA_ANGLE = 75f;
	private final static float PAUSE_CAMERA_ANGLE = 45f;
	private final static float DEFAULT_ZOOM = 1.0f;
	public final static int MIN_ZOOM = -70; // As close as you can zoom
	public final static int MAX_ZOOM = -MIN_ZOOM; // As far as you can zoom
	public final static float ANGLE_STEP = 8.0f; // Amount to rotate on each step
	private final static float MAX_TILT_COEFFICIENT = 3.0f;
	private final static float TILT_THRESHOLD = 0.1f;
	private final static int LEVEL_TIME = 60; // Initial level time, in seconds
	private final static int MIN_UPDATE_INTERVAL = 20; // Milliseconds
	private final static int MIN_FPS_STATS_COUNT = 20;
	
	// Game states
	public final static int NOT_STARTED = 0; // When in start menu
	public final static int TRANSITION_ANIMATION_ONGOING = 1;
	public final static int ONGOING = 2;
	public final static int PAUSED = 3;
	public final static int LEVEL_FINISHED = 4;
	public final static int GAME_OVER = 5;
	
	private final static int MARGIN = 6;
	public final static int TEXT_COLOR = 0xff81daff;

	// Members
	private final Main _midlet;
	private WorldBuilder _builder;
	private Menu _menu;
	private final MenuItem _blinkingMenuItem = new MenuItem("Tap screen to start game");
	private Thread _mainThread = null;
	private GameModel _gameModel = null;
	private InteractionManager _interactionManager = null;
	private Maze _maze = null;
	private MarbleModel _marbleModel = null;
	private Graphics3D _graphics3d; // Graphics 3D global instance
	private World _world;
	private Camera _topCamera = null;
	private Camera _povCamera = null;	
	private Background _background = null;
	private Graphics _graphics = null;
	private CameraAnimator _cameraAnimator = null;
	private Appearance _wallClearAppearance = new Appearance();
	private Appearance _wallAppearance = new Appearance();
	private Mesh _marble = null; // Simple mesh representing the marble
	private IconButton[] _iconButtons = new IconButton[4];
	private InfoDialog _infoDialog = null;
	private float _zoom = DEFAULT_ZOOM;
	private float _previousZoom = MIN_ZOOM;
	private float _relativeZoom = 0.5f;
	private float _cameraOffset = 1;
	private float _povAngleY = 0.0f;
	private boolean _povMode = true;
	private float _prevTiltX = 0f;
	private float _prevTiltY = 0f;	
	private volatile boolean _running = true; // Flag to keep the drawing going
	private int _gameState = NOT_STARTED;

	// The length of single steps to directions X and Z
	// Stored to avoid extra trigonometric calculations
	private float _stepLengthX = 0.0f;
	private float _stepLengthZ = -GameModel.DEFAULT_STEP_LENGTH_PER_AXIS;
	
	private volatile int _loopCounter = 1;
	private long _drawingTime = 0;
	private volatile float _fps = 0;
	private float _averageFps = 0;
	private int _fpsCount = 0; // For calculating the average
	private volatile long _levelStartTime = 0;
	private volatile long _timeUsed = 0;
	private volatile long _timeToBeat = 0;
	private int _level = 1;
	private int _width = 0;
	private int _height = 0;
	private boolean _isPortrait = true;
	private boolean _hasBackground = true;
	private boolean _firstTime = true;
	public boolean _debugMode = false;	
	
	/**
	 * Constructor.
	 * @param _midlet The parent MIDlet.
	 */
	public MazeCanvas(Main midlet) {
		super(true);
		_midlet = midlet;
	}

	/*
	 * Methods from GameCanvas.
	 * Implementation moved to InteractionManager to reduce the size of this
	 * class.
	 */
	protected void keyPressed(int key) { _interactionManager.onKeyPressed(key); }
	protected void pointerPressed(int x, int y) { _interactionManager.onPointerPressed(x, y); }
	protected void pointerReleased(int x, int y) { _interactionManager.onPointerReleased(x, y); }
	protected void pointerDragged(int x, int y) { _interactionManager.onPointerDragged(x, y); }
	protected void pointerRepeated(int x, int y) { _interactionManager.onPointerRepeated(x, y); }

	/**
	 * From Runnable.
	 */
	public void run() {
		_graphics = getGraphics();
		long tempTime = System.currentTimeMillis();
		long tempTimeForPause = 0;

		while (_running) {
			long startTime = System.currentTimeMillis();

			if (isShown()) {
				MyTimer.instance().update(startTime);
				
				if (_gameState == ONGOING) {
					//---------------------------------------------------------
					// 1. Do operations that need to be done as often as possible
					//---------------------------------------------------------
					_timeUsed = startTime - _levelStartTime;
					
					if (_timeToBeat - _timeUsed <= 0) {
						// Out of time! End the game.
						endGame();
					}

					if (!_povMode) {
						// Update the marble position
						float[] position = _marbleModel.position();
						_marble.setTranslation(
								position[0] + MarbleModel.DEFAULT_SIZE,
								position[1],
								position[2] + MarbleModel.DEFAULT_SIZE);
					}

					//---------------------------------------------------------
					// 2. Do operations that need to be done less often
					//---------------------------------------------------------
					if (startTime - tempTime > MIN_UPDATE_INTERVAL) {
						//-----------------------------------------------------
						// 3. Do operations that need to be done every
						//    MIN_UPDATE_INTERVAL milliseconds
						//-----------------------------------------------------
						// Move the marble and update the camera
						if (_povMode) {
							moveMarbleInPovMode();
						}
						else {
							moveMarble();
						}
						
						updateCamera();
						
						//-----------------------------------------------------
						// 4. Do operations that don't need to be done very
						//    MIN_UPDATE_INTERVAL milliseconds
						//-----------------------------------------------------						
						if (_loopCounter % 2 == 0) {
							tiltBoard();
						}
						
						if (_loopCounter % 9 == 0) {
							// Check if the marble reached the end
							if (!_gameModel.goalReached()
									&& _gameModel.isAtEnd())
							{
								// We're there!
								_gameModel.setGoalReached(true);
								finishLevel();
							}							
						}
						
						// Update the counter
						if (_loopCounter > 9) {
							_loopCounter = 1;
						}
						else {
							_loopCounter++;
						}
						
						tempTime = startTime;
					}
				} // if (_gameState == ONGOING)

				// Camera animator is updated all the time if running
				if (_cameraAnimator.running()) {
					_cameraAnimator.update();
					
					if (_cameraAnimator.animationType() ==
							CameraAnimator.PAUSE_ANIMATION
							&& startTime - tempTimeForPause > MIN_UPDATE_INTERVAL)
					{
						shiftBackground(-1);
						tempTimeForPause = startTime;
					}
				}
				
				// Update graphics every time
				updateGraphics();
			} // if (isShown())

			// Wait for a little and give the other threads the chance to run
			long timeTaken = System.currentTimeMillis() - startTime;

			if (timeTaken < MILLIS_PER_TICK) {
				synchronized (this) {
					try {
						wait(MILLIS_PER_TICK - timeTaken);
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
			else {
				Thread.yield();
			}
		}
	}

	/**
	 * From OrientationListener.
	 */
	public void displayOrientationChanged(int newDisplayOrientation) {
		System.out.println("MazeCanvas::displayOrientationChanged(): "
				+ newDisplayOrientation);
		boolean orientationChanged = false;
		
		if (newDisplayOrientation == Orientation.ORIENTATION_LANDSCAPE
				&& !_povMode && _isPortrait)
		{
			orientationChanged = true;
			_isPortrait = false;
		}
		else if (!_isPortrait) {
			orientationChanged = true;
			_isPortrait = true;
		}
		
		if (orientationChanged) {
			_iconButtons[IconButton.PAUSE].setIsPortrait(_isPortrait);
			_topCamera.setTransform(defaultTopCameraTransform());
			_interactionManager.setCalibration(InteractionManager.UNDEFINED);
		}
	}
	
	/**
	 * From CameraAnimator.Listener.
	 * A lot of the transition logic is done here.
	 */
	public void onAnimationFinished(int animationType) {
		System.out.println("MazeCanvas::onAnimationFinished(): " + animationType);
		
		switch (animationType) {
		case CameraAnimator.PAUSE_ANIMATION: {
			// Start a new animation to go to the current game view
			transitionToCurrentViewMode();
			break;
		}		
		case CameraAnimator.TRANSITION_ANIMATION_TO_POV: {
			_world.setActiveCamera(_povCamera);
			_marble.setRenderingEnable(false); // Do not render the marble in POV mode	
			_gameState = ONGOING;
			updateCamera();
			
			// Trigger a new average FPS calculation
			_averageFps = 0;
			_fpsCount = 0;			
			break;
		}
		case CameraAnimator.TRANSITION_ANIMATION_TO_TOP: {
			_world.setActiveCamera(_topCamera);
			_marble.setRenderingEnable(true);
			_gameState = ONGOING;
			updateCamera();
			
			// Trigger a new average FPS calculation
			_averageFps = 0;
			_fpsCount = 0;			
			break;
		}
		case CameraAnimator.LEVEL_RESET_ANIMATION_STEP: {
			if (_gameState == LEVEL_FINISHED) {
				// Advance a level
				nextLevel();
		    	transitionToCurrentViewMode();
			}
			else {
				// Game over, create a new level and go to the start view
				createNewLevel();
				showStartMenu();
			}
			
			break;
		}
		} // switch (animationType)
	}
	
	/**
	 * Starts/stops the game thread. 
	 * @param running If true, will start the thread/drawing. If false, will
	 * 				  stop the game thread.
	 */
	public synchronized void setRunning(final boolean running) {
		_running = running;
		
		if (_running) {
		    if (_mainThread == null) {
		    	_mainThread = new Thread(this);
		    	_mainThread.start();
		    }			
		}
	}
	
	/**
	 * Initializes the game canvas.
	 */
	public void init() {		
		_gameModel = GameModel.instance();
		_interactionManager = new InteractionManager(this, _gameModel);	    
		_maze = _gameModel.maze();
		_marbleModel = _gameModel.marble();
		_topCamera = new Camera();
		_povCamera = new Camera();
		
		setFullScreenMode(true);
		
		_width = getWidth();
		_height = getHeight();
		_menu = new Menu(_interactionManager, _width, _height);

		// Get the Graphics3D instance
		_graphics3d = Graphics3D.getInstance();
		
		// Create buttons
		_iconButtons[IconButton.EXIT] = new IconButton(IconButton.EXIT);
		_iconButtons[IconButton.INFO] = new IconButton(IconButton.INFO);
		_iconButtons[IconButton.PAUSE] = new IconButton(IconButton.PAUSE);
		_iconButtons[IconButton.VIEW_MODE] = new IconButton(IconButton.VIEW_MODE);

		// Create the world and the maze
		_world = new World();
		_world.addChild(_topCamera);
		_world.addChild(_povCamera);
		_world.setActiveCamera(_topCamera);
		
		_builder = new WorldBuilder();
		_background = _builder.createBackground(_world);
		_builder.createFloor(_world);	    
		_builder.createWallAppearances(_wallAppearance, _wallClearAppearance);
		_marble = _builder.createMarble(_world);
		
		_povCamera.setPerspective(60.f,
				(float)getWidth() / (float)getHeight(),
				0.1f, 1000.f);
		_povMode = false;	    
		_povAngleY = -180f;
		
		calculateRelativeZoomAndCameraOffset();
		
		_cameraAnimator = new CameraAnimator(this, _world, _topCamera);
		
		createNewLevel();	
		
		setRunning(true);

		_interactionManager.onUIInitialized();
		Orientation.addOrientationListener(this);
		
		showStartMenu();
	}
	
	/**
	 * To control the background setting. Painting the background has a
	 * significant impact on performance with low-performing devices.
	 * @param on If true, will paint the background.
	 */
	public void setBackground(final boolean on) {
		if (on && _background == null) {
			// Create the background
			_background = _builder.createBackground(_world);
		}
		else if (!on && _background != null) {
			// Clear the background
			_world.setBackground(null);
			_background = null;
		}

		_hasBackground = on;
		
		// Trigger a new average FPS calculation
		_averageFps = 0;
		_fpsCount = 0;		
	}
	
	/** 
	 * @return True if the background is painted, false otherwise.
	 */
	public final boolean hasBackground() {
		return _hasBackground;
	}
	
	/**
	 * Starts a new game.
	 */
	public void startNewGame() {
		_gameModel.setGoalReached(false);
		_level = 1;
    	_levelStartTime = System.currentTimeMillis();
    	_timeToBeat = LEVEL_TIME * 1000;
    	_timeUsed = 0;
		
    	if (_gameState == NOT_STARTED) {
    		// Starting the game from the start menu (view)
    		// No need to create a new level
	    	MyTimer.instance().removeListener(_blinkingMenuItem);
    	}
    	else {
    		createNewLevel();
    	}
    	
    	if (_cameraAnimator.running()) {
    		_cameraAnimator.stopAnimation();
    	}
    	else {
    		_gameState = ONGOING;
    	}
    	
    	_interactionManager.reset();
    	DeviceControl.setLights(0, 100); // Prevent screen lock
    	TipBox.showTips(); // This only works when called the first time
	}
	
	/**
	 * Resumes the current game.
	 */
	public void resume() {
		System.out.println("MazeCanvas::resume()");
	
		if (_gameState == PAUSED) {
			_levelStartTime = System.currentTimeMillis() - _timeUsed;
			_cameraAnimator.stopAnimation();
		}
		else {
			System.out.println("MazeCanvas::resume(): No game to resume!");
		}		
	}
	
	/**
	 * Pauses the game.
	 */
	public void pause() {
		if (_gameState == PAUSED) {
			// Already paused
			return;
		}
		
		// Create the pause menu
		_menu.clear();
		_menu.addItem("Resume");
		_menu.addItem((_hasBackground ? "Set background off"
				: "Set background on"));
		_menu.addItem((_debugMode ? "Switch debug mode off"
				: "Switch debug mode on"));
		_menu.addItem("Restart game");
		
	    Transform targetTransform = new Transform();
	    targetTransform.postRotate(PAUSE_CAMERA_ANGLE, -1f, 0f, 0f);
	    targetTransform.postTranslate(0f, 0f,
	    		DEFAULT_ZOOM * GameModel.MAZE_SIDE_LENGTH);

		_cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION,
				   					   targetTransform, null);
		
		// Resume will result in re-calibration
		_interactionManager.setCalibration(InteractionManager.UNDEFINED);
		
		// Update the game state
		_gameState = PAUSED;
	}
	
	/**
	 * Ends the current game and shows the game over view.
	 */
	public void endGame() {
		_gameState = GAME_OVER;
		_menu.clear();
		MenuItem item = new MenuItem("Game Over");
		item.setDisabled(true);
		_menu.addItem(item);
		_menu.addItem("You reached level " + _level);
		_blinkingMenuItem.setText("Tap screen to continue");
		MyTimer.instance().addListener(_blinkingMenuItem,  true, MenuItem.BLINK_INTEVAL);
		_menu.addItem(_blinkingMenuItem);
		_cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION);
	}

	/**
	 * Finishes the level and prepares to start a new one. This method should be
	 * called when the marble gets to the end of the maze.
	 */
	public void finishLevel() {
		_gameState = LEVEL_FINISHED;
		_menu.clear();
		_menu.addItem("Level " + _level + " finished!");
		_cameraAnimator.startAnimation(CameraAnimator.LEVEL_RESET_ANIMATION_STEP);	
	}
	
	/** 
	 * @return True if in portrait, false otherwise.
	 */
	public final boolean isPortrait() {
		return _isPortrait;
	}
	
	/**
	 * Sets the info dialog visibility. 
	 * @param visible If true, the info dialog is shown, otherwise it is hidden.
	 */
	public void setInfoDialogVisible(final boolean visible) {
		if (visible) {
			if (_infoDialog == null) {
				_infoDialog = new InfoDialog();
			}
		}
		else {
			_infoDialog = null;
		}
	}
	
	/** 
	 * @return True if the info dialog is visible, false otherwise.
	 */
	public final boolean infoDialogVisible() {
		return (_infoDialog != null);
	}
	
	/**
	 * Tries to open the Nokia Developer Projects webpage. If successful, this
	 * app will be terminated.
	 */
	public void openProjectsLink() {
		System.out.println("MazeCanvas::openProjectsLink()");
		
		try {
			if (_midlet.platformRequest("http://projects.developer.nokia.com/amaze")) {
				quit();
			}
		}
		catch (ConnectionNotFoundException cnfe) {}
	}
	
	/**
	 * Exits the application. 
	 */
	public final void quit() {
		_midlet.quitApp();
	}
	
	/** 
	 * @return The game state.
	 */
	public final int gameState() {
		return _gameState;
	}
	
	/** 
	 * @return The menu instance.
	 */
	public final Menu menu() {
		return _menu;
	}
	
	/** 
	 * @return The top camera.
	 */
	public final Camera topCamera() {
		return _topCamera;
	}
	
	/** 
	 * @return The camera animator instance.
	 */
	public final CameraAnimator cameraAnimator() {
		return _cameraAnimator;
	}
	
	/** 
	 * Modifies the current zoom value.
	 * @param diff The difference to current zoom.
	 */
	public void doZoom(final float diff) {
		_zoom += diff;
		
		if (_zoom < MIN_ZOOM) {
			_zoom = MIN_ZOOM;
		}
		else if (_zoom > MAX_ZOOM) {
			_zoom = MAX_ZOOM;
		}
		
		calculateRelativeZoomAndCameraOffset();
		updateCamera();
	}
	
	/**
	 * Reset the zoom. If the current zoom is the default, the previous zoom
	 * level is restored. 
	 */
	public void resetZoom() {
		if (_povMode) {
			// No zoom in point-of-view mode
			return;
		}
		
		if (_zoom != DEFAULT_ZOOM) {
			// Store the current zoom level and reset to default
			System.out.println("MazeCanvas::resetZoom(): Resetting the default zoom.");
			_previousZoom = _zoom;
			_zoom = DEFAULT_ZOOM;
		}
		else {
			// Restore the previous zoom
			System.out.println("MazeCanvas::resetZoom(): Restoring the previous zoom.");
			_zoom = _previousZoom;
		}
		
		calculateRelativeZoomAndCameraOffset();
		updateCamera();
	}

	/** 
	 * @param angle The point-of-view angle to set.
	 */
	public void setPovAngleY(final float angle) {
		_povAngleY = angle;
    	_stepLengthX = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS
    			* (float)Math.sin(Math.toRadians(_povAngleY));
    	_stepLengthZ = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS
    			* (float)Math.cos(Math.toRadians(_povAngleY));		
	}
	
	/** 
	 * @return The point-of-view angle.
	 */
	public final float povAngleY() {
		return _povAngleY;
	}
	
	/**
	 * Rotates the point-of-view angle based on the given difference.
	 * @param diff The rotation difference.
	 */
	public void rotatePovAngleY(final float diff) {
		_povAngleY += diff;
		_stepLengthX = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS
				* (float)Math.sin(Math.toRadians(_povAngleY));
		_stepLengthZ = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS
				* (float)Math.cos(Math.toRadians(_povAngleY));		
	}
	
	// Methods for getting the steps on both axis
	public final float stepLengthX() { return _stepLengthX; }
	public final float stepLengthZ() { return _stepLengthZ; }
	
	/** 
	 * @return True if the point-of-view mode is on, false if the top camera
	 * 		   mode is on.
	 */
	public final boolean povMode() {
		return _povMode;
	}	
	
	/**
	 * Switches from top view to normal view and vice versa.
	 */
	public void toggleViewMode() {
		_povMode = !_povMode;
		System.out.println("MazeCanvas::toggleViewMode(): To "
				+ (_povMode ? "POV" : "TOP"));
		
		if (_povMode) {
			_iconButtons[IconButton.VIEW_MODE].setPressed(true);
		}
		else {
			_iconButtons[IconButton.VIEW_MODE].setPressed(false);
		}
		
		// Do the transition animation.
		transitionToCurrentViewMode();
	}
	
	/** 
	 * @param id
	 * @param button
	 */
	public void setButtonPressed(int id) {
		_iconButtons[IconButton.EXIT].setPressed(false);
		_iconButtons[IconButton.INFO].setPressed(false);
		_iconButtons[IconButton.PAUSE].setPressed(false);
		
		if (id == IconButton.EXIT
				|| id == IconButton.INFO
				|| id == IconButton.PAUSE)
		{
			_iconButtons[id].setPressed(true);
		}
	}
	
	/** 
	 * @return The ID of the button pressed, or -1 if no button is pressed.
	 */
	public final int buttonPressed() {
		if (_iconButtons[IconButton.EXIT].pressed()) { return IconButton.EXIT; }
		if (_iconButtons[IconButton.INFO].pressed()) { return IconButton.INFO; }
		if (_iconButtons[IconButton.PAUSE].pressed()) { return IconButton.PAUSE; }
		return -1;		
	}
	
	/**
	 * Shifts the background to support the illusion when the camera rotates.
	 * @param delta The amount the background is shifted. A positive value
	 * 				shifts the background to the right and negative to the
	 * 				left.
	 */
	public final void shiftBackground(final int delta) {
		if (_background != null) {
			_background.setCrop(_background.getCropX() + delta, 0,
								_width, _height);
		}
	}
	
	/** 
	 * @return The average measured FPS rate or -1 if not calculated.
	 */
	public final float averageFps() {
		if (_fpsCount < MIN_FPS_STATS_COUNT) {
			return -1f;
		}
		
		return _averageFps;
	}
	
	/**
	 * Updates the properties of the active camera. Note that this is very
	 * expensive operation!
	 */
	public void updateCamera() {
		final float[] position = _marbleModel.position();
		
		if (_povMode) {
			_povCamera.setOrientation(_povAngleY, 0f, 1f, 0f);
		    _povCamera.setTranslation(position[0], 10f, position[2]);			
		}
		else {
			// Follow the marble with the camera but if the view is zoomed out
			// i.e. the maze and the marble are smaller, then try to show more
			// of the maze.
			_topCamera.setTranslation(position[0] / _cameraOffset, _zoom,
					position[2] / _cameraOffset);
		}
	}
	
	/**
	 * Decides whether it is allowed to move to a given place or not. This
	 * prevents the player walking through walls and going outside the maze.
	 * @param stepX The distance to move on X axis.
	 * @param stepZ The distance to move on Z axis.
	 * @return - 0 if no collision
	 * 		   - 1 if collision on X axis
	 * 		   - 2 if collision on Z axis
	 * 		   - 3 if collision on both axis
	 */
	public final int detectCollision(final float stepX,
									 final float stepZ)
	{
		float[] position = _marbleModel.position();
	    float x = position[0] + stepX;
	    float z = position[2] + stepZ;
	    
	    // First check if the target is inside the maze area
	    float mazeSize = GameModel.MAZE_SIDE_LENGTH / 2 - 2;
	    
	    int outside = 0;
	    
	    if (x - MarbleModel.DEFAULT_SIZE_HALVED <= -mazeSize
	    		|| x + MarbleModel.DEFAULT_SIZE_HALVED > mazeSize)
	    {
	    	outside = 1;
	    }
	    if (z - MarbleModel.DEFAULT_SIZE_HALVED <= -mazeSize
	    		|| z + MarbleModel.DEFAULT_SIZE_HALVED > mazeSize)
	    {
	    	outside += 2;
	    }
	    if (outside > 0) {
	    	System.out.println("MazeCanvas::canMoveTo(): Would go outside of the maze.");
	    	return outside;
	    }

	    return marbleCollidesAt(x, z);
	}
		
	/**
	 * Checks the possible collision between the marble and the walls.
	 * @param x
	 * @param z
	 * @return - 0 if no collision
	 * 		   - 1 if collision on X axis
	 * 		   - 2 if collision on Z axis
	 * 		   - 3 if collision on both axis
	 */
	private final int marbleCollidesAt(final float x, final float z) {
		RayIntersection intersection = new RayIntersection();
		final float minimumDistance = MarbleModel.DEFAULT_SIZE + 0.5f;
		
		if (_world.pick(-1, x, MarbleModel.DEFAULT_SIZE_HALVED, z,
				MarbleModel.DEFAULT_SIZE_HALVED, 0.1f,
				MarbleModel.DEFAULT_SIZE_HALVED,
	    		intersection))
	    {
	    	Node selected = intersection.getIntersected();
	    	
	    	if (selected instanceof Mesh) {
	    		float distance = intersection.getDistance();
	    		
	    		if (distance < minimumDistance) {
	    			float[] position = _marbleModel.position();
	    			int retval = 0;

	    			// Check X axis
	    			if (_world.pick(-1, x, MarbleModel.DEFAULT_SIZE_HALVED, position[2],
	    					MarbleModel.DEFAULT_SIZE_HALVED, 0.1f,
	    					MarbleModel.DEFAULT_SIZE_HALVED,
	    		    		intersection))
	    		    {
	    				distance = intersection.getDistance();
	    	    		
	    	    		if (distance < minimumDistance) {
	    	    			retval = 1;
	    	    		}
	    		    }

	    			// Check Z axis
	    			if (_world.pick(-1, position[0], MarbleModel.DEFAULT_SIZE_HALVED, z,
	    					MarbleModel.DEFAULT_SIZE_HALVED, 0.1f,
	    					MarbleModel.DEFAULT_SIZE_HALVED,
	    		    		intersection))
	    		    {
	    				distance = intersection.getDistance();
	    	    		
	    	    		if (distance < minimumDistance) {
	    	    			retval += 2;
	    	    		}
	    		    }
	    			
	    			return retval;
	    		}
	    	}
	    }
		
		return 0;
	}
	
	/**
	 * Constructs and shows the start menu.
	 */
	private void showStartMenu() {
		_gameState = NOT_STARTED;
		 
	    // Create the start menu
		_menu.clear();
		MyTimer.instance().removeListener(_blinkingMenuItem);
		_blinkingMenuItem.setText("Tap screen to start game");
		MyTimer.instance().addListener(_blinkingMenuItem,  true, MenuItem.BLINK_INTEVAL);
	    _menu.addItem(_blinkingMenuItem);	
	    
	    // Set the camera
	    final float width = (float)_width;
	    final float height = (float)_height;
	    _topCamera.setPerspective(60.f, width / height, 0.1f, 1000.f);
	    Transform transform = new Transform();
	    transform.postRotate(PAUSE_CAMERA_ANGLE, -1f, 0f, 0f);
	    transform.postTranslate(0f, 0f,
	    		DEFAULT_ZOOM * GameModel.MAZE_SIDE_LENGTH);
	    _topCamera.setTransform(transform);
	    updateCamera();
	    
	    // Start the pause animation  
		_cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION);
	}
	
	/**
	 * Creates a new level. The old level, if one exists, will be destroyed.
	 */
	private void createNewLevel() {
		System.out.println("MazeCanvas::createNewLevel()");
	    _builder.createNewMaze(_world, _maze, _wallAppearance);

	    // Set the marble's initial location
	    _marbleModel.setPosition(_maze.startPosition());		
	}

	/**
	 * Recreates the maze and re-initializes the values for the next level. 
	 */
	private void nextLevel() {
		System.out.println("MazeCanvas::nextLevel()");
		_gameModel.setGoalReached(false);
		_level++;
    	_levelStartTime = System.currentTimeMillis();
    	
    	_timeToBeat = LEVEL_TIME * 1000
    			+ _timeToBeat - _timeUsed // Add the time left from previous level
    			- (_level - 1) * 5000; // Take 5 seconds off on each level
    	
    	if (_timeToBeat < 10000) {
    		// Keep it ten seconds minimum
    		_timeToBeat = 10000;
    	}
    	
    	_timeUsed = 0;
    	
    	createNewLevel();
    	updateCamera();
    	_interactionManager.reset();
	}
	
	/**
	 * Returns the default top camera transform.
	 */
	private Transform defaultTopCameraTransform() {
		Transform transform = new Transform();
		
		if (_isPortrait) {
			transform.postRotate(DEFAULT_TOP_CAMERA_ANGLE, -1f, 0f, 0f);
		}
		else {
			transform.postRotate(90, -1f, 0f, 0f);
			transform.postRotate(90 - DEFAULT_TOP_CAMERA_ANGLE, 0f, -1f, 0f);
		}
		
		transform.postTranslate(0f, 0f, GameModel.MAZE_SIDE_LENGTH);
		return transform;
	}	
	
	/**
	 * Starts a camera animation based on the current view mode setting.
	 */
	private void transitionToCurrentViewMode() {
		if (_povMode) {
			_cameraAnimator.startAnimation(CameraAnimator.TRANSITION_ANIMATION_TO_POV,
										   _povCamera);
		}
		else {
			Transform transform = defaultTopCameraTransform();
			float[] orientation = new float[4];
			orientation[0] = 0;
			orientation[1] = 0;
			orientation[2] = 0;
			orientation[3] = 1;
			_cameraAnimator.startAnimation(CameraAnimator.TRANSITION_ANIMATION_TO_TOP,
										   transform, orientation);
		}
	}

	/**
	 * Calculates and returns the relative zoom [0.0 - 1.0], where 0 is the
	 * maximum zoom in and 1 the maximum zoom out.
	 * @return The relative zoom.
	 */
	private final void calculateRelativeZoomAndCameraOffset() {
		final int scale = MazeCanvas.MAX_ZOOM - MazeCanvas.MIN_ZOOM;
		_relativeZoom =  (_zoom - (float)MazeCanvas.MIN_ZOOM) / scale;
		
		_cameraOffset = _relativeZoom * 4;
		
		if (_cameraOffset < 1) {
			_cameraOffset = 1;
		}
	}

	/**
	 * Tilts the board based on the current accelerometer sensor readings.
	 */
	private final void tiltBoard() {
		// Tilt the board based on the accelerometer readings
		final float tiltX = (float)(-_interactionManager.ax());
		final float tiltY = (float)(-_interactionManager.ay());
		boolean updateTilt = false;
		
		if (Math.abs(tiltX - _prevTiltX) > TILT_THRESHOLD) {
			_prevTiltX = tiltX;
			updateTilt = true;
		}
		
		if (Math.abs(tiltY - _prevTiltY) > TILT_THRESHOLD) {
			_prevTiltY = tiltY;
			updateTilt = true;
		}
		
		if (updateTilt) {
			// Calculate the tilt coefficient based on the current zoom
			final float coefficient = _relativeZoom * MAX_TILT_COEFFICIENT;
			
			// Update the camera angle based on the accelerometer values
			_topCamera.setOrientation(_prevTiltY * coefficient, 1f, 0f, 0f);
			_topCamera.postRotate(_prevTiltX * coefficient, 0f, 0f, 1f);
		}	
	}

	/**
	 * Moves the marble when in POV mode.
	 * 
	 * Note that this method does not update the camera.
	 */
	private final void moveMarbleInPovMode() {
		final double ax = _interactionManager.ax();
		final double ay = _interactionManager.ay();
		
		// Check if we should be turning to some direction
		if (Math.abs(ax) > InteractionManager.ACCELERATION_THRESHOLD) {
			rotatePovAngleY((float)ax * InteractionManager.ACCELERATION_COEFFICIENT_X);
			shiftBackground((int)(-ax * InteractionManager.ACCELERATION_COEFFICIENT_X) * 3);
		}
		
		// Check if we should be moving back or forth
		final float absCalibratedAy =(float)Math.abs(ay);
		
		if (absCalibratedAy > InteractionManager.ACCELERATION_THRESHOLD) {
			final int mark = (ay > 0) ? 1 : -1;
			
			final float stepLength =
					GameModel.DEFAULT_STEP_LENGTH_PER_AXIS
					* (absCalibratedAy
							* InteractionManager.ACCELERATION_COEFFICIENT_Y);
			
			final float stepX = mark * stepLength * (float)Math.sin(Math.toRadians(_povAngleY));
			final float stepZ = mark * stepLength * (float)Math.cos(Math.toRadians(_povAngleY)); 
			final int collision = detectCollision(stepX, stepZ);
			
			if (collision == 0) {
				_marbleModel.move(stepX, 0, stepZ);
			}
		}
	}

	/**
	 * Calculates the velocity of the marble based on the current accelerometer
	 * sensor readings and moves the marble based on the velocity and the
	 * collision detection.
	 * 
	 * Note that this method does not update the camera.
	 */
	private final void moveMarble() {
		// Calculate the velocity
		float[] velocity =
				_marbleModel.calculateVelocity(_interactionManager.ax(),
											   _interactionManager.ay());

		// Check for collisions
		int collision = -1;
		int safety = 0; // To prevent forever loop
		
		while (collision != 0 && safety < 4) {
			collision = detectCollision(velocity[0], velocity[2]);
			
			if (collision == 1 || collision == 3) { // X axis or both 
				velocity[0] = -velocity[0] * MarbleModel.DEFAULT_BOUNCE_DRAG;
			}
			else if (collision == 2 || collision == 3) { // Z axis or both
				velocity[2] = -velocity[2] * MarbleModel.DEFAULT_BOUNCE_DRAG;
			}
			
			safety++;
		}

		// Set the velocity and move the marble
		_marbleModel.setVelocity(velocity);
		_marbleModel.move(velocity[0], 0, velocity[2]);
	}

	/**
	 * Draws the graphics.
	 */
	private final void updateGraphics() {
		if (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT) {
			_drawingTime = System.currentTimeMillis();
		}
		
		// Draw the graphics
		draw3D(_graphics);
		draw2D(_graphics);

		if (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT) {
			// Calculate the FPS
			_drawingTime = System.currentTimeMillis() - _drawingTime;
			
			if (_drawingTime > 0) {
				_fps = 1000 / _drawingTime;
			}
			
			if (_debugMode) {
				// Draw the FPS on the screen
				_graphics.setColor(TEXT_COLOR);
				_graphics.drawString("FPS: " + _fps + " (" + 
						((_fpsCount < MIN_FPS_STATS_COUNT) ? -1 : _averageFps) + ")",
						42, _height, Graphics.BOTTOM | Graphics.LEFT);
			}
			
			if (_fpsCount < MIN_FPS_STATS_COUNT) {
				_averageFps += _fps;
				_fpsCount++;
				
				if (_fpsCount == MIN_FPS_STATS_COUNT) {
					// Enough figures calculated. Calculate now the average.
					_averageFps /= _fpsCount;
					
					if (_firstTime && _averageFps > 1) {
						if (_averageFps < 25) {
							// Remove the background to boost the performance.
							setBackground(false);
						}
						
						_firstTime = false;
					}
				}
			}
		}
		
		// Flush the buffer to the screen
		flushGraphics();
	}

	/**
	 * Draws 2D graphics e.g. information on top of the screen.
	 * @param graphics
	 */
	private void draw2D(Graphics graphics) {
		graphics.setColor(TEXT_COLOR);
		
		if (_debugMode) {
			// Draw the accelerometer sensor readings in the bottom
			graphics.drawString("AX: " + (float)_interactionManager.ax(),
					42, _height - 30, Graphics.BOTTOM | Graphics.LEFT);
			graphics.drawString("AY: " + (float)_interactionManager.ay(),
					42, _height - 15, Graphics.BOTTOM | Graphics.LEFT);
		}

		if (_isPortrait
				&& (_gameState == ONGOING
				|| _gameState == PAUSED
				|| _gameState == GAME_OVER))
		{
			// Draw the current level in the top-left corner
			StringBuffer buffer = new StringBuffer();
			buffer.append("Level ").append(_level);
			graphics.drawString(buffer.toString(), MARGIN, MARGIN,
					Graphics.TOP | Graphics.LEFT);

			// Draw the time left in the top-right corner
			buffer.delete(0,  buffer.length());
			buffer.append("Time left: ");
			buffer.append((_timeToBeat - _timeUsed) / 1000);
			graphics.drawString(buffer.toString(), _width - MARGIN, MARGIN,
					Graphics.TOP | Graphics.RIGHT);
		}

		if (_gameState == NOT_STARTED
				|| _gameState == PAUSED
				|| _gameState == GAME_OVER)
		{
			if (_infoDialog == null) {
				// Draw the menu
				_menu.paint(graphics);

				// Draw the info and exit button
				_iconButtons[IconButton.INFO].paint(graphics, MARGIN, _height - 36);
				_iconButtons[IconButton.EXIT].paint(graphics, _width - 36, _height - 36);
			}
			else {
				_infoDialog.paint(graphics);
			}
		}
		else if (_gameState == ONGOING) {
			// Draw the view mode and pause buttons
			_iconButtons[IconButton.PAUSE].paint(graphics, _width - 36, _height - 36);
			
			if (_isPortrait) {
				_iconButtons[IconButton.VIEW_MODE].paint(graphics, MARGIN, _height - 36);

				if (TipBox.visible()) {
					// Draw the tip box
					TipBox.paint(graphics);
				}
			}
		}
		else if (_gameState == LEVEL_FINISHED) {
			_menu.paint(graphics); // Shows "Level X finished!"
		}
	}

	/**
	 * Paints the scene.
	 * @param graphics
	 */
	private void draw3D(Graphics graphics) {
	    boolean bound = false;
	    
	    try {
	    	// Bind the target
	    	_graphics3d.bindTarget(graphics);
	    	bound = true;
	    	
	    	// Advance the animation
	    	_world.animate((int)(System.currentTimeMillis() - _levelStartTime));
	    	
	    	// Do the rendering
	    	_graphics3d.render(_world);
	    }
	    finally {
	    	// Release the target
	    	if (bound) {
	    		_graphics3d.releaseTarget();
	    	}
	    }
	}
}